    const pages = {
      "工作台": "先看今天最该处理的事情，再进入对应工作台。",
      "图片制作": "上传产品素材，选择效果图、文字生图、主图、详情图或广告图，生成结果留在当前页面。",
      "询盘处理": "粘贴客户发来的需求，系统帮你拆解、回复、判断下一步。",
      "报价系统": "直接填写材料、尺寸、工艺、数量和报价条件；需要时再更换产品。",
      "报价结果": "先核对内部成本，再查看建议报价；需要修改时可返回原报价继续填写。",
      "AI效果图": "根据客户需求生成客户确认图、主图、详情页图方案。",
      "确认打样": "报价后生成客户确认单、打样任务和争议预防提醒。",
      "报价参数": "材料价格、加工费、损耗、税费或利润变化时，都可以随时回来修改；运费统一另计。",
      "制前资料": "检查设计稿、图纸、工艺风险、制前资料和生产注意事项。",
      "跟单工单": "推进客户确认、报价、交期、交付和回款；逐道生产工序统一在生产中心执行。",
      "生产中心": "只处理已经成交订单的工序、材料、机台、外协和质检，不重复处理客户跟进。",
      "专业经营": "库存、财务、扫码报工、客户自助和商用运维共用订单数据，形成完整经营闭环。",
      "跟单详情": "按进入入口显示订单跟进或生产执行内容，避免重复操作。",
      "经营报表": "把今天的待办、报价和风险，以及本月的成交、利润和重点产品集中显示。",
      "客户档案": "沉淀客户信息、历史询盘、报价偏好、内容素材和成交状态。",
      "账号套餐": "查看当前套餐、AI点数、到期时间和可用服务。",
      "图片转矢量": "把客户发来的图片、Logo、草图转成可生产用的矢量文件。",
      "刀板排版": "按刀板有效幅面、边距、咬口、刀间距和订单数量生成规则刀位，并导出生产核对文件。",
      "团队权限": "老板管理员工账号和功能权限，分配负责人并追溯价格、报价和跟单操作。",
      "渠道中心": "管理渠道申请、专属推广入口、客户报备、成交归属、佣金和结算。",
      "提醒中心": "分开查看客户业务提醒与软件系统消息，重要事项不再混在一起。",
      "更多": "集中管理当前账号、员工协作、套餐、备份和系统服务。"
    };
    const pageTitles = {
      "工作台": "首页",
      "图片制作": "制图",
      "询盘处理": "AI接单",
      "报价系统": "报价",
      "报价结果": "报价结果",
      "AI效果图": "效果图",
      "跟单工单": "订单跟进",
      "跟单详情": "订单详情",
      "经营报表": "经营报表",
      "确认打样": "确认/打样",
      "报价参数": "价格表",
      "制前资料": "制前资料",
      "图片转矢量": "图片转矢量",
      "刀板排版": "刀板排版",
      "生产中心": "生产执行中心",
      "专业经营": "专业经营",
      "客户档案": "客户档案",
      "账号套餐": "会员中心",
      "团队权限": "团队与权限",
      "渠道中心": "渠道中心",
      "提醒中心": "提醒中心",
      "更多": "设置与管理"
    };
    let channelProgramData = null;
    async function channelApi(path, options = {}) {
      if (typeof window.__jianglaiAppApi !== "function") {
        throw new Error("渠道服务正在初始化，请稍后再试。");
      }
      return window.__jianglaiAppApi(path, options);
    }
    function channelMoney(value) {
      return `¥${Number(value || 0).toLocaleString("zh-CN", { minimumFractionDigits:2, maximumFractionDigits:2 })}`;
    }
    function channelDate(value, withTime = false) {
      if (!value) return "—";
      const date = new Date(value);
      if (Number.isNaN(date.getTime())) return String(value).slice(0, 10) || "—";
      return date.toLocaleString("zh-CN", withTime
        ? { year:"numeric", month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit", hour12:false }
        : { year:"numeric", month:"2-digit", day:"2-digit" });
    }
    function setChannelMessage(id, message = "", success = false) {
      const node = document.getElementById(id);
      if (!node) return;
      node.textContent = message;
      node.classList.toggle("success", Boolean(message) && success);
    }
    function channelTableEmpty(tbody, columns, text) {
      const row = document.createElement("tr");
      const cell = document.createElement("td");
      cell.colSpan = columns;
      cell.className = "channel-empty";
      cell.textContent = text;
      row.appendChild(cell);
      tbody.appendChild(row);
    }
    function channelCell(row, value) {
      const cell = document.createElement("td");
      if (value instanceof Node) cell.appendChild(value);
      else cell.textContent = String(value ?? "—");
      row.appendChild(cell);
      return cell;
    }
    function channelTag(status, labels) {
      const tag = document.createElement("span");
      const good = ["active","won","available","paid"].includes(status);
      const warn = ["pending","reported","contacted","demo","negotiating","requested","reviewing","approved","settling"].includes(status);
      tag.className = `channel-state-tag${good ? " good" : warn ? " warn" : ""}`;
      tag.textContent = labels[status] || status || "—";
      return tag;
    }
    function renderChannelPolicy(policy = {}) {
      const tiers = policy.buyout_tiers || [];
      const percent = value => `${(Number(value || 0) / 100).toFixed(Number(value || 0) % 100 ? 1 : 0)}%`;
      document.getElementById("channelTierOne").textContent = percent(tiers[0]?.rate_bps || 1200);
      document.getElementById("channelTierTwo").textContent = percent(tiers[1]?.rate_bps || 1500);
      document.getElementById("channelTierThree").textContent = percent(tiers[2]?.rate_bps || 1800);
      document.getElementById("channelCampaignMaxRate").textContent = `最高 ${percent(policy.campaign_max_rate_bps || 2000)}`;
      document.getElementById("channelSubscriptionRate").textContent = percent(policy.subscription_rate_bps || 1500);
      document.getElementById("channelRechargeRate").textContent = percent(policy.recharge_rate_bps || 500);
      document.getElementById("channelHoldDays").textContent = `${Number(policy.commission_hold_days || 30)} 天`;
    }
    function renderChannelCenter(data) {
      channelProgramData = data;
      renderChannelPolicy(data.policy || {});
      const partner = data.partner;
      const applicationPanel = document.getElementById("channelApplicationPanel");
      const activePanel = document.getElementById("channelActivePanel");
      const pill = document.getElementById("channelStatusPill");
      pill.className = "channel-status-pill";
      if (!partner) {
        pill.textContent = "尚未申请";
        document.getElementById("channelStatusTitle").textContent = "申请渠道合作";
        document.getElementById("channelStatusDescription").textContent = "提交真实联系人和覆盖地区，审核通过后会生成专属推广入口。";
        applicationPanel.classList.remove("is-hidden");
        activePanel.classList.add("is-hidden");
        return;
      }
      const statusLabels = { pending:"等待审核", active:"渠道已开通", suspended:"渠道已暂停", rejected:"申请未通过" };
      pill.textContent = statusLabels[partner.status] || partner.status;
      pill.classList.add(`is-${partner.status}`);
      document.getElementById("channelStatusTitle").textContent = partner.status === "active"
        ? `渠道编号 ${partner.code}` : statusLabels[partner.status] || "渠道状态";
      document.getElementById("channelStatusDescription").textContent = partner.status === "active"
        ? "客户报备、成交归属、佣金和结算都已启用。"
        : partner.status === "pending" ? "平台正在核验合作资料，审核通过后自动开通。"
          : partner.status === "suspended" ? "渠道功能暂时停用，请联系平台确认原因。"
            : "可以修改合作资料后重新提交审核。";
      applicationPanel.classList.toggle("is-hidden", partner.status === "active");
      activePanel.classList.toggle("is-hidden", partner.status !== "active");
      const applyButton = document.querySelector("#channelApplicationForm button[type=submit]");
      if (applyButton) {
        applyButton.disabled = partner.status === "pending" || partner.status === "suspended";
        applyButton.textContent = partner.status === "pending" ? "申请正在审核" : partner.status === "suspended" ? "渠道已暂停" : "重新提交申请";
      }
      if (partner.status !== "active") return;
      const stats = data.stats || {};
      document.getElementById("channelStatLeads").textContent = Number(stats.reported_leads || 0).toLocaleString("zh-CN");
      document.getElementById("channelStatWon").textContent = Number(stats.won_leads || 0).toLocaleString("zh-CN");
      document.getElementById("channelStatPending").textContent = channelMoney(stats.pending);
      document.getElementById("channelStatAvailable").textContent = channelMoney(stats.available);
      document.getElementById("channelStatPaid").textContent = channelMoney(stats.paid);
      document.getElementById("channelReferralCode").textContent = partner.code || "—";
      document.getElementById("channelReferralLink").textContent = partner.referral_link || "—";
      const qr = document.getElementById("channelQrImage");
      if (partner.qr_data_url) {
        qr.src = partner.qr_data_url;
        qr.classList.remove("is-hidden");
      } else {
        qr.removeAttribute("src");
        qr.classList.add("is-hidden");
      }
      const leadLabels = { reported:"已报备", contacted:"已联系", demo:"已演示", negotiating:"商务沟通", won:"已成交", lost:"未成交", expired:"保护期结束" };
      const leadBody = document.getElementById("channelLeadTableBody");
      leadBody.replaceChildren();
      (data.leads || []).forEach(item => {
        const row = document.createElement("tr");
        channelCell(row, item.lead_no);
        const company = document.createElement("div");
        const strong = document.createElement("strong");
        strong.textContent = item.company_name || "—";
        const small = document.createElement("small");
        small.textContent = `${item.region || "地区未填"} · ${item.industry || "行业未填"}`;
        company.append(strong, document.createElement("br"), small);
        channelCell(row, company);
        channelCell(row, `${item.contact_name || "联系人未填"} ${item.contact_phone || ""}`);
        channelCell(row, channelTag(item.status, leadLabels));
        channelCell(row, channelDate(item.protected_until));
        channelCell(row, channelDate(item.created_at, true));
        leadBody.appendChild(row);
      });
      if (!(data.leads || []).length) channelTableEmpty(leadBody, 6, "还没有报备客户");
      const commissionLabels = { pending:"冷静期", available:"可结算", settling:"结算中", paid:"已结算", cancelled:"已取消" };
      const commissionBody = document.getElementById("channelCommissionTableBody");
      commissionBody.replaceChildren();
      (data.commissions || []).forEach(item => {
        const row = document.createElement("tr");
        channelCell(row, item.order_no);
        channelCell(row, `${item.company_name || "—"} · ${item.lead_no || ""}`);
        channelCell(row, channelMoney(item.basis_amount));
        channelCell(row, `${(Number(item.rate_bps || 0) / 100).toFixed(0)}%`);
        channelCell(row, channelMoney(item.amount));
        channelCell(row, channelTag(item.status, commissionLabels));
        channelCell(row, channelDate(item.available_at, true));
        commissionBody.appendChild(row);
      });
      if (!(data.commissions || []).length) channelTableEmpty(commissionBody, 7, "真实订单付款后，佣金会自动出现在这里");
      const settlementLabels = { requested:"已申请", reviewing:"复核中", approved:"待付款", paid:"已付款", rejected:"已驳回" };
      const settlementBody = document.getElementById("channelSettlementTableBody");
      settlementBody.replaceChildren();
      (data.settlements || []).forEach(item => {
        const row = document.createElement("tr");
        channelCell(row, item.settlement_no);
        channelCell(row, channelMoney(item.amount));
        channelCell(row, `${item.payee_name || "—"} · ${item.payout_account_masked || ""}`);
        channelCell(row, channelTag(item.status, settlementLabels));
        channelCell(row, channelDate(item.requested_at, true));
        settlementBody.appendChild(row);
      });
      if (!(data.settlements || []).length) channelTableEmpty(settlementBody, 5, "暂无结算记录");
      const settlementButton = document.getElementById("channelSettlementButton");
      settlementButton.disabled = Number(stats.available_cents || 0) < Number(data.policy?.settlement_min_cents || 0);
      settlementButton.textContent = settlementButton.disabled
        ? `满 ${channelMoney(data.policy?.settlement_min)} 可结算`
        : `结算 ${channelMoney(stats.available)}`;
    }
    async function loadChannelCenter(force = false) {
      if (!force && channelProgramData?.partner?.status === "active") renderChannelCenter(channelProgramData);
      const pill = document.getElementById("channelStatusPill");
      if (pill) pill.textContent = "正在同步";
      try {
        const data = await channelApi("/api/channel/me");
        renderChannelCenter(data);
      } catch (error) {
        if (pill) pill.textContent = "连接失败";
        document.getElementById("channelStatusTitle").textContent = "渠道数据暂时没有读取成功";
        document.getElementById("channelStatusDescription").textContent = error.message || "请检查网络后重试。";
      }
    }
    async function submitChannelApplication(event) {
      event.preventDefault();
      const button = event.currentTarget.querySelector("button[type=submit]");
      button.disabled = true;
      setChannelMessage("channelApplicationMessage", "正在提交申请…");
      try {
        const data = await channelApi("/api/channel/apply", {
          method:"POST",
          body:JSON.stringify({
            contact_name:document.getElementById("channelApplyContact").value.trim(),
            contact_phone:document.getElementById("channelApplyPhone").value.trim(),
            region:document.getElementById("channelApplyRegion").value.trim(),
            partner_type:document.getElementById("channelApplyType").value,
            notes:document.getElementById("channelApplyNotes").value.trim()
          })
        });
        setChannelMessage("channelApplicationMessage", data.message || "渠道申请已提交。", true);
        renderChannelCenter(data);
      } catch (error) {
        setChannelMessage("channelApplicationMessage", error.message);
      } finally {
        button.disabled = false;
      }
    }
    async function submitChannelLead(event) {
      event.preventDefault();
      const button = event.currentTarget.querySelector("button[type=submit]");
      button.disabled = true;
      setChannelMessage("channelLeadMessage", "正在核验客户归属…");
      try {
        const data = await channelApi("/api/channel/leads", {
          method:"POST",
          body:JSON.stringify({
            company_name:document.getElementById("channelLeadCompany").value.trim(),
            contact_phone:document.getElementById("channelLeadPhone").value.trim(),
            contact_name:document.getElementById("channelLeadContact").value.trim(),
            region:document.getElementById("channelLeadRegion").value.trim(),
            industry:document.getElementById("channelLeadIndustry").value.trim(),
            requirement:document.getElementById("channelLeadRequirement").value.trim()
          })
        });
        event.currentTarget.reset();
        setChannelMessage("channelLeadMessage", data.message || "客户已报备。", true);
        await loadChannelCenter(true);
      } catch (error) {
        setChannelMessage("channelLeadMessage", error.message);
      } finally {
        button.disabled = false;
      }
    }
    async function submitChannelSettlement(event) {
      event.preventDefault();
      const button = document.getElementById("channelSettlementButton");
      button.disabled = true;
      setChannelMessage("channelSettlementMessage", "正在锁定可结算佣金…");
      try {
        const data = await channelApi("/api/channel/settlements", {
          method:"POST",
          body:JSON.stringify({
            payout_method:document.getElementById("channelPayoutMethod").value,
            payout_account:document.getElementById("channelPayoutAccount").value.trim(),
            payee_name:document.getElementById("channelPayeeName").value.trim()
          })
        });
        event.currentTarget.reset();
        setChannelMessage("channelSettlementMessage", data.message || "结算申请已提交。", true);
        await loadChannelCenter(true);
      } catch (error) {
        setChannelMessage("channelSettlementMessage", error.message);
      } finally {
        button.disabled = false;
      }
    }
    async function copyChannelReferral(kind) {
      const partner = channelProgramData?.partner || {};
      const value = kind === "code" ? partner.code : partner.referral_link;
      if (!value) return notify("渠道推广信息尚未生成。");
      try {
        await navigator.clipboard.writeText(value);
        notify(kind === "code" ? "推广码已复制。" : "推广链接已复制。");
      } catch (_) {
        window.prompt("请手动复制", value);
      }
    }
    document.getElementById("channelApplicationForm")?.addEventListener("submit", submitChannelApplication);
    document.getElementById("channelLeadForm")?.addEventListener("submit", submitChannelLead);
    document.getElementById("channelSettlementForm")?.addEventListener("submit", submitChannelSettlement);
    const tenantStateManager = window.JIANGLAI_TENANT_STATE;
    if (!tenantStateManager) throw new Error("账号数据隔离模块没有加载");
    const initialBusinessTenant = tenantStateManager.initialContext(window.localStorage);
    let businessStoreKey = initialBusinessTenant.storageKey;
    let activeBusinessCompanyId = initialBusinessTenant.companyId;
    function currentPreRestoreStoreKey() { return `${businessStoreKey}_before_restore`; }
    const referencePriceSeed = window.JIANGLAI_QUOTE_REFERENCE_PRICES?.createSettingsSeed?.() || {};
    const defaults = {
      tasks: [],
      taskRecycleBin: [],
      quoteHistory: [],
      quoteBatchDraft: { id:"", name:"", customer:"", items:[], createdAt:"", updatedAt:"" },
      quoteRuleHistory: [],
      customerProfiles: [],
      customerSessions: [],
      activeCustomerSessionId: "",
      visualVersions: [],
      lastBackupAt: "",
      businessSync: {
        status: "idle",
        lastSuccessAt: "",
        lastAttemptAt: "",
        error: "",
        syncedDomains: []
      },
      member: {
        loggedIn: false,
        companyName: "",
        account: "",
        contact: "",
        plan: "未开通",
        planKey: "none",
        expiresAt: "",
        quotaUsed: 0,
        quotaTotal: 100,
        role: "管理员", painPointAnalysisRemaining: 0
      },
      demoMembers: [
        { companyName: "星河包装厂", plan: "标准版", quotaUsed: 820, quotaTotal: 3000, expiresAt: "2026-08-18", status: "正常" },
        { companyName: "华南彩印", plan: "专业版", quotaUsed: 3400, quotaTotal: 12000, expiresAt: "2026-09-02", status: "正常" },
        { companyName: "启明纸品", plan: "永久免费报价版", quotaUsed: 0, quotaTotal: 100, expiresAt: "", status: "永久有效" }
      ],
      settings: {
        configured: false,
        quoteRulesUpdatedAt: "",
        costRulesConfirmedAt: "",
        industryCalibration: null,
        costLibrary: {
          schemaVersion: 1,
          activeVersionId: "",
          effectiveAt: "",
          updatedAt: "",
          sourceFile: "",
          draftSourceFile: "",
          status: "empty",
          items: [],
          draftItems: [],
          versions: []
        },
        specMaterialCosts: {},
        industrySelected: false,
        industryCategory: "包装印刷",
        factoryType: "包装彩盒",
        productType: "包装彩盒",
        quoteProfiles: {},
        quoteCatalogPreferences: {
          schemaVersion: 1,
          selectionMode: "all",
          selectedProductIds: [],
          setupCompletedAt: "direct-quote-default",
          catalogVersion: "",
          order: [],
          hidden: [],
          customCategories: []
        },
        quoteSavedTemplates: [],
        quoteMaterialPrices: [],
        quoteOperationPrices: [],
        dieLayout: {
          boardWidthMm: 1050,
          boardHeightMm: 750,
          marginsMm: { top:17, right:17, bottom:17, left:17 },
          gripperMm: 12,
          gripperEdge: "top",
          gapMm: { x:5, y:5 },
          rotation: "forbid",
          align: "center",
          strategy: "balanced"
        },
        defaultMaterial: "材料待确认",
        capacityRules: "包装印刷=彩盒/纸袋/标签/画册，起订500-1000，交期7-12天，特殊工艺需打样\n纺织服装=棉布袋/无纺布袋/服装辅料，起订1000，交期10-15天，需确认克重和色牢度\n五金加工=钣金/冲压/金属标牌，按图报价，交期按工艺确认，需提供图纸和公差\n礼品日用=亚克力/木制品/礼品定制，起订按产品确认，需确认Logo位置和包装标准",
        materialRules: "350g白卡=0.42\n300g铜版纸=0.36\n牛皮纸=0.31\n特种纸=0.58",
        processRules: "覆膜=0.08\n哑膜=0.08\n烫银=0.16\n烫金=0.16\n模切=0.06\n糊盒=0.07\nUV=0.12",
        print: 180,
        printUnit: .12,
        sampleDefault: 0,
        freightDefault: 0,
        moq: 1000,
        urgentRate: .08,
        sampleCredit: "否",
        ladderDiscount: .03,
        freightRules: "",
        freightRulesUserConfirmed: false,
        delivery: "7",
        payment: "30%定金，尾款出货前付清",
        loss: .08,
        tax: .06,
        laborHourly: 0,
        machineHourly: 0,
        overheadRate: 0,
        utilizationRate: .90,
        profitTrial: .3,
        profitNormal: .24,
        profitLong: .2,
        profitVip: .16,
        paper: .42,
        film: .08,
        foil: .16,
        diecut: .06,
        glue: .07,
        profit: .22,
        ...referencePriceSeed
      }
    };
    const planConfig = {
      trial: { name: "永久免费报价版", days: 36500, quota: 100, price: "永久免费", rank: 1, seats: 1, perpetual: true },
      standard: { name: "AI跟单版", days: 31, quota: 2000, price: "推广价¥199/月", rank: 2, seats: 2 },
      standard_year: { name: "AI跟单版年付", days: 365, quota: 24000, price: "¥1,980/年", rank: 2, seats: 2, grantPlan: "standard", painPointAnalysisSessions: 1 },
      pro: { name: "团队协作版", days: 31, quota: 6000, price: "推广价¥499/月", rank: 3, seats: 10 },
      pro_year: { name: "团队协作版年付", days: 365, quota: 72000, price: "¥4,980/年", rank: 3, seats: 10, grantPlan: "pro", painPointAnalysisSessions: 1 },
      enterprise: { name: "企业版（原套餐）", days: 365, quota: 200000, price: "原合同", rank: 4, seats: 10 },
      buyout: { name: "工厂买断版", days: 36500, quota: 20000, price: "前50家¥16,800", rank: 5, seats: 20 },
      private_buyout: { name: "企业定制版", days: 36500, quota: 200000, price: "¥59,800起", rank: 6, seats: 0 }
    };
    const aiRechargeConfig = {
      ai_100: { name: "AI点数2,000点", price: "¥100", points: 2000 },
      ai_300: { name: "AI点数6,000点", price: "¥300", points: 6000 },
      ai_500: { name: "AI点数10,000点", price: "¥500", points: 10000 }
    };
    const featurePointCost = {
      "报价系统": 0,
      "AI真实生图": 30,
      "AI宝贝详情": 3,
      "图片转矢量": 30,
      "制前资料": 0,
      "报价导出": 0
    };
    const productTemplates = {
      "包装彩盒": { product: "包装彩盒", material: "350g白卡", process: "覆膜, LOGO工艺, 模切, 糊盒", delivery: "10" },
      "无纺布袋": { product: "无纺布手提袋", material: "80g无纺布", process: "丝印, 车缝, 包边, 手提", delivery: "12" },
      "棉布纺织": { product: "棉布制品", material: "纯棉帆布", process: "裁剪, 车缝, 印花, 水洗", delivery: "15" },
      "名片印刷": { product: "名片", material: "300g铜版纸", process: "双面印刷, 覆膜, 圆角", delivery: "4" },
      "手提袋": { product: "手提袋", material: "250g白卡", process: "覆膜, 穿绳, 模切, 糊袋", delivery: "12" },
      "标签贴纸": { product: "标签贴纸", material: "不干胶铜版纸", process: "覆膜, 模切, 排废", delivery: "7" },
      "画册/说明书": { product: "画册/说明书", material: "157g铜版纸", process: "骑马钉, 折页, 覆膜", delivery: "9" },
      "通用加工": { product: "定制产品", material: "材料待确认", process: "加工工艺待确认", delivery: "7" }
    };
    const quoteIndustryEngine = window.JIANGLAI_QUOTE_INDUSTRIES;
    if (!quoteIndustryEngine) throw new Error("行业报价模块没有加载");
    const industryCategories = quoteIndustryEngine.categories;
    const industryDescriptions = quoteIndustryEngine.descriptions;
    Object.assign(productTemplates, quoteIndustryEngine.productTemplateExtensions);
    const quoteProfileKeys = [
      "configured", "quoteRulesUpdatedAt", "costRulesConfirmedAt", "industryCalibration", "industryCategory", "factoryType", "productType", "defaultMaterial", "capacityRules", "materialRules", "processRules",
      "print", "printUnit", "sampleDefault", "freightDefault", "moq", "urgentRate", "sampleCredit",
      "ladderDiscount", "freightRules", "freightRulesUserConfirmed", "delivery", "payment", "loss", "tax", "profitTrial",
      "profitNormal", "profitLong", "profitVip", "laborHourly", "machineHourly", "overheadRate", "utilizationRate",
      "paper", "film", "foil", "diecut", "glue", "profit"
    ];
    const industryQuotePresets = quoteIndustryEngine.presets;
    let state = loadState();
    let taskFilter = "接单前";
    let taskFilterManuallySelected = false;
    let taskSearch = "";
    let taskRecycleItems = [];
    let formalTaskRecycleItems = [];
    let taskRecycleExpiryTimer = 0;
    let selectedTaskDetailId = "";
    let taskListReturnState = { filter:"接单前", search:"", mainScrollTop:0, windowScrollY:0 };
    let productionCenterSearch = "";
    let productionCenterFilter = "全部";
    let productionOutsourceOrders = [];
    let productionOutsourceLoading = false;
    let productionOutsourceLoadedAt = 0;
    let productionOutsourceError = "";
    let ruleMode = "simple";
    let priceSetupMethod = "";
    let ruleGuidePage = 1;
    let ruleGuideDirection = "next";
    let quoteGuideStep = 1;
    let quoteGuideHasResult = false;
    let quoteResultAttentionTimer = 0;
    let quoteGuideDirection = "next";
    let lastVisualPrompts = [];
    let quoteCatalogCategoryId = "";
    let quoteCatalogProductId = "";
    let quoteCatalogSearchText = "";
    let quoteCatalogCategoryDrag = null;
    let quoteCatalogProductEditorCategoryId = "";
    let quoteCatalogHiddenDraft = null;
    let quoteCatalogPreferencesSaveQueue = Promise.resolve();
    let quoteCatalogSetupDraft = null;
    let quoteCatalogSetupSearchText = "";
    let quoteCatalogSetupEditing = false;
    const quoteCatalogDrafts = Object.create(null);
    function quoteCatalogBaseData() {
      const catalog = window.QUOTE_PRODUCT_CATALOG;
      return catalog && Array.isArray(catalog.categories) && catalog.profiles ? catalog : { schemaVersion: 0, catalogVersion: "", categories: [], profiles: {} };
    }
    function normalizeQuoteCatalogPreferences(input = {}) {
      const safe = input && typeof input === "object" && !Array.isArray(input) ? input : {};
      const textList = (value, limit = 80, maxItems = 80) => Array.isArray(value)
        ? [...new Set(value.map(item => String(item || "").trim().slice(0, limit)).filter(Boolean))].slice(0, maxItems)
        : [];
      const customIdMap = new Map();
      const usedCustomIds = new Set();
      const customCategories = (Array.isArray(safe.customCategories) ? safe.customCategories : []).slice(0, 30).map((item, index) => {
        if (!item || typeof item !== "object") return null;
        const name = String(item.name || "").trim().slice(0, 30);
        const rawProductNames = Array.isArray(item.productNames) ? item.productNames : [item.productName || name];
        const productNames = [...new Set(rawProductNames.map(value => String(value || "").trim().replace(/\s+/g, " ").slice(0, 50)).filter(Boolean))].slice(0, 20);
        if (!productNames.length && name) productNames.push(name);
        const productName = productNames[0] || name;
        const industryCategory = Object.prototype.hasOwnProperty.call(industryCategories, String(item.industryCategory || ""))
          ? String(item.industryCategory)
          : "";
        const template = String(item.template || "").trim().slice(0, 50);
        const rawId = String(item.id || "").trim();
        let id = /^custom-[a-z0-9-]{4,73}$/i.test(rawId) ? rawId : `custom-saved-${index + 1}`;
        while (usedCustomIds.has(id)) id = `custom-saved-${index + 1}-${usedCustomIds.size + 1}`;
        usedCustomIds.add(id);
        if (rawId) customIdMap.set(rawId, id);
        return name ? { id, name, productName: productName || name, productNames, industryCategory, template } : null;
      }).filter(Boolean);
      const hasExplicitSchema = Object.prototype.hasOwnProperty.call(safe, "schemaVersion");
      const schemaVersion = Number(safe.schemaVersion) === 1 ? 1 : Number(safe.schemaVersion) === 2 ? 2 : hasExplicitSchema ? 2 : 1;
      const selectionMode = schemaVersion === 2 ? "selected" : "all";
      const remapCategoryId = value => customIdMap.get(value) || value;
      const remapProductId = value => {
        for (const [rawId, savedId] of customIdMap.entries()) {
          if (value === `${rawId}-product`) return `${savedId}-product`;
          if (value.startsWith(`${rawId}-product-`)) return `${savedId}${value.slice(rawId.length)}`;
        }
        return value;
      };
      const selectedProductIds = [...new Set(textList(safe.selectedProductIds, 120, 400).map(remapProductId))];
      return {
        schemaVersion,
        selectionMode,
        selectedProductIds,
        setupCompletedAt: selectionMode === "all" ? (String(safe.setupCompletedAt || "legacy").slice(0, 60) || "legacy") : String(safe.setupCompletedAt || "").slice(0, 60),
        catalogVersion: String(safe.catalogVersion || "").slice(0, 60),
        order: [...new Set(textList(safe.order).map(remapCategoryId))],
        hidden: [...new Set(textList(safe.hidden).map(remapCategoryId))],
        customCategories
      };
    }
    function quoteCatalogPreferences() {
      state.settings.quoteCatalogPreferences = normalizeQuoteCatalogPreferences(state.settings.quoteCatalogPreferences);
      return state.settings.quoteCatalogPreferences;
    }
    function quoteCatalogEffectiveHidden() {
      return new Set(quoteCatalogHiddenDraft ? [...quoteCatalogHiddenDraft] : quoteCatalogPreferences().hidden);
    }
    function quoteCatalogVisibilityChanges() {
      if (!quoteCatalogHiddenDraft) return [];
      const saved = new Set(quoteCatalogPreferences().hidden);
      return quoteCatalogAllCategories().filter(category => saved.has(category.id) !== quoteCatalogHiddenDraft.has(category.id));
    }
    function normalizeQuoteMaterialPrices(input = []) {
      const allowedUnits = new Set(["元/吨", "元/kg", "元/㎡", "元/米", "元/张", "元/个", "元/小时"]);
      const seen = new Set();
      return (Array.isArray(input) ? input : []).slice(0, 300).map(item => {
        if (!item || typeof item !== "object" || Array.isArray(item)) return null;
        const name = String(item.name || "").trim().replace(/\s+/g, " ").slice(0, 80);
        const price = Number(item.price);
        const unit = String(item.unit || "").trim();
        const key = name.toLowerCase().replace(/\s+/g, "");
        if (!name || !(price > 0) || !Number.isFinite(price) || price > 100_000_000 || !allowedUnits.has(unit) || seen.has(key)) return null;
        seen.add(key);
        return { name, price: Math.round(price * 1_000_000) / 1_000_000, unit, updatedAt: String(item.updatedAt || "").slice(0, 40) };
      }).filter(Boolean);
    }
    function quoteMaterialPrices(settings = state.settings) {
      settings.quoteMaterialPrices = normalizeQuoteMaterialPrices(settings.quoteMaterialPrices);
      return settings.quoteMaterialPrices;
    }
    function findQuoteMaterialPrice(name, settings = state.settings) {
      const query = String(name || "").trim().toLowerCase().replace(/\s+/g, "");
      return quoteMaterialPrices(settings).find(item => item.name.toLowerCase().replace(/\s+/g, "") === query) || null;
    }
    const QUOTE_PRINT_SHEET_TIER_DEFINITIONS = Object.freeze([
      Object.freeze({ id:"full_open", label:"全开", prefix:"fullOpen" }),
      Object.freeze({ id:"two_open", label:"对开", prefix:"twoOpen" }),
      Object.freeze({ id:"four_open", label:"四开", prefix:"fourOpen" }),
      Object.freeze({ id:"eight_open", label:"八开", prefix:"eightOpen" })
    ]);
    function normalizePrintSheetTierPricing(rawTier) {
      if (!rawTier || typeof rawTier !== "object" || Array.isArray(rawTier) || rawTier.enabled === false) return null;
      const tiers = {};
      QUOTE_PRINT_SHEET_TIER_DEFINITIONS.forEach(definition => {
        const raw = rawTier[definition.id] && typeof rawTier[definition.id] === "object"
          ? rawTier[definition.id] : {};
        const read = (field, fallbackField) => {
          const value = Number(raw[field] ?? rawTier[`${definition.prefix}${fallbackField}`] ?? 0);
          return Number.isFinite(value) && value >= 0 && value <= 10_000_000
            ? Math.round(value * 1_000_000) / 1_000_000 : null;
        };
        const unitPrice = read("unitPrice", "UnitPrice");
        const minimumCharge = read("minimumCharge", "MinimumCharge");
        const includedQuantity = read("includedQuantity", "IncludedQuantity");
        const excessUnitPrice = read("excessUnitPrice", "ExcessUnitPrice");
        if ([unitPrice, minimumCharge, includedQuantity, excessUnitPrice].some(value => value === null)) return;
        const effectiveExcessUnitPrice = excessUnitPrice > 0 ? excessUnitPrice : unitPrice;
        const hasLinearRate = unitPrice > 0;
        const hasCompletePackage = includedQuantity > 0 && minimumCharge > 0 && effectiveExcessUnitPrice > 0;
        if (!hasLinearRate && !hasCompletePackage) return;
        if (includedQuantity > 0 && !hasCompletePackage) return;
        tiers[definition.id] = {
          unitPrice,
          minimumCharge,
          includedQuantity,
          excessUnitPrice: effectiveExcessUnitPrice
        };
      });
      if (!Object.keys(tiers).length) return null;
      return { schemaVersion:1, enabled:true, selection:"auto_by_press_sheet", ...tiers };
    }
    function normalizeQuoteOperationPrices(input = []) {
      const allowedBases = new Set(["piece", "sheet", "area_m2", "local_m2", "meter", "kg", "hole", "fold", "page", "book", "run", "hour"]);
      const allowedChargeModes = new Set(["linear", "minimum", "package_plus_excess"]);
      const seen = new Set();
      return (Array.isArray(input) ? input : []).slice(0, 300).map(item => {
        if (!item || typeof item !== "object" || Array.isArray(item)) return null;
        const name = String(item.name || "").trim().replace(/\s+/g, " ").slice(0, 80);
        const kind = item.kind === "print" ? "print" : "process";
        const unitPrice = Number(item.unitPrice);
        // Older saved operation rows predate fixed/setup fees.  Treat a missing
        // field as zero instead of dropping an otherwise valid saved price.
        const setupFee = Number(item.setupFee ?? 0);
        const minimumCharge = Number(item.minimumCharge ?? 0);
        const rawBasis = item.basis === undefined || item.basis === null || item.basis === "" ? "piece" : String(item.basis);
        const rawChargeMode = item.chargeMode === undefined || item.chargeMode === null || item.chargeMode === "" ? "minimum" : String(item.chargeMode);
        if (!allowedBases.has(rawBasis) || !allowedChargeModes.has(rawChargeMode)) return null;
        const basis = rawBasis;
        const chargeMode = rawChargeMode;
        const includedQuantity = Number(item.includedQuantity ?? item.packageQuantity ?? 0);
        const excessUnitPrice = Number(item.excessUnitPrice ?? item.unitPrice ?? 0);
        const usagePerPiece = Number(item.usagePerPiece ?? (basis === "piece" ? 1 : 0));
        const rawTier = item.laminationTierPricing;
        const laminationPriceFields = [
          "fullOpenUnitPrice", "twoOpenUnitPrice", "fourOpenUnitPrice",
          "eightOpenUnitPrice", "sixteenOpenUnitPrice", "thirtyTwoOpenUnitPrice"
        ];
        const normalizedLaminationPrices = Object.fromEntries(laminationPriceFields.map(field => {
          const value = Number(rawTier?.[field] ?? 0);
          return Number.isFinite(value) && value > 0 && value <= 10_000_000
            ? [field, Math.round(value * 1_000_000) / 1_000_000] : null;
        }).filter(Boolean));
        const laminationTierPricing = rawTier && rawTier.enabled !== false && Object.keys(normalizedLaminationPrices).length
          ? { enabled: true, ...normalizedLaminationPrices }
          : null;
        const printSheetTierPricing = kind === "print" ? normalizePrintSheetTierPricing(item.printSheetTierPricing) : null;
        const key = `${kind}:${name.toLowerCase().replace(/\s+/g, "")}`;
        if (!name || !Number.isFinite(unitPrice) || unitPrice < 0 || unitPrice > 10_000_000
          || !Number.isFinite(setupFee) || setupFee < 0 || setupFee > 10_000_000 || seen.has(key)) return null;
        if (![minimumCharge, includedQuantity, excessUnitPrice, usagePerPiece].every(value => Number.isFinite(value) && value >= 0 && value <= 10_000_000)) return null;
        if (chargeMode === "package_plus_excess" && (!(minimumCharge > 0) || !(includedQuantity > 0) || !(excessUnitPrice > 0))) return null;
        if (["local_m2", "meter", "kg", "hole", "fold", "page", "hour"].includes(basis) && !(usagePerPiece > 0)) return null;
        seen.add(key);
        const normalized = {
          name,
          kind,
          basis,
          chargeMode,
          unitPrice: Math.round(unitPrice * 1_000_000) / 1_000_000,
          setupFee: Math.round(setupFee * 1_000_000) / 1_000_000,
          minimumCharge: Math.round(minimumCharge * 1_000_000) / 1_000_000,
          includedQuantity: Math.round(includedQuantity * 1_000_000) / 1_000_000,
          excessUnitPrice: Math.round(excessUnitPrice * 1_000_000) / 1_000_000,
          usagePerPiece: Math.round(usagePerPiece * 1_000_000) / 1_000_000,
          updatedAt: String(item.updatedAt || "").slice(0, 40)
        };
        const source = String(item.source || "").trim().replace(/\s+/g, " ").slice(0, 80), sourceUrl = String(item.sourceUrl || "").trim().slice(0, 500), referenceCheckedAt = String(item.referenceCheckedAt || "").trim().slice(0, 40);
        if (source) normalized.source = source; if (/^https?:\/\//.test(sourceUrl)) normalized.sourceUrl = sourceUrl; if (referenceCheckedAt) normalized.referenceCheckedAt = referenceCheckedAt;
        if (laminationTierPricing) normalized.laminationTierPricing = laminationTierPricing;
        if (printSheetTierPricing) normalized.printSheetTierPricing = printSheetTierPricing;
        return normalized;
      }).filter(Boolean);
    }
    const QUOTE_OPERATION_BASIS_OPTIONS = Object.freeze([
      ["piece", "按成品个数", "个"],
      ["sheet", "按生产张数", "张"],
      ["area_m2", "按展开面积", "㎡"],
      ["local_m2", "按局部加工面积", "㎡"],
      ["meter", "按长度", "米"],
      ["kg", "按重量", "kg"],
      ["hole", "按孔数", "孔"],
      ["fold", "按折数", "折"],
      ["page", "按页数", "页"],
      ["book", "按本数", "本"],
      ["run", "按开机/版次", "次"],
      ["hour", "按工时", "小时"]
    ]);
    const QUOTE_OPERATION_CHARGE_MODE_OPTIONS = Object.freeze([
      ["minimum", "最低收费＋按量"],
      ["package_plus_excess", "套餐含量＋超量"],
      ["linear", "固定费＋按量"]
    ]);
    function quoteOperationBasisMeta(basis) {
      const found = QUOTE_OPERATION_BASIS_OPTIONS.find(item => item[0] === String(basis || "piece"));
      return found || QUOTE_OPERATION_BASIS_OPTIONS[0];
    }
    function quoteOperationBasisUnit(basis) {
      return quoteOperationBasisMeta(basis)[2];
    }
    function quoteOperationChargeModeLabel(mode) {
      const normalized = String(mode || "minimum");
      if (normalized === "minimum") return "最低收费与按量取高";
      return QUOTE_OPERATION_CHARGE_MODE_OPTIONS.find(item => item[0] === normalized)?.[1] || "最低收费与按量取高";
    }
    function quotePriceOutlierWarning({ kind = "material", name = "该项目", price = 0, unit = "元/个", setupFee = 0, minimumCharge = 0, basis = "piece" } = {}) {
      const value = Number(price || 0);
      const fixed = Number(setupFee || 0);
      const materialLimits = { "元/吨":100000, "元/kg":2000, "元/㎡":5000, "元/米":5000, "元/张":500, "元/个":500, "元/小时":5000 };
      const limit = kind === "material" ? (materialLimits[unit] || 500) : kind === "print" ? 5 : 100;
      const messages = [];
      if (value > limit) messages.push(`${name}的${kind === "material" ? "采购价" : "单位费"} ¥${value.toLocaleString()}${kind === "material" ? unit.replace("元", "") : `/${quoteOperationBasisUnit(basis)}`}明显高于常见录入范围`);
      if (fixed > 50000) messages.push(`${name}的固定费 ¥${fixed.toLocaleString()}/单明显偏高`);
      if (Number(minimumCharge || 0) > 100000) messages.push(`${name}的最低收费/套餐费 ¥${Number(minimumCharge).toLocaleString()}明显偏高`);
      return messages.join("；");
    }
    function confirmQuotePriceOutlier(warning) {
      if (!warning) return true;
      return confirm(`价格异常提醒：\n\n${warning}。\n\n请确认没有把吨价、整单价或小数点填到“单位费”里。确定仍按这个数值保存吗？`);
    }
    function quoteOperationPrices(settings = state.settings) {
      settings.quoteOperationPrices = normalizeQuoteOperationPrices(settings.quoteOperationPrices);
      return settings.quoteOperationPrices;
    }
    function findQuoteOperationPrice(name, kind = "process", settings = state.settings) {
      const query = String(name || "").trim().toLowerCase().replace(/\s+/g, "");
      const normalizedKind = kind === "print" ? "print" : "process";
      return quoteOperationPrices(settings).find(item => item.kind === normalizedKind && item.name.toLowerCase().replace(/\s+/g, "") === query) || null;
    }
    function quoteLegacyRulesConfirmed(settings = state.settings) {
      const engine = typeof quoteIndustryEngine !== "undefined"
        ? quoteIndustryEngine
        : (typeof window !== "undefined" ? window.JIANGLAI_QUOTE_INDUSTRIES : null);
      if (!engine) return Boolean(settings?.configured && settings?.costRulesConfirmedAt);
      const strategy = engine.resolveStrategy(settings?.industryCategory, settings?.factoryType);
      return Boolean(
        settings?.configured
        && settings?.costRulesConfirmedAt
        && settings?.industryCalibration?.confirmedAt
        && settings.industryCalibration.strategyId === strategy.id
        && settings.industryCalibration.category === settings.industryCategory
      );
    }
    function quoteCatalogCustomProfileKey(category, productName) {
      const text = normalizeRuleText(`${category?.name || ""} ${productName || ""} ${category?.template || ""}`);
      if (/木盒|木制|木质|竹木/.test(text)) return "wood_box";
      if (/纸箱|瓦楞/.test(text)) return "corrugated";
      if (/彩盒|纸盒|礼盒|包装盒/.test(text)) return "paper_box";
      if (/纸袋|手提袋/.test(text)) return "paper_bag";
      if (/标签|不干胶/.test(text)) return "label";
      if (/名片|卡片|卡券|门票/.test(text)) return "card";
      if (/画册|书刊|书本|说明书/.test(text)) return "book";
      if (/布袋|服装|纺织|帆布/.test(text)) return "textile";
      if (/塑料袋|软包装|复合袋/.test(text)) return "plastic";
      if (/展架|广告|喷绘|写真/.test(text)) return "display";
      if (/电子|线束|pcba/i.test(text)) return "electronic";
      if (/五金|钣金|金属|结构件/.test(text)) return "industrial";
      return "custom_generic";
    }
    function quoteCatalogAllCategories() {
      const base = quoteCatalogBaseData();
      const preferences = quoteCatalogPreferences();
      const custom = preferences.customCategories.map(item => ({
        id: item.id,
        name: item.name,
        custom: true,
        products: (item.productNames?.length ? item.productNames : [item.productName || item.name]).map((productName, index) => ({
          id: index === 0 ? `${item.id}-product` : `${item.id}-product-${index + 1}`, name: productName, aliases: [], profile: quoteCatalogCustomProfileKey(item, productName), custom: true,
          industryCategory: item.industryCategory || "", template: item.template || ""
        }))
      }));
      const all = [...base.categories, ...custom];
      const byId = new Map(all.map(category => [category.id, category]));
      const orderedIds = [...new Set([...preferences.order, ...all.map(category => category.id)])].filter(id => byId.has(id));
      return orderedIds.map(id => byId.get(id));
    }
    function quoteCatalogFullData() {
      const base = quoteCatalogBaseData();
      return { ...base, categories: quoteCatalogAllCategories() };
    }
    function quoteCatalogVisibleData() {
      const full = quoteCatalogFullData();
      const hidden = quoteCatalogEffectiveHidden();
      return { ...full, categories: full.categories.filter(category => !hidden.has(category.id)) };
    }
    function quoteCatalogAllProducts() {
      return quoteCatalogFullData().categories.flatMap(category => category.products.map(product => ({ ...product, categoryId: category.id, categoryName: category.name })));
    }
    function quoteCatalogSelectedProductIds() {
      const preferences = quoteCatalogPreferences();
      if (preferences.selectionMode === "all") return new Set(quoteCatalogAllProducts().map(product => product.id));
      const available = new Set(quoteCatalogAllProducts().map(product => product.id));
      return new Set(preferences.selectedProductIds.filter(id => available.has(id)));
    }
    function quoteCatalogData() {
      const full = quoteCatalogVisibleData();
      const preferences = quoteCatalogPreferences();
      if (preferences.selectionMode === "all") return full;
      const enabled = quoteCatalogSelectedProductIds();
      return {
        ...full,
        categories: full.categories
          .map(category => ({ ...category, products: category.products.filter(product => enabled.has(product.id)) }))
          .filter(category => category.products.length)
      };
    }
    function quoteCatalogProducts() {
      return quoteCatalogData().categories.flatMap(category => category.products.map(product => ({ ...product, categoryId: category.id, categoryName: category.name })));
    }
    function quoteCatalogFullEntry(productId = quoteCatalogProductId) {
      return quoteCatalogAllProducts().find(product => product.id === productId) || null;
    }
    function quoteCatalogEntry(productId = quoteCatalogProductId) {
      return quoteCatalogProducts().find(product => product.id === productId) || null;
    }
    function quoteCatalogProfile(entry = quoteCatalogEntry()) {
      if (!entry) return null;
      const profiles = quoteCatalogData().profiles;
      if (entry.custom) return profiles[entry.profile] || profiles.custom_generic || null;
      return profiles[entry.profile] || null;
    }
    function quoteCatalogDraft(productId = quoteCatalogProductId) {
      if (!productId) return { selections: {}, custom: {} };
      if (!quoteCatalogDrafts[productId]) quoteCatalogDrafts[productId] = { selections: {}, custom: {} };
      return quoteCatalogDrafts[productId];
    }
    function quoteCatalogOptionLabel(group, optionId) {
      return group?.options?.find(item => item.id === optionId)?.label || "";
    }
    function quoteCatalogSelectedLabels(group, draft = quoteCatalogDraft()) {
      const selected = draft.selections[group.id];
      const ids = Array.isArray(selected) ? selected : selected ? [selected] : [];
      return ids.map(id => quoteCatalogOptionLabel(group, id)).filter(Boolean);
    }
    function quoteCatalogLegacyProfile(entry) {
      const name = entry?.name || "";
      const profile = entry?.profile || "";
      if (entry?.custom) return {
        category: entry.industryCategory || currentIndustryCategory(),
        template: entry.template || state.settings.factoryType || "通用加工"
      };
      if (profile === "label") return { category: "包装印刷", template: "标签贴纸" };
      if (profile === "book") return { category: "包装印刷", template: "画册/说明书" };
      if (profile === "office_folder") return { category: "包装印刷", template: "画册/说明书" };
      if (profile === "hang_tag") return { category: "包装印刷", template: "标签贴纸" };
      if (profile === "instruction") return { category: "包装印刷", template: "画册/说明书" };
      if (profile === "paper_bag") return { category: "包装印刷", template: "手提袋" };
      if (["paper_box", "corrugated"].includes(profile)) return { category: "包装印刷", template: "包装彩盒" };
      if (["paper_flat", "card", "office", "calendar", "display"].includes(profile)) return { category: "包装印刷", template: name.includes("名片") ? "名片印刷" : "画册/说明书" };
      if (profile === "textile") return { category: "纺织服装", template: name.includes("无纺布") ? "无纺布袋" : "棉布纺织" };
      if (profile === "plastic") return { category: "塑料制品", template: "塑料袋/OPP袋" };
      if (profile === "electronic") return { category: "电子配件", template: "电子配件/小家电" };
      if (profile === "industrial") {
        if (/木|竹/.test(name)) return { category: "礼品日用", template: "木制品/竹制品" };
        if (/亚克力/.test(name)) return { category: "礼品日用", template: "亚克力展示架" };
        return { category: "五金加工", template: /标牌|铭牌|徽章/.test(name) ? "金属标牌/徽章" : "五金钣金" };
      }
      return { category: "礼品日用", template: "礼品日用品" };
    }
    function quoteCatalogFindProduct(text) {
      const query = normalizeRuleText(text || "");
      if (!query || ["定制产品", "待确认"].includes(String(text || "").trim())) return null;
      const products = quoteCatalogProducts();
      return products.find(product => {
        const names = [product.name, ...(product.aliases || [])].map(normalizeRuleText).filter(Boolean);
        return names.some(name => name === query);
      }) || products.find(product => {
        const names = [product.name, ...(product.aliases || [])].map(normalizeRuleText).filter(Boolean);
        return names.some(name => name.length >= 2 && (query.includes(name) || name.includes(query)));
      }) || null;
    }
    function quoteCatalogProductIdentity(text) {
      const product = quoteCatalogFindProduct(text);
      return product ? `catalog:${product.id}` : `text:${normalizeRuleText(text || "")}`;
    }
    function quoteProductsEquivalent(left, right) {
      const leftIdentity = quoteCatalogProductIdentity(left);
      const rightIdentity = quoteCatalogProductIdentity(right);
      return Boolean(leftIdentity && leftIdentity === rightIdentity);
    }
    function quoteSizesEquivalent(left, right) {
      const leftSignature = quoteCatalogSizeSignature(left);
      const rightSignature = quoteCatalogSizeSignature(right);
      return leftSignature && rightSignature
        ? leftSignature === rightSignature
        : normalizedQuoteValue(left) === normalizedQuoteValue(right);
    }
    function quoteBusinessProfileForProduct(text) {
      const catalogEntry = quoteCatalogFindProduct(text);
      if (catalogEntry) return quoteCatalogLegacyProfile(catalogEntry);
      const template = inferTemplateName(text);
      return { category: categoryForTemplate(template), template };
    }
    function quoteCatalogSetupRequired() {
      const preferences = quoteCatalogPreferences();
      return preferences.selectionMode === "selected"
        && (!preferences.setupCompletedAt || quoteCatalogSelectedProductIds().size === 0);
    }
    function quoteCatalogPriceAccessMissing() {
      return window.location.protocol !== "file:"
        && Boolean(window.__currentUser)
        && Boolean(window.__hasPermission?.("quote.create"))
        && !window.__hasPermission?.("price.view");
    }
    function ensureQuoteCatalogSetupDraft() {
      if (quoteCatalogSetupDraft instanceof Set) return quoteCatalogSetupDraft;
      quoteCatalogSetupDraft = new Set(quoteCatalogSelectedProductIds());
      return quoteCatalogSetupDraft;
    }
    function quoteCatalogSetupFilteredProducts() {
      const query = normalizeRuleText(quoteCatalogSetupSearchText);
      return quoteCatalogAllProducts().filter(product => !query || normalizeRuleText(`${product.name} ${(product.aliases || []).join(" ")} ${product.categoryName}`).includes(query));
    }
    function renderQuoteCatalogSetupProducts() {
      const box = document.getElementById("quoteCatalogSetupProducts");
      const count = document.getElementById("quoteCatalogSetupSelectedCount");
      if (!box) return;
      const selected = ensureQuoteCatalogSetupDraft();
      const filteredIds = new Set(quoteCatalogSetupFilteredProducts().map(product => product.id));
      const categories = quoteCatalogFullData().categories.map(category => ({
        ...category,
        products: category.products.filter(product => filteredIds.has(product.id))
      })).filter(category => category.products.length);
      if (count) count.textContent = `已选 ${selected.size} 个产品`;
      box.innerHTML = categories.map(category => `
        <div class="quote-catalog-setup-category">
          <strong>${escapeHtml(category.name)} · ${category.products.length} 个</strong>
          <div class="quote-catalog-setup-product-grid">
            ${category.products.map(product => `<label class="quote-catalog-setup-product"><input type="checkbox" data-quote-setup-product="${escapeHtml(product.id)}" ${selected.has(product.id) ? "checked" : ""} onchange="toggleQuoteCatalogSetupProduct('${encodeURIComponent(product.id)}',this.checked)"><span>${escapeHtml(product.name)}</span></label>`).join("")}
          </div>
        </div>
      `).join("") || `<div class="quote-product-empty">没有找到匹配产品，请换一个关键词。</div>`;
    }
    function renderQuoteCatalogSetup(options) {
      options = options && typeof options === "object" ? options : {};
      const panel = document.getElementById("quoteCatalogSetupPanel");
      const guide = document.getElementById("quoteGuideLayout");
      if (!panel || !guide) return;
      const accessNotice = document.getElementById("quoteCatalogAccessNotice");
      const priceAccessMissing = quoteCatalogPriceAccessMissing();
      accessNotice?.classList.toggle("is-hidden", !priceAccessMissing);
      if (priceAccessMissing) {
        panel.classList.add("is-hidden");
        guide.classList.add("is-hidden");
        document.querySelector(".quote-history-details")?.classList.add("is-hidden");
        return;
      }
      const required = quoteCatalogSetupRequired();
      const visible = required || quoteCatalogSetupEditing;
      panel.classList.toggle("is-hidden", !visible);
      guide.classList.toggle("is-hidden", visible);
      document.querySelector(".quote-history-details")?.classList.toggle("is-hidden", visible);
      if (!visible) return;
      ensureQuoteCatalogSetupDraft();
      const title = document.getElementById("quoteCatalogSetupTitle");
      if (title) title.textContent = quoteCatalogSetupEditing ? "调整本厂可报价产品与成本" : "先选择本厂要报价的产品";
      const save = document.getElementById("quoteCatalogSetupSave");
      if (save) save.textContent = quoteCatalogSetupEditing ? "保存修改，返回报价" : "完成初始化，进入报价";
      document.getElementById("quoteCatalogSetupCancel")?.classList.toggle("is-hidden", !quoteCatalogSetupEditing || required);
      const search = document.getElementById("quoteCatalogSetupSearch");
      if (search && search.value !== quoteCatalogSetupSearchText) search.value = quoteCatalogSetupSearchText;
      const materials = document.getElementById("quoteCatalogSetupMaterials");
      const processes = document.getElementById("quoteCatalogSetupProcesses");
      const labor = document.getElementById("quoteCatalogSetupLabor");
      if (materials && !materials.dataset.setupReady) {
        materials.value = quoteCatalogSetupMaterialText();
        materials.dataset.setupReady = "1";
        renderQuoteCatalogSetupCostRows("material", materials.value);
      }
      if (processes && !processes.dataset.setupReady) {
        processes.value = quoteCatalogSetupProcessText();
        processes.dataset.setupReady = "1";
        renderQuoteCatalogSetupCostRows("process", processes.value);
      }
      if (labor && !labor.dataset.setupReady) {
        labor.value = Number(state.settings.laborHourly || 0) > 0 ? String(state.settings.laborHourly) : "";
        labor.dataset.setupReady = "1";
      }
      if (options.resetConfirmation !== false) {
        const confirmation = document.getElementById("quoteCatalogSetupConfirmed");
        if (confirmation) confirmation.checked = false;
      }
      renderQuoteCatalogSetupProducts();
    }
    function searchQuoteCatalogSetupProducts(value) {
      quoteCatalogSetupSearchText = String(value || "").slice(0, 80);
      renderQuoteCatalogSetupProducts();
    }
    function toggleQuoteCatalogSetupProduct(productCode, checked) {
      const productId = decodeURIComponent(productCode || "");
      if (!quoteCatalogAllProducts().some(product => product.id === productId)) return;
      const selected = ensureQuoteCatalogSetupDraft();
      if (checked) selected.add(productId); else selected.delete(productId);
      renderQuoteCatalogSetupProducts();
    }
    function selectAllFilteredQuoteCatalogProducts(checked) {
      const selected = ensureQuoteCatalogSetupDraft();
      quoteCatalogSetupFilteredProducts().forEach(product => checked ? selected.add(product.id) : selected.delete(product.id));
      renderQuoteCatalogSetupProducts();
    }
    function resetQuoteCatalogSetupInputs() {
      ["quoteCatalogSetupMaterials", "quoteCatalogSetupProcesses", "quoteCatalogSetupLabor"].forEach(id => {
        const input = document.getElementById(id);
        if (input) delete input.dataset.setupReady;
      });
      ["quoteCatalogSetupMaterialRows", "quoteCatalogSetupProcessRows"].forEach(id => {
        const rows = document.getElementById(id);
        if (rows) rows.innerHTML = "";
      });
    }
    function quoteCatalogSetupMarkCostsChanged() {
      const confirmation = document.getElementById("quoteCatalogSetupConfirmed");
      if (confirmation) confirmation.checked = false;
    }
    function quoteCatalogSetupRowsText(kind, rows) {
      return (Array.isArray(rows) ? rows : []).map(item => {
        const name = String(item?.name || "").trim();
        const chargeMode = String(item?.chargeMode || "minimum");
        // 套餐模式不显示普通按量单价；保存时用超量单价作为主单价，
        // 避免用户填完套餐仍因隐藏的空单价而无法保存。
        const rawPrice = String(item?.price ?? "").trim();
        const price = chargeMode === "package_plus_excess" && !rawPrice
          ? String(item?.excessUnitPrice ?? "").trim()
          : rawPrice;
        const setupFee = String(item?.setupFee ?? "").trim();
        const unit = String(item?.unit || "").trim();
        if (!name && !price && !setupFee) return "";
        return kind === "material"
          ? `${name}=${price}${unit ? ` ${unit}` : ""}`
          : `${name}=${price}|basis=${item?.basis || "piece"}|mode=${chargeMode}|setup=${setupFee || "0"}|minimum=${item?.minimumCharge || "0"}|included=${item?.includedQuantity || "0"}|excess=${item?.excessUnitPrice ?? price ?? "0"}|usage=${item?.usagePerPiece ?? ((item?.basis || "piece") === "piece" ? "1" : "0")}`;
      }).filter(Boolean).join("\n");
    }
    function quoteOperationBasisOptionsHtml(current = "piece") {
      return QUOTE_OPERATION_BASIS_OPTIONS.map(([value, label, unit]) => `<option value="${value}"${value === current ? " selected" : ""}>${label}（元/${unit}）</option>`).join("");
    }
    function quoteOperationChargeModeOptionsHtml(current = "minimum") {
      return QUOTE_OPERATION_CHARGE_MODE_OPTIONS.map(([value, label]) => `<option value="${value}"${value === current ? " selected" : ""}>${value === "minimum" ? "最低收费与按量取高" : label}</option>`).join("");
    }
    function quoteCatalogSetupChargeModeHelp(mode = "minimum", basis = "piece") {
      const unit = quoteOperationBasisUnit(basis);
      if (mode === "linear") {
        return `<strong>固定费＋按量：</strong>每单先收一次开机/制版费，再按实际用量计算。例：固定费 100 元、0.08 元/${escapeHtml(unit)}，做 1000 ${escapeHtml(unit)}就是 100＋0.08×1000＝180 元。`;
      }
      if (mode === "package_plus_excess") {
        return `<strong>套餐＋超量：</strong>先收一笔套餐费，套餐内不再按量收费，超过包含量才按超量单价计算。例：200 元含 1000 ${escapeHtml(unit)}，超量 0.08 元/${escapeHtml(unit)}，做 1200 ${escapeHtml(unit)}就是 200＋200×0.08＝216 元。`;
      }
      return `<strong>最低收费：</strong>先按单价×实际用量计算，不足最低收费时按最低收费收取。例：最低 50 元、0.08 元/${escapeHtml(unit)}，100 ${escapeHtml(unit)}仍收 50 元，1000 ${escapeHtml(unit)}收 80 元。`;
    }
    function updateQuoteCatalogSetupCostRowMode(row) {
      if (!row?.classList?.contains("is-process")) return;
      const mode = row.querySelector('[data-quote-setup-cost-field="chargeMode"]')?.value || "minimum";
      const basis = row.querySelector('[data-quote-setup-cost-field="basis"]')?.value || "piece";
      const modeFields = {
        minimum: new Set(["unitPrice", "setupFee", "minimumCharge"]),
        linear: new Set(["unitPrice", "setupFee"]),
        package_plus_excess: new Set(["setupFee", "minimumCharge", "includedQuantity", "excessUnitPrice"])
      };
      const visible = modeFields[mode] || modeFields.minimum;
      row.querySelectorAll("[data-quote-setup-mode-field]").forEach(label => {
        label.hidden = !visible.has(label.dataset.quoteSetupModeField || "");
      });
      const usage = row.querySelector('[data-quote-setup-mode-field="usagePerPiece"]');
      if (usage) {
        const usageLabels = {
          local_m2:"每个成品的局部加工面积（㎡）",
          meter:"每个成品的长度用量（米）",
          kg:"每个成品的重量用量（kg）",
          hole:"每个成品的孔数",
          fold:"每个成品的折数",
          page:"每个成品的页数",
          hour:"每个成品的工时（小时）"
        };
        usage.hidden = !Object.prototype.hasOwnProperty.call(usageLabels, basis);
        const label = usage.querySelector("span");
        if (label) label.textContent = usageLabels[basis] || "每个成品的实际用量";
      }
      const minimumLabel = row.querySelector('[data-quote-setup-label="minimumCharge"]');
      if (minimumLabel) minimumLabel.textContent = mode === "package_plus_excess" ? "套餐费（套餐内总价）" : "最低收费（小单保底）";
      const help = row.querySelector("[data-quote-setup-mode-help]");
      if (help) help.innerHTML = quoteCatalogSetupChargeModeHelp(mode, basis);
      if (mode === "package_plus_excess") {
        const unitPrice = row.querySelector('[data-quote-setup-cost-field="price"]');
        const excessPrice = row.querySelector('[data-quote-setup-cost-field="excessUnitPrice"]');
        if (excessPrice && !String(excessPrice.value || "").trim() && unitPrice && String(unitPrice.value || "").trim()) {
          excessPrice.value = unitPrice.value;
        }
      }
    }
    function updateQuoteCatalogSetupCostRowModeFromField(field, kind = "process") {
      const row = field?.closest?.("[data-quote-setup-cost-row]");
      updateQuoteCatalogSetupCostRowMode(row);
      syncQuoteCatalogSetupCostRows(kind);
    }
    function quoteCatalogSetupCostConfig(kind) {
      return kind === "process"
        ? { mountId:"quoteCatalogSetupProcessRows", hiddenId:"quoteCatalogSetupProcesses", label:"工艺价格", operation:true }
        : { mountId:"quoteCatalogSetupMaterialRows", hiddenId:"quoteCatalogSetupMaterials", label:"材料价格", material:true };
    }
    function quoteCatalogSetupCostRowHtml(kind, item = {}) {
      const process = kind === "process";
      const name = escapeHtml(String(item.name || ""));
      const price = item.price === null || item.price === undefined ? "" : escapeHtml(quoteCatalogSetupNumber(item.price));
      const setupFee = item.setupFee === null || item.setupFee === undefined ? "0" : escapeHtml(quoteCatalogSetupNumber(item.setupFee));
      const minimumCharge = item.minimumCharge === null || item.minimumCharge === undefined ? "0" : escapeHtml(quoteCatalogSetupNumber(item.minimumCharge));
      const includedQuantity = item.includedQuantity === null || item.includedQuantity === undefined ? "0" : escapeHtml(quoteCatalogSetupNumber(item.includedQuantity));
      const excessUnitPrice = item.excessUnitPrice === null || item.excessUnitPrice === undefined ? price : escapeHtml(quoteCatalogSetupNumber(item.excessUnitPrice));
      const basis = QUOTE_OPERATION_BASIS_OPTIONS.some(option => option[0] === item.basis) ? item.basis : "piece";
      const chargeMode = QUOTE_OPERATION_CHARGE_MODE_OPTIONS.some(option => option[0] === item.chargeMode) ? item.chargeMode : "minimum";
      const usagePerPiece = item.usagePerPiece === null || item.usagePerPiece === undefined ? (basis === "piece" ? "1" : "0") : escapeHtml(quoteCatalogSetupNumber(item.usagePerPiece));
      const unit = item.unit ? normalizedQuoteMaterialPurchaseUnit(item.unit) : "";
      const units = ["元/吨", "元/kg", "元/㎡", "元/米", "元/张", "元/个", "元/小时"];
      return `<div class="quote-catalog-setup-cost-row${process ? " is-process" : ""}" data-quote-setup-cost-row="${kind}">
        <label><span>${process ? "工艺名称" : "材料名称"}</span><input data-quote-setup-cost-field="name" value="${name}" placeholder="例如：${process ? "覆膜" : "350g白卡"}" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
        ${process ? `<label><span>按什么数量计价</span><select data-quote-setup-cost-field="basis" onchange="updateQuoteCatalogSetupCostRowModeFromField(this,'${kind}')">${quoteOperationBasisOptionsHtml(basis)}</select></label>
        <label><span>本厂怎样收费</span><select data-quote-setup-cost-field="chargeMode" onchange="updateQuoteCatalogSetupCostRowModeFromField(this,'${kind}')">${quoteOperationChargeModeOptionsHtml(chargeMode)}</select></label>` : ""}
        <label${process ? ' data-quote-setup-mode-field="unitPrice"' : ""}><span>${process ? "按量单价" : "采购单价"}</span><input data-quote-setup-cost-field="price" inputmode="decimal" value="${price}" placeholder="例如：${process ? "0.08" : "6800"}" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
        ${process
          ? `<label data-quote-setup-mode-field="setupFee"><span>固定开机/制版费（每单一次）</span><input data-quote-setup-cost-field="setupFee" inputmode="decimal" value="${setupFee}" placeholder="不收固定费可填 0" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
          <label data-quote-setup-mode-field="minimumCharge"><span data-quote-setup-label="minimumCharge">最低收费（小单保底）</span><input data-quote-setup-cost-field="minimumCharge" inputmode="decimal" value="${minimumCharge}" placeholder="例如：50" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
          <label data-quote-setup-mode-field="includedQuantity"><span>套餐包含多少数量</span><input data-quote-setup-cost-field="includedQuantity" inputmode="decimal" value="${includedQuantity}" placeholder="例如：1000" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
          <label data-quote-setup-mode-field="excessUnitPrice"><span>超过套餐后的单价</span><input data-quote-setup-cost-field="excessUnitPrice" inputmode="decimal" value="${excessUnitPrice}" placeholder="例如：0.08" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
          <label data-quote-setup-mode-field="usagePerPiece"><span>每个成品的实际用量</span><input data-quote-setup-cost-field="usagePerPiece" inputmode="decimal" value="${usagePerPiece}" placeholder="例如：0.25" oninput="syncQuoteCatalogSetupCostRows('${kind}')"></label>
          <p class="quote-catalog-setup-cost-mode-help" data-quote-setup-mode-help>${quoteCatalogSetupChargeModeHelp(chargeMode, basis)}</p>`
          : `<label><span>采购计价单位</span><select data-quote-setup-cost-field="unit" onchange="syncQuoteCatalogSetupCostRows('${kind}')"><option value=""${unit ? "" : " selected"}>请选择采购单位</option>${units.map(option => `<option value="${option}"${option === unit ? " selected" : ""}>${option}</option>`).join("")}</select><small>纸张通常按元/吨，薄膜可按元/kg或元/㎡；只有采购时本身按成品计价才选元/个。</small></label>`}
        <button class="quote-catalog-setup-cost-delete" type="button" onclick="removeQuoteCatalogSetupCostRow('${kind}',this)">删除</button>
      </div>`;
    }
    function renderQuoteCatalogSetupCostRows(kind, text = "") {
      const config = quoteCatalogSetupCostConfig(kind);
      const mount = document.getElementById(config.mountId);
      const hidden = document.getElementById(config.hiddenId);
      if (!mount || !hidden) return;
      const parsed = parseStrictSetupRules(String(text || ""), config.label, { material:Boolean(config.material), operation:Boolean(config.operation) });
      const items = parsed.values.length ? parsed.values : [{}];
      mount.innerHTML = items.map(item => quoteCatalogSetupCostRowHtml(kind, item)).join("");
      mount.querySelectorAll("[data-quote-setup-cost-row]").forEach(updateQuoteCatalogSetupCostRowMode);
      hidden.value = quoteCatalogSetupRowsText(kind, items.map(item => ({
        name:item.name || "", price:item.price ?? "", unit:item.unit || "", setupFee:item.setupFee ?? 0,
        basis:item.basis || "piece", chargeMode:item.chargeMode || "minimum", minimumCharge:item.minimumCharge ?? 0,
        includedQuantity:item.includedQuantity ?? 0, excessUnitPrice:item.excessUnitPrice ?? item.price ?? 0,
        usagePerPiece:item.usagePerPiece ?? ((item.basis || "piece") === "piece" ? 1 : 0)
      })));
    }
    function quoteCatalogSetupCostRowValues(kind) {
      const config = quoteCatalogSetupCostConfig(kind);
      return [...(document.getElementById(config.mountId)?.querySelectorAll("[data-quote-setup-cost-row]") || [])].map(row => ({
        name:row.querySelector('[data-quote-setup-cost-field="name"]')?.value || "",
        price:row.querySelector('[data-quote-setup-cost-field="price"]')?.value || "",
        unit:row.querySelector('[data-quote-setup-cost-field="unit"]')?.value || "",
        setupFee:row.querySelector('[data-quote-setup-cost-field="setupFee"]')?.value || "",
        basis:row.querySelector('[data-quote-setup-cost-field="basis"]')?.value || "piece",
        chargeMode:row.querySelector('[data-quote-setup-cost-field="chargeMode"]')?.value || "minimum",
        minimumCharge:row.querySelector('[data-quote-setup-cost-field="minimumCharge"]')?.value || "0",
        includedQuantity:row.querySelector('[data-quote-setup-cost-field="includedQuantity"]')?.value || "0",
        excessUnitPrice:row.querySelector('[data-quote-setup-cost-field="excessUnitPrice"]')?.value || "",
        usagePerPiece:row.querySelector('[data-quote-setup-cost-field="usagePerPiece"]')?.value || "0"
      }));
    }
    function syncQuoteCatalogSetupCostRows(kind, markChanged = true) {
      const config = quoteCatalogSetupCostConfig(kind);
      const hidden = document.getElementById(config.hiddenId);
      if (!hidden) return "";
      hidden.value = quoteCatalogSetupRowsText(kind, quoteCatalogSetupCostRowValues(kind));
      if (markChanged) quoteCatalogSetupMarkCostsChanged();
      return hidden.value;
    }
    function addQuoteCatalogSetupCostRow(kind) {
      const config = quoteCatalogSetupCostConfig(kind);
      const mount = document.getElementById(config.mountId);
      if (!mount) return;
      mount.insertAdjacentHTML("beforeend", quoteCatalogSetupCostRowHtml(kind));
      updateQuoteCatalogSetupCostRowMode(mount.lastElementChild);
      syncQuoteCatalogSetupCostRows(kind);
      mount.lastElementChild?.querySelector('[data-quote-setup-cost-field="name"]')?.focus();
    }
    function removeQuoteCatalogSetupCostRow(kind, button) {
      const config = quoteCatalogSetupCostConfig(kind);
      const mount = document.getElementById(config.mountId);
      button?.closest("[data-quote-setup-cost-row]")?.remove();
      if (mount && !mount.children.length) mount.innerHTML = quoteCatalogSetupCostRowHtml(kind);
      syncQuoteCatalogSetupCostRows(kind);
    }
    function quoteCatalogSetupFocusFirstCostRow(kind) {
      const config = quoteCatalogSetupCostConfig(kind);
      const mount = document.getElementById(config.mountId);
      const row = [...(mount?.querySelectorAll("[data-quote-setup-cost-row]") || [])].find(item => {
        const name = item.querySelector('[data-quote-setup-cost-field="name"]')?.value.trim();
        const price = Number(String(item.querySelector('[data-quote-setup-cost-field="price"]')?.value || "").replace(/[,￥¥\s]/g, ""));
        if (kind !== "process") {
          const unit = item.querySelector('[data-quote-setup-cost-field="unit"]')?.value || "";
          return !name || !(price > 0) || !Number.isFinite(price) || !unit;
        }
        const setupFee = Number(item.querySelector('[data-quote-setup-cost-field="setupFee"]')?.value || 0);
        const minimumCharge = Number(item.querySelector('[data-quote-setup-cost-field="minimumCharge"]')?.value || 0);
        return !name || ![price, setupFee, minimumCharge].some(value => Number.isFinite(value) && value > 0);
      }) || mount?.querySelector("[data-quote-setup-cost-row]");
      const missingUnit = kind !== "process" && !row?.querySelector('[data-quote-setup-cost-field="unit"]')?.value;
      (missingUnit ? row?.querySelector('[data-quote-setup-cost-field="unit"]') : row?.querySelector("input"))?.focus();
    }
    function quoteCatalogSetupNumber(value) {
      const number = Number(value);
      return Number.isFinite(number) ? String(Math.round(number * 1_000_000) / 1_000_000) : "0";
    }
    function quoteCatalogSetupMaterialText(settings = state.settings) {
      const refined = normalizeQuoteMaterialPrices(settings?.quoteMaterialPrices);
      const seen = new Set(refined.map(item => normalizeRuleText(item.name)));
      const legacy = quoteLegacyRulesConfirmed(settings)
        ? parseRules(settings?.materialRules || "").filter(item => !seen.has(normalizeRuleText(item.name)))
        : [];
      return [
        ...refined.map(item => `${item.name}=${quoteCatalogSetupNumber(item.price)} ${item.unit}`),
        ...legacy.map(item => `${item.name}=${quoteCatalogSetupNumber(item.price)} 元/个`)
      ].join("\n");
    }
    function quoteCatalogSetupProcessText(settings = state.settings) {
      const refined = normalizeQuoteOperationPrices(settings?.quoteOperationPrices);
      const seen = new Set(refined.map(item => normalizeRuleText(item.name)));
      const legacy = quoteLegacyRulesConfirmed(settings)
        ? parseRules(settings?.processRules || "").filter(item => !seen.has(normalizeRuleText(item.name)))
        : [];
      return [
        ...refined.map(item => quoteCatalogSetupRowsText("process", [{
          name:item.name, price:item.unitPrice, setupFee:item.setupFee, basis:item.basis, chargeMode:item.chargeMode,
          minimumCharge:item.minimumCharge, includedQuantity:item.includedQuantity,
          excessUnitPrice:item.excessUnitPrice, usagePerPiece:item.usagePerPiece
        }])),
        ...legacy.map(item => `${item.name}=${quoteCatalogSetupNumber(item.price)} + 固定费=0 元/单`)
      ].join("\n");
    }
    function mergeQuoteCatalogSetupMaterialPrices(input, materials, savedAt) {
      const prices = normalizeQuoteMaterialPrices(input);
      materials.forEach(item => {
        const key = normalizeRuleText(item.name);
        const next = { name:item.name, price:item.price, unit:item.unit, updatedAt:savedAt };
        const index = prices.findIndex(current => normalizeRuleText(current.name) === key);
        if (index >= 0) prices[index] = next; else prices.push(next);
      });
      return normalizeQuoteMaterialPrices(prices);
    }
    function mergeQuoteCatalogSetupOperationPrices(input, processes, savedAt) {
      const prices = normalizeQuoteOperationPrices(input);
      processes.forEach(item => {
        const name = canonicalQuoteOperationLabel(item.name);
        const key = normalizeRuleText(name);
        const index = prices.findIndex(current => normalizeRuleText(current.name) === key);
        const previous = index >= 0 ? prices[index] : null;
        const legacyOperation = item.legacyOperation === true;
        const next = {
          name,
          kind: previous?.kind === "print" ? "print" : "process",
          unitPrice: item.price,
          setupFee: item.setupFee === null ? Number(previous?.setupFee || 0) : Number(item.setupFee || 0),
          basis: legacyOperation ? (previous?.basis || "piece") : (item.basis || previous?.basis || "piece"),
          chargeMode: legacyOperation ? (previous?.chargeMode || "minimum") : (item.chargeMode || previous?.chargeMode || "minimum"),
          minimumCharge: Number(legacyOperation ? (previous?.minimumCharge ?? 0) : (item.minimumCharge ?? previous?.minimumCharge ?? 0)),
          includedQuantity: Number(legacyOperation ? (previous?.includedQuantity ?? 0) : (item.includedQuantity ?? previous?.includedQuantity ?? 0)),
          excessUnitPrice: Number(legacyOperation
            ? (previous?.excessUnitPrice ?? item.price ?? 0)
            : (item.excessUnitPrice ?? item.price ?? previous?.excessUnitPrice ?? 0)),
          usagePerPiece: Number(legacyOperation
            ? (previous?.usagePerPiece ?? ((previous?.basis || "piece") === "piece" ? 1 : 0))
            : (item.usagePerPiece ?? previous?.usagePerPiece ?? ((item.basis || previous?.basis || "piece") === "piece" ? 1 : 0))),
          laminationTierPricing: previous?.laminationTierPricing || null,
          printSheetTierPricing: previous?.printSheetTierPricing || null,
          updatedAt: savedAt
        };
        if (index >= 0) prices[index] = next; else prices.push(next);
      });
      return normalizeQuoteOperationPrices(prices);
    }
    function openQuoteCatalogProductManager() {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      quoteCatalogSetupEditing = true;
      quoteCatalogSetupDraft = new Set(quoteCatalogSelectedProductIds());
      quoteCatalogSetupSearchText = "";
      resetQuoteCatalogSetupInputs();
      renderQuoteCatalogSetup();
      document.getElementById("quoteCatalogSetupPanel")?.scrollIntoView({ behavior:"smooth", block:"start" });
    }
    function cancelQuoteCatalogSetupEdit() {
      quoteCatalogSetupEditing = false;
      quoteCatalogSetupDraft = null;
      quoteCatalogSetupSearchText = "";
      resetQuoteCatalogSetupInputs();
      renderQuoteCatalogSetup();
    }
    async function completeQuoteCatalogSetup() {
      if (!requireQuoteCatalogPreferenceEdit()) return false;
      const selected = [...ensureQuoteCatalogSetupDraft()].filter(id => quoteCatalogAllProducts().some(product => product.id === id));
      syncQuoteCatalogSetupCostRows("material", false);
      syncQuoteCatalogSetupCostRows("process", false);
      const materialsText = String(document.getElementById("quoteCatalogSetupMaterials")?.value || "").trim();
      const processesText = String(document.getElementById("quoteCatalogSetupProcesses")?.value || "").trim();
      const laborHourly = Number(String(document.getElementById("quoteCatalogSetupLabor")?.value || "").replace(/[,￥¥\s]/g, ""));
      const materialResult = parseStrictSetupRules(materialsText, "材料价格", { material: true });
      const processResult = parseStrictSetupRules(processesText, "工艺价格", { operation: true });
      const materials = materialResult.values;
      const processes = processResult.values;
      if (!selected.length) {
        document.getElementById("quoteCatalogSetupSearch")?.focus();
        notify("请至少勾选 1 个本厂需要报价的产品。");
        return false;
      }
      if (!materials.length || materialResult.errors.length) {
        quoteCatalogSetupFocusFirstCostRow("material");
        notify(materialResult.errors.length ? "请检查材料名称、采购单价和计价单位是否填写正确。" : "请至少添加 1 种大于 0 的真实材料采购价。");
        return false;
      }
      if (!processes.length || processResult.errors.length) {
        quoteCatalogSetupFocusFirstCostRow("process");
        notify(processResult.errors.length ? "请检查工艺名称、计价单位、收费方式和价格；不收固定费时填 0。" : "请至少添加 1 种大于 0 的真实工艺价格。");
        return false;
      }
      if (!(laborHourly > 0) || !Number.isFinite(laborHourly)) {
        document.getElementById("quoteCatalogSetupLabor")?.focus();
        notify("请填写大于 0 的本厂完全人工成本（元/小时）。");
        return false;
      }
      if (!document.getElementById("quoteCatalogSetupConfirmed")?.checked) {
        document.getElementById("quoteCatalogSetupConfirmed")?.focus();
        notify("请先勾选确认这些产品和价格来自本厂真实数据。");
        return false;
      }
      const priceWarnings = [
        ...materials.map(item => quotePriceOutlierWarning({
          kind:"material", name:item.name, price:item.price, unit:item.unit
        })),
        ...processes.map(item => quotePriceOutlierWarning({
          kind:"process", name:item.name, price:item.price, setupFee:item.setupFee,
          minimumCharge:item.minimumCharge, basis:item.basis
        }))
      ].filter(Boolean).join("；");
      if (!confirmQuotePriceOutlier(priceWarnings)) return false;
      const saveButton = document.getElementById("quoteCatalogSetupSave");
      if (saveButton) { saveButton.disabled = true; saveButton.textContent = "正在保存…"; }
      const savedAt = new Date().toISOString();
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      const selectedCategoryIds = new Set(selected.map(productId => quoteCatalogFullEntry(productId)?.categoryId).filter(Boolean));
      const currentCatalogPreferences = normalizeQuoteCatalogPreferences(proposedSettings.quoteCatalogPreferences);
      proposedSettings.quoteCatalogPreferences = normalizeQuoteCatalogPreferences({
        ...currentCatalogPreferences,
        schemaVersion: 2,
        selectionMode: "selected",
        selectedProductIds: selected,
        hidden: currentCatalogPreferences.hidden.filter(categoryId => !selectedCategoryIds.has(categoryId)),
        setupCompletedAt: savedAt,
        catalogVersion: quoteCatalogBaseData().catalogVersion || ""
      });
      proposedSettings.materialRules = materials.map(item => `${item.name}=${quoteCatalogSetupNumber(item.price)}`).join("\n");
      proposedSettings.processRules = processes.map(item => `${canonicalQuoteOperationLabel(item.name)}=${quoteCatalogSetupNumber(item.price)}`).join("\n");
      const materialKeys = new Set(materials.map(item => normalizeRuleText(item.name)));
      const processKeys = new Set(processes.map(item => normalizeRuleText(canonicalQuoteOperationLabel(item.name))));
      proposedSettings.quoteMaterialPrices = normalizeQuoteMaterialPrices(proposedSettings.quoteMaterialPrices)
        .filter(item => materialKeys.has(normalizeRuleText(item.name)));
      proposedSettings.quoteOperationPrices = normalizeQuoteOperationPrices(proposedSettings.quoteOperationPrices)
        .filter(item => processKeys.has(normalizeRuleText(item.name)));
      proposedSettings.quoteMaterialPrices = mergeQuoteCatalogSetupMaterialPrices(proposedSettings.quoteMaterialPrices, materials, savedAt);
      proposedSettings.quoteOperationPrices = mergeQuoteCatalogSetupOperationPrices(proposedSettings.quoteOperationPrices, processes, savedAt);
      proposedSettings.laborHourly = laborHourly;
      proposedSettings.configured = true;
      proposedSettings.costRulesConfirmedAt = savedAt;
      proposedSettings.quoteRulesUpdatedAt = savedAt;
      const selectedProfiles = new Map();
      selected.map(productId => quoteCatalogFullEntry(productId)).filter(Boolean).forEach(entry => {
        const legacy = quoteCatalogLegacyProfile(entry);
        if (!selectedProfiles.has(legacy.category)) selectedProfiles.set(legacy.category, legacy.template);
      });
      const primaryProfile = selectedProfiles.entries().next().value;
      proposedSettings.industrySelected = true;
      proposedSettings.industryCategory = primaryProfile?.[0] || proposedSettings.industryCategory || categoryForTemplate(proposedSettings.factoryType);
      proposedSettings.factoryType = primaryProfile?.[1] || proposedSettings.factoryType || "通用加工";
      proposedSettings.productType = proposedSettings.factoryType;
      proposedSettings.quoteProfiles = proposedSettings.quoteProfiles && typeof proposedSettings.quoteProfiles === "object"
        ? proposedSettings.quoteProfiles
        : {};
      selectedProfiles.forEach((template, category) => {
        proposedSettings.quoteProfiles[category] = quoteProfileSnapshot({
          ...proposedSettings,
          configured: true,
          industryCategory: category,
          factoryType: template,
          productType: template
        });
      });
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError:true });
          if (!saved?.ok) throw new Error("云端没有确认这次初始化");
        }
        state.settings = proposedSettings;
        saveState();
        quoteCatalogSetupEditing = false;
        quoteCatalogSetupDraft = null;
        quoteCatalogSetupSearchText = "";
        quoteCatalogHiddenDraft = null;
        resetQuoteCatalogSetupInputs();
        initializeQuoteCatalog();
        restoreSettings();
        renderQuoteRules();
        renderQuoteCatalogSetup();
        notify(`报价初始化完成，已启用 ${selected.length} 个产品。后续可随时增删产品或修改价格。`);
        return true;
      } catch (error) {
        notify(`初始化没有保存：${error.message}。已勾选产品和填写价格都还在，可以原地重试。`);
        return false;
      } finally {
        if (saveButton) {
          saveButton.disabled = false;
          saveButton.textContent = quoteCatalogSetupEditing ? "保存修改，返回报价" : "完成初始化，进入报价";
        }
      }
    }
    function closeQuoteCatalogCategoryManager() {
      const manager = document.getElementById("quoteCategoryManager");
      if (!manager) return;
      manager.classList.add("is-hidden");
      const toggle = document.querySelector("#quoteCategoryTabs .quote-category-manage");
      toggle?.setAttribute("aria-expanded", "false");
      toggle?.focus({ preventScroll: true });
      toggle?.scrollIntoView({ block: "nearest" });
    }
    function toggleQuoteCatalogCategoryManager() {
      const manager = document.getElementById("quoteCategoryManager");
      if (!manager) return;
      manager.classList.toggle("is-hidden");
      document.querySelector("#quoteCategoryTabs .quote-category-manage")?.setAttribute("aria-expanded", String(!manager.classList.contains("is-hidden")));
      if (!manager.classList.contains("is-hidden")) renderQuoteCategoryManager();
    }
    function renderQuoteCategoryTabs() {
      const box = document.getElementById("quoteCategoryTabs");
      const count = document.getElementById("quoteCatalogCount");
      const base = quoteCatalogBaseData();
      const catalog = quoteCatalogData();
      const systemProductCount = base.categories.reduce((total, category) => total + (category.products?.length || 0), 0);
      const customCount = quoteCatalogPreferences().customCategories.length;
      const selectedCount = quoteCatalogSelectedProductIds().size;
      const hiddenCount = [...quoteCatalogEffectiveHidden()].filter(id => quoteCatalogAllCategories().some(category => category.id === id)).length;
      const pendingCount = quoteCatalogVisibilityChanges().length;
      if (count) count.textContent = `本厂启用 ${selectedCount} 个 · 系统保留 ${systemProductCount} 个${customCount ? ` · 自定义 ${customCount} 类` : ""}${hiddenCount ? ` · ${pendingCount ? "预览隐藏" : "暂时隐藏"} ${hiddenCount} 类` : ""}`;
      if (!box) return;
      box.innerHTML = catalog.categories.map(category => `<button type="button" class="${category.id === quoteCatalogCategoryId ? "active" : ""}" onclick="selectQuoteCatalogCategory('${encodeURIComponent(category.id)}')">${escapeHtml(category.name)}</button>`).join("")
        + `<button type="button" class="quote-category-manage" aria-controls="quoteCategoryManager" aria-expanded="false" onclick="toggleQuoteCatalogCategoryManager()">＋ 自定义/整理分类</button>`;
      renderQuoteCategoryManager();
    }
    function renderQuoteCategoryManager() {
      const box = document.getElementById("quoteCategoryManagerList");
      if (!box) return;
      const canEdit = window.location.protocol === "file:" || Boolean(window.__hasPermission?.("price.edit"));
      const preferences = quoteCatalogPreferences();
      const savedHidden = new Set(preferences.hidden);
      const hidden = quoteCatalogEffectiveHidden();
      const categories = quoteCatalogAllCategories();
      box.innerHTML = categories.map((category, index) => {
        const isHidden = hidden.has(category.id);
        const isPending = savedHidden.has(category.id) !== isHidden;
        const visibilityStatus = isPending ? (isHidden ? " · 待隐藏" : " · 待恢复") : (isHidden ? " · 已隐藏" : "");
        const productNames = (category.products || []).map(product => product.name).filter(Boolean);
        const productSummary = productNames.slice(0, 3).join("、") + (productNames.length > 3 ? `等${productNames.length}个` : "");
        const productEditorOpen = category.custom && quoteCatalogProductEditorCategoryId === category.id;
        const productInputId = `quoteCustomCategoryProductAdd-${category.id}`;
        return `
        <div class="quote-category-manager-row ${isHidden ? "is-hidden-category" : ""} ${isPending ? "is-pending-category" : ""}" data-quote-category-id="${escapeHtml(category.id)}" onpointerdown="beginQuoteCatalogCategoryDrag(event,'${encodeURIComponent(category.id)}')">
          <span class="quote-category-drag-handle" title="长按后拖动排序" aria-hidden="true">⠿</span>
          <span><strong>${escapeHtml(category.name)}</strong><small>${category.custom ? `自定义分类 · ${productNames.length}个产品：${escapeHtml(productSummary || category.name)}` : "系统分类"}${visibilityStatus}</small></span>
          <span class="quote-category-manager-actions">
            <button type="button" onclick="moveQuoteCatalogCategory('${encodeURIComponent(category.id)}',-1)" ${!canEdit || index === 0 ? "disabled" : ""}>前移</button>
            <button type="button" onclick="moveQuoteCatalogCategory('${encodeURIComponent(category.id)}',1)" ${!canEdit || index === categories.length - 1 ? "disabled" : ""}>后移</button>
            <button type="button" onclick="toggleQuoteCatalogCategory('${encodeURIComponent(category.id)}')" ${!canEdit ? "disabled" : ""}>${isPending ? "撤销" : (isHidden ? "显示" : "隐藏")}</button>
            ${category.custom ? `<button type="button" onclick="toggleQuoteCatalogProductEditor('${encodeURIComponent(category.id)}')" ${!canEdit ? "disabled" : ""}>${productEditorOpen ? "收起产品" : "管理产品"}</button>` : ""}
            ${category.custom ? `<button type="button" onclick="deleteQuoteCatalogCategory('${encodeURIComponent(category.id)}')" ${!canEdit ? "disabled" : ""}>删除</button>` : ""}
          </span>
          ${category.custom ? `<div class="quote-category-product-editor ${productEditorOpen ? "" : "is-hidden"}">
            <strong>“${escapeHtml(category.name)}”下面的产品</strong>
            <div class="quote-category-product-editor-list">${productNames.map((name, productIndex) => `<span><em>${escapeHtml(name)}</em><button type="button" aria-label="删除${escapeHtml(name)}" title="删除这个子产品" onclick="deleteQuoteCatalogProductFromCategory('${encodeURIComponent(category.id)}',${productIndex})" ${!canEdit ? "disabled" : ""}>×</button></span>`).join("")}</div>
            <div class="quote-category-product-editor-add">
              <input id="${escapeHtml(productInputId)}" maxlength="50" placeholder="继续添加子产品，例如：密度板木盒" onkeydown="if(event.key==='Enter'){event.preventDefault();addQuoteCatalogProductToCategory('${encodeURIComponent(category.id)}')}">
              <button class="btn light" type="button" onclick="addQuoteCatalogProductToCategory('${encodeURIComponent(category.id)}')" ${!canEdit ? "disabled" : ""}>＋ 添加子产品</button>
            </div>
          </div>` : ""}
        </div>
      `;
      }).join("");
      renderQuoteCategoryConfirmation();
      ["quoteCustomCategoryName", "quoteCustomCategoryProduct"].forEach(id => {
        const input = document.getElementById(id);
        if (input) input.disabled = !canEdit;
      });
      document.querySelectorAll("#quoteCategoryManager .quote-custom-category-product-input, #quoteCategoryManager .quote-category-product-editor input").forEach(input => { input.disabled = !canEdit; });
      document.querySelectorAll("#quoteCategoryManager .quote-category-add button, #quoteCategoryManager .actions button").forEach(button => { button.disabled = !canEdit; });
    }
    function renderQuoteCategoryConfirmation() {
      const bar = document.getElementById("quoteCategoryConfirmBar");
      const text = document.getElementById("quoteCategoryPendingText");
      if (!bar) return;
      const changes = quoteCatalogVisibilityChanges();
      bar.classList.remove("is-hidden");
      bar.classList.toggle("has-pending-changes", changes.length > 0);
      const cancelButton = document.getElementById("quoteCategoryCancelButton");
      const confirmButton = document.getElementById("quoteCategoryConfirmButton");
      if (cancelButton) cancelButton.disabled = !changes.length;
      if (confirmButton) {
        confirmButton.disabled = false;
        confirmButton.textContent = changes.length ? "确认保存" : "完成并收起";
      }
      const title = bar.querySelector(".quote-category-confirm-copy strong");
      if (title) title.textContent = changes.length ? "分类显示有改动，尚未保存" : "分类设置已保存";
      if (text && changes.length) {
        const hidden = quoteCatalogEffectiveHidden();
        const hideCount = changes.filter(category => hidden.has(category.id)).length;
        const restoreCount = changes.length - hideCount;
        text.textContent = `${hideCount ? `待隐藏 ${hideCount} 类` : ""}${hideCount && restoreCount ? "，" : ""}${restoreCount ? `待恢复 ${restoreCount} 类` : ""}；上方分类已按新设置预览。`;
      } else if (text) {
        text.textContent = "隐藏或恢复分类后在这里确认；拖动顺序会立即保存。";
      }
    }
    function clearQuoteCatalogCategoryDrag() {
      if (!quoteCatalogCategoryDrag) return;
      window.clearTimeout(quoteCatalogCategoryDrag.holdTimer);
      quoteCatalogCategoryDrag.row?.classList.remove("is-dragging");
      document.body.classList.remove("quote-category-dragging");
      quoteCatalogCategoryDrag = null;
    }
    function beginQuoteCatalogCategoryDrag(event, categoryCode) {
      if (!requireQuoteCatalogPreferenceEdit() || event.button > 0 || event.target.closest("button,input,select")) return;
      clearQuoteCatalogCategoryDrag();
      const row = event.currentTarget;
      const startX = event.clientX;
      const startY = event.clientY;
      quoteCatalogCategoryDrag = {
        categoryId: decodeURIComponent(categoryCode || ""), pointerId: event.pointerId, row,
        startX, startY, active: false, moved: false, holdTimer: 0
      };
      quoteCatalogCategoryDrag.holdTimer = window.setTimeout(() => {
        if (!quoteCatalogCategoryDrag || quoteCatalogCategoryDrag.row !== row) return;
        quoteCatalogCategoryDrag.active = true;
        row.classList.add("is-dragging");
        document.body.classList.add("quote-category-dragging");
        try { row.setPointerCapture(event.pointerId); } catch (_) {}
      }, 420);
    }
    function moveQuoteCatalogCategoryDrag(event) {
      const drag = quoteCatalogCategoryDrag;
      if (!drag || drag.pointerId !== event.pointerId) return;
      if (!drag.active) {
        if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 10) clearQuoteCatalogCategoryDrag();
        return;
      }
      event.preventDefault();
      const target = document.elementFromPoint(event.clientX, event.clientY)?.closest(".quote-category-manager-row");
      const list = drag.row?.parentElement;
      if (!target || target === drag.row || target.parentElement !== list) return;
      const before = event.clientY < target.getBoundingClientRect().top + target.getBoundingClientRect().height / 2;
      list.insertBefore(drag.row, before ? target : target.nextSibling);
      drag.moved = true;
    }
    function finishQuoteCatalogCategoryDrag(event) {
      const drag = quoteCatalogCategoryDrag;
      if (!drag || drag.pointerId !== event.pointerId) return;
      window.clearTimeout(drag.holdTimer);
      if (!drag.active) return clearQuoteCatalogCategoryDrag();
      const list = drag.row?.parentElement;
      const orderedIds = [...(list?.querySelectorAll("[data-quote-category-id]") || [])].map(row => row.dataset.quoteCategoryId).filter(Boolean);
      clearQuoteCatalogCategoryDrag();
      if (!drag.moved || !orderedIds.length) return;
      quoteCatalogPreferences().order = orderedIds;
      persistQuoteCatalogPreferences("分类拖动顺序已保存，常用分类会按新顺序显示。");
    }
    if (!window.__quoteCatalogDragListenersReady) {
      window.__quoteCatalogDragListenersReady = true;
      document.addEventListener("pointermove", moveQuoteCatalogCategoryDrag, { passive: false });
      document.addEventListener("pointerup", finishQuoteCatalogCategoryDrag);
      document.addEventListener("pointercancel", finishQuoteCatalogCategoryDrag);
    }
    function requireQuoteCatalogPreferenceEdit() {
      if (window.location.protocol === "file:" || window.__hasPermission?.("price.edit")) return true;
      notify("当前账号只有查看和报价权限，请联系老板调整产品分类。");
      return false;
    }
    async function persistQuoteCatalogPreferences(message = "产品分类设置已保存。") {
      state.settings.quoteCatalogPreferences = normalizeQuoteCatalogPreferences(state.settings.quoteCatalogPreferences);
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      state.settings = proposedSettings;
      saveState();
      const visible = quoteCatalogData().categories;
      if (!visible.some(category => category.id === quoteCatalogCategoryId)) {
        quoteCatalogCategoryId = visible[0]?.id || "";
        quoteCatalogProductId = "";
      }
      renderQuoteCategoryTabs();
      renderQuoteProductPicker();
      renderQuoteProductConfigurator();
      const saveCloud = async () => {
        if (!window.__saveSettingsCloud) {
          notify(`${message}（已保存在本机）`);
          return true;
        }
        try {
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("云端没有确认保存");
          notify(message);
          return true;
        } catch (err) {
          notify(`本机已保存，云端未同步：${err?.message || "请检查网络后重试"}`);
          return false;
        }
      };
      quoteCatalogPreferencesSaveQueue = quoteCatalogPreferencesSaveQueue.then(saveCloud, saveCloud);
      return quoteCatalogPreferencesSaveQueue;
    }
    function moveQuoteCatalogCategory(categoryCode, direction) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const categoryId = decodeURIComponent(categoryCode || "");
      const preferences = quoteCatalogPreferences();
      const order = quoteCatalogAllCategories().map(category => category.id);
      const index = order.indexOf(categoryId);
      const target = index + (Number(direction) < 0 ? -1 : 1);
      if (index < 0 || target < 0 || target >= order.length) return;
      [order[index], order[target]] = [order[target], order[index]];
      preferences.order = order;
      persistQuoteCatalogPreferences("分类顺序已保存，常用分类会按新顺序显示。");
    }
    function toggleQuoteCatalogCategory(categoryCode) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const categoryId = decodeURIComponent(categoryCode || "");
      const preferences = quoteCatalogPreferences();
      if (!quoteCatalogHiddenDraft) quoteCatalogHiddenDraft = new Set(preferences.hidden);
      const hidden = quoteCatalogHiddenDraft;
      if (hidden.has(categoryId)) hidden.delete(categoryId);
      else {
        const visibleCount = quoteCatalogAllCategories().filter(category => !hidden.has(category.id)).length;
        if (visibleCount <= 1) return notify("至少保留一个可见产品分类。");
        hidden.add(categoryId);
      }
      const visible = quoteCatalogData().categories;
      if (!visible.some(category => category.id === quoteCatalogCategoryId)) {
        quoteCatalogCategoryId = visible[0]?.id || "";
        quoteCatalogProductId = "";
      }
      renderQuoteCategoryTabs();
      renderQuoteProductPicker();
      renderQuoteProductConfigurator();
      notify(hidden.has(categoryId) ? "已预览隐藏，请点击“确认保存”生效。" : "已预览显示，请点击“确认保存”生效。");
    }
    async function confirmQuoteCatalogCategoryVisibility() {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const changes = quoteCatalogVisibilityChanges();
      if (!quoteCatalogHiddenDraft || !changes.length) {
        closeQuoteCatalogCategoryManager();
        notify("分类设置已保存，已收起分类管理。");
        return true;
      }
      const preferences = quoteCatalogPreferences();
      preferences.hidden = [...quoteCatalogHiddenDraft];
      quoteCatalogHiddenDraft = null;
      const saveRequest = persistQuoteCatalogPreferences("分类显示设置已确认保存；隐藏分类可随时在这里恢复。");
      closeQuoteCatalogCategoryManager();
      await saveRequest;
      return true;
    }
    function cancelQuoteCatalogCategoryVisibility() {
      if (!quoteCatalogHiddenDraft) return;
      quoteCatalogHiddenDraft = null;
      const visible = quoteCatalogData().categories;
      if (!visible.some(category => category.id === quoteCatalogCategoryId)) {
        quoteCatalogCategoryId = visible[0]?.id || "";
        quoteCatalogProductId = "";
      }
      renderQuoteCategoryTabs();
      renderQuoteProductPicker();
      renderQuoteProductConfigurator();
      notify("已取消尚未保存的分类显示改动。");
    }
    function quoteCustomCategoryProductValues() {
      return [...new Set([...document.querySelectorAll("#quoteCustomCategoryProducts .quote-custom-category-product-input")]
        .map(input => String(input.value || "").trim().replace(/\s+/g, " ").slice(0, 50))
        .filter(Boolean))].slice(0, 20);
    }
    function addQuoteCustomCategoryProductInput() {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const list = document.getElementById("quoteCustomCategoryProducts");
      if (!list) return;
      if (list.querySelectorAll(".quote-custom-category-product-input").length >= 20) return notify("每个自定义分类最多添加20个子产品。");
      const row = document.createElement("div");
      row.className = "quote-custom-category-product-row";
      const input = document.createElement("input");
      input.className = "quote-custom-category-product-input";
      input.maxLength = 50;
      input.placeholder = "继续填写子产品，例如：密度板木盒";
      input.addEventListener("keydown", event => {
        if (event.key !== "Enter") return;
        event.preventDefault();
        addQuoteCustomCategoryProductInput();
      });
      const remove = document.createElement("button");
      remove.type = "button";
      remove.className = "btn light";
      remove.textContent = "移除";
      remove.addEventListener("click", () => row.remove());
      row.append(input, remove);
      list.appendChild(row);
      input.focus();
    }
    function resetQuoteCustomCategoryProductInputs() {
      const list = document.getElementById("quoteCustomCategoryProducts");
      if (!list) return;
      [...list.querySelectorAll(".quote-custom-category-product-row")].slice(1).forEach(row => row.remove());
      const first = document.getElementById("quoteCustomCategoryProduct");
      if (first) first.value = "";
    }
    function toggleQuoteCatalogProductEditor(categoryCode) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const categoryId = decodeURIComponent(categoryCode || "");
      quoteCatalogProductEditorCategoryId = quoteCatalogProductEditorCategoryId === categoryId ? "" : categoryId;
      renderQuoteCategoryManager();
      if (quoteCatalogProductEditorCategoryId) {
        window.setTimeout(() => document.getElementById(`quoteCustomCategoryProductAdd-${categoryId}`)?.focus(), 0);
      }
    }
    function addQuoteCatalogProductToCategory(categoryCode) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const categoryId = decodeURIComponent(categoryCode || "");
      const preferences = quoteCatalogPreferences();
      const category = preferences.customCategories.find(item => item.id === categoryId);
      const input = document.getElementById(`quoteCustomCategoryProductAdd-${categoryId}`);
      const productName = String(input?.value || "").trim().replace(/\s+/g, " ").slice(0, 50);
      if (!category || !productName) return notify("请先填写要添加的子产品名称。");
      const productNames = category.productNames?.length ? [...category.productNames] : [category.productName || category.name];
      if (productNames.some(name => normalizeRuleText(name) === normalizeRuleText(productName))) return notify("这个分类下已经有同名产品。");
      if (productNames.length >= 20) return notify("每个自定义分类最多添加20个子产品。");
      productNames.push(productName);
      category.productNames = productNames;
      category.productName = productNames[0];
      const productId = `${categoryId}-product-${productNames.length}`;
      if (preferences.selectionMode === "selected" && preferences.setupCompletedAt) {
        preferences.selectedProductIds = [...new Set([...(preferences.selectedProductIds || []), productId])];
      }
      quoteCatalogProductEditorCategoryId = categoryId;
      persistQuoteCatalogPreferences(`已在“${category.name}”下添加“${productName}”，现在可直接选择报价。`);
    }
    function deleteQuoteCatalogProductFromCategory(categoryCode, productIndexValue) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const categoryId = decodeURIComponent(categoryCode || "");
      const productIndex = Number(productIndexValue);
      const preferences = quoteCatalogPreferences();
      const category = preferences.customCategories.find(item => item.id === categoryId);
      const productNames = category?.productNames?.length ? [...category.productNames] : [category?.productName || category?.name].filter(Boolean);
      if (!category || !Number.isInteger(productIndex) || productIndex < 0 || productIndex >= productNames.length) return;
      if (productNames.length <= 1) return notify("每个自定义分类至少保留一个子产品；如不再需要，请删除整个分类。");
      const removedName = productNames[productIndex];
      if (!confirm(`确定从“${category.name}”中删除子产品“${removedName}”吗？\n\n已有报价记录不会受影响。`)) return;
      const productIdAt = index => index === 0 ? `${categoryId}-product` : `${categoryId}-product-${index + 1}`;
      const oldProductIds = productNames.map((_, index) => productIdAt(index));
      const selectedBefore = new Set(preferences.selectedProductIds || []);
      const setupBefore = quoteCatalogSetupDraft instanceof Set ? new Set(quoteCatalogSetupDraft) : null;
      const draftBefore = new Map(oldProductIds.map(id => [id, quoteCatalogDrafts[id]]));
      const productIdRemap = new Map();
      productNames.forEach((_, oldIndex) => {
        if (oldIndex === productIndex) return;
        const newIndex = oldIndex < productIndex ? oldIndex : oldIndex - 1;
        productIdRemap.set(productIdAt(oldIndex), productIdAt(newIndex));
      });

      category.productNames = productNames.filter((_, index) => index !== productIndex);
      category.productName = category.productNames[0];
      const unrelatedSelected = (preferences.selectedProductIds || []).filter(id => !oldProductIds.includes(id));
      productIdRemap.forEach((newId, oldId) => {
        if (selectedBefore.has(oldId)) unrelatedSelected.push(newId);
      });
      preferences.selectedProductIds = [...new Set(unrelatedSelected)];

      if (setupBefore) {
        const nextSetup = new Set([...setupBefore].filter(id => !oldProductIds.includes(id)));
        productIdRemap.forEach((newId, oldId) => {
          if (setupBefore.has(oldId)) nextSetup.add(newId);
        });
        quoteCatalogSetupDraft = nextSetup;
      }
      oldProductIds.forEach(id => delete quoteCatalogDrafts[id]);
      productIdRemap.forEach((newId, oldId) => {
        if (draftBefore.get(oldId)) quoteCatalogDrafts[newId] = draftBefore.get(oldId);
      });
      if (quoteCatalogProductId === productIdAt(productIndex)) quoteCatalogProductId = "";
      else if (productIdRemap.has(quoteCatalogProductId)) quoteCatalogProductId = productIdRemap.get(quoteCatalogProductId);

      quoteCatalogProductEditorCategoryId = categoryId;
      persistQuoteCatalogPreferences(`已从“${category.name}”中删除“${removedName}”，其他子产品和已有报价不受影响。`);
    }
    function addQuoteCatalogCategory(options) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const setupMode = Boolean(options?.setup);
      const nameInput = document.getElementById(setupMode ? "quoteCatalogSetupCustomCategory" : "quoteCustomCategoryName");
      const productInput = document.getElementById(setupMode ? "quoteCatalogSetupCustomProduct" : "quoteCustomCategoryProduct");
      const name = String(nameInput?.value || "").trim().slice(0, 30);
      const productNames = setupMode
        ? [String(productInput?.value || "").trim().replace(/\s+/g, " ").slice(0, 50)].filter(Boolean)
        : quoteCustomCategoryProductValues();
      if (!productNames.length && name) productNames.push(name);
      const productName = productNames[0] || name;
      if (!name) return notify("请先填写新分类名称。");
      if (quoteCatalogAllCategories().some(category => normalizeRuleText(category.name) === normalizeRuleText(name))) return notify("已经有同名分类，请直接调整它的顺序或显示状态。");
      const preferences = quoteCatalogPreferences();
      const id = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
      preferences.customCategories.push({
        id, name, productName, productNames,
        industryCategory: currentIndustryCategory(),
        template: state.settings.factoryType || "通用加工"
      });
      preferences.order = [...quoteCatalogAllCategories().map(category => category.id), id];
      const productIds = productNames.map((_, index) => index === 0 ? `${id}-product` : `${id}-product-${index + 1}`);
      if (preferences.selectionMode === "selected" && preferences.setupCompletedAt) {
        preferences.selectedProductIds = [...new Set([...(preferences.selectedProductIds || []), ...productIds])];
      }
      if (setupMode) productIds.forEach(productId => ensureQuoteCatalogSetupDraft().add(productId));
      if (nameInput) nameInput.value = "";
      if (setupMode && productInput) productInput.value = "";
      else resetQuoteCustomCategoryProductInputs();
      quoteCatalogCategoryId = id;
      quoteCatalogProductId = "";
      persistQuoteCatalogPreferences(setupMode
        ? `已添加并勾选“${productName}”，请继续确认成本。`
        : `已新增“${name}”分类和${productNames.length}个子产品，可直接选择并填写规格。`);
      if (setupMode) renderQuoteCatalogSetupProducts();
      return id;
    }
    function deleteQuoteCatalogCategory(categoryCode) {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const categoryId = decodeURIComponent(categoryCode || "");
      const preferences = quoteCatalogPreferences();
      if (!preferences.customCategories.some(item => item.id === categoryId)) return;
      preferences.customCategories = preferences.customCategories.filter(item => item.id !== categoryId);
      preferences.order = preferences.order.filter(id => id !== categoryId);
      preferences.hidden = preferences.hidden.filter(id => id !== categoryId);
      preferences.selectedProductIds = (preferences.selectedProductIds || []).filter(id => id !== `${categoryId}-product` && !id.startsWith(`${categoryId}-product-`));
      if (quoteCatalogProductEditorCategoryId === categoryId) quoteCatalogProductEditorCategoryId = "";
      if (quoteCatalogHiddenDraft) quoteCatalogHiddenDraft.delete(categoryId);
      persistQuoteCatalogPreferences("自定义分类已删除，已有报价记录不会受影响。");
    }
    function resetQuoteCatalogCategoryOrder() {
      if (!requireQuoteCatalogPreferenceEdit()) return;
      const preferences = quoteCatalogPreferences();
      preferences.order = [];
      preferences.hidden = [];
      quoteCatalogHiddenDraft = null;
      persistQuoteCatalogPreferences("原有系统分类已全部恢复并按原顺序显示；自定义分类仍保留在末尾。");
    }
    function renderDirectQuoteProductBar() { const label = document.getElementById("quoteDirectProductName"), entry = quoteCatalogEntry(); if (label) label.textContent = entry ? `${entry.categoryName} · ${entry.name}` : "请选择本次报价产品"; }
    function setQuoteProductChooser(open) {
      const chooser = document.getElementById("quoteProductChooser"), toggle = document.getElementById("quoteProductChooserToggle");
      if (!chooser || !toggle) return;
      const visible = Boolean(open);
      chooser.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-expanded", String(visible));
      toggle.textContent = visible ? "收起产品选择" : "更换产品";
      if (visible) { renderQuoteCategoryTabs(); renderQuoteProductPicker(); window.setTimeout(() => document.getElementById("quoteCatalogSearch")?.focus(), 0); }
    }
    function toggleQuoteProductChooser() { const chooser = document.getElementById("quoteProductChooser"); if (chooser) setQuoteProductChooser(chooser.classList.contains("is-hidden")); }
    function renderQuoteProductPicker() {
      const box = document.getElementById("quoteProductPicker");
      if (!box) return;
      const query = normalizeRuleText(quoteCatalogSearchText);
      const categories = quoteCatalogData().categories;
      let products = query
        ? quoteCatalogProducts().filter(product => normalizeRuleText(`${product.name} ${(product.aliases || []).join(" ")} ${product.categoryName}`).includes(query))
        : (categories.find(category => category.id === quoteCatalogCategoryId)?.products || []).map(product => ({ ...product, categoryId: quoteCatalogCategoryId }));
      products = products.slice(0, 80);
      if (!products.length) {
        box.innerHTML = `<div class="quote-product-empty">没有找到匹配产品。可以换关键词，或展开下方“自定义产品”手动填写。</div>`;
        return;
      }
      box.innerHTML = products.map(product => `<button type="button" class="${product.id === quoteCatalogProductId ? "active" : ""}" onclick="selectQuoteCatalogProduct('${encodeURIComponent(product.id)}')">${escapeHtml(product.name)}</button>`).join("");
    }
    function quoteCatalogGroupIsMaterial(group) {
      return ["material", "cover_material", "inner_material"].includes(String(group?.id || ""));
    }
    function canonicalQuoteOperationLabel(label) {
      const text = String(label || "").trim();
      return {
        "单黑": "胶印单色",
        "单色": "胶印单色",
        "四色彩印": "胶印四色",
        "四色彩印裱面": "胶印四色",
        "专色": "专色印刷",
        "四色+专色": "胶印四色加1专",
        "覆亮膜": "覆亮膜",
        "亮膜": "覆亮膜",
        "光膜": "覆亮膜",
        "单面覆亮膜": "覆亮膜",
        "覆哑膜": "覆哑膜",
        "哑膜": "覆哑膜",
        "单面覆哑膜": "覆哑膜",
        "UV彩印": "UV",
        "激光打标": "激光雕刻"
      }[text] || text;
    }
    function quoteCatalogOperationIsNoOp(label) {
      return /^(?:不印刷|不做logo|无需加工(?:\/仅供货)?|仅供货|不上漆|无需卷筒|无(?:需)?工艺)$/i
        .test(String(label || "").replace(/\s+/g, "").trim());
    }
    function isLaminationOperationName(name) {
      return /(?:覆膜|亮膜|哑膜|光膜)/.test(String(name || ""));
    }
    function quoteCatalogGroupPriceKind(group) {
      const id = String(group?.id || "");
      // 单/双面只决定印张倍数，不是一笔独立印刷费用；开数价挂在印刷颜色上。
      if (id === "print_color") return "print";
      // Every selectable production/service group must expose a price editor.
      // Earlier builds only recognized names containing process/binding/handle,
      // leaving output modes, proofing, delivery, packaging and certification
      // without an entry point in several product categories.
      if (/process|binding|handle|assembly|print_mode|proofing|service|packaging|certification/i.test(id)) return "process";
      return "";
    }
    function quoteCatalogProfileIsPrintable(entry = quoteCatalogEntry()) {
      return ["paper_flat", "card", "book", "label", "paper_box", "paper_bag", "corrugated", "office", "office_folder", "hang_tag", "instruction", "calendar", "display"].includes(String(entry?.profile || ""));
    }
    function quoteCatalogMaterialPriceSnapshot(name) {
      const override = findQuoteMaterialPrice(name);
      if (override) return { price: Number(override.price), unit: override.unit, source: "本厂快捷价" };
      const libraryItem = findActiveCostItem(name, ["material"]);
      if (libraryItem && Number(libraryItem.price) > 0) {
        return { price: Number(libraryItem.price), unit: normalizedQuoteMaterialPurchaseUnit(libraryItem.unit), source: quoteReferenceItem(libraryItem) ? "行业参考价 · 可修改" : "本厂成本库" };
      }
      if (quoteLegacyRulesConfirmed()) {
        const normalized = normalizeRuleText(name);
        const legacy = parseRules(state.settings.materialRules || "").find(item => normalizeRuleText(item.name) === normalized);
        if (legacy && Number(legacy.price) > 0) return { price: Number(legacy.price), unit: "元/个", source: "已确认价格表" };
      }
      return { price: null, unit: quoteMaterialIsPaperLike(name) ? "元/吨" : "元/个", source: "待设置" };
    }
    function quoteCatalogMaterialPriceEditor(group) {
      if (!quoteCatalogGroupIsMaterial(group)) return "";
      const options = Array.isArray(group.options) ? group.options : [];
      const materialSnapshots = options.map(item => quoteCatalogMaterialPriceSnapshot(item.label));
      const priced = materialSnapshots.filter(item => item.price !== null && !String(item.source || "").includes("行业参考价")).length;
      const referencePriced = materialSnapshots.filter(item => item.price !== null && String(item.source || "").includes("行业参考价")).length;
      const canEdit = window.location.protocol === "file:" || Boolean(window.__hasPermission?.("price.edit"));
      const units = ["元/吨", "元/kg", "元/㎡", "元/米", "元/张", "元/个"];
      const rows = options.map((item, index) => {
        const current = quoteCatalogMaterialPriceSnapshot(item.label);
        const outlierWarning = current.price === null ? "" : quotePriceOutlierWarning({ kind:"material", name:item.label, price:current.price, unit:current.unit });
        const normalizedName = normalizeRuleText(item.label);
        const hasSavedPrice = Boolean(findQuoteMaterialPrice(item.label))
          || (quoteLegacyRulesConfirmed() && parseRules(state.settings.materialRules || "").some(rule => normalizeRuleText(rule.name) === normalizedName));
        const priceId = `quoteCatalogMaterialPrice-${group.id}-${index}`;
        const unitId = `quoteCatalogMaterialUnit-${group.id}-${index}`;
        return `<div class="quote-material-price-row${outlierWarning ? " is-price-outlier" : ""}">
          <span><strong>${escapeHtml(item.label)}</strong><small>${escapeHtml(current.source)}${current.price !== null ? ` · ¥${Number(current.price).toFixed(current.price >= 100 ? 2 : 4).replace(/0+$/, "").replace(/\.$/, "")}${escapeHtml(current.unit.replace("元", ""))}` : " · 尚未填写价格"}${outlierWarning ? ` · ⚠ 请复核` : ""}</small></span>
          <input id="${escapeHtml(priceId)}" inputmode="decimal" value="${current.price === null ? "" : escapeHtml(String(current.price))}" placeholder="填写采购价" aria-label="${escapeHtml(item.label)}采购价" ${canEdit ? "" : "disabled"}>
          <select id="${escapeHtml(unitId)}" aria-label="${escapeHtml(item.label)}计价单位" ${canEdit ? "" : "disabled"}>${units.map(unit => `<option value="${unit}" ${unit === current.unit ? "selected" : ""}>${unit}</option>`).join("")}</select>
          <div class="quote-price-row-actions">
            <button class="btn light" type="button" onclick="saveQuoteCatalogMaterialPrice('${encodeURIComponent(item.label)}','${escapeHtml(priceId)}','${escapeHtml(unitId)}')" ${canEdit ? "" : "disabled"}>保存价格</button>
            <button class="btn light quote-price-delete" type="button" onclick="deleteQuoteCatalogMaterialPrice('${encodeURIComponent(item.label)}')" title="${hasSavedPrice ? "删除本厂为此材料单独保存的价格" : "当前没有可删除的单独价格；成本库价格请在价格表中管理"}" ${(canEdit && hasSavedPrice) ? "" : "disabled"}>删除</button>
          </div>
        </div>`;
      }).join("");
      return `<details class="quote-material-price-editor" data-price-group="${escapeHtml(group.id || "")}"><summary>修改${escapeHtml(group.label || "材料")}费用（本厂已设 ${priced}/${options.length}${referencePriced ? `，待核对参考价 ${referencePriced} 项` : ""}）</summary><div class="quote-material-price-editor-note">这里修改的是本厂材料采购价，支持元/吨、元/kg、元/㎡、元/米、元/张和元/个；保存后同名材料报价优先使用此价格。</div><div class="quote-material-price-list">${rows}</div></details>`;
    }
    async function saveQuoteCatalogMaterialPrice(nameCode, priceInputId, unitInputId) {
      if (!requireQuoteCatalogPreferenceEdit()) return false;
      const name = decodeURIComponent(nameCode || "").trim().slice(0, 80);
      const priceInput = document.getElementById(priceInputId);
      const unitInput = document.getElementById(unitInputId);
      const price = Number(String(priceInput?.value || "").replace(/[,￥¥\s]/g, ""));
      const unit = normalizedQuoteMaterialPurchaseUnit(unitInput?.value || "元/个");
      priceInput?.classList.toggle("quote-field-error", !(price > 0) || !Number.isFinite(price));
      if (!name || !(price > 0) || !Number.isFinite(price) || price > 100_000_000) {
        notify("请填写大于 0 的本厂真实材料采购价，并核对计价单位。");
        priceInput?.focus();
        return false;
      }
      const outlierWarning = quotePriceOutlierWarning({ kind:"material", name, price, unit });
      if (!confirmQuotePriceOutlier(outlierWarning)) {
        priceInput?.focus();
        return false;
      }
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      const current = normalizeQuoteMaterialPrices(proposedSettings.quoteMaterialPrices);
      const key = name.toLowerCase().replace(/\s+/g, "");
      const updatedAt = new Date().toISOString();
      const next = { name, price: Math.round(price * 1_000_000) / 1_000_000, unit, updatedAt };
      const index = current.findIndex(item => item.name.toLowerCase().replace(/\s+/g, "") === key);
      if (index >= 0) current[index] = next; else current.push(next);
      proposedSettings.quoteMaterialPrices = normalizeQuoteMaterialPrices(current);
      if (unit === "元/个") proposedSettings.materialRules = mergeQuoteRuleCosts(proposedSettings.materialRules || "", [{ name, price }]);
      proposedSettings.quoteRulesUpdatedAt = updatedAt;
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("服务端没有确认这次材料价格修改");
        }
        state.settings = proposedSettings;
        saveState();
        renderQuoteProductConfigurator();
        if (normalizeRuleText(val("quoteMaterial")) === normalizeRuleText(name)) {
          renderQuoteMaterialCostCheck(true);
          const confirmation = document.getElementById("quoteMaterialCostConfirmed");
          if (confirmation) confirmation.checked = false;
        }
        renderQuoteRules();
        notify(`${name}已保存为 ¥${price.toFixed(price >= 100 ? 2 : 4).replace(/0+$/, "").replace(/\.$/, "")}${unit.replace("元", "")}，后续同名材料优先使用。`);
        return true;
      } catch (error) {
        notify(`材料价格没有保存：${error.message}。原价格未改变。`);
        return false;
      }
    }
    function removeQuoteRuleCost(ruleText, name) {
      const key = normalizeRuleText(name);
      return parseRules(ruleText).filter(rule => normalizeRuleText(rule.name) !== key)
        .map(rule => `${rule.name}=${Number(rule.price).toFixed(3).replace(/0+$/, "").replace(/\.$/, "")}`)
        .join("\n");
    }
    async function deleteQuoteCatalogMaterialPrice(nameCode) {
      if (!requireQuoteCatalogPreferenceEdit()) return false;
      const name = decodeURIComponent(nameCode || "").trim().slice(0, 80);
      const key = normalizeRuleText(name);
      const currentPrices = normalizeQuoteMaterialPrices(state.settings.quoteMaterialPrices);
      const currentRules = parseRules(state.settings.materialRules || "");
      const hasSavedPrice = currentPrices.some(item => normalizeRuleText(item.name) === key)
        || currentRules.some(rule => normalizeRuleText(rule.name) === key);
      if (!name || !hasSavedPrice) {
        notify("这项没有单独保存的材料价格；如价格来自成本库，请到价格表中管理。");
        return false;
      }
      if (!confirm(`确定删除“${name}”的材料价格吗？\n\n删除后报价不再使用这条单独价格；如成本库中另有同名价格，系统会改用成本库价格。`)) return false;
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      proposedSettings.quoteMaterialPrices = normalizeQuoteMaterialPrices(proposedSettings.quoteMaterialPrices)
        .filter(item => normalizeRuleText(item.name) !== key);
      proposedSettings.materialRules = removeQuoteRuleCost(proposedSettings.materialRules || "", name);
      proposedSettings.quoteRulesUpdatedAt = new Date().toISOString();
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("服务端没有确认这次材料价格删除");
        }
        state.settings = proposedSettings;
        saveState();
        renderQuoteProductConfigurator();
        if (normalizeRuleText(val("quoteMaterial")) === key) {
          renderQuoteMaterialCostCheck(true);
          const confirmation = document.getElementById("quoteMaterialCostConfirmed");
          if (confirmation) confirmation.checked = false;
        }
        renderQuoteRules();
        notify(`${name}的单独材料价格已删除。`);
        return true;
      } catch (error) {
        notify(`材料价格没有删除：${error.message}。原价格仍保留。`);
        return false;
      }
    }
    function quoteOperationPricingPreset(name, kind = "process") {
      const text = canonicalQuoteOperationLabel(name);
      if (kind === "print") {
        return { basis:"sheet", chargeMode:"package_plus_excess", usagePerPiece:0,
          help:"按实际上机开数和印张计费；各开数分别维护最低开机费、起步印张和超量印张单价，单双面自动乘面数。" };
      }
      if (isLaminationOperationName(text) || /(?:过油|上光油|满版UV|逆向UV|磨砂|防水处理|局部哑光|裱瓦楞|裱板)/.test(text)) {
        return { basis:"sheet", chargeMode:"minimum", usagePerPiece:0,
          help:"按实际覆膜生产张数×元/张计费，低于最低收费时收最低收费；不要按成品个数直接相乘。" };
      }
      if (/(?:局部UV|烫金|烫银|浮雕烫|击凸|压凹|压纹|镂空|贴窗|异形雕刻|激光雕刻)/.test(text)) {
        return { basis:"local_m2", chargeMode:"minimum", usagePerPiece:0,
          help:"通常由制版/开机费＋局部加工面积组成；请填写每件实际加工面积，避免拿整张面积代替。" };
      }
      if (/(?:模切|压线|压痕|裁切|开槽|排废|撕线|异形切割)/.test(text)) {
        return { basis:"sheet", chargeMode:"minimum", usagePerPiece:0,
          help:"通常按生产印张计费，并另收刀模/压线版/开机费；模切已含压痕时不要重复收费。" };
      }
      if (/(?:折页|风琴折|关门折)/.test(text)) {
        return { basis:"fold", chargeMode:"minimum", usagePerPiece:1,
          help:"按每件折数×成品数量计费，并可设最低开机量；多折请把每件折数填在“每件用量”。" };
      }
      if (/(?:打孔|提手孔|透气孔|鸡眼扣|装别针)/.test(text)) {
        return { basis:"hole", chargeMode:"minimum", usagePerPiece:1,
          help:"按每件孔数×数量计费，孔径、鸡眼或五金材料费可另计；每件孔数填在“每件用量”。" };
      }
      if (/(?:骑马钉|骑马订|胶装|锁线|包本|裱壳)/.test(text)) {
        return { basis:"book", chargeMode:"minimum", usagePerPiece:1,
          help:"装订通常按本数计费；贴数、页数或封面工艺不同应分别建价格，另设最低开机费。" };
      }
      if (/(?:流水号|可变数据|可变码|可变二维码|可变条码|喷码|打码)/.test(text)) {
        return { basis:"piece", chargeMode:"package_plus_excess", usagePerPiece:1,
          help:"通常包含数据整理/起机保底量，超过后按每条记录或每件计费；适合“套餐含量＋超量单价”。" };
      }
      if (/(?:穿绳|绳扣|金属挂钩|提手|糊盒|糊袋|粘箱|粘口|胶头|手工组装|装扣件|独立包装)/.test(text)) {
        return { basis:"piece", chargeMode:"minimum", usagePerPiece:1,
          help:"通常按件计费并设最低加工费；结构复杂、材料不同或手工工时差异较大时应拆分价格。" };
      }
      return { basis:"piece", chargeMode:"minimum", usagePerPiece:1,
        help:"默认按成品件数并设置最低收费；如本厂实际按张、面积、长度、重量、开机次数或工时结算，请切换计价单位。" };
    }
    function quoteCatalogOperationPriceSnapshot(name, kind) {
      const canonicalName = canonicalQuoteOperationLabel(name);
      const preset = quoteOperationPricingPreset(canonicalName, kind);
      const override = findQuoteOperationPrice(canonicalName, kind);
      if (override) {
        if (kind === "print" && !override.printSheetTierPricing && canonicalName !== "基础印刷") {
          return { ...preset, ...override, configured:false, legacyGenericPrice:true,
            pricingHelp:"检测到旧版统一印工。请按本厂机台分别填写全开、对开、四开或八开价格；系统不会把旧单价擅自套到所有开数。",
            source:"旧版统一印工待迁移" };
        }
        if (isLaminationOperationName(canonicalName) && !override.laminationTierPricing) {
          return { ...preset, ...override, pricingModel:"lamination_sheet_minimum", basis:"sheet",
            setupFee:0, minimumCharge:Math.max(0, Number(override.minimumCharge || override.setupFee || 0)),
            pricingHelp:preset.help, configured:true, source:override.source || "本厂快捷价" };
        }
        return { ...preset, ...override, pricingHelp:preset.help, configured: true, source: override.source || "本厂快捷价" };
      }
      const libraryItem = findActiveCostItem(canonicalName, ["process", "plate"]);
      if (libraryItem && Number(libraryItem.price) >= 0) {
        const fixedBasis = libraryItem.type === "plate" || ["run", "specification"].includes(String(libraryItem.basis || "")) || /次|单|版/.test(String(libraryItem.unit || ""));
        const pieceBasis = ["piece", "sheet", "book"].includes(String(libraryItem.basis || "")) || /个|件|只|套|张|本/.test(String(libraryItem.unit || ""));
        if (isLaminationOperationName(canonicalName)) {
          return {
            ...preset,
            pricingModel:"lamination_sheet_minimum",
            unitPrice:Number(libraryItem.price),
            setupFee:0,
            minimumCharge:Math.max(0, Number(libraryItem.minimumCharge || libraryItem.setupFee || 0)),
            includedQuantity:0,
            excessUnitPrice:Number(libraryItem.price),
            usagePerPiece:0,
            basis:"sheet",
            chargeMode:"minimum",
            kind,
            configured:true,
            pricingHelp:preset.help,
            source:quoteReferenceItem(libraryItem) ? "行业参考价 · 可修改" : "本厂成本库"
          };
        }
        if (fixedBasis || pieceBasis) {
          return {
            ...preset,
            unitPrice: fixedBasis ? 0 : Number(libraryItem.price),
            setupFee: Number(libraryItem.setupFee || 0) + (fixedBasis ? Number(libraryItem.price) : 0),
            minimumCharge: Number(libraryItem.minimumCharge || 0),
            includedQuantity: 0,
            excessUnitPrice: fixedBasis ? 0 : Number(libraryItem.price),
            usagePerPiece: pieceBasis ? 1 : 0,
            basis: fixedBasis ? "run" : (String(libraryItem.basis || "") === "sheet" ? "sheet" : "piece"),
            chargeMode: "minimum",
            kind,
            configured: true,
            pricingHelp:preset.help,
            source: quoteReferenceItem(libraryItem) ? "行业参考价 · 可修改" : "本厂成本库"
          };
        }
      }
      if (quoteLegacyRulesConfirmed()) {
        const legacy = parseRules(state.settings.processRules || "").find(item => normalizeRuleText(item.name) === normalizeRuleText(canonicalName));
        if (legacy && Number(legacy.price) >= 0) {
          return { ...preset, unitPrice: Number(legacy.price), setupFee: 0, minimumCharge:0, includedQuantity:0, excessUnitPrice:Number(legacy.price), kind, pricingHelp:preset.help, configured: true, source: "已确认工艺价格表" };
        }
      }
      return { ...preset, unitPrice: null, setupFee: null, minimumCharge:0, includedQuantity:0, excessUnitPrice:null, kind, pricingHelp:preset.help, configured: false, source: "待设置" };
    }
    function quotePrintSheetTierSummary(tierPricing) {
      const tier = normalizePrintSheetTierPricing(tierPricing);
      if (!tier) return "";
      return QUOTE_PRINT_SHEET_TIER_DEFINITIONS.map(definition => {
        const rule = tier[definition.id];
        if (!rule) return "";
        const rate = Number(rule.excessUnitPrice || rule.unitPrice || 0);
        const threshold = Number(rule.includedQuantity || 0);
        const withinPrice = Number(rule.minimumCharge || 0);
        const formattedRate = rate.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
        if (threshold > 0 && withinPrice > 0 && rate > 0) {
          return `${definition.label}：${threshold}张以内共¥${withinPrice.toFixed(2)}，超过后¥${formattedRate}/张`;
        }
        const parts = [];
        if (withinPrice > 0) parts.push(`最低¥${withinPrice.toFixed(2)}`);
        if (rate > 0) parts.push(`¥${formattedRate}/张`);
        return `${definition.label}：${parts.join("，")}`;
      }).filter(Boolean).join("；");
    }
    function quotePrintSheetTierEditor(prefix, tierPricing, canEdit) {
      const tier = normalizePrintSheetTierPricing(tierPricing) || {};
      return `<div class="quote-print-sheet-tier-grid">${QUOTE_PRINT_SHEET_TIER_DEFINITIONS.map(definition => {
        const rule = tier[definition.id] || {};
        const baseId = `${prefix}-${definition.id}`;
        return `<section class="quote-print-sheet-tier-card" data-print-sheet-tier="${escapeHtml(definition.id)}">
          <strong>${escapeHtml(definition.label)}</strong><small>单独维护该开数的数量分界和价格；例如：1000张以内共120元，超过后每张0.08元</small>
          <label>多少张以内<input id="${escapeHtml(baseId)}-included" inputmode="numeric" value="${Number(rule.includedQuantity || 0) > 0 ? escapeHtml(String(rule.includedQuantity)) : ""}" placeholder="例如 1000" aria-label="${escapeHtml(definition.label)}多少张以内" ${canEdit ? "" : "disabled"}></label>
          <label>以内总价（元）<input id="${escapeHtml(baseId)}-minimum" inputmode="decimal" value="${Number(rule.minimumCharge || 0) > 0 ? escapeHtml(String(rule.minimumCharge)) : ""}" placeholder="例如 120" aria-label="${escapeHtml(definition.label)}以内总价" ${canEdit ? "" : "disabled"}></label>
          <label>超过后每张（元）<input id="${escapeHtml(baseId)}-unit" inputmode="decimal" value="${Number(rule.excessUnitPrice || rule.unitPrice || 0) > 0 ? escapeHtml(String(rule.excessUnitPrice || rule.unitPrice)) : ""}" placeholder="例如 0.08" aria-label="${escapeHtml(definition.label)}超过后每张" ${canEdit ? "" : "disabled"}></label>
        </section>`;
      }).join("")}</div>`;
    }
    function quoteCatalogOperationPriceEditor(group) {
      const kind = quoteCatalogGroupPriceKind(group);
      if (!kind) return "";
      const options = (Array.isArray(group.options) ? group.options : [])
        .filter(item => !quoteCatalogOperationIsNoOp(item.label));
      const operationSnapshots = options.map(item => quoteCatalogOperationPriceSnapshot(item.label, kind));
      const priced = operationSnapshots.filter(item => item.configured && !String(item.source || "").includes("行业参考价")).length;
      const referencePriced = operationSnapshots.filter(item => item.configured && String(item.source || "").includes("行业参考价")).length;
      const canEdit = window.location.protocol === "file:" || Boolean(window.__hasPermission?.("price.edit"));
      const rows = options.map((item, index) => {
        const canonicalName = canonicalQuoteOperationLabel(item.label);
        const current = quoteCatalogOperationPriceSnapshot(canonicalName, kind);
        const normalizedName = normalizeRuleText(canonicalName);
        const hasSavedPrice = Boolean(findQuoteOperationPrice(canonicalName, kind))
          || (quoteLegacyRulesConfirmed() && parseRules(state.settings.processRules || "").some(rule => normalizeRuleText(rule.name) === normalizedName));
        const unitId = `quoteCatalogOperationUnit-${group.id}-${index}`;
        const setupId = `quoteCatalogOperationSetup-${group.id}-${index}`;
        const basisId = `quoteCatalogOperationBasis-${group.id}-${index}`;
        const modeId = `quoteCatalogOperationMode-${group.id}-${index}`;
        const minimumId = `quoteCatalogOperationMinimum-${group.id}-${index}`;
        const includedId = `quoteCatalogOperationIncluded-${group.id}-${index}`;
        const excessId = `quoteCatalogOperationExcess-${group.id}-${index}`;
        const usageId = `quoteCatalogOperationUsage-${group.id}-${index}`;
        const fullOpenId = `quoteCatalogOperationFullOpen-${group.id}-${index}`;
        const twoOpenId = `quoteCatalogOperationTwoOpen-${group.id}-${index}`;
        const fourOpenId = `quoteCatalogOperationFourOpen-${group.id}-${index}`;
        const eightOpenId = `quoteCatalogOperationEightOpen-${group.id}-${index}`;
        const sixteenOpenId = `quoteCatalogOperationSixteenOpen-${group.id}-${index}`;
        const thirtyTwoOpenId = `quoteCatalogOperationThirtyTwoOpen-${group.id}-${index}`;
        const printTierPrefix = `quoteCatalogPrintTier-${group.id}-${index}`;
        const isLamination = isLaminationOperationName(canonicalName);
        const isSheetFormatPrint = kind === "print";
        const tierPricing = current.laminationTierPricing || null;
        const basisUnit = quoteOperationBasisUnit(current.basis);
        const displayedRate = current.chargeMode === "package_plus_excess" ? Number(current.excessUnitPrice ?? current.unitPrice ?? 0) : Number(current.unitPrice || 0);
        const tierPriceText = tierPricing ? `自动按实际尺寸：${[
          ["全开", tierPricing.fullOpenUnitPrice], ["对开", tierPricing.twoOpenUnitPrice], ["四开", tierPricing.fourOpenUnitPrice],
          ["八开", tierPricing.eightOpenUnitPrice], ["十六开", tierPricing.sixteenOpenUnitPrice], ["三十二开", tierPricing.thirtyTwoOpenUnitPrice]
        ].filter(([, price]) => Number(price) > 0).map(([label, price]) => `${label} ¥${Number(price).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}/张`).join("，")}` : "";
        const printTierText = quotePrintSheetTierSummary(current.printSheetTierPricing);
        const priceText = current.configured
          ? printTierText || tierPriceText || `${quoteOperationChargeModeLabel(current.chargeMode)} · ¥${displayedRate.toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}/${basisUnit} · 固定 ¥${Number(current.setupFee || 0).toFixed(2)}`
          : "尚未填写费用";
        const outlierWarning = current.configured ? quotePriceOutlierWarning({ kind, name:canonicalName, price:current.unitPrice, setupFee:current.setupFee, minimumCharge:current.minimumCharge, basis:current.basis }) : "";
        const genericPrimary = `<div class="quote-operation-price-primary">
          <label>计价单位<select id="${escapeHtml(basisId)}" aria-label="${escapeHtml(item.label)}计价单位" ${canEdit ? "" : "disabled"}>${quoteOperationBasisOptionsHtml(current.basis || "piece")}</select></label>
          <label>收费方式<select id="${escapeHtml(modeId)}" aria-label="${escapeHtml(item.label)}收费方式" ${canEdit ? "" : "disabled"}>${quoteOperationChargeModeOptionsHtml(current.chargeMode || "minimum")}</select></label>
          <label>单位/超量单价<input id="${escapeHtml(unitId)}" inputmode="decimal" value="${current.configured ? escapeHtml(String(current.unitPrice || 0)) : ""}" placeholder="请输入金额" aria-label="${escapeHtml(item.label)}单位费用" ${canEdit ? "" : "disabled"}></label>
        </div>`;
        const printPrimary = `<div class="quote-print-sheet-tier-heading"><strong>按上机开数和张数阶梯定价</strong><span>每种开数分别填写“多少张以内多少钱，超过后每张多少钱”；单双面自动换算实际印张</span></div>
          ${quotePrintSheetTierEditor(printTierPrefix, current.printSheetTierPricing, canEdit)}
          <input type="hidden" id="${escapeHtml(basisId)}" value="sheet"><input type="hidden" id="${escapeHtml(modeId)}" value="minimum">
          <input type="hidden" id="${escapeHtml(unitId)}" value="${escapeHtml(String(current.unitPrice || 0))}"><input type="hidden" id="${escapeHtml(minimumId)}" value="0">
          <input type="hidden" id="${escapeHtml(includedId)}" value="0"><input type="hidden" id="${escapeHtml(excessId)}" value="${escapeHtml(String(current.excessUnitPrice || current.unitPrice || 0))}"><input type="hidden" id="${escapeHtml(usageId)}" value="0">`;
        return `<div class="quote-operation-price-row${outlierWarning ? " is-price-outlier" : ""}">
          <span><strong>${escapeHtml(item.label)}</strong><small>${escapeHtml(current.source)} · ${escapeHtml(priceText)}${outlierWarning ? " · ⚠ 请复核" : ""}</small></span>
          <div class="quote-operation-pricing-help"><strong>同行常用计价：</strong>${escapeHtml(current.pricingHelp || quoteOperationPricingPreset(canonicalName, kind).help)}</div>
          ${isSheetFormatPrint ? printPrimary : genericPrimary}
          <div class="quote-price-row-actions">
            <button class="btn light" type="button" onclick="saveQuoteCatalogOperationPrice('${encodeURIComponent(canonicalName)}','${kind}','${escapeHtml(unitId)}','${escapeHtml(setupId)}','${escapeHtml(basisId)}','${escapeHtml(modeId)}','${escapeHtml(minimumId)}','${escapeHtml(includedId)}','${escapeHtml(excessId)}','${escapeHtml(usageId)}','${escapeHtml(fourOpenId)}','${escapeHtml(eightOpenId)}','${escapeHtml(fullOpenId)}','${escapeHtml(twoOpenId)}','${escapeHtml(sixteenOpenId)}','${escapeHtml(thirtyTwoOpenId)}','${escapeHtml(printTierPrefix)}')" ${canEdit ? "" : "disabled"}>保存费用</button>
            <button class="btn light quote-price-delete" type="button" onclick="deleteQuoteCatalogOperationPrice('${encodeURIComponent(canonicalName)}','${kind}')" title="${hasSavedPrice ? "删除本厂为此项目单独保存的费用" : "当前没有可删除的单独费用；成本库价格请在价格表中管理"}" ${(canEdit && hasSavedPrice) ? "" : "disabled"}>删除</button>
          </div>
          <div class="quote-operation-price-advanced">
            ${isLamination ? `<label>全开工价（元/张）<input id="${escapeHtml(fullOpenId)}" inputmode="decimal" value="${escapeHtml(String(tierPricing?.fullOpenUnitPrice ?? ""))}" placeholder="可按本厂能力填写" ${canEdit ? "" : "disabled"}></label>
            <label>对开工价（元/张）<input id="${escapeHtml(twoOpenId)}" inputmode="decimal" value="${escapeHtml(String(tierPricing?.twoOpenUnitPrice ?? ""))}" placeholder="可按本厂能力填写" ${canEdit ? "" : "disabled"}></label>
            <label>四开工价（元/张）<input id="${escapeHtml(fourOpenId)}" inputmode="decimal" value="${escapeHtml(String(tierPricing?.fourOpenUnitPrice ?? current.unitPrice ?? ""))}" placeholder="填写四开每张工价" ${canEdit ? "" : "disabled"}></label>
            <label>八开工价（元/张）<input id="${escapeHtml(eightOpenId)}" inputmode="decimal" value="${escapeHtml(String(tierPricing?.eightOpenUnitPrice ?? current.unitPrice ?? ""))}" placeholder="填写八开每张工价" ${canEdit ? "" : "disabled"}></label>
            <label>十六开工价（元/张）<input id="${escapeHtml(sixteenOpenId)}" inputmode="decimal" value="${escapeHtml(String(tierPricing?.sixteenOpenUnitPrice ?? ""))}" placeholder="可按本厂能力填写" ${canEdit ? "" : "disabled"}></label>
            <label>三十二开工价（元/张）<input id="${escapeHtml(thirtyTwoOpenId)}" inputmode="decimal" value="${escapeHtml(String(tierPricing?.thirtyTwoOpenUnitPrice ?? ""))}" placeholder="可按本厂能力填写" ${canEdit ? "" : "disabled"}></label>` : ""}
            <label>${isSheetFormatPrint ? "该颜色规则开机费（元/单，不含印版）" : "固定开机/制版费"}<input id="${escapeHtml(setupId)}" inputmode="decimal" value="${current.configured ? escapeHtml(String(current.setupFee || 0)) : "0"}" placeholder="没有填0" ${canEdit ? "" : "disabled"}></label>
            ${isSheetFormatPrint ? "" : `<label>最低收费/套餐费<input id="${escapeHtml(minimumId)}" inputmode="decimal" value="${escapeHtml(String(current.minimumCharge || 0))}" placeholder="没有填0" ${canEdit ? "" : "disabled"}></label>
            <label>套餐包含量<input id="${escapeHtml(includedId)}" inputmode="decimal" value="${escapeHtml(String(current.includedQuantity || 0))}" placeholder="非套餐填0" ${canEdit ? "" : "disabled"}></label>
            <label>超量单价<input id="${escapeHtml(excessId)}" inputmode="decimal" value="${current.configured ? escapeHtml(String(current.excessUnitPrice ?? current.unitPrice ?? 0)) : ""}" placeholder="套餐模式填写" ${canEdit ? "" : "disabled"}></label>
            <label>每件用量（面积/米/kg/孔/折/页/小时）<input id="${escapeHtml(usageId)}" inputmode="decimal" value="${escapeHtml(String(current.usagePerPiece ?? (current.basis === "piece" ? 1 : 0)))}" placeholder="按所选单位填写" ${canEdit ? "" : "disabled"}></label>`}
          </div>
          ${isLamination ? `<div class="field-help">覆膜支持全开、对开、四开、八开、十六开和三十二开，按实际尺寸、覆膜张数及单双面计价；本厂不用的档位可以留空。</div>` : ""}
          ${isSheetFormatPrint ? `<div class="field-help">“几开”按实际上机纸/机台幅面判断，不等同于成品开本。系统按上机纸尺寸自动推荐，厂家可手动覆盖；未配置的开数会停止报价，不会误套其他档位。</div>` : ""}
        </div>`;
      }).join("");
      const note = kind === "print"
        ? "单黑、四色、专色分别保存一套价格。每种开数填写：多少张以内的总价，以及超过该数量后每增加一张的价格；系统按实际印张自动命中。"
        : "选择本厂真实计价单位；支持固定费、最低收费，以及“套餐含量＋超量单价”。报价时只计算客户实际勾选的工艺。";
      return `<details class="quote-operation-price-editor" data-price-group="${escapeHtml(group.id || "")}"><summary>修改${escapeHtml(group.label || (kind === "print" ? "印刷" : "工艺"))}费用（本厂已设 ${priced}/${options.length}${referencePriced ? `，待核对参考价 ${referencePriced} 项` : ""}）</summary><div class="quote-operation-price-note">${note}</div><div class="quote-operation-price-list">${rows}</div></details>`;
    }
    async function saveQuoteCatalogOperationPrice(nameCode, kind, unitInputId, setupInputId, basisInputId, modeInputId, minimumInputId, includedInputId, excessInputId, usageInputId, fourOpenInputId = "", eightOpenInputId = "", fullOpenInputId = "", twoOpenInputId = "", sixteenOpenInputId = "", thirtyTwoOpenInputId = "", printTierPrefix = "") {
      if (!requireQuoteCatalogPreferenceEdit()) return false;
      const name = canonicalQuoteOperationLabel(decodeURIComponent(nameCode || "")).slice(0, 80);
      const normalizedKind = kind === "print" ? "print" : "process";
      const unitInput = document.getElementById(unitInputId);
      const setupInput = document.getElementById(setupInputId);
      const basisInput = document.getElementById(basisInputId);
      const modeInput = document.getElementById(modeInputId);
      const minimumInput = document.getElementById(minimumInputId);
      const includedInput = document.getElementById(includedInputId);
      const excessInput = document.getElementById(excessInputId);
      const usageInput = document.getElementById(usageInputId);
      const fullOpenInput = fullOpenInputId ? document.getElementById(fullOpenInputId) : null;
      const twoOpenInput = twoOpenInputId ? document.getElementById(twoOpenInputId) : null;
      const fourOpenInput = fourOpenInputId ? document.getElementById(fourOpenInputId) : null;
      const eightOpenInput = eightOpenInputId ? document.getElementById(eightOpenInputId) : null;
      const sixteenOpenInput = sixteenOpenInputId ? document.getElementById(sixteenOpenInputId) : null;
      const thirtyTwoOpenInput = thirtyTwoOpenInputId ? document.getElementById(thirtyTwoOpenInputId) : null;
      const laminationInputs = {
        fullOpenUnitPrice: fullOpenInput,
        twoOpenUnitPrice: twoOpenInput,
        fourOpenUnitPrice: fourOpenInput,
        eightOpenUnitPrice: eightOpenInput,
        sixteenOpenUnitPrice: sixteenOpenInput,
        thirtyTwoOpenUnitPrice: thirtyTwoOpenInput
      };
      const laminationTierEnabled = isLaminationOperationName(name) && Object.values(laminationInputs).some(Boolean);
      const readPriceInput = input => {
        const text = String(input?.value || "").replace(/[,￥¥\s]/g, "");
        return { input, text, value:text === "" ? 0 : Number(text) };
      };
      const printTierInputs = normalizedKind === "print" && printTierPrefix ? Object.fromEntries(QUOTE_PRINT_SHEET_TIER_DEFINITIONS.map(definition => {
        const baseId = `${printTierPrefix}-${definition.id}`;
        return [definition.id, {
          definition,
          minimum:readPriceInput(document.getElementById(`${baseId}-minimum`)),
          included:readPriceInput(document.getElementById(`${baseId}-included`)),
          unit:readPriceInput(document.getElementById(`${baseId}-unit`))
        }];
      })) : {};
      const configuredPrintTiers = Object.values(printTierInputs).filter(tier => tier.minimum.value > 0 || tier.included.value > 0 || tier.unit.value > 0);
      const invalidPrintTier = Object.values(printTierInputs).some(tier => {
        const values = [tier.minimum, tier.included, tier.unit];
        const configured = values.some(item => item.value > 0 || item.text !== "");
        const invalidNumber = values.some(item => item.text !== "" && (!Number.isFinite(item.value) || item.value < 0 || item.value > 10_000_000));
        const incompleteTier = configured && !(tier.included.value > 0 && tier.minimum.value > 0 && tier.unit.value > 0);
        return invalidNumber || incompleteTier;
      });
      const printTierEnabled = normalizedKind === "print" && Object.keys(printTierInputs).length > 0;
      const unitText = String(unitInput?.value || "").replace(/[,￥¥\s]/g, "");
      const setupText = String(setupInput?.value || "").replace(/[,￥¥\s]/g, "");
      let unitPrice = unitText === "" ? 0 : Number(unitText);
      const setupFee = setupText === "" ? 0 : Number(setupText);
      const minimumCharge = Number(String(minimumInput?.value || "0").replace(/[,￥¥\s]/g, ""));
      const includedQuantity = Number(String(includedInput?.value || "0").replace(/[,￥¥\s]/g, ""));
      const excessUnitPrice = String(excessInput?.value || "").trim() === "" ? unitPrice : Number(String(excessInput.value).replace(/[,￥¥\s]/g, ""));
      const basis = String(basisInput?.value || "piece");
      const chargeMode = String(modeInput?.value || "minimum");
      const usagePerPiece = Number(String(usageInput?.value || (basis === "piece" ? "1" : "0")).replace(/[,￥¥\s]/g, ""));
      const laminationPrices = Object.fromEntries(Object.entries(laminationInputs).map(([field, input]) => {
        const text = String(input?.value || "").replace(/[,\uffe5\u00a5\s]/g, "");
        return [field, { input, text, value: text === "" ? 0 : Number(text) }];
      }));
      const laminationPriceValues = Object.values(laminationPrices).map(item => item.value);
      const hasLaminationPrice = laminationPriceValues.some(value => value > 0);
      const invalidLaminationPrice = Object.values(laminationPrices).some(item => item.text !== "" && !(item.value > 0));
      if (printTierEnabled && configuredPrintTiers.length) unitPrice = Number(configuredPrintTiers[0].unit.value || 0);
      const printTierValues = Object.values(printTierInputs).flatMap(tier => [tier.minimum.value, tier.included.value, tier.unit.value]);
      const numbers = [unitPrice, setupFee, minimumCharge, includedQuantity, excessUnitPrice, usagePerPiece, ...laminationPriceValues, ...printTierValues];
      const hasAnyPrice = printTierEnabled ? configuredPrintTiers.length > 0 : (unitText !== "" || setupText !== "" || minimumCharge > 0 || hasLaminationPrice);
      const valid = Boolean(name && hasAnyPrice && numbers.every(value => Number.isFinite(value) && value >= 0 && value <= 10_000_000)
        && QUOTE_OPERATION_BASIS_OPTIONS.some(item => item[0] === basis) && QUOTE_OPERATION_CHARGE_MODE_OPTIONS.some(item => item[0] === chargeMode)
        && (chargeMode !== "package_plus_excess" || (includedQuantity > 0 && minimumCharge > 0))
        && (!["local_m2", "meter", "kg", "hole", "fold", "page", "hour"].includes(basis) || usagePerPiece > 0)
        && (!laminationTierEnabled || (hasLaminationPrice && !invalidLaminationPrice))
        && (!printTierEnabled || (configuredPrintTiers.length > 0 && !invalidPrintTier)));
      unitInput?.classList.toggle("quote-field-error", !valid);
      setupInput?.classList.toggle("quote-field-error", !valid);
      Object.values(laminationPrices).forEach(item => item.input?.classList.toggle("quote-field-error", item.text !== "" && !(item.value > 0)));
      Object.values(printTierInputs).forEach(tier => {
        [tier.minimum, tier.included, tier.unit].forEach(item => item.input?.classList.toggle("quote-field-error", item.text !== "" && (!Number.isFinite(item.value) || item.value < 0)));
        const configured = [tier.minimum, tier.included, tier.unit].some(item => item.value > 0 || item.text !== "");
        if (configured) {
          [tier.included, tier.minimum, tier.unit].forEach(item => item.input?.classList.toggle("quote-field-error", !(item.value > 0)));
        }
      });
      if (!valid) {
        notify(printTierEnabled && (!configuredPrintTiers.length || invalidPrintTier)
          ? "印刷价格至少完整填写一种开数：多少张以内、以内总价、超过后每张价格，三项要一起填写。"
          : laminationTierEnabled && (!hasLaminationPrice || invalidLaminationPrice)
          ? "覆膜自动计价至少填写一个实际使用的开数工价；已填写的工价必须大于0。"
          : chargeMode === "package_plus_excess" ? "套餐计费必须填写套餐费、包含量和超量单价。" : "请填写真实费用；按局部面积、米、kg、孔、折、页或小时计费还要填写每件用量。");
        unitInput?.focus();
        return false;
      }
      const outlierWarning = quotePriceOutlierWarning({ kind:normalizedKind, name, price:unitPrice, setupFee, minimumCharge, basis });
      if (!confirmQuotePriceOutlier(outlierWarning)) {
        unitInput?.focus();
        return false;
      }
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      const current = normalizeQuoteOperationPrices(proposedSettings.quoteOperationPrices);
      const key = `${normalizedKind}:${name.toLowerCase().replace(/\s+/g, "")}`;
      const next = {
        name, kind: normalizedKind, basis: (laminationTierEnabled || printTierEnabled) ? "sheet" : basis, chargeMode: printTierEnabled ? "minimum" : chargeMode,
        unitPrice: Math.round(unitPrice * 1_000_000) / 1_000_000,
        setupFee: Math.round(setupFee * 1_000_000) / 1_000_000,
        minimumCharge: Math.round(minimumCharge * 1_000_000) / 1_000_000,
        includedQuantity: Math.round(includedQuantity * 1_000_000) / 1_000_000,
        excessUnitPrice: Math.round(excessUnitPrice * 1_000_000) / 1_000_000,
        usagePerPiece: Math.round(usagePerPiece * 1_000_000) / 1_000_000,
        updatedAt: new Date().toISOString()
      };
      if (laminationTierEnabled) {
        next.laminationTierPricing = { enabled: true };
        Object.entries(laminationPrices).forEach(([field, item]) => {
          if (item.value > 0) next.laminationTierPricing[field] = Math.round(item.value * 1_000_000) / 1_000_000;
        });
      }
      if (printTierEnabled) {
        next.printSheetTierPricing = { schemaVersion:1, enabled:true, selection:"auto_by_press_sheet" };
        configuredPrintTiers.forEach(tier => {
          next.printSheetTierPricing[tier.definition.id] = {
            unitPrice:Math.round(tier.unit.value * 1_000_000) / 1_000_000,
            minimumCharge:Math.round(tier.minimum.value * 1_000_000) / 1_000_000,
            includedQuantity:Math.round(tier.included.value * 1_000_000) / 1_000_000,
            excessUnitPrice:Math.round(tier.unit.value * 1_000_000) / 1_000_000
          };
        });
      }
      const index = current.findIndex(item => `${item.kind}:${item.name.toLowerCase().replace(/\s+/g, "")}` === key);
      if (index >= 0) current[index] = next; else current.push(next);
      proposedSettings.quoteOperationPrices = normalizeQuoteOperationPrices(current);
      if (normalizedKind === "process" && unitPrice > 0) proposedSettings.processRules = mergeQuoteRuleCosts(proposedSettings.processRules || "", [{ name, price: unitPrice }]);
      proposedSettings.quoteRulesUpdatedAt = next.updatedAt;
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("服务端没有确认这次费用修改");
        }
        state.settings = proposedSettings;
        saveState();
        renderQuoteProductConfigurator();
        syncQuoteCatalogToLegacyFields({ preserveProduct: true, preserveMaterial: true });
        renderQuoteRules();
        notify(printTierEnabled
          ? `${name}已保存 ${configuredPrintTiers.length} 种开数的数量阶梯价；报价会按开数和实际印张自动计算。`
          : `${name}费用已保存：按${quoteOperationBasisUnit(basis)}计价，${quoteOperationChargeModeLabel(chargeMode)}。`);
        return true;
      } catch (error) {
        notify(`费用没有保存：${error.message}。原价格未改变。`);
        return false;
      }
    }
    async function deleteQuoteCatalogOperationPrice(nameCode, kind) {
      if (!requireQuoteCatalogPreferenceEdit()) return false;
      const name = canonicalQuoteOperationLabel(decodeURIComponent(nameCode || "")).slice(0, 80);
      const normalizedKind = kind === "print" ? "print" : "process";
      const key = normalizeRuleText(name);
      const currentPrices = normalizeQuoteOperationPrices(state.settings.quoteOperationPrices);
      const currentRules = parseRules(state.settings.processRules || "");
      const hasSavedPrice = currentPrices.some(item => item.kind === normalizedKind && normalizeRuleText(item.name) === key)
        || currentRules.some(rule => normalizeRuleText(rule.name) === key);
      if (!name || !hasSavedPrice) {
        notify("这项没有单独保存的工艺费用；如价格来自成本库，请到价格表中管理。");
        return false;
      }
      if (!confirm(`确定删除“${name}”的单位费和固定费吗？\n\n删除后报价不再使用这条单独费用；如成本库中另有同名价格，系统会改用成本库价格。`)) return false;
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      proposedSettings.quoteOperationPrices = normalizeQuoteOperationPrices(proposedSettings.quoteOperationPrices)
        .filter(item => !(item.kind === normalizedKind && normalizeRuleText(item.name) === key));
      proposedSettings.processRules = removeQuoteRuleCost(proposedSettings.processRules || "", name);
      proposedSettings.quoteRulesUpdatedAt = new Date().toISOString();
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("服务端没有确认这次工艺费用删除");
        }
        state.settings = proposedSettings;
        saveState();
        renderQuoteProductConfigurator();
        syncQuoteCatalogToLegacyFields({ preserveProduct: true, preserveMaterial: true });
        renderQuoteRules();
        notify(`${name}的单独工艺费用已删除。`);
        return true;
      } catch (error) {
        notify(`工艺费用没有删除：${error.message}。原费用仍保留。`);
        return false;
      }
    }
    function quoteCatalogSelectedOperationRequirements() {
      const entry = quoteCatalogEntry();
      const profile = quoteCatalogProfile();
      if (!profile) return [];
      const draft = quoteCatalogDraft();
      const requirements = profile.groups.flatMap(group => {
        const kind = quoteCatalogGroupPriceKind(group);
        if (!kind) return [];
        return quoteCatalogSelectedLabels(group, draft).filter(label => !quoteCatalogOperationIsNoOp(label)).map(label => {
          const name = canonicalQuoteOperationLabel(label);
          const price = quoteCatalogOperationPriceSnapshot(name, kind);
          return { ...price, name, kind, label };
        });
      });
      // Do not synthesize the legacy “基础印刷” row. It was priced per
      // finished piece and bypassed the factory's full/two/four/eight-open
      // table. Offset-print products expose print_color explicitly; other
      // products use their own output/process pricing.
      return requirements;
    }
    function quoteCatalogSelectedOperationPrices() {
      return quoteCatalogSelectedOperationRequirements().filter(item => item.configured);
    }
    function quoteAutomaticFlatSizeResult() {
      const engine = window.JIANGLAI_QUOTE_FORMULA;
      const entry = quoteCatalogEntry();
      if (!entry || typeof engine?.estimateFlatSize !== "function") return { status:"manual_required", note:"展开尺寸引擎尚未加载" };
      const configuration = quoteCatalogConfigurationSnapshot();
      const structure = (configuration?.groups || [])
        .filter(group => /structure|box_structure|bag_structure|folder_process/.test(String(group.id || "")))
        .flatMap(group => group.values || []).join(" ");
      return engine.estimateFlatSize({ profileId:entry.profile || "", finishedSize:val("quoteSize"), structure });
    }
    function quoteOperationPriceCalculation(item, quantity) {
      const engine = window.JIANGLAI_QUOTE_FORMULA;
      const qty = Math.max(0, Number(quantity || 0));
      if (typeof engine?.operationBasisQuantity !== "function" || typeof engine?.operationCharge !== "function") {
        return { ok:false, error:"工艺计价引擎未加载" };
      }
      if (["base_print_sheet_minimum", "lamination_sheet_minimum"].includes(item?.pricingModel)) {
        const automatic = quoteAutomaticFlatSizeResult();
        const flatSize = String(document.getElementById("quoteMaterialFlatSize")?.value || "").trim()
          || (automatic?.widthMm && automatic?.heightMm ? `${automatic.widthMm}×${automatic.heightMm}` : "");
        const configuration = quoteCatalogConfigurationSnapshot();
        const printSideText = (configuration?.groups || []).find(group => group.id === "print_side")?.values?.join(" ") || "";
        const explicitSideCount = item?.pricingModel === "base_print_sheet_minimum"
          ? (printSideText.includes("双面") ? 2 : 1)
          : Math.max(1, Number(document.getElementById("quoteLaminationSideCount")?.value || (String(item?.name || "").includes("双面") ? 2 : 1)));
        const materialWasteRate = rateNum("quoteMaterialWasteRate", Number(state.settings.loss || .05));
        const recommendation = window.JIANGLAI_QUOTE_COST_DEFAULTS?.recommendPressSheet({
          flatSize,
          quantity: qty,
          sideCount: explicitSideCount,
          setupWasteSheets:Number(window.JIANGLAI_QUOTE_COST_DEFAULTS?.publicPrintReferences?.setupWasteSheets || 100),
          printWasteRate:materialWasteRate * 100,
          finishingWasteRate:0
        });
        const explicitLaminationSheets = item?.pricingModel === "lamination_sheet_minimum"
          ? Math.max(0, Number(document.getElementById("quoteLaminationQuantity")?.value || 0))
          : 0;
        const productionSheets = explicitLaminationSheets || Math.max(0, Number(recommendation?.productionSheets || 0));
        if (!(productionSheets > 0)) {
          return { ok:false, error:`${item?.pricingModel === "lamination_sheet_minimum" ? "覆膜" : "基础印刷"}按生产印张计价，需要先确认单件展开尺寸` };
        }
        const basisQuantity = productionSheets * explicitSideCount;
        const unitRate = Math.max(0, Number(item?.unitPrice || 0));
        const minimumCharge = Math.max(0, Number(item?.minimumCharge || 0));
        const total = Number(item?.setupFee || 0) + Math.max(minimumCharge, basisQuantity * unitRate);
        return { ok:true, total, basisQuantity, productionSheets, sideCount:explicitSideCount, basis:"sheet", unitRate,
          chargeMode:"minimum", minimumCharge, includedQuantity:0,
          sheetFormat:recommendation || null, estimatedProductionSheets:!explicitLaminationSheets };
      }
      if (item?.basis === "sheet" && !item?.printSheetTierPricing && !item?.laminationTierPricing
        && String(document.getElementById("quoteCostMode")?.value || "") !== "printing") {
        return { ok:false, error:`${item?.name || "该工艺"}按生产张计价，必须先使用“精细印刷公式”完成拼版；系统不会把成品数量冒充生产张数` };
      }
      let flatSize = String(document.getElementById("quoteMaterialFlatSize")?.value || document.getElementById("quotePrintFlatSize")?.value || "").trim();
      if (!flatSize) {
        const automatic = quoteAutomaticFlatSizeResult();
        if (automatic?.widthMm && automatic?.heightMm) flatSize = `${automatic.widthMm}×${automatic.heightMm}`;
      }
      let areaM2 = 0;
      let flatWidthMm = 0;
      let flatHeightMm = 0;
      let printSheetWidthMm = 0;
      let printSheetHeightMm = 0;
      if (item?.basis === "area_m2" || item?.laminationTierPricing) {
        try {
          const laminationSize = item?.laminationTierPricing
            ? String(document.getElementById("quoteLaminationSize")?.value || flatSize).trim()
            : flatSize;
          const [widthMm, heightMm] = engine.parseDimensions(laminationSize, 2);
          flatWidthMm = widthMm;
          flatHeightMm = heightMm;
          areaM2 = widthMm / 1000 * heightMm / 1000 * qty;
        } catch (_) {
          return { ok:false, error:item?.laminationTierPricing
            ? `${item?.name || "覆膜"}需要实际覆膜尺寸，才能自动判断开数`
            : `${item?.name || "该工艺"}按平方米计价，但还没有可用的单件展开尺寸` };
        }
      }
      if (item?.printSheetTierPricing) {
        const printSheetSize = String(document.getElementById("quotePrintSheetSize")?.value || "").trim();
        if (!printSheetSize && String(document.getElementById("quotePrintSheetFormat")?.value || "auto") === "auto") {
          return { ok:false, error:`${item?.name || "印刷"}按上机开数计价，请先填写上机纸尺寸或手动选择印刷开数` };
        }
        if (printSheetSize) {
          try {
            [printSheetWidthMm, printSheetHeightMm] = engine.parseDimensions(printSheetSize, 2);
          } catch (_) {
            return { ok:false, error:"上机纸尺寸格式不正确，请按宽×高填写，例如 393×546mm" };
          }
        }
      }
      try {
        const configuration = quoteCatalogConfigurationSnapshot();
        const selectedSideText = (configuration?.groups || []).find(group => group.id === "print_side")?.values?.join(" ") || "";
        const printSideCount = Number(document.getElementById("quotePrintSideCount")?.value || (selectedSideText.includes("双面") ? 2 : 1));
        const printTierRate = item?.printSheetTierPricing && typeof engine.printSheetTierRate === "function"
          ? engine.printSheetTierRate(item, {
              printSheetWidthMm,
              printSheetHeightMm,
              sheetWidthMm:printSheetWidthMm,
              sheetHeightMm:printSheetHeightMm,
              productionSheets:qty,
              printSheetQuantity:qty,
              printSideCount,
              printSheetFormat:document.getElementById("quotePrintSheetFormat")?.value || "auto",
              printSheetStandard:document.getElementById("quotePrintSheetStandard")?.value || "auto"
            }) : null;
        const laminationRate = item?.laminationTierPricing && typeof engine.laminationTierRate === "function"
          ? engine.laminationTierRate(item, {
              pieceWidthMm: flatWidthMm,
              pieceHeightMm: flatHeightMm,
              laminationWidthMm: flatWidthMm,
              laminationHeightMm: flatHeightMm,
              productionSheets: qty,
              laminationSheetFormat: document.getElementById("quoteLaminationSheetFormat")?.value || "auto",
              laminationSheetStandard: document.getElementById("quoteLaminationSheetStandard")?.value || "auto",
              laminationSideCount: Number(document.getElementById("quoteLaminationSideCount")?.value || 1),
              laminationQuantity: Number(document.getElementById("quoteLaminationQuantity")?.value || qty),
              laminationFilmType: document.getElementById("quoteLaminationFilmType")?.value || "auto"
            }) : null;
        const tierRate = printTierRate || laminationRate;
        const usagePerPiece = Math.max(0, Number(item?.usagePerPiece || 0));
        const basisSpecificFields = {
          localAreaM2:item?.basis === "local_m2" ? qty * usagePerPiece : undefined,
          countPerPiece:["hole", "fold"].includes(item?.basis) ? usagePerPiece : undefined,
          pagesPerPiece:item?.basis === "page" ? usagePerPiece : undefined
        };
        const effectiveItem = tierRate
          ? { ...item, ...basisSpecificFields, basis:"sheet", unitRate:tierRate.unitRate }
          : { ...item, ...basisSpecificFields };
        const basisQuantity = tierRate?.basisQuantity ?? engine.operationBasisQuantity(effectiveItem, {
          quantity: qty,
          productionSheets: qty,
          areaM2,
          runs: item?.basis === "run" ? Math.max(1, Number(item?.usagePerPiece || 1)) : undefined,
          hours: item?.basis === "hour" ? qty * Number(item?.usagePerPiece || 0) : undefined,
          meters: item?.basis === "meter" ? qty * Number(item?.usagePerPiece || 0) : undefined,
          kg: item?.basis === "kg" ? qty * Number(item?.usagePerPiece || 0) : undefined
        });
        if (qty > 0 && basisQuantity <= 0 && !["run"].includes(item?.basis)) {
          return { ok:false, error:`${item?.name || "该工艺"}按${quoteOperationBasisUnit(item?.basis)}计价，但计价用量尚未填写` };
        }
        const unitRate = tierRate?.unitRate ?? (item?.chargeMode === "package_plus_excess"
          ? Number(item?.excessUnitPrice ?? item?.unitPrice ?? 0)
          : Number(item?.unitPrice || 0));
        const effectiveMinimumCharge = Number(tierRate?.minimumCharge ?? item?.minimumCharge ?? 0);
        const effectiveIncludedQuantity = Number(tierRate?.includedQuantity ?? item?.includedQuantity ?? 0);
        const effectiveChargeMode = tierRate?.chargeMode || item?.chargeMode || "minimum";
        const total = engine.operationCharge({
          setupFee:Number(item?.setupFee || 0),
          unitRate,
          basisQuantity,
          minimumCharge:effectiveMinimumCharge,
          chargeMode:effectiveChargeMode,
          includedQuantity:effectiveIncludedQuantity,
          packageQuantity:effectiveIncludedQuantity,
          packageFee:Number(tierRate?.packageFee ?? effectiveMinimumCharge)
        });
        return { ok:true, total, basisQuantity, basis:effectiveItem?.basis || "piece", unitRate, sheetFormat:tierRate?.sheetFormat || null,
          chargeMode:effectiveChargeMode, minimumCharge:effectiveMinimumCharge, includedQuantity:effectiveIncludedQuantity,
          printSideCount:printTierRate?.sideCount || null, laminationSideCount:laminationRate?.sideCount || null, laminationFilmType:laminationRate?.filmType || "" };
      } catch (error) {
        return { ok:false, error:error.message || "工艺计价参数不完整" };
      }
    }
    function focusQuoteCostMode(requiredMode = "printing") {
      const select = document.getElementById("quoteCostMode");
      if (!select) return;
      const details = select.closest("details");
      if (details) details.open = true;
      select.classList.add("quote-field-error");
      select.setAttribute("aria-invalid", String(select.value !== requiredMode));
      window.setTimeout(() => { select.scrollIntoView({ behavior:"smooth", block:"center" }); select.focus({ preventScroll:true }); }, 0);
      select.addEventListener("change", () => { select.classList.remove("quote-field-error"); select.removeAttribute("aria-invalid"); }, { once:true });
    }
    function quoteOperationPriceTotal(item, quantity) {
      const result = quoteOperationPriceCalculation(item, quantity);
      return result.ok ? result.total : NaN;
    }
    function quoteOperationRuleDescription(item, quantity) {
      const calculation = quoteOperationPriceCalculation(item, quantity);
      if (!calculation.ok) return calculation.error;
      if (["base_print_sheet_minimum", "lamination_sheet_minimum"].includes(item?.pricingModel)) {
        return `最低收费与按印张取高 · ${Number(calculation.basisQuantity || 0).toLocaleString()}张 × ¥${Number(calculation.unitRate || 0).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}，最低收费 ¥${Number(calculation.minimumCharge || 0).toFixed(2)}`;
      }
      const unit = quoteOperationBasisUnit(calculation.basis || item?.basis);
      const quantityText = Number(calculation.basisQuantity || 0).toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
      const rateText = Number(calculation.unitRate || 0).toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
      const parts = [];
      if (calculation.sheetFormat?.label) parts.push(`${calculation.sheetFormat.automatic ? "自动识别" : "手动指定"}${calculation.sheetFormat.label}`);
      if (calculation.printSideCount) parts.push(calculation.printSideCount === 2 ? "双面印刷" : "单面印刷");
      if (calculation.laminationSideCount) parts.push(calculation.laminationSideCount === 2 ? "双面覆膜" : "单面覆膜");
      if (calculation.chargeMode === "package_plus_excess") {
        const includedQuantity = Math.max(0, Number(calculation.includedQuantity || 0));
        const excessQuantity = Math.max(0, Number(calculation.basisQuantity || 0) - includedQuantity);
        parts.push(`套餐 ¥${Number(calculation.minimumCharge || 0).toFixed(2)}（含 ${includedQuantity}${unit}）`);
        if (excessQuantity > 0) parts.push(`超出 ${excessQuantity.toFixed(3).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1")}${unit} × ¥${rateText}`);
      } else {
        parts.push(`${quantityText}${unit} × ¥${rateText}`);
      }
      if (Number(item?.setupFee || 0) > 0) parts.push(`固定费 ¥${Number(item.setupFee).toFixed(2)}`);
      if (calculation.chargeMode !== "package_plus_excess" && Number(calculation.minimumCharge || 0) > 0) parts.push(`最低收费 ¥${Number(calculation.minimumCharge).toFixed(2)}`);
      return `${quoteOperationChargeModeLabel(calculation.chargeMode || item?.chargeMode)} · ${parts.join(" + ")}`;
    }
    function quoteBookPricingContext(pageCount = "") { const rates = { "试单客户":state.settings.profitTrial, "普通客户":state.settings.profitNormal, "长期客户":state.settings.profitLong, "VIP客户":state.settings.profitVip }, library = ensureCostLibrary(); return { engine:window.JIANGLAI_QUOTE_FORMULA, configuration:quoteCatalogConfigurationSnapshot(), pageCount, quantity:Number(val("quoteQty") || 1), settings:state.settings, profitRate:Number(rates[val("quoteLevel")] ?? state.settings.profit ?? 0), taxIncluded:val("quoteTaxMode") === "含税", materialPrice:name => quoteCatalogMaterialPriceSnapshot(name), operationPrice:(name, kind) => quoteCatalogOperationPriceSnapshot(canonicalQuoteOperationLabel(name), kind), costItem:(name, types) => findActiveCostItem(name, types), costVersionId:library.activeVersionId || "", costVersionEffectiveAt:library.effectiveAt || "" }; }
    function quoteBookPagePriceLabel(pageCount) { return quoteCatalogEntry()?.profile === "book" ? (window.JIANGLAI_BOOK_QUOTE?.optionLabel(quoteBookPricingContext(pageCount)) || "") : ""; }
    function refreshQuoteBookPagePrices() { const group = quoteCatalogProfile()?.groups?.find(item => item.id === "page_count"); if (!group) return; document.querySelectorAll('[data-quote-group="page_count"] input').forEach(input => { const option = group.options.find(item => item.id === input.value), label = input.parentElement?.querySelector(".quote-page-price"); if (option && label) label.textContent = quoteBookPagePriceLabel(option.label); }); }
    function quoteCatalogOptionPriceText(group, label) {
      if (quoteCatalogGroupIsMaterial(group)) {
        const price = quoteCatalogMaterialPriceSnapshot(label);
        if (price.price === null) return "待改价";
        const decimals = Number(price.price) >= 100 ? 2 : 4;
        return `¥${Number(price.price).toFixed(decimals).replace(/0+$/, "").replace(/\.$/, "")}/${String(price.unit || "元/个").replace("\u5143/", "")}`;
      }
      const kind = quoteCatalogGroupPriceKind(group);
      if (!kind || quoteCatalogOperationIsNoOp(label)) return "";
      const price = quoteCatalogOperationPriceSnapshot(label, kind);
      if (!price.configured) return "待改价";
      if (price.printSheetTierPricing) return "已设印刷开数价";
      if (price.laminationTierPricing) return "已设覆膜开数价";
      const unitRate = Number(price.chargeMode === "package_plus_excess" ? price.excessUnitPrice ?? price.unitPrice ?? 0 : price.unitPrice || 0);
      const unit = quoteOperationBasisUnit(price.basis || "piece");
      const rateLabel = unitRate > 0 ? `¥${unitRate.toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}/${unit}` : "";
      const fixed = Number(price.setupFee || 0) > 0 ? `固定¥${Number(price.setupFee).toFixed(0)}` : "";
      const minimum = Number(price.minimumCharge || 0) > 0 ? `保底¥${Number(price.minimumCharge).toFixed(0)}` : "";
      return [rateLabel, fixed, minimum].filter(Boolean).join("·") || "已设价";
    }
    function openQuotePriceEditorForGroup(groupCode) {
      const groupId = decodeURIComponent(groupCode || "");
      const workbench = document.querySelector(".quote-product-price-workbench");
      const editor = document.querySelector(`[data-price-group="${CSS.escape(groupId)}"]`);
      if (!workbench || !editor) {
        notify("这个选项没有独立费用，请到全厂价格库核对。");
        return false;
      }
      workbench.open = true;
      editor.open = true;
      window.setTimeout(() => editor.scrollIntoView({ behavior:"smooth", block:"center" }), 0);
      return true;
    }
    function renderQuoteProductConfigurator() {
      const box = document.getElementById("quoteProductConfigurator");
      const entry = quoteCatalogEntry();
      const profile = quoteCatalogProfile(entry);
      renderDirectQuoteProductBar();
      if (!box) return;
      if (!entry || !profile) {
        box.innerHTML = `<div class="muted">选择产品后，会按顺序显示材料、尺寸、印刷和必选工艺。</div>`;
        renderQuoteQuantityPresets(null);
        renderQuoteCatalogSummary();
        return;
      }
      const draft = quoteCatalogDraft();
      const groups = Array.isArray(profile.groups) ? profile.groups : [];
      const renderGroup = group => {
        const selected = draft.selections[group.id];
        const selectedIds = new Set(Array.isArray(selected) ? selected : selected ? [selected] : []);
        const controls = group.options.map(item => { const pagePrice = group.id === "page_count" ? quoteBookPagePriceLabel(item.label) : "", priceText = quoteCatalogOptionPriceText(group, item.label); return `<label><input type="${group.type === "multi" ? "checkbox" : "radio"}" name="quote-option-${escapeHtml(group.id)}" value="${escapeHtml(item.id)}" ${selectedIds.has(item.id) ? "checked" : ""} onchange="changeQuoteCatalogOption('${encodeURIComponent(group.id)}','${encodeURIComponent(item.id)}',${group.type === "multi" ? "true" : "false"},this.checked)"><span>${escapeHtml(item.label)}${pagePrice ? `<small class="quote-page-price">${escapeHtml(pagePrice)}</small>` : ""}${priceText ? `<small class="quote-option-price-inline">${escapeHtml(priceText)}</small>` : ""}</span></label>`; }).join("");
        const selectedLabels = quoteCatalogSelectedLabels(group, draft);
        const customSelected = selectedLabels.some(label => label.includes("自定义"));
        const customPlaceholder = group.id === "ink_coverage" ? "请输入具体印刷方式或油墨覆盖要求" : `请输入${group.label}`;
        const customInput = customSelected ? `<input class="quote-option-custom" value="${escapeHtml(draft.custom[group.id] || "")}" placeholder="${escapeHtml(customPlaceholder)}" oninput="setQuoteCatalogCustomValue('${encodeURIComponent(group.id)}',this.value)">` : "";
        const canEditPrice = quoteCatalogGroupIsMaterial(group) || Boolean(quoteCatalogGroupPriceKind(group));
        return `<div class="quote-option-group" data-quote-group="${escapeHtml(group.id)}"><div class="quote-option-label"><span>${escapeHtml(group.label)}${group.required ? "<em>*</em>" : ""}</span>${canEditPrice ? `<button class="quote-inline-price-button" type="button" onclick="openQuotePriceEditorForGroup('${encodeURIComponent(group.id)}')">改价</button>` : ""}</div><div><div class="quote-option-values">${controls}</div>${customInput}</div></div>`;
      };
      const requiredGroups = groups.filter(group => group.required);
      const optionalGroups = groups.filter(group => !group.required);
      const pricingGroups = groups.filter(group => quoteCatalogGroupIsMaterial(group) || quoteCatalogGroupPriceKind(group));
      const selectedPricing = pricingGroups.flatMap(group => {
        const labels = quoteCatalogSelectedLabels(group, draft).filter(label => !quoteCatalogOperationIsNoOp(label));
        return labels.map(label => {
          if (quoteCatalogGroupIsMaterial(group)) {
            const snapshot = quoteCatalogMaterialPriceSnapshot(label);
            return { label, configured:snapshot.price !== null, reference:String(snapshot.source || "").includes("行业参考价") };
          }
          const snapshot = quoteCatalogOperationPriceSnapshot(label, quoteCatalogGroupPriceKind(group));
          return { label, configured:snapshot.configured, reference:String(snapshot.source || "").includes("行业参考价") };
        });
      });
      const missingSelectedPrices = selectedPricing.filter(item => !item.configured);
      const referenceSelectedPrices = selectedPricing.filter(item => item.configured && item.reference);
      const pricingStatus = missingSelectedPrices.length ? `已选项还缺 ${missingSelectedPrices.length} 项价格`
        : referenceSelectedPrices.length ? `已选项有 ${referenceSelectedPrices.length} 项待改为本厂价` : "已选项本厂价格齐全";
      const pricingWorkbench = pricingGroups.length ? `<details class="quote-product-price-workbench" ${missingSelectedPrices.length || referenceSelectedPrices.length ? "open" : ""}>
        <summary><span><strong>修改本品材料、印刷与工艺价格</strong><small>所有品类都在这里统一改价，不再分散在各选项下</small></span><em>${pricingStatus}</em></summary>
        <div class="quote-product-price-workbench-note">可维护本品全部可选材料、印刷方式、后道工艺、打样、包装和交付服务价格。正式报价只计算当前实际选中项。</div>
        ${pricingGroups.map(group => quoteCatalogMaterialPriceEditor(group) || quoteCatalogOperationPriceEditor(group)).join("")}
        <button class="btn light quote-product-price-library-button" type="button" onclick="showPage('报价参数')">管理全厂材料、人工和设备价格库</button>
      </details>` : "";
      box.innerHTML = `<div class="quote-configurator-title"><div><i>2</i><strong>${escapeHtml(entry.name)} · 产品信息</strong></div><span>${escapeHtml(profile.label || "动态参数")}</span></div>`
        + requiredGroups.map(renderGroup).join("")
        + (optionalGroups.length ? `<details class="quote-optional-groups"><summary>更多工艺与服务（${optionalGroups.length} 项，可选）</summary>${optionalGroups.map(renderGroup).join("")}</details>` : "")
        + pricingWorkbench;
      renderQuoteQuantityPresets(profile);
      renderQuoteCatalogSummary();
      scheduleQuoteLiveDock();
    }
    function renderQuoteQuantityPresets(profile = quoteCatalogProfile()) {
      const box = document.getElementById("quoteQuantityPresets");
      if (!box) return;
      const presets = Array.isArray(profile?.quantityPresets) && profile.quantityPresets.length
        ? profile.quantityPresets
        : [100, 500, 1000, 2000, 5000];
      const current = Number(String(val("quoteQty") || "").replace(/[^\d.]/g, ""));
      box.innerHTML = presets.map(qty => `<button type="button" class="${current === Number(qty) ? "active" : ""}" onclick="setQuoteCatalogQuantity(${Number(qty)})">${Number(qty).toLocaleString()} 个</button>`).join("");
    }
    function quoteCatalogConfigurationSnapshot() {
      const entry = quoteCatalogEntry();
      const profile = quoteCatalogProfile(entry);
      if (!entry || !profile) return null;
      const draft = quoteCatalogDraft();
      const groups = profile.groups.map(group => ({
        id: group.id,
        label: group.label,
        required: Boolean(group.required),
        values: quoteCatalogSelectedLabels(group, draft),
        customValue: String(draft.custom[group.id] || "").trim()
      }));
      return {
        schemaVersion: Number(quoteCatalogData().schemaVersion || 2),
        catalogVersion: String(quoteCatalogData().catalogVersion || ""),
        categoryId: entry.categoryId,
        categoryName: entry.categoryName,
        productId: entry.id,
        productName: entry.name,
        profile: entry.profile,
        groups,
        quantity: Number(val("quoteQty") || 0)
      };
    }
    function quoteCatalogConfigurationText(configuration = quoteCatalogConfigurationSnapshot()) {
      if (!configuration) return "";
      const parts = configuration.groups.flatMap(group => {
        const values = group.customValue
          ? [...group.values.filter(value => !String(value).includes("自定义")), group.customValue]
          : [...group.values];
        return values.length ? [`${group.label}：${values.join("、")}`] : [];
      });
      return parts.join("；");
    }
    function renderQuoteCatalogSummary() {
      const box = document.getElementById("quoteCatalogSummary");
      if (!box) return;
      const configuration = quoteCatalogConfigurationSnapshot();
      if (!configuration) {
        box.innerHTML = "请选择一个产品；如果目录里没有，也可以展开下方手动填写。";
        return;
      }
      const text = quoteCatalogConfigurationText(configuration);
      box.innerHTML = `<strong>${escapeHtml(configuration.categoryName)} · ${escapeHtml(configuration.productName)}</strong>${text ? `<br>${escapeHtml(text)}` : " · 请选择材料、尺寸和必要参数"}`;
    }
    function syncQuoteCatalogToLegacyFields(options = {}) {
      const entry = quoteCatalogEntry();
      const profile = quoteCatalogProfile(entry);
      if (!entry || !profile) return;
      const draft = quoteCatalogDraft();
      const groupLabels = Object.fromEntries(profile.groups.map(group => [group.id, quoteCatalogSelectedLabels(group, draft)]));
      const custom = draft.custom || {};
      if (!options.preserveProduct) setIfExists("quoteProduct", entry.name);
      const materialParts = ["material", "cover_material", "inner_material"].flatMap(id => groupLabels[id] || []);
      if (!options.preserveMaterial && materialParts.length) setIfExists("quoteMaterial", materialParts.join(" / "));
      const sizeLabel = (groupLabels.size || [])[0] || "";
      if (!options.preserveSize && sizeLabel) setIfExists("quoteSize", sizeLabel.includes("自定义") ? String(custom.size || "") : sizeLabel);
      const processGroups = profile.groups.filter(group => quoteCatalogGroupPriceKind(group));
      const processes = processGroups.flatMap(group => {
        const labels = quoteCatalogSelectedLabels(group, draft);
        return labels.map(canonicalQuoteOperationLabel);
      });
      if (!options.preserveProcess) {
        const printable = quoteCatalogProfileIsPrintable(entry);
        setIfExists("quoteProcess", processes.length ? [...new Set(processes)].join(", ") : printable ? "印刷" : "");
      }
      renderQuoteReviewSummary();
      renderQuoteMaterialCostCheck(true);
      markQuoteDirty();
    }
    function selectQuoteCatalogCategory(categoryCode) {
      const categoryId = decodeURIComponent(categoryCode || "");
      if (!quoteCatalogData().categories.some(category => category.id === categoryId)) return;
      quoteCatalogCategoryId = categoryId;
      quoteCatalogSearchText = "";
      setIfExists("quoteCatalogSearch", "");
      renderQuoteCategoryTabs();
      renderQuoteProductPicker();
      if (quoteCatalogEntry()?.categoryId !== categoryId) {
        quoteCatalogProductId = "";
        renderQuoteProductConfigurator();
      }
    }
    function quoteRecommendedCostMode(entry = quoteCatalogEntry()) { const family = window.JIANGLAI_QUOTE_FORMULA?.profileRule(entry?.profile || "")?.family || "manual", hasPrintTier = ["sheet", "dieline"].includes(family) && quoteCatalogSelectedOperationRequirements().some(item => item.kind === "print" && item.printSheetTierPricing); return window.JIANGLAI_QUOTE_AUTO_ROUTER?.resolve({ profile:entry?.profile, family, hasPrintTier })?.mode || "industry"; }
    function selectQuoteCatalogProduct(productCode, options = {}) {
      const productId = decodeURIComponent(productCode || "");
      const entry = quoteCatalogEntry(productId);
      if (!entry) return false;
      quoteCatalogProductId = entry.id;
      quoteCatalogCategoryId = entry.categoryId;
      quoteCatalogSearchText = "";
      setIfExists("quoteCatalogSearch", "");
      const legacy = quoteCatalogLegacyProfile(entry); window.JIANGLAI_QUOTE_RECOMMENDER?.apply({ product:entry, profile:quoteCatalogProfile(entry), draft:quoteCatalogDraft(entry.id) });
      if (!options.preserveLegacy) {
        setActiveQuoteTemplate(legacy.category, legacy.template);
        renderIndustryCategories();
        renderQuoteTemplateOptions();
        setIfExists("quoteProduct", entry.name);
        setIfExists("quoteSize", "");
        setIfExists("quoteMaterial", "");
        setIfExists("quoteProcess", "");
        setQuoteDeliveryValue(productTemplates[legacy.template]?.delivery || state.settings.delivery || "7");
        if (entry.custom && !quoteCatalogProfile(entry)?.groups?.length) {
          const manual = document.getElementById("quoteManualCompat");
          if (manual) manual.open = true;
        }
      }
      renderQuoteCategoryTabs();
      renderQuoteProductPicker();
      renderQuoteProductConfigurator();
      syncQuoteCatalogToLegacyFields({
        preserveProduct: Boolean(options.preserveLegacy),
        preserveMaterial: Boolean(options.preserveLegacy),
        preserveSize: Boolean(options.preserveLegacy),
        preserveProcess: Boolean(options.preserveLegacy)
      });
      setIfExists("quoteCostMode", quoteRecommendedCostMode(entry)); renderIndustryQuoteCostFields();
      if (options.closeChooser) setQuoteProductChooser(false);
      setActiveQuoteTemplate(legacy.category, legacy.template);
      renderQuoteTemplateOptions();
      saveState();
      setQuoteIndustryWarning(""); setQuoteCostWarning("");
      return true;
    }
    function changeQuoteCatalogOption(groupCode, optionCode, isMulti, checked) {
      const groupId = decodeURIComponent(groupCode || "");
      const optionId = decodeURIComponent(optionCode || "");
      const profile = quoteCatalogProfile();
      const group = profile?.groups?.find(item => item.id === groupId);
      if (!group || !group.options.some(item => item.id === optionId)) return;
      const draft = quoteCatalogDraft();
      if (isMulti) {
        const selected = new Set(Array.isArray(draft.selections[groupId]) ? draft.selections[groupId] : []);
        if (checked) selected.add(optionId); else selected.delete(optionId);
        draft.selections[groupId] = [...selected];
      } else {
        draft.selections[groupId] = checked ? optionId : "";
      }
      const label = quoteCatalogOptionLabel(group, optionId);
      if (!label.includes("自定义")) delete draft.custom[groupId];
      renderQuoteProductConfigurator();
      syncQuoteCatalogToLegacyFields(); setIfExists("quoteCostMode", quoteRecommendedCostMode()); renderIndustryQuoteCostFields();
      document.querySelector(`[data-quote-group="${CSS.escape(groupId)}"]`)?.classList.remove("quote-field-error");
    }
    function setQuoteCatalogCustomValue(groupCode, value) {
      const groupId = decodeURIComponent(groupCode || "");
      quoteCatalogDraft().custom[groupId] = String(value || "").slice(0, 120);
      if (String(value || "").trim()) document.querySelector(`[data-quote-group="${CSS.escape(groupId)}"]`)?.classList.remove("quote-field-error");
      syncQuoteCatalogToLegacyFields();
      renderQuoteCatalogSummary();
    }
    function setQuoteCatalogQuantity(quantity) {
      setIfExists("quoteQty", String(Math.max(1, Number(quantity) || 1)));
      renderQuoteQuantityPresets();
      refreshQuoteBookPagePrices();
      renderQuoteReviewSummary();
      markQuoteDirty();
    }
    function searchQuoteCatalogProducts(text) {
      quoteCatalogSearchText = String(text || "").slice(0, 80);
      renderQuoteProductPicker();
    }
    function validateQuoteCatalogSelection(showNotice = true) {
      if (quoteCatalogPriceAccessMissing()) {
        if (showNotice) notify("当前账号不能读取本厂价格表，请联系老板完成初始化并开放“查看价格表”权限。");
        return false;
      }
      if (quoteCatalogSetupRequired() || !quoteCatalogProducts().length) {
        if (showNotice) {
          openQuoteCatalogProductManager();
          notify("本厂还没有启用可报价产品，请先选择产品并完成成本初始化。");
        }
        return false;
      }
      const entry = quoteCatalogEntry();
      const profile = quoteCatalogProfile(entry);
      if (!entry || !profile) {
        if (showNotice) notify("请先从本厂已启用的产品中选择一个产品。");
        return false;
      }
      const draft = quoteCatalogDraft();
      const missing = profile.groups.filter(group => {
        const values = quoteCatalogSelectedLabels(group, draft);
        if (values.some(value => value.includes("自定义")) && !String(draft.custom[group.id] || "").trim()) return true;
        if (group.id === "handle_color") {
          const handleGroup = profile.groups.find(item => item.id === "handle");
          const handle = quoteCatalogSelectedLabels(handleGroup, draft)[0] || "";
          if (handle && !/不要绳|自备绳/.test(handle) && !values.length) return true;
        }
        if (!group.required) return false;
        if (!values.length) return true;
        return false;
      });
      document.querySelectorAll("[data-quote-group]").forEach(node => node.classList.remove("quote-field-error"));
      const missingNodes = missing.map(group => {
        const node = document.querySelector(`[data-quote-group="${CSS.escape(group.id)}"]`);
        const optionalSection = node?.closest("details.quote-optional-groups");
        if (optionalSection) optionalSection.open = true;
        node?.classList.add("quote-field-error");
        return node;
      }).filter(Boolean);
      if (missing.length && showNotice) {
        missingNodes[0]?.scrollIntoView({ behavior: "smooth", block: "center" });
        notify(`当前产品还缺：${missing.map(group => group.label).join("、")}。请填写红框标注的项目。`);
      }
      return missing.length === 0;
    }
    function quoteCatalogSizeSignature(value) {
      const match = String(value || "").trim().match(
        /(\d+(?:\.\d+)?)\s*[xX×*]\s*(\d+(?:\.\d+)?)(?:\s*[xX×*]\s*(\d+(?:\.\d+)?))?\s*(mm|cm|毫米|厘米|m|米)?/i
      );
      if (!match) return "";
      const unit = String(match[4] || "mm").toLowerCase();
      const scale = unit === "cm" || unit === "厘米" ? 10 : unit === "m" || unit === "米" ? 1000 : 1;
      return match.slice(1, 4).filter(value => value !== undefined).map(value => {
        const millimeters = Number(value) * scale;
        return Number.isFinite(millimeters) ? String(Number(millimeters.toFixed(6))) : "";
      }).filter(Boolean).join("×");
    }
    function quoteCatalogRecognizedSizeSelection(group, value) {
      const raw = Array.isArray(value) ? value.filter(Boolean).join("×") : String(value || "").trim();
      if (!raw || quoteAnalysisIsPending(raw)) return null;
      const signature = quoteCatalogSizeSignature(raw);
      const preset = signature ? group.options.find(item => {
        return !String(item.label || "").includes("自定义")
          && quoteCatalogSizeSignature(item.label) === signature;
      }) : null;
      if (preset) return { optionId:preset.id, customValue:"" };
      const custom = group.options.find(item => String(item.label || "").includes("自定义"));
      return custom ? { optionId:custom.id, customValue:raw } : null;
    }
    function applyQuoteCatalogRecognizedValues(analysis) {
      const product = quoteCatalogFindProduct(analysis?.product);
      if (!product) {
        const fullQuery = normalizeRuleText(analysis?.product || "");
        const disabledProduct = quoteCatalogAllProducts().find(item => [item.name, ...(item.aliases || [])].map(normalizeRuleText).some(name => name && (name === fullQuery || fullQuery.includes(name) || name.includes(fullQuery))));
        quoteCatalogProductId = "";
        quoteCatalogSearchText = "";
        setIfExists("quoteCatalogSearch", "");
        renderQuoteCategoryTabs();
        renderQuoteProductPicker();
        renderQuoteProductConfigurator();
        if (disabledProduct) setQuoteCostWarning(`AI识别到“${disabledProduct.name}”，但本厂尚未启用这个产品。请点击“管理报价产品”添加后再报价。`);
        return false;
      }
      quoteCatalogDrafts[product.id] = { selections: {}, custom: {} };
      if (!selectQuoteCatalogProduct(encodeURIComponent(product.id), { preserveLegacy: true })) return false;
      const profile = quoteCatalogProfile(product);
      const draft = quoteCatalogDraft(product.id);
      const materialText = normalizeRuleText(Array.isArray(analysis?.material) ? analysis.material.join(" ") : analysis?.material || "");
      const rawSize = Array.isArray(analysis?.size) ? analysis.size.filter(Boolean).join("×") : String(analysis?.size || "").trim();
      const processText = normalizeRuleText(Array.isArray(analysis?.process) ? analysis.process.join(" ") : analysis?.process || analysis?.finish || "");
      profile.groups.forEach(group => {
        if (group.id === "size") {
          const sizeSelection = quoteCatalogRecognizedSizeSelection(group, rawSize);
          if (!sizeSelection) return;
          draft.selections[group.id] = sizeSelection.optionId;
          if (sizeSelection.customValue) draft.custom[group.id] = sizeSelection.customValue;
          else delete draft.custom[group.id];
          return;
        }
        const source = /material/.test(group.id) ? materialText : processText;
        if (!source) return;
        const matched = group.options.filter(item => {
          const label = normalizeRuleText(item.label);
          return label && (source.includes(label) || label.includes(source));
        });
        if (!matched.length) return;
        draft.selections[group.id] = group.type === "multi" ? matched.map(item => item.id) : matched[0].id;
      });
      syncQuoteCatalogToLegacyFields(); renderQuoteProductConfigurator();
      return true;
    }
    function restoreQuoteCatalogConfiguration(configuration) {
      if (!configuration || typeof configuration !== "object") return false;
      const entry = quoteCatalogFullEntry(String(configuration.productId || ""));
      if (!entry) return false;
      const enabledEntry = quoteCatalogEntry(entry.id);
      if (!enabledEntry) {
        notify(`历史报价中的“${entry.name}”目前未启用或所在分类已隐藏。请先在“管理报价产品”中恢复后再编辑；历史记录本身没有丢失。`);
        return false;
      }
      const profile = quoteCatalogProfile(enabledEntry);
      const draft = quoteCatalogDraft(entry.id);
      (configuration.groups || []).forEach(savedGroup => {
        const group = profile?.groups?.find(item => item.id === savedGroup.id);
        if (!group) return;
        const ids = (savedGroup.values || []).map(value => group.options.find(option => option.label === value)?.id).filter(Boolean);
        draft.selections[group.id] = group.type === "multi" ? ids : ids[0] || "";
        if (savedGroup.customValue) draft.custom[group.id] = String(savedGroup.customValue).slice(0, 120);
      });
      window.JIANGLAI_QUOTE_RECOMMENDER?.apply({ product:entry, profile, draft });
      quoteCatalogProductId = entry.id;
      quoteCatalogCategoryId = entry.categoryId;
      renderQuoteCategoryTabs();
      renderQuoteProductPicker();
      renderQuoteProductConfigurator();
      return true;
    }
    function initializeQuoteCatalog() {
      const catalog = quoteCatalogData(); quoteCatalogCategoryId = catalog.categories[0]?.id || ""; quoteCatalogProductId = "";
      const matched = quoteCatalogFindProduct(val("quoteProduct")), initialProduct = matched || quoteCatalogProducts()[0] || null;
      if (initialProduct) selectQuoteCatalogProduct(encodeURIComponent(initialProduct.id), { preserveLegacy:Boolean(matched), keepChooser:true });
      else { renderQuoteCategoryTabs(); renderQuoteProductPicker(); renderQuoteProductConfigurator(); }
      setQuoteProductChooser(true); renderQuoteCatalogSetup();
    }
    function ensureDirectQuoteProduct() {
      if (quoteCatalogEntry()) { renderDirectQuoteProductBar(); return true; }
      const legacyProduct = val("quoteProduct").trim();
      const matchedProduct = quoteCatalogFindProduct(legacyProduct);
      if (legacyProduct && !/(?:待确认|定制产品)/.test(legacyProduct) && !matchedProduct) {
        renderDirectQuoteProductBar();
        return false;
      }
      const product = matchedProduct || quoteCatalogProducts().find(item => item.categoryId === quoteCatalogCategoryId) || quoteCatalogProducts()[0]; if (!product) { renderDirectQuoteProductBar(); return false; }
      return selectQuoteCatalogProduct(encodeURIComponent(product.id), { preserveLegacy:Boolean(matchedProduct), keepChooser:true });
    }
    function quoteProfileSnapshot(source) {
      return Object.fromEntries(quoteProfileKeys.map(key => [key, source[key]]));
    }
    function ensureIndustryQuoteProfiles() {
      state.settings.quoteProfiles = state.settings.quoteProfiles && typeof state.settings.quoteProfiles === "object"
        ? state.settings.quoteProfiles
        : {};
      const category = state.settings.industryCategory || categoryForTemplate(state.settings.factoryType);
      if (state.settings.configured && !state.settings.quoteProfiles[category]) {
        state.settings.quoteProfiles[category] = quoteProfileSnapshot(state.settings);
      }
    }
    function activateIndustryQuoteProfile(category, templateName) {
      ensureIndustryQuoteProfiles();
      const profiles = state.settings.quoteProfiles;
      const saved = profiles[category];
      const safeSaved = saved ? { ...saved } : null;
      if (safeSaved?.freightRules === "浙江=120\n广东=180\n江苏=150\n上海=120\n北京=260" && !safeSaved.freightRulesUserConfirmed) {
        safeSaved.freightRules = "";
        safeSaved.freightDefault = 0;
      }
      const template = productTemplates[templateName] || productTemplates["通用加工"];
      const preset = industryQuotePresets[category] || {};
      state.settings = {
        ...state.settings,
        ...preset,
        ...(safeSaved || {}),
        configured: !!safeSaved?.configured,
        industrySelected: true,
        industryCategory: category,
        factoryType: safeSaved?.factoryType || templateName,
        productType: safeSaved?.productType || templateName,
        defaultMaterial: safeSaved?.defaultMaterial || template.material,
        quoteProfiles: profiles
      };
    }
    ensureIndustryQuoteProfiles();

    function normalizeCustomerSessionForState(value) {
      if (!value || typeof value !== "object" || Array.isArray(value)) return null;
      const limitedText = (input, limit) => typeof input === "string" ? input.slice(0, limit) : "";
      const id = limitedText(value.id, 180).trim();
      const customer = limitedText(value.customer, 300).trim();
      if (!id || !customer) return null;
      const normalizeIds = input => Array.isArray(input)
        ? [...new Set(input.map(Number).filter(item => Number.isSafeInteger(item) && item > 0))].slice(0, 50)
        : [];
      const normalizeNames = input => Array.isArray(input)
        ? input.slice(0, 50).map(item => String(item ?? "").slice(0, 500))
        : [];
      const normalizeFileRecords = input => Array.isArray(input)
        ? input.slice(0, 50).filter(item => item && typeof item === "object" && !Array.isArray(item)).map(item => ({
            id: Number.isSafeInteger(Number(item.id)) && Number(item.id) > 0 ? Number(item.id) : 0,
            key: limitedText(item.key, 300),
            name: limitedText(item.name, 500),
            mimeType: limitedText(item.mimeType, 180)
          })).filter(item => item.id > 0)
        : [];
      const rawDetailJob = value.detailPackageJob && typeof value.detailPackageJob === "object" && !Array.isArray(value.detailPackageJob)
        ? value.detailPackageJob
        : null;
      const detailPackageJob = rawDetailJob ? {
        id: limitedText(rawDetailJob.id, 180) || `detail-${id}`,
        sourceText: limitedText(rawDetailJob.sourceText, 20000),
        product: limitedText(rawDetailJob.product, 300) || "定制产品",
        platform: limitedText(rawDetailJob.platform, 80) || "阿里巴巴/1688",
        targetCount: Number.isFinite(Number(rawDetailJob.targetCount))
          ? Math.max(1, Math.min(12, Math.round(Number(rawDetailJob.targetCount))))
          : 8,
        referenceFileIds: normalizeIds(rawDetailJob.referenceFileIds),
        referenceRole: ["auto", "artwork", "product_reference", "style_reference"].includes(limitedText(rawDetailJob.referenceRole, 40))
          ? limitedText(rawDetailJob.referenceRole, 40)
          : "auto",
        extraInstructions: limitedText(rawDetailJob.extraInstructions, 4000),
        pageInstructions: rawDetailJob.pageInstructions && typeof rawDetailJob.pageInstructions === "object" && !Array.isArray(rawDetailJob.pageInstructions)
          ? Object.fromEntries(Object.entries(rawDetailJob.pageInstructions)
              .map(([key, instruction]) => [String(Math.round(Number(key))), limitedText(instruction, 2000)])
              .filter(([key, instruction]) => Number(key) >= 1 && Number(key) <= 12 && instruction))
          : {},
        status: ["awaiting_confirmation", "generating", "completed", "partial", "failed"].includes(limitedText(rawDetailJob.status, 40))
          ? limitedText(rawDetailJob.status, 40)
          : "awaiting_confirmation",
        generatedCount: Number.isFinite(Number(rawDetailJob.generatedCount))
          ? Math.max(0, Math.min(12, Math.round(Number(rawDetailJob.generatedCount))))
          : 0,
        progressPercent: Number.isFinite(Number(rawDetailJob.progressPercent))
          ? Math.max(0, Math.min(100, Math.round(Number(rawDetailJob.progressPercent))))
          : 0,
        progressMessage: limitedText(rawDetailJob.progressMessage, 500),
        failures: Array.isArray(rawDetailJob.failures)
          ? rawDetailJob.failures.slice(0, 12).map(item => limitedText(item, 1000)).filter(Boolean)
          : [],
        detailContent: limitedText(rawDetailJob.detailContent, 30000),
        generatedImages: Array.isArray(rawDetailJob.generatedImages)
          ? rawDetailJob.generatedImages.slice(0, 12).filter(item => item && typeof item === "object" && !Array.isArray(item)).map((item, fallbackIndex) => ({
              taskIndex: Number.isFinite(Number(item.taskIndex))
                ? Math.max(0, Math.min(11, Math.round(Number(item.taskIndex))))
                : fallbackIndex,
              url: limitedText(item.url, 5000),
              filename: limitedText(item.filename, 500),
              name: limitedText(item.name, 500),
              caption: limitedText(item.caption, 2000),
              file_id: Number.isSafeInteger(Number(item.file_id)) && Number(item.file_id) > 0 ? Number(item.file_id) : 0
            })).filter(item => item.url)
          : [],
        resultHtml: limitedText(rawDetailJob.resultHtml, 50000),
        createdAt: limitedText(rawDetailJob.createdAt, 80),
        updatedAt: limitedText(rawDetailJob.updatedAt, 80)
      } : null;
      const allowedModes = new Set(["", "analyze", "quote", "visual", "detail", "task", "chat"]);
      const mode = limitedText(value.mode, 40);
      const analysisMode = normalizeHomeAnalysisMode(value.analysisMode);
      const lastRequestedAnalysisMode = normalizeHomeAnalysisMode(value.lastRequestedAnalysisMode);
      const persistedProcessingStatus = limitedText(value.processingStatus, 40);
      const processingStatus = persistedProcessingStatus === "processing"
        ? "interrupted"
        : ["", "completed", "failed", "interrupted"].includes(persistedProcessingStatus)
          ? persistedProcessingStatus
          : "";
      const scrollTop = Number(value.chatScrollTop);
      const restoredHtml = input => typeof input === "string" ? input : "";
      return {
        ...value,
        id,
        customer,
        draft: limitedText(value.draft, 20000),
        lastMessage: limitedText(value.lastMessage, 1000),
        updatedAt: limitedText(value.updatedAt, 80),
        archivedAt: limitedText(value.archivedAt, 80),
        mode: allowedModes.has(mode) ? mode : "",
        analysisMode,
        lastRequestedAnalysisMode,
        chatHistoryHtml: restoredHtml(value.chatHistoryHtml),
        userMessageHtml: restoredHtml(value.userMessageHtml),
        answerHtml: restoredHtml(value.answerHtml),
        nextActionsHtml: restoredHtml(value.nextActionsHtml),
        businessSourceText: limitedText(value.businessSourceText, 20000),
        userFileIds: normalizeIds(value.userFileIds),
        userFileNames: normalizeNames(value.userFileNames),
        userFileRecords: normalizeFileRecords(value.userFileRecords),
        pendingFileIds: normalizeIds(value.pendingFileIds),
        pendingFileRecords: normalizeFileRecords(value.pendingFileRecords),
        uploadFailures: Array.isArray(value.uploadFailures) ? value.uploadFailures.slice(0, 50).map(item => ({
          key: limitedText(item?.key, 300), name: limitedText(item?.name, 500), reason: limitedText(item?.reason, 500)
        })) : [],
        retainedFileIds: normalizeIds(value.retainedFileIds),
        retainedFileNames: normalizeNames(value.retainedFileNames),
        conversationContext: limitedText(value.conversationContext, 20000),
        activePrompt: limitedText(value.activePrompt, 20000),
        visiblePrompt: limitedText(value.visiblePrompt, 20000),
        lastVisualUserPrompt: limitedText(value.lastVisualUserPrompt, 20000),
        detailPackageJob,
        processingStatus,
        processingKind: ["", "analysis", "visual", "detail_package"].includes(limitedText(value.processingKind, 40)) ? limitedText(value.processingKind, 40) : "",
        processingRequestId: processingStatus === "processing" ? limitedText(value.processingRequestId, 180) : "",
        processingStartedAt: processingStatus === "processing" ? limitedText(value.processingStartedAt, 80) : "",
        processingProgressMessage: processingStatus === "processing" ? limitedText(value.processingProgressMessage, 500) : "",
        hasUnreadResult: ["completed", "failed"].includes(processingStatus) && value.hasUnreadResult === true,
        chatScrollTop: Number.isFinite(scrollTop) && scrollTop >= 0 ? Math.min(scrollTop, 10_000_000) : 0
      };
    }
    const quoteDraftTextFields = [
      "product", "size", "material", "process", "qty", "template", "costMode",
      "delivery", "payment", "taxMode", "sampleMode", "sample", "customerLevel"
    ];
    function normalizeQuoteDraftSnapshot(value) {
      if (!value || typeof value !== "object" || Array.isArray(value)) return null;
      const snapshot = {};
      quoteDraftTextFields.forEach(key => { snapshot[key] = String(value[key] ?? "").slice(0, key === "process" ? 1200 : 300); });
      snapshot.productConfiguration = value.productConfiguration && typeof value.productConfiguration === "object" && !Array.isArray(value.productConfiguration)
        ? cloneQuoteSnapshot(value.productConfiguration) : null;
      snapshot.materialCostDraft = value.materialCostDraft && typeof value.materialCostDraft === "object" && !Array.isArray(value.materialCostDraft)
        ? cloneQuoteSnapshot(value.materialCostDraft) : null;
      const manualAdjustments = value.manualAdjustments && typeof value.manualAdjustments === "object" && !Array.isArray(value.manualAdjustments)
        ? value.manualAdjustments : {};
      snapshot.manualAdjustments = {
        productionSheets:String(manualAdjustments.productionSheets ?? "").replace(/[^\d]/g, "").slice(0, 12),
        plateCount:String(manualAdjustments.plateCount ?? "").replace(/[^\d]/g, "").slice(0, 8),
        reason:String(manualAdjustments.reason ?? "").trim().slice(0, 160)
      };
      return snapshot;
    }
    function normalizeQuoteSavedTemplates(value) {
      if (!Array.isArray(value)) return [];
      const ids = new Set();
      return value.slice(0, 30).map((item, index) => {
        const snapshot = normalizeQuoteDraftSnapshot(item?.snapshot);
        if (!snapshot) return null;
        let id = String(item?.id || `quote-template-${index + 1}`).replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 80);
        if (!id || ids.has(id)) id = `quote-template-${Date.now()}-${index}`;
        ids.add(id);
        return {
          id,
          name: String(item?.name || snapshot.product || `常用方案${index + 1}`).trim().slice(0, 40),
          snapshot,
          createdAt: String(item?.createdAt || "").slice(0, 50),
          updatedAt: String(item?.updatedAt || item?.createdAt || "").slice(0, 50)
        };
      }).filter(Boolean);
    }
    function normalizeQuoteBatchDraft(value) {
      if (!value || typeof value !== "object" || Array.isArray(value)) return { id:"", name:"", customer:"", items:[], createdAt:"", updatedAt:"" };
      const ids = new Set();
      const items = (Array.isArray(value.items) ? value.items : []).slice(0, 20).map((item, index) => {
        const snapshot = normalizeQuoteDraftSnapshot(item?.snapshot);
        if (!snapshot) return null;
        let id = String(item?.id || `batch-item-${index + 1}`).replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 80);
        if (!id || ids.has(id)) id = `batch-item-${Date.now()}-${index}`;
        ids.add(id);
        return {
          id,
          snapshot,
          status: item?.status === "quoted" ? "quoted" : "pending",
          quoteKey: String(item?.quoteKey || "").slice(0, 150)
        };
      }).filter(Boolean);
      return {
        id: String(value.id || "").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 80),
        name: String(value.name || "").trim().slice(0, 40),
        customer: String(value.customer || "").trim().slice(0, 100),
        items,
        createdAt: String(value.createdAt || "").slice(0, 50),
        updatedAt: String(value.updatedAt || "").slice(0, 50)
      };
    }
    function loadState(storageKey = businessStoreKey) {
      const fresh = JSON.parse(JSON.stringify(defaults));
      try {
        const parsed = JSON.parse(localStorage.getItem(storageKey) || "{}");
        const saved = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
        const savedSettings = saved.settings && typeof saved.settings === "object" && !Array.isArray(saved.settings)
          ? saved.settings
          : {};
        const settings = { ...fresh.settings, ...savedSettings };
        settings.quoteSavedTemplates = normalizeQuoteSavedTemplates(settings.quoteSavedTemplates);
        if (Object.keys(savedSettings).length && !Object.prototype.hasOwnProperty.call(savedSettings, "quoteCatalogPreferences")) {
          settings.quoteCatalogPreferences = { schemaVersion:1, selectionMode:"all", selectedProductIds:[], setupCompletedAt:"legacy", order:[], hidden:[], customCategories:[] };
        }
        const legacyDemoFreight = "浙江=120\n广东=180\n江苏=150\n上海=120\n北京=260";
        if (settings.freightRules === legacyDemoFreight && !settings.freightRulesUserConfirmed) {
          settings.freightRules = "";
          settings.freightDefault = 0;
        }
        // 清理开发回归测试期间生成的固定报价记录，不影响厂家后续真实报价。
        const internalTestQuoteTimes = new Set([
          "2026/7/10 16:36:35", "2026/7/10 18:54:21", "2026/7/10 19:15:48",
          "2026/7/10 20:28:09", "2026/7/10 20:31:40", "2026/7/10 20:40:20", "2026/7/10 20:41:28"
        ]);
        const internalTestCustomers = [
          "系统完善测试客户-AI", "正式报价单测试客户", "粘性功能测试客户", "星河包装", "星河定制客户",
          "杭州新客回归厂", "上海极速回归厂", "宁波快速回归厂", "宁波客户名回归厂", "审批功能回归厂",
          "全流程验收测试客户-0711", "地址验收测试厂", "隐私回归厂", "隐私最终验收厂", "云记忆测试客户-AI"
        ];
        const isInternalTestRecord = item => {
          const text = `${item?.customer || ""} ${item?.company || ""} ${item?.title || ""}`;
          return internalTestCustomers.some(name => text.includes(name));
        };
        const quoteHistorySource = Array.isArray(saved.quoteHistory) ? saved.quoteHistory : fresh.quoteHistory;
        const quoteHistory = quoteHistorySource.filter(item => item && typeof item === "object" && !Array.isArray(item)).filter(item => {
          const isKnownTestProduct = ["包装盒", "包装彩盒"].includes(item.product) && Number(item.qty) === 5000;
          return !isInternalTestRecord(item) && !(isKnownTestProduct && internalTestQuoteTimes.has(item.time));
        });
        const tasksSource = Array.isArray(saved.tasks) ? saved.tasks : fresh.tasks;
        const tasks = tasksSource.filter(item => item && typeof item === "object" && !Array.isArray(item)).filter(item => !isInternalTestRecord(item));
        const customerProfileSource = Array.isArray(saved.customerProfiles) ? saved.customerProfiles : fresh.customerProfiles;
        const customerProfiles = customerProfileSource.filter(item => item && typeof item === "object" && !Array.isArray(item)).filter(item => !isInternalTestRecord(item));
        const customerSessions = Array.isArray(saved.customerSessions)
          ? saved.customerSessions.map(normalizeCustomerSessionForState).filter(Boolean).filter(item => !isInternalTestRecord({ customer: item.customer })).slice(0, 40)
          : [];
        const savedMember = saved.member && typeof saved.member === "object" && !Array.isArray(saved.member) ? saved.member : {};
        return {
          ...fresh,
          ...saved,
          settings,
          quoteHistory,
          quoteBatchDraft: normalizeQuoteBatchDraft(saved.quoteBatchDraft),
          tasks,
          customerProfiles,
          customerSessions,
          activeCustomerSessionId: saved.activeCustomerSessionId || "",
          member: { ...fresh.member, ...savedMember },
          demoMembers: Array.isArray(saved.demoMembers) ? saved.demoMembers : fresh.demoMembers
        };
      }
      catch { return fresh; }
    }
    function saveState() {
      const payload = JSON.stringify(state);
      window.__pendingBusinessStateSave = payload;
      window.clearTimeout(window.__retryBusinessStateSaveTimer);
      try {
        window.localStorage?.setItem(businessStoreKey, payload);
        const companyId = String(window.__currentUser?.company_id || "").trim();
        if (companyId) window.localStorage?.setItem(`jianglai_settings_company_${companyId}`, JSON.stringify(state.settings));
        window.__pendingBusinessStateSave = "";
        return true;
      } catch (_) {
        // WKWebView 在应用刚升级/切换进程的瞬间可能暂时返回空 localStorage。
        // 聊天和缩略图先继续显示，稍后再尝试落盘，不让客户记录恢复被中断。
        window.__retryBusinessStateSaveTimer = window.setTimeout(() => {
          const pending = window.__pendingBusinessStateSave;
          if (!pending) return;
          try {
            window.localStorage?.setItem(businessStoreKey, pending);
            window.__pendingBusinessStateSave = "";
          } catch (_) {}
        }, 900);
        return false;
      }
    }
    function refreshBusinessTenantViews() {
      try { restoreSettings(); } catch (_) {}
      try { renderHomeCommandCenter(); } catch (_) {}
      try { renderDashboard(); } catch (_) {}
      try { renderQuoteHistory(); } catch (_) {}
      try { renderTasks(); } catch (_) {}
      try { renderCustomerProfiles(); } catch (_) {}
      try { renderCustomerWorkbench(); } catch (_) {}
    }
    function activateBusinessTenant(companyId) {
      const next = tenantStateManager.activate(window.localStorage, companyId);
      if (next.companyId === activeBusinessCompanyId && next.storageKey === businessStoreKey) {
        return { ...next, changed:false };
      }
      activeBusinessCompanyId = next.companyId;
      businessStoreKey = next.storageKey;
      state = next.companyId ? loadState(businessStoreKey) : JSON.parse(JSON.stringify(defaults));
      window.__formalTaskCacheCompanyId = "";
      window.dispatchEvent(new CustomEvent("jianglai:business-tenant-changed", {
        detail:{ companyId:activeBusinessCompanyId, storageKey:businessStoreKey, migrated:next.migrated }
      }));
      refreshBusinessTenantViews();
      return { ...next, changed:true };
    }
    window.__activateBusinessTenant = activateBusinessTenant;
    window.__businessTenantContext = () => ({ companyId:activeBusinessCompanyId, storageKey:businessStoreKey });
    const dieLayoutDefaultSettings = Object.freeze({
      boardWidthMm:1050,
      boardHeightMm:750,
      marginsMm:Object.freeze({ top:17, right:17, bottom:17, left:17 }),
      gripperMm:12,
      gripperEdge:"top",
      gapMm:Object.freeze({ x:5, y:5 }),
      rotation:"forbid",
      align:"center",
      strategy:"balanced"
    });
    window.__getDieLayoutSettings = () => {
      const saved = state.settings?.dieLayout && typeof state.settings.dieLayout === "object" ? state.settings.dieLayout : {};
      return {
        ...dieLayoutDefaultSettings,
        ...saved,
        marginsMm:{ ...dieLayoutDefaultSettings.marginsMm, ...(saved.marginsMm || {}) },
        gapMm:{ ...dieLayoutDefaultSettings.gapMm, ...(saved.gapMm || {}) }
      };
    };
    window.__saveDieLayoutSettings = value => {
      const source = value && typeof value === "object" ? value : {};
      state.settings.dieLayout = {
        ...dieLayoutDefaultSettings,
        ...source,
        marginsMm:{ ...dieLayoutDefaultSettings.marginsMm, ...(source.marginsMm || {}) },
        gapMm:{ ...dieLayoutDefaultSettings.gapMm, ...(source.gapMm || {}) }
      };
      saveState();
      Promise.resolve(window.__saveSettingsCloud?.(state.settings)).catch(() => {});
      return window.__getDieLayoutSettings();
    };
    function dieLayoutDimensionPair(value) {
      const values = String(value ?? "")
        .replace(/[×＊*Xx]/g, "×")
        .match(/\d+(?:\.\d+)?/g)
        ?.map(Number)
        .filter(item => Number.isFinite(item) && item > 0) || [];
      return values.length === 2 ? values : null;
    }
    function dieLayoutQuantity(value) {
      if (Number.isInteger(Number(value)) && Number(value) > 0) return Number(value);
      const matched = String(value ?? "").replace(/,/g, "").match(/\d+/);
      const quantity = Number(matched?.[0] || 0);
      return Number.isInteger(quantity) && quantity > 0 ? quantity : 0;
    }
    window.__getDieLayoutCustomerContext = () => {
      const session = typeof activeCustomerSession === "function" ? activeCustomerSession() : null;
      const customer = String(session?.customer || currentCustomerName?.() || "").trim();
      const quote = (state.quoteHistory || []).find(item => String(item?.customer || "").trim() === customer) || null;
      const formulaInput = quote?.formulaSnapshot?.input && typeof quote.formulaSnapshot.input === "object"
        ? quote.formulaSnapshot.input : {};
      let dimensions = Number(formulaInput.pieceWidthMm) > 0 && Number(formulaInput.pieceHeightMm) > 0
        ? [Number(formulaInput.pieceWidthMm), Number(formulaInput.pieceHeightMm)]
        : dieLayoutDimensionPair(quote?.materialCostDraft?.flatSize);
      const analysis = session?.businessAnalysis && typeof session.businessAnalysis === "object"
        ? session.businessAnalysis : null;
      const sourceText = String(session?.businessSourceText || "");
      const analysisDimensions = dieLayoutDimensionPair(Array.isArray(analysis?.size) ? analysis.size.join("×") : analysis?.size);
      // 只有明确的二维尺寸，或客户原话说明是展开尺寸时才自动带入；
      // 普通长×宽×高不能直接冒充刀线展开尺寸。
      if (!dimensions && analysisDimensions && (analysisDimensions.length === 2 || /展开(?:尺寸|宽|高)|刀线|刀版|刀板/.test(sourceText))) {
        dimensions = analysisDimensions;
      }
      const quantity = dieLayoutQuantity(quote?.qty || analysis?.qty || analysis?.quantity);
      const product = String(quote?.product || analysis?.product || "").trim();
      if (!customer) return null;
      const missing = [];
      if (!dimensions) missing.push("单模展开尺寸");
      if (!quantity) missing.push("订单数量");
      return {
        contextId:String(session?.id || customer),
        projectName:[customer, product].filter(Boolean).join(" · ") || `${customer}刀板排版`,
        partWidthMm:dimensions?.[0] || null,
        partHeightMm:dimensions?.[1] || null,
        quantity:quantity || null,
        ready:missing.length === 0,
        missing,
        sourceLabel:quote ? "最近正式报价" : "当前客户需求"
      };
    };
    function normalizedBusinessSyncStatus() {
      const source = state.businessSync && typeof state.businessSync === "object" ? state.businessSync : {};
      return {
        status: ["idle", "syncing", "ok", "error"].includes(source.status) ? source.status : "idle",
        lastSuccessAt: String(source.lastSuccessAt || ""),
        lastAttemptAt: String(source.lastAttemptAt || ""),
        error: String(source.error || "").slice(0, 500),
        syncedDomains: Array.isArray(source.syncedDomains) ? source.syncedDomains.map(item => String(item || "")).filter(Boolean).slice(0, 12) : []
      };
    }
    function formatBusinessSyncTime(value) {
      const parsed = Date.parse(String(value || ""));
      return Number.isFinite(parsed) ? new Date(parsed).toLocaleString("zh-CN", { hour12:false }) : "尚未完成同步";
    }
    function renderBusinessSyncStatus() {
      const info = normalizedBusinessSyncStatus();
      const badge = document.getElementById("businessSyncState");
      const last = document.getElementById("businessLastSyncAt");
      const error = document.getElementById("businessSyncError");
      const cloud = document.getElementById("businessCloudDataStatus");
      const retry = document.getElementById("businessSyncRetryButton");
      const label = info.status === "ok" ? "云端已同步" : info.status === "syncing" ? "正在同步" : info.status === "error" ? "同步失败" : "等待首次同步";
      if (badge) {
        badge.textContent = label;
        badge.className = `business-sync-state${info.status === "ok" ? " is-ok" : info.status === "syncing" ? " is-syncing" : info.status === "error" ? " is-error" : ""}`;
      }
      if (last) last.textContent = formatBusinessSyncTime(info.lastSuccessAt);
      if (error) error.textContent = info.error || (info.status === "syncing" ? "正在连接云端，请稍候…" : "暂无异常");
      if (cloud) cloud.textContent = info.syncedDomains.length
        ? `${info.syncedDomains.join("、")}（云端已确认）`
        : "客户与聊天、报价、跟单、价格设置（登录后同步）";
      if (retry) {
        retry.disabled = info.status === "syncing";
        retry.textContent = info.status === "syncing" ? "正在同步…" : "立即重试同步";
      }
    }
    function markBusinessSyncAttempt() {
      state.businessSync = { ...normalizedBusinessSyncStatus(), status:"syncing", lastAttemptAt:new Date().toISOString() };
      saveState();
      renderBusinessSyncStatus();
    }
    function markBusinessSyncSuccess(domains = []) {
      state.businessSync = {
        status:"ok",
        lastSuccessAt:new Date().toISOString(),
        lastAttemptAt:new Date().toISOString(),
        error:"",
        syncedDomains:[...new Set((domains || []).map(item => String(item || "")).filter(Boolean))]
      };
      saveState();
      renderBusinessSyncStatus();
    }
    function markBusinessSyncFailure(reason, domains = []) {
      const previous = normalizedBusinessSyncStatus();
      state.businessSync = {
        ...previous,
        status:"error",
        lastAttemptAt:new Date().toISOString(),
        error:String(reason?.message || reason || "云端暂时没有回应，请检查网络后重试").slice(0, 500),
        syncedDomains:[...new Set((domains || previous.syncedDomains || []).map(item => String(item || "")).filter(Boolean))]
      };
      saveState();
      renderBusinessSyncStatus();
    }
    function retryBusinessCloudSync() {
      markBusinessSyncFailure("当前尚未登录正式工厂账号，请先登录后再同步。", normalizedBusinessSyncStatus().syncedDomains);
      showPage("更多");
    }
    function renderBackupStatus() {
      const note = document.getElementById("backupStatusNote");
      if (!note) return;
      const automatic = state.serverBackup;
      note.textContent = automatic?.ok
        ? `客户资料、报价和跟单每天自动创建数据库安全快照；最近备份：${new Date(automatic.latest_at).toLocaleString("zh-CN", { hour12: false })}，共 ${automatic.count} 份，保留最近 ${automatic.retention_days} 天。${state.lastBackupAt ? `最近手动导出：${new Date(state.lastBackupAt).toLocaleString("zh-CN", { hour12: false })}。` : "仍可随时导出一份离线备份。"}`
        : state.lastBackupAt
          ? `客户资料和聊天已启用服务端保存与云端同步。本机最近导出备份：${new Date(state.lastBackupAt).toLocaleString("zh-CN", { hour12: false })}。`
          : "客户资料和聊天已启用服务端保存与云端同步，本机同时保留缓存。目前还没有导出离线备份。";
      document.getElementById("undoBusinessRestoreButton")?.classList.toggle("is-hidden", !localStorage.getItem(currentPreRestoreStoreKey()));
      renderBusinessSyncStatus();
    }
    function downloadBusinessBackup() {
      state.lastBackupAt = new Date().toISOString();
      saveState();
      const payload = {
        product: "将莱数字员工·工厂经营智能助手",
        version: "1.0.9",
        exportedAt: state.lastBackupAt,
        state
      };
      const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = `将莱数字员工数据备份-${state.lastBackupAt.slice(0, 10)}.json`;
      a.click();
      URL.revokeObjectURL(a.href);
      renderBackupStatus();
      notify("数据备份已导出，请妥善保存。" );
    }
    async function restoreBusinessBackup(event) {
      const file = event.target?.files?.[0];
      if (!file) return;
      try {
        const parsed = JSON.parse(await file.text());
        const restored = parsed.state || parsed;
        if (!restored || !Array.isArray(restored.tasks) || !Array.isArray(restored.quoteHistory) || !restored.settings) {
          throw new Error("这不是有效的将莱数字员工备份文件");
        }
        if (!confirm(`确定恢复 ${parsed.exportedAt ? new Date(parsed.exportedAt).toLocaleString("zh-CN", { hour12: false }) : "该时间点"} 的备份吗？当前本机数据将被替换。`)) return;
        localStorage.setItem(currentPreRestoreStoreKey(), localStorage.getItem(businessStoreKey) || JSON.stringify(defaults));
        localStorage.setItem(businessStoreKey, JSON.stringify(restored));
        location.reload();
      } catch (err) {
        notify(`恢复失败：${err.message}`);
        event.target.value = "";
      }
    }
    function undoBusinessRestore() {
      const previous = localStorage.getItem(currentPreRestoreStoreKey());
      if (!previous) return notify("没有可以撤销的恢复记录。");
      if (!confirm("撤销最近一次数据恢复，回到恢复前的本机数据吗？")) return;
      const current = localStorage.getItem(businessStoreKey) || JSON.stringify(defaults);
      localStorage.setItem(businessStoreKey, previous);
      localStorage.setItem(currentPreRestoreStoreKey(), current);
      location.reload();
    }
    function clearDemoDataSafely() {
      if (!confirm("确定清空本机演示数据吗？建议先导出备份。此操作不能撤销。")) return;
      localStorage.removeItem(businessStoreKey);
      location.reload();
    }
    function val(id) { return String(document.getElementById(id)?.value || "").trim(); }
    function num(id, fallback = 0) {
      const n = parseFloat(String(val(id)).replace(/[^\d.+-]/g, ""));
      return Number.isFinite(n) ? n : fallback;
    }
    function rateNum(id, fallback = 0) {
      const n = num(id, fallback);
      return n > 1 ? n / 100 : n;
    }
    function rateText(rate) {
      return Number.isFinite(rate) ? (rate * 100).toFixed(rate * 100 % 1 ? 1 : 0) : "0";
    }
    function requiredNumber(id, label, options = {}) {
      const raw = val(id);
      const fail = message => { const error = new Error(message); error.fieldId = id; throw error; };
      if (!raw) fail(`${label}还没有填写`);
      const normalized = raw.replace(/,/g, "").replace(/%$/, "").trim();
      if (!/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/.test(normalized)) fail(`${label}必须是有效数字`);
      let value = Number(normalized);
      if (!Number.isFinite(value)) fail(`${label}必须是有效数字`);
      if (options.rate && (raw.includes("%") || value > 1)) value /= 100;
      if (value < 0 || (!options.allowZero && value === 0)) fail(`${label}必须${options.allowZero ? "大于或等于0" : "大于0"}`);
      if (options.integer && !Number.isInteger(value)) fail(`${label}必须是整数`);
      if (options.rate && value >= 1) fail(`${label}必须小于100%`);
      return value;
    }
    function requiredDimensions(engine, id, label) {
      try { return engine.parseDimensions(val(id), 2); } catch (error) { error.fieldId = id; error.message = `${label}：${error.message}`; throw error; }
    }
    function parseRules(text) {
      return String(text || "").split(/\n+/).map(line => {
        const parts = line.split(/[=：:]/);
        const name = (parts[0] || "").trim();
        const price = parseFloat(String(parts.slice(1).join("=")).replace(/[^\d.]/g, ""));
        return name && Number.isFinite(price) ? { name, price } : null;
      }).filter(Boolean);
    }
    function parseStrictSetupRules(text, label, options) {
      options = options && typeof options === "object" ? options : {};
      const lines = String(text || "").split(/\r?\n/).map(line => line.trim()).filter(Boolean);
      const values = [];
      const errors = [];
      const seen = new Set();
      lines.forEach((line, index) => {
        if (options.operation && line.includes("|")) {
          const parts = line.split("|").map(item => item.trim()).filter(Boolean);
          const head = parts.shift()?.match(/^([^=：:\n]{1,80})\s*[=：:]\s*(\d+(?:\.\d{1,6})?)$/);
          const attrs = Object.fromEntries(parts.map(part => {
            const separator = part.indexOf("=");
            return separator > 0 ? [part.slice(0, separator).trim(), part.slice(separator + 1).trim()] : [part, ""];
          }));
          const name = String(head?.[1] || "").trim().replace(/\s+/g, " ");
          const key = normalizeRuleText(name);
          const price = Number(head?.[2]);
          const basis = String(attrs.basis || "piece");
          const chargeMode = String(attrs.mode || "minimum");
          const setupFee = Number(attrs.setup || 0);
          const minimumCharge = Number(attrs.minimum || 0);
          const includedQuantity = Number(attrs.included || 0);
          const excessUnitPrice = attrs.excess === undefined || attrs.excess === "" ? price : Number(attrs.excess);
          const usagePerPiece = attrs.usage === undefined || attrs.usage === "" ? (basis === "piece" ? 1 : 0) : Number(attrs.usage);
          const basisUnits = {
            piece:"个", sheet:"张", area_m2:"㎡", local_m2:"局部㎡", meter:"米", kg:"kg",
            hole:"孔", fold:"折", page:"页", book:"本", run:"次", hour:"小时"
          };
          const allowedBasis = Object.prototype.hasOwnProperty.call(basisUnits, basis);
          const allowedMode = ["linear", "minimum", "package_plus_excess"].includes(chargeMode);
          const numbersValid = [price, setupFee, minimumCharge, includedQuantity, excessUnitPrice, usagePerPiece]
            .every(value => Number.isFinite(value) && value >= 0 && value <= 10_000_000);
          const modeFieldsValid = chargeMode !== "package_plus_excess" || (minimumCharge > 0 && includedQuantity > 0);
          const usageValid = !["local_m2", "meter", "kg", "hole", "fold", "page", "hour"].includes(basis) || usagePerPiece > 0;
          if (!head || !key || !allowedBasis || !allowedMode || !numbersValid || !modeFieldsValid || !usageValid || (price <= 0 && setupFee <= 0 && minimumCharge <= 0)) {
            errors.push(`${label}第 ${index + 1} 行`);
            return;
          }
          if (seen.has(key)) {
            errors.push(`${label}第 ${index + 1} 行（名称重复）`);
            return;
          }
          seen.add(key);
          values.push({ name, price, unit:`元/${basisUnits[basis]}`, setupFee, basis, chargeMode, minimumCharge, includedQuantity, excessUnitPrice, usagePerPiece });
          return;
        }
        const matched = options.operation
          ? line.match(/^([^=：:\n]{1,80})\s*[=：:]\s*(\d+(?:\.\d{1,6})?)\s*(?:\+\s*固定费\s*[=：:]?\s*(\d+(?:\.\d{1,6})?)\s*(?:元\s*\/\s*单)?)?\s*$/i)
          : line.match(/^([^=：:\n]{1,80})\s*[=：:]\s*(\d+(?:\.\d{1,6})?)\s*(元\s*\/\s*(?:吨|kg|公斤|㎡|平方米|米|张|个|小时))?\s*$/i);
        const name = String(matched?.[1] || "").trim().replace(/\s+/g, " ");
        const price = Number(matched?.[2]);
        const setupFee = options.operation && matched?.[3] !== undefined ? Number(matched[3]) : null;
        const key = normalizeRuleText(name);
        const validPrice = options.operation
          ? Number.isFinite(price) && price >= 0 && (price > 0 || Number(setupFee) > 0)
          : Number.isFinite(price) && price > 0;
        if (!matched || !key || !validPrice || (setupFee !== null && (!Number.isFinite(setupFee) || setupFee < 0))) {
          errors.push(`${label}第 ${index + 1} 行`);
          return;
        }
        if (options.material && !matched[3]) {
          errors.push(`${label}第 ${index + 1} 行（缺少计价单位）`);
          return;
        }
        if (seen.has(key)) {
          errors.push(`${label}第 ${index + 1} 行（名称重复）`);
          return;
        }
        seen.add(key);
        values.push({
          name,
          price,
          unit: options.operation ? "元/个" : normalizedQuoteMaterialPurchaseUnit(matched?.[3] || "元/个"),
          setupFee,
          ...(options.operation ? {
            legacyOperation:true,
            basis:"piece", chargeMode:"minimum", minimumCharge:0,
            includedQuantity:0, excessUnitPrice:price, usagePerPiece:1
          } : {})
        });
      });
      return { values, errors };
    }
    function matchRule(rules, text, fallbackName, fallbackPrice) {
      const found = rules.find(rule => String(text || "").includes(rule.name));
      return found || { name: fallbackName, price: fallbackPrice };
    }
    function normalizeRuleText(text) {
      return String(text || "").toLowerCase().replace(/[\s,，、+\/;；()（）-]/g, "");
    }
    function findMatchingRule(rules, text) {
      const normalized = normalizeRuleText(text);
      return rules.find(rule => {
        const name = normalizeRuleText(rule.name);
        return name && (normalized.includes(name) || name.includes(normalized));
      }) || null;
    }
    const freightRegionAliases = {
      "浙江": ["杭州", "宁波", "温州", "绍兴", "嘉兴", "湖州", "金华", "台州", "丽水", "衢州", "舟山"],
      "广东": ["广州", "深圳", "东莞", "佛山", "惠州", "中山", "珠海", "汕头", "江门"],
      "江苏": ["南京", "苏州", "无锡", "常州", "南通", "扬州", "盐城", "徐州", "泰州", "镇江", "淮安", "连云港"]
    };
    function normalizeFreightRegion(region) {
      const raw = String(region || "").trim();
      const province = Object.entries(freightRegionAliases).find(([, cities]) => cities.some(city => raw.includes(city)))?.[0];
      return province ? `${province} ${raw}` : raw;
    }
    function matchFreightRule(region, fallback = 0) {
      const normalizedRegion = normalizeFreightRegion(region);
      const found = parseRules(state.settings.freightRules).find(rule => normalizedRegion.includes(rule.name));
      if (found) return { ...found, matched: true, region: normalizedRegion };
      if (Number(fallback) > 0) return { name: "默认运费", price: Number(fallback), matched: true, region: normalizedRegion };
      return { name: region || "未匹配地区", price: 0, matched: false, region: normalizedRegion };
    }
    function matchedProcessRules(rules, text) {
      const normalized = normalizeRuleText(text);
      return rules.filter(rule => normalized.includes(normalizeRuleText(rule.name)));
    }
    function processTokens(text) {
      return String(text || "").split(/[,，、+\/;；]+/).map(item => item.trim()).filter(Boolean);
    }
    function unmatchedProcessTokens(rules, text) {
      const baseCovered = token => /^(?:单色|双色|四色|彩色|双面)?(?:印刷|彩印)$/.test(token) || ["基础加工", "开机", "排版"].includes(token);
      return processTokens(text).filter(token => {
        if (baseCovered(token)) return false;
        const rule = findMatchingRule(rules, token);
        return !rule || !(Number(rule.price) > 0);
      });
    }
    function quoteRulesAgeDays() {
      const updatedAt = state.settings?.quoteRulesUpdatedAt;
      if (!updatedAt) return 0;
      const time = new Date(updatedAt).getTime();
      return Number.isFinite(time) ? Math.max(0, Math.floor((Date.now() - time) / 86400000)) : 0;
    }
    function setQuoteCostWarning(message = "") {
      const box = document.getElementById("quoteCostWarning");
      if (!box) return;
      box.textContent = message;
      box.classList.toggle("is-hidden", !message);
    }
    function renderQuoteReferencePriceNotice() {
      const box = document.getElementById("quoteReferencePriceNotice");
      if (!box) return;
      const visible = quoteReferencePricePackActive();
      box.textContent = visible
        ? `当前为行业参考价估算模式：${quoteReferencePriceNoticeText()}`
        : "";
      box.classList.toggle("is-hidden", !visible);
    }
    function hasAny(text, words) { return words.some(w => text.includes(w)); }
    function extractDeliveryAddress(text, fallback = "收货地待确认") {
      const raw = String(text || "");
      const pattern = /(?:送货到|配送到|发货到|含运到|送到|(?<!赠)送(?=[\u4e00-\u9fff])|发到|寄到|运到|收货(?:地址|地)?(?:是|为|：|:)?|送货地址(?:是|为|：|:)?|地址(?:是|为|：|:)?)\s*([^\n，。；;]{2,100})/gi;
      const matches = [...raw.matchAll(pattern)];
      if (!matches.length) return fallback;
      const value = String(matches[matches.length - 1][1] || "").trim();
      const address = value.split(/\s+(?:请|麻烦|并|报价|交货|到货)|(?:请|麻烦)(?:再|帮|确认)?|是否(?:含税|含运|需要)|(?:不含税|含税|不含运|含运|到付|运费另计)/, 1)[0].replace(/^[ ,，。；;：:]+|[ ,，。；;：:]+$/g, "");
      return address || fallback;
    }
    function isCasualAiQuestion(text, files = []) {
      const raw = String(text || "").trim();
      if (!raw) return false;
      const normalized = raw.replace(/[，。！？!?\s~～,.]/g, "").toLowerCase();
      const casualWords = ["你好", "您好", "在吗", "hi", "hello", "哈喽", "嗨", "测试", "你是谁", "能做什么", "能为我做什么", "可以做什么", "怎么用", "如何使用", "软件怎么用", "帮我介绍"];
      const businessWords = ["客户", "报价", "多少钱", "价格", "单价", "数量", "尺寸", "材质", "工艺", "交货", "交期", "logo", "印刷", "丝印", "图纸", "效果图", "详情页", "宝贝", "袋", "盒", "名片", "标签", "亚克力", "五金", "无纺布", "棉布", "塑料", "打样"];
      const hasBusiness = businessWords.some(word => normalized.includes(word.toLowerCase()));
      if (raw.length <= 40 && !hasBusiness && casualWords.some(word => normalized.includes(word.toLowerCase()))) return true;
      return false;
    }
    function renderCasualAiReply(text = "") {
      showHomeResult();
      const answer = document.getElementById("aiAnswer");
      hideHomeNextActions();
      if (!answer) return;
      const normalized = String(text || "").trim();
      const files = homeFileNames();
      const helpText = files.length
        ? `已收到 ${files.length} 个文件。直接告诉我你想知道什么，或者只点发送让我先客观描述图片内容；系统会自动判断，不需要先选功能。`
        : normalized.includes("怎么") || normalized.includes("能做") || normalized.includes("介绍")
        ? "你可以直接把客户发来的聊天内容、截图、产品图、Logo 或图纸发给我。我会帮你看客户要什么、缺什么资料、怎么回复、能不能报价，后面还可以继续做产品图、宝贝详情和跟单。"
        : "你好，我在。你可以直接把客户发来的消息、截图、产品图或图纸放进来，我会帮你整理需求、回复客户、判断报价和安排下一步。";
      answer.innerHTML = `
        <div class="ai-answer-body">
          <p class="ai-answer-lead">${escapeHtml(helpText)}</p>
          <div class="actions"><button class="btn light" type="button" onclick="document.getElementById('aiQuestion')?.focus()">继续聊</button></div>
        </div>
      `.trim();
    }
    function extract(pattern, text, fallback = "待确认") {
      const m = text.match(pattern);
      return m ? m[0] : fallback;
    }
    function analyze(text) {
      const size = extract(/\d+(?:\.\d+)?\s*[xX×*]\s*\d+(?:\.\d+)?(?:\s*[xX×*]\s*\d+(?:\.\d+)?)?\s*(?:mm|cm|m)?/i, text);
      const qty = extract(/\d+\s*(个|只|pcs|PCS|套|张|盒|米|码|件|条|袋)/i, text);
      const materialMatches = [...text.matchAll(/(?:(?:\d+(?:\.\d+)?\s*(?:mm|毫米)\s*)?(?:透明|磨砂|乳白|黑色|白色|彩色)?\s*亚克力(?:板)?|(?:\d+(?:\.\d+)?\s*g\s*)?(?:白卡纸?|铜版纸|牛皮纸|特种纸|无纺布|棉布|帆布|纯棉|涤纶|牛津布|不干胶(?:铜版纸)?|莫代尔|锦纶|PE|OPP|PVC|PET|PP|硅胶|橡胶|不锈钢|铝合金|铝材|锌合金|铁|铜|木材|竹材|电子元件))/gi)];
      const material = materialMatches.length ? materialMatches[materialMatches.length - 1][0].replace(/\s+/g, "") : "材质待确认";
      const finish = [];
      const printMatch = text.match(/(?:(?:单面|双面)\s*)?(?:(?:单色|双色|四色|彩色)\s*)?(?:印刷|彩印)/i);
      if (printMatch) finish.push(printMatch[0].replace(/\s+/g, ""));
      const logoPrint = text.match(/(?:印|印刷|丝印|印制|印上|做)(?:一个|黑色|白色|红色|蓝色|金色|银色|彩色)?(?:色)?\s*(?:logo|标志)/i);
      if (logoPrint) {
        const color = logoPrint[0].match(/黑色|白色|红色|蓝色|金色|银色|彩色/)?.[0] || "单色";
        const method = logoPrint[0].includes("丝印") ? "丝印" : "印刷";
        finish.push(`${color}Logo${method}`);
      }
      if (hasAny(text, ["膜", "哑膜", "覆膜"])) finish.push(hasAny(text, ["哑膜", "覆哑膜"]) ? "覆哑膜" : "覆膜");
      if (text.includes("烫银")) finish.push("烫银");
      if (text.includes("烫金")) finish.push("烫金");
      if (text.includes("模切")) finish.push("模切");
      if (text.includes("糊盒")) finish.push("糊盒");
      if (/UV/i.test(text)) finish.push("UV");
      if (hasAny(text, ["丝印", "印花", "热转印", "烫画"])) finish.push("印花/丝印");
      if (hasAny(text, ["车缝", "包边", "穿绳"])) finish.push("车缝/包边");
      if (hasAny(text, ["刺绣", "水洗标", "包装袋"])) finish.push("服装辅料/包装");
      if (hasAny(text, ["圆角", "覆亮膜"])) finish.push("印刷后道");
      if (hasAny(text, ["激光", "切割", "折弯", "冲压", "焊接", "电镀", "喷涂", "氧化"])) finish.push("五金/板材加工");
      if (text.includes("拉丝")) finish.push("拉丝");
      if (hasAny(text, ["填漆", "上漆"])) finish.push("填漆");
      if (text.includes("背胶")) finish.push("背胶");
      if (hasAny(text, ["开模", "注塑", "吹膜", "制袋", "封边"])) finish.push("塑料/模具加工");
      if (hasAny(text, ["组装", "测试", "认证", "质保", "参数"])) finish.push("电子组装/测试");
      const finishList = [...new Set(finish)].filter(item => !(
        finish.some(value => /Logo(?:印刷|丝印)/.test(value)) && ["印花/丝印", "印刷"].includes(item)
      ));
      const risk = [];
      if (hasAny(text, ["黑底", "满版", "大面积黑", "深色满版"])) risk.push("黑色满版易色差、折痕露白");
      if (hasAny(text, ["烫银", "烫金"])) risk.push("烫银位置和牢度需先确认");
      if (hasAny(text, ["无纺布", "克重", "承重", "车缝"])) risk.push("无纺布需确认克重、承重、车线和印刷牢度");
      if (hasAny(text, ["棉布", "帆布", "缩水", "水洗", "印花"])) risk.push("纺织品需确认克重、幅宽、缩水率、色差和印花牢度");
      if (hasAny(text, ["名片", "双面", "圆角"])) risk.push("名片需确认纸张厚度、色差、出血、裁切偏差和盒数");
      if (hasAny(text, ["标签", "不干胶", "卷装"])) risk.push("标签需确认材质、胶性、卷装方向、模切尺寸和使用环境");
      if (hasAny(text, ["五金", "钣金", "冲压", "不锈钢", "铝合金", "公差"])) risk.push("五金件需确认图纸、公差、材质厚度、表面处理和包装防刮");
      if (hasAny(text, ["亚克力", "展示架"])) risk.push("亚克力需确认板材厚度、结构承重、粘接方式和运输防护");
      if (hasAny(text, ["塑料袋", "OPP", "PE", "厚度", "封边"])) risk.push("塑料袋需确认材质、厚度、尺寸公差、封边方式和印刷牢度");
      if (hasAny(text, ["硅胶", "橡胶", "开模"])) risk.push("硅胶橡胶需确认模具费、样品周期、硬度、颜色和环保要求");
      if (hasAny(text, ["电子", "小家电", "数据线", "认证"])) risk.push("电子类需确认参数、认证、质检标准、包装和售后质保边界");
      if (hasAny(text, ["下周", "急", "马上"])) risk.push("交期偏紧，需要先查排产");
      let product = "定制产品";
      if (hasAny(text, ["无纺布袋", "环保袋"])) product = "无纺布袋";
      else if (hasAny(text, ["棉布袋", "帆布袋", "束口袋"])) product = "棉布袋";
      else if (hasAny(text, ["手提袋", "纸袋", "礼品袋", "购物袋"])) product = "手提袋";
      else if (hasAny(text, ["塑料袋", "OPP袋", "PE袋", "PVC袋", "包装袋"])) product = "塑料袋/OPP袋";
      else if (hasAny(text, ["双插盒", "反插盒"])) product = "双插盒（插口彩盒）";
      else if (hasAny(text, ["彩盒"])) product = "包装彩盒";
      else if (hasAny(text, ["包装盒", "纸盒", "礼盒", "盒子"])) product = "包装盒";
      else if (hasAny(text, ["名片", "商务卡"])) product = "名片";
      else if (hasAny(text, ["标签", "不干胶", "贴纸"])) product = "标签贴纸";
      else if (hasAny(text, ["冰箱贴", "磁贴", "磁性贴"])) product = "冰箱贴";
      else if (hasAny(text, ["钥匙扣", "钥匙链"])) product = "钥匙扣";
      else if (hasAny(text, ["徽章", "胸章", "吧唧"])) product = "徽章";
      else if (hasAny(text, ["立牌", "摆件"])) product = "立牌/摆件";
      else if (hasAny(text, ["杯垫", "鼠标垫"])) product = text.includes("鼠标垫") ? "鼠标垫" : "杯垫";
      else if (hasAny(text, ["画册", "说明书", "折页"])) product = "画册/说明书";
      else if (hasAny(text, ["硅胶", "橡胶"])) product = "硅胶/橡胶制品";
      else if (hasAny(text, ["标牌", "徽章"])) product = "金属标牌/徽章";
      else if (hasAny(text, ["五金", "钣金", "冲压", "不锈钢", "铝合金"])) product = "五金钣金件";
      else if (hasAny(text, ["亚克力", "展示架"])) product = "亚克力展示架";
      else if (hasAny(text, ["木制", "竹制"])) product = "木制品/竹制品";
      else if (hasAny(text, ["电子", "小家电", "数据线"])) product = "电子配件/小家电";
      else if (hasAny(text, ["化妆"])) product = "化妆品彩盒";
      else if (hasAny(text, ["内裤", "内衣", "服装"])) product = "服装内衣";
      else if (hasAny(text, ["鱼饼", "海鲜", "食品"])) product = "食品/海产";
      const delivery = extract(/(?:\d+\s*(?:个)?(?:工作)?天(?:内)?(?:交货|出货|完成|到货)?|\d+\s*(?:个)?周(?:内)?(?:交货|出货|完成|到货)?|下周(?:[一二三四五六日天])?(?:交货|出货|完成|到货)?)/, text, "交期待确认");
      const address = extractDeliveryAddress(text);
      const taxParts = [];
      if (hasAny(text, ["不含税", "不开票"])) taxParts.push("不含税");
      else if (hasAny(text, ["含税", "开票"])) taxParts.push("含税");
      taxParts.push("不含运");
      const sampleRequired = hasAny(text, ["不打样", "不需要打样", "无需打样", "不用打样"])
        ? false
        : hasAny(text, ["需要打样", "要打样", "先打样", "做样", "打个样", "打样"])
          ? true
          : null;
      const sampleFee = Number(text.match(/(?:收费打样(?:费)?|打样费|样品费)(?:为|是|：|:)?\s*[¥￥]?\s*(\d+(?:\.\d+)?)/)?.[1] || 0);
      const sampleMode = sampleRequired === false
        ? "不需要打样"
        : sampleRequired === true
          ? (text.includes("免费打样") ? "免费打样" : (text.includes("收费打样") || sampleFee > 0 ? "收费打样" : "需要打样，费用待确认"))
          : "待确认";
      const taxKnown = hasAny(text, ["含税", "不含税", "开票", "不开票"]);
      return {
        product, size, qty, material,
        finish: finishList.join(" + ") || "工艺待确认",
        delivery,
        taxShipping: taxParts.join("，") || "税费待确认，不含运",
        address,
        sampleRequired,
        sampleMode,
        sampleFee,
        sampleStatus: sampleRequired === true ? "需要打样" : sampleRequired === false ? "不需要打样" : "待确认",
        confidence: {
          product: product === "定制产品" ? "low" : "high",
          size: size.includes("待确认") ? "low" : "high",
          qty: qty.includes("待确认") ? "low" : "high",
          material: material.includes("待确认") ? "low" : "high",
          process: finishList.length ? "high" : "low",
          delivery: delivery.includes("待确认") ? "low" : "high",
          tax_shipping: taxKnown ? "high" : "low",
          address: address.includes("待确认") ? "low" : "high",
          sample: sampleRequired === null ? "low" : "high"
        },
        risk: risk.length ? risk : ["需补齐规格、数量、材质、交期后再报价"]
      };
    }
    function addDays(days) {
      const d = new Date();
      d.setDate(d.getDate() + days);
      return d.toISOString().slice(0, 10);
    }
    function nowStamp() {
      return new Date().toLocaleString("zh-CN", { hour12: false });
    }
    function dateValue(item) {
      const raw = item?.createdAt || item?.time || item?.updatedAt || "";
      const d = raw ? new Date(raw) : new Date();
      return Number.isNaN(d.getTime()) ? new Date() : d;
    }
    function startOfDay(date = new Date()) {
      return new Date(date.getFullYear(), date.getMonth(), date.getDate());
    }
    function startOfWeek(date = new Date()) {
      const d = startOfDay(date);
      d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
      return d;
    }
    function startOfMonth(date = new Date()) {
      return new Date(date.getFullYear(), date.getMonth(), 1);
    }
    function isExpired(date) {
      if (!date) return true;
      return new Date(date + "T23:59:59") < new Date();
    }
    function planName() {
      return state.member.plan || "未开通";
    }
    function memberPlanKey(member = state.member) {
      if (member.planKey && member.planKey !== "none") return member.planKey;
      return ({ "永久免费报价版": "trial", "体验版": "trial", "试用版": "trial", "AI跟单版": "standard", "标准版": "standard", "云端标准版": "standard", "团队协作版": "pro", "专业版": "pro", "云端专业版": "pro", "企业版": "enterprise", "企业版（原套餐）": "enterprise", "买断版": "buyout", "工厂买断版": "buyout", "企业定制版": "private_buyout", "私有定制买断版": "private_buyout" }[member.plan]) || "none";
    }
    function isBuyoutPlanKey(key = memberPlanKey()) {
      return ["buyout", "private_buyout"].includes(key);
    }
    function memberPlanRank(member = state.member) {
      return planConfig[memberPlanKey(member)]?.rank || 0;
    }
    function hasEnterpriseTeamAccess() {
      const member = state.member || {};
      return !!member.loggedIn && memberPlanRank(member) >= planConfig.pro.rank
        && (isBuyoutPlanKey(memberPlanKey(member)) || !isExpired(member.expiresAt));
    }
    function hasPaidFollowupAccess() {
      const member = state.member || {};
      const key = memberPlanKey(member);
      return !!member.loggedIn && memberPlanRank(member) >= planConfig.standard.rank
        && (planConfig[key]?.perpetual || isBuyoutPlanKey(key) || !isExpired(member.expiresAt));
    }
    function renderPlanVisibility() {
      const canUseTeam = hasEnterpriseTeamAccess();
      const staffLocked = !!window.__currentUser && !window.__hasPermission?.("task.manage");
      const hidden = !canUseTeam || staffLocked;
      document.querySelectorAll('#nav button[data-page="团队权限"]').forEach(button => button.classList.toggle("is-hidden", hidden));
      document.getElementById("enterpriseTeamCard")?.classList.toggle("is-hidden", hidden);
      document.querySelector(".more-management-grid")?.classList.toggle("without-team", hidden);
    }
    function quotaPercent(member = state.member) {
      if (!member.quotaTotal) return 0;
      return Math.min(100, Math.round(member.quotaUsed / member.quotaTotal * 100));
    }
    function renderMemberStatus() {
      const badge = document.getElementById("memberBadge");
      renderPlanVisibility();
      if (!badge) return;
      const m = state.member;
      if (!m.loggedIn) {
        badge.textContent = "套餐";
        badge.title = "当前未登录；点击进入账号与套餐中心";
        badge.setAttribute("aria-label", "当前未登录，查看账号与套餐");
        return;
      }
      let detail = "";
      if (planConfig[memberPlanKey(m)]?.perpetual || isBuyoutPlanKey(memberPlanKey(m))) {
        detail = `${m.plan} · 永久有效 · 剩余 ${Math.max(m.quotaTotal - m.quotaUsed, 0).toLocaleString()} 点`;
      } else {
        const expiredText = isExpired(m.expiresAt) ? "已到期" : `到期 ${m.expiresAt}`;
        detail = `${m.plan} · ${expiredText} · 剩余 ${Math.max(m.quotaTotal - m.quotaUsed, 0).toLocaleString()} 点`;
      }
      badge.textContent = "套餐";
      badge.title = `${detail}；点击查看详情`;
      badge.setAttribute("aria-label", `${detail}，点击查看详情`);
    }
    function renderMemberSystem() {
      const m = state.member;
      const summary = document.getElementById("memberSummary");
      if (!summary) return;
      const planKey = memberPlanKey(m);
      const isBuyout = isBuyoutPlanKey(planKey);
      const isPerpetual = planConfig[planKey]?.perpetual || isBuyout;
      const remainingPoints = Math.max(Number(m.quotaTotal || 0) - Number(m.quotaUsed || 0), 0);
      summary.innerHTML = `
        <div class="summary-tile"><span>当前账号</span><strong>${m.loggedIn ? m.companyName || "未填写公司" : "未登录"}</strong></div>
        <div class="summary-tile"><span>套餐</span><strong>${planName()}</strong></div>
        <div class="summary-tile"><span>AI点数使用</span><strong>${m.quotaUsed} / ${m.quotaTotal}</strong><div class="progress"><span style="width:${quotaPercent()}%"></span></div></div>
        <div class="summary-tile"><span>${isPerpetual ? "授权状态" : "到期时间"}</span><strong>${isPerpetual ? "永久有效" : m.expiresAt || "未开通"}${Number(m.painPointAnalysisRemaining || 0) > 0 ? ` · ${Number(m.painPointAnalysisRemaining)}次分析待预约` : ""}</strong></div>
      `;
      document.getElementById("buyoutWalletCard")?.classList.toggle("is-hidden", !isBuyout);
      setTextIfExists("buyoutWalletBalance", `${remainingPoints.toLocaleString()}点`);
      memberCompany.value = m.companyName || "";
      memberAccount.value = m.account || "";
      memberContact.value = m.contact || "";
      const currentKey = memberPlanKey(m);
      document.querySelectorAll("[data-member-plan]").forEach(card => {
        const key = card.dataset.memberPlan;
        const isCurrent = m.loggedIn && key === currentKey && (planConfig[key]?.perpetual || isBuyoutPlanKey(key) || !isExpired(m.expiresAt));
        card.classList.toggle("current", isCurrent);
        card.querySelectorAll("[data-plan-action]").forEach(button => {
          button.disabled = isCurrent && key === "trial";
          if (key === "trial") button.textContent = isCurrent ? "正在免费使用" : "免费使用";
        });
      });
      renderMemberStatus();
    }
    function isDueSoon(task) {
      const state = taskDueState(task);
      return ["overdue", "today", "tomorrow"].includes(state);
    }
    function taskDueRaw(task) {
      return `${task?.nextFollow || ""} ${task?.due || ""}`.trim();
    }
    function taskDueHasExplicitTime(task) {
      return /(?:\d{1,2}[:：]\d{2}|(?:上午|下午|中午|晚上|晚间)?\s*\d{1,2}\s*[点时])/.test(taskDueRaw(task));
    }
    function taskDueDate(task) {
      const raw = taskDueRaw(task);
      const isoMatch = raw.match(/(\d{4}-\d{2}-\d{2})(?:[T\s]+(\d{1,2})[:：](\d{2}))?/);
      const timeMatch = raw.match(/(\d{1,2})[:：](\d{2})/);
      let date = null;
      if (isoMatch) {
        date = new Date(`${isoMatch[1]}T00:00:00`);
      } else if (raw.includes("今天") || raw.includes("明天")) {
        date = new Date();
        date.setHours(0, 0, 0, 0);
        if (raw.includes("明天")) date.setDate(date.getDate() + 1);
      }
      if (!date || Number.isNaN(date.getTime())) return null;
      if (isoMatch?.[2] || timeMatch) {
        date.setHours(Number(isoMatch?.[2] || timeMatch?.[1] || 0), Number(isoMatch?.[3] || timeMatch?.[2] || 0), 0, 0);
      } else if (/上午/.test(raw)) date.setHours(9, 0, 0, 0);
      else if (/中午/.test(raw)) date.setHours(12, 0, 0, 0);
      else if (/下午/.test(raw)) date.setHours(15, 0, 0, 0);
      else if (/晚上|晚间/.test(raw)) date.setHours(19, 0, 0, 0);
      else date.setHours(23, 59, 59, 999);
      return date;
    }
    function localDateIso(date = new Date()) {
      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
    }
    function taskDueState(task) {
      if (taskPhase(task) === "已完成") return "done";
      const due = taskDueDate(task);
      if (!due) return "unscheduled";
      const nowDate = new Date();
      if (due.getTime() < nowDate.getTime()) return "overdue";
      if (localDateIso(due) === localDateIso(nowDate)) return "today";
      const tomorrow = new Date(nowDate);
      tomorrow.setDate(tomorrow.getDate() + 1);
      if (localDateIso(due) === localDateIso(tomorrow)) return "tomorrow";
      return "upcoming";
    }
    function formatTaskDue(task) {
      const date = taskDueDate(task);
      if (!date) return task?.due || "待排";
      const state = taskDueState(task);
      const label = state === "overdue" ? "待跟进" : state === "today" ? "今天" : state === "tomorrow" ? "明天" : date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" });
      const time = taskDueHasExplicitTime(task) ? ` ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}` : "";
      return `${label} · ${date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" })}${time}`;
    }
    function isInternalDemoRecord(item = {}) {
      const text = `${item.customer || ""} ${item.title || ""}`;
      return [
        "系统完善测试客户-AI", "正式报价单测试客户", "粘性功能测试客户", "星河包装", "星河定制客户",
        "杭州新客回归厂", "上海极速回归厂", "宁波快速回归厂", "宁波客户名回归厂", "审批功能回归厂",
        "全流程验收测试客户-0711", "地址验收测试厂", "隐私回归厂", "隐私最终验收厂", "云记忆测试客户-AI"
      ].some(name => text.includes(name));
    }
    function taskBelongsToCurrentCompany(task) {
      if (!task || typeof task !== "object") return false;
      if (task.serverId) return true;
      const ownerKey = String(task.localCompanyId || "");
      const companyKey = currentTaskRecycleCompanyKey();
      if (window.location.protocol === "file:") return !ownerKey || ownerKey === companyKey;
      return Boolean(companyKey && ownerKey === companyKey);
    }
    function businessTasks() {
      if (window.location.protocol !== "file:") {
        const currentCompanyId = String(window.__currentUser?.company_id || "");
        const cachedCompanyId = String(window.__formalTaskCacheCompanyId || "");
        if (!currentCompanyId || cachedCompanyId !== currentCompanyId) return [];
      }
      return (state.tasks || []).filter(task => taskBelongsToCurrentCompany(task) && !isInternalDemoRecord(task));
    }
    function businessQuotes() {
      return (state.quoteHistory || []).filter(item => !isInternalDemoRecord(item) && !["新客户", "未命名客户", ""].includes(String(item.customer || "")));
    }
    function businessProfiles() {
      return (state.customerProfiles || []).filter(item => !isInternalDemoRecord({ customer: item.company }));
    }
    function renderTaskReminderSummary() {
      const box = document.getElementById("taskReminderSummary");
      if (!box) return;
      const active = businessTasks().filter(task => taskPhase(task) !== "已完成");
      const overdue = active.filter(task => taskDueState(task) === "overdue");
      const today = active.filter(task => taskDueState(task) === "today");
      const tomorrow = active.filter(task => taskDueState(task) === "tomorrow");
      box.classList.toggle("is-overdue", overdue.length > 0);
      const notificationReady = typeof Notification !== "undefined" && Notification.permission === "granted";
      box.innerHTML = `<span><strong>${overdue.length ? `${overdue.length} 条待继续跟进` : "今日跟单提醒"}</strong>今天 ${today.length} 条 · 明天 ${tomorrow.length} 条${overdue.length ? " · 可按计划时间继续联系" : ""}</span><button class="btn light" type="button" onclick="enableTaskNotifications()">${notificationReady ? "桌面提醒已开启" : "开启桌面提醒"}</button>`;
      notifyDueTasks(overdue, today);
    }
    async function enableTaskNotifications() {
      if (typeof Notification === "undefined") {
        notify("当前系统不支持桌面通知，请在跟单页查看提醒。" );
        return;
      }
      const permission = await Notification.requestPermission();
      renderTaskReminderSummary();
      if (permission === "granted") notify("桌面跟单提醒已开启。" );
      else notify("未开启通知权限，软件内提醒仍然有效。" );
    }
    function notifyDueTasks(overdue, today) {
      if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
      if (!overdue.length && !today.length) return;
      const key = `factory_ai_task_notice_${new Date().toISOString().slice(0, 10)}`;
      if (localStorage.getItem(key)) return;
      new Notification("将莱数字员工 · 今日跟单提醒", {
        body: `待继续跟进 ${overdue.length} 条，今天计划跟进 ${today.length} 条。打开软件即可处理。`
      });
      localStorage.setItem(key, "1");
    }
    function newTaskId() {
      const date = new Date();
      const day = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}${String(date.getDate()).padStart(2, "0")}`;
      // Local task ids are also used as UI action keys. Keep them unique across
      // every cached company and recycle bin so an old hidden task can never be
      // selected when another factory creates or restores a task on the same day.
      const active = state.tasks || [];
      const recycled = (state.taskRecycleBin || []).filter(task => task?.localRecycle === true);
      const maxStored = [...active, ...recycled].reduce((maximum, task) => {
        const match = String(task?.id || "").match(new RegExp(`^GD-${day}-(\\d+)$`));
        return match ? Math.max(maximum, Number(match[1]) || 0) : maximum;
      }, 0);
      const sequences = window.__taskIdSequences || (window.__taskIdSequences = {});
      const next = Math.max(maxStored, Number(sequences[day] || 0)) + 1;
      sequences[day] = next;
      return `GD-${day}-${String(next).padStart(3, "0")}`;
    }
    function displayTaskId(task) {
      const raw = String(task?.id || "");
      if (raw.startsWith("GD-")) return raw;
      const fallbackTime = /^T\d{10,}$/.test(raw) ? Number(raw.slice(1)) : NaN;
      const date = new Date(task?.createdAt || fallbackTime || Date.now());
      const day = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}${String(date.getDate()).padStart(2, "0")}`;
      const suffix = (raw.match(/\d+/)?.[0] || "1").slice(-3).padStart(3, "0");
      return `GD-${day}-${suffix}`;
    }
    function isUrgentTask(task) {
      const text = `${task.status || ""}${task.due || ""}${task.risk || ""}${task.nextAction || ""}`;
      return ["急", "逾期", "异常", "下周要货", "马上", "交期偏紧"].some(word => text.includes(word));
    }
    function quoteRulesReadyCount() {
      const s = state.settings || {};
      return [
        String(s.capacityRules || "").trim().length > 0,
        parseRules(s.materialRules).length > 0,
        parseRules(s.processRules).length > 0,
        Number.isFinite(Number(s.print)),
        Number.isFinite(Number(s.profitNormal)),
        Boolean(s.delivery),
        Number.isFinite(Number(s.moq))
      ].filter(Boolean).length;
    }
    function renderHomeDailyBrief() {
      const box = document.getElementById("homeDailyBrief");
      if (!box) return;
      const tasks = businessTasks();
      const quotes = businessQuotes();
      const activeSessions = (state.customerSessions || []).filter(session =>
        session && !session.archivedAt && session.customer !== "新客户" && (session.hasConversation || String(session.draft || "").trim() || isPendingCustomerName(session.customer))
      ).slice().sort((a, b) => (Date.parse(b.updatedAt || "") || 0) - (Date.parse(a.updatedAt || "") || 0));
      if (!tasks.length && !quotes.length) {
        if (activeSessions.length) {
          box.innerHTML = `
            <div class="home-daily-head"><strong>今天经营重点</strong><span>${new Date().toLocaleDateString("zh-CN", { month: "long", day: "numeric", weekday: "short" })}</span></div>
            <div class="home-priority-three">
              ${activeSessions.slice(0, 3).map((session, index) => { const display = customerSessionDisplay(session); return `<div class="home-priority-row"><span class="home-priority-rank">${index + 1}</span><span class="home-priority-copy"><strong>${escapeHtml(session.customer || "新客户")}</strong><span>${escapeHtml(display.title)} · ${escapeHtml(display.action)}</span></span><button class="btn light" type="button" data-daily-session-id="${escapeHtml(session.id)}">继续处理</button></div>`; }).join("")}
            </div>`;
          box.querySelectorAll("[data-daily-session-id]").forEach(button => {
            button.addEventListener("click", () => continuePendingCustomerSession(
              encodeURIComponent(button.dataset.dailySessionId || "")
            ));
          });
          return;
        }
        box.innerHTML = `
          <div class="home-daily-head"><strong>今天经营重点</strong><span>${new Date().toLocaleDateString("zh-CN", { month: "long", day: "numeric", weekday: "short" })}</span></div>
          <div class="home-daily-empty">
            <div><strong>今天暂无需要优先处理的事项</strong><span>可以新建报价、添加跟单或进入制图处理客户商品图片。</span></div>
            <button class="btn dark" type="button" onclick="showPage('报价系统')">新建报价</button>
          </div>
        `;
        return;
      }
      const activeTasks = tasks.filter(task => taskPhase(task) !== "已完成");
      const priorityTasks = [...activeTasks].sort((a, b) => {
        const score = task => (taskDueState(task) === "overdue" ? 400 : taskDueState(task) === "today" ? 300 : taskDueState(task) === "tomorrow" ? 200 : 0) + (isUrgentTask(task) ? 100 : 0);
        return score(b) - score(a);
      }).slice(0, 3);
      box.innerHTML = `
        <div class="home-priority-three">
          ${priorityTasks.length ? priorityTasks.map((task, index) => `<div class="home-priority-row"><span class="home-priority-rank">${index + 1}</span><span class="home-priority-copy"><strong>${escapeHtml(task.customer || task.title || "未命名客户")}</strong><span>${escapeHtml(taskDueState(task) === "overdue" ? "待继续跟进" : taskDueState(task) === "today" ? "今天计划跟进" : taskDueState(task) === "tomorrow" ? "明天跟进" : task.status || "待处理")} · ${escapeHtml(task.nextAction || "继续跟进")}</span></span><button class="btn light" onclick="showPage('跟单工单')">处理</button></div>`).join("") : `<div class="home-daily-next"><span><strong>暂无紧急待办</strong> 可以开始处理新的报价或商品图片。</span><button class="btn light" onclick="showPage('图片制作')">开始制图</button></div>`}
        </div>
      `;
    }
    function renderHomeBusinessPulse() {
      const box = document.getElementById("homeBusinessPulse");
      if (!box) return;
      const monthStart = startOfMonth().getTime();
      const quotes = businessQuotes().filter(item => dateValue(item).getTime() >= monthStart);
      const profiles = businessProfiles().filter(item => {
        const timestamp = Date.parse(item?.updatedAt || item?.updated_at || item?.createdAt || item?.created_at || "");
        return timestamp >= monthStart;
      });
      const hasDecidedCustomer = profiles.some(item => ["已成交", "已流失"].includes(item.dealStatus) || Number(item.actualDealTotal || 0) > 0)
        || quotes.some(item => ["已成交", "已流失"].includes(item.outcomeStatus));
      if (!quotes.length && !hasDecidedCustomer) {
        box.classList.add("is-hidden");
        box.innerHTML = "";
        return;
      }
      box.classList.remove("is-hidden");
      const quotedCustomers = new Set(quotes.map(item => item.customer).filter(Boolean));
      const wonCustomers = new Map();
      const lostCustomers = new Set();
      profiles.forEach(item => {
        const customer = String(item.company || "").trim();
        if (!customer) return;
        if (item.dealStatus === "已成交") wonCustomers.set(customer, Number(item.actualDealTotal || 0));
        if (item.dealStatus === "已流失") lostCustomers.add(customer);
      });
      quotes.forEach(item => {
        const customer = String(item.customer || "").trim();
        if (!customer) return;
        if (item.outcomeStatus === "已成交") wonCustomers.set(customer, Number(item.actualDealTotal || item.total || 0));
        if (item.outcomeStatus === "已流失") lostCustomers.add(customer);
      });
      wonCustomers.forEach((value, customer) => lostCustomers.delete(customer));
      const decided = wonCustomers.size + lostCustomers.size;
      const conversion = decided ? wonCustomers.size / decided * 100 : 0;
      const revenue = [...wonCustomers.values()].reduce((sum, value) => sum + Number(value || 0), 0);
      const actualProfitQuotes = quotes.filter(item => item.outcomeStatus === "已成交" && item.actualProfit != null && Number.isFinite(Number(item.actualProfit)));
      const actualProfit = actualProfitQuotes.reduce((sum, item) => sum + Number(item.actualProfit || 0), 0);
      const actualCostFilled = actualProfitQuotes.length;
      const tasks = businessTasks();
      const activeRisks = tasks.filter(item => taskPhase(item) !== "已完成" && taskRiskText(item) !== "无明显风险").length;
      const guardedQuotes = quotes.filter(item => item.approvalStatus === "待老板确认" || Number(item.marginRate || 100) < 15).length;
      const visualVersions = (state.visualVersions || []).filter(item => new Date(item.createdAt || 0).getTime() >= monthStart);
      const confirmedVisuals = visualVersions.filter(item => item.status === "confirmed_locked").length;
      const productTotals = new Map();
      quotes.filter(item => item.outcomeStatus === "已成交").forEach(item => productTotals.set(item.product || "未命名产品", (productTotals.get(item.product || "未命名产品") || 0) + Number(item.actualDealTotal || item.total || 0)));
      const topProduct = [...productTotals.entries()].sort((a, b) => b[1] - a[1])[0];
      box.innerHTML = `
        <div class="home-pulse-item"><span>本月成交额</span><strong>${wonCustomers.size && !revenue ? "待补录" : `¥${revenue.toFixed(0)}`}</strong><em>${wonCustomers.size} 个客户已成交</em></div>
        <div class="home-pulse-item"><span>已成交客户</span><strong>${wonCustomers.size}</strong><em>以本月实际成交结果为准</em></div>
        <div class="home-pulse-item"><span>实际订单利润</span><strong>${actualCostFilled ? `¥${actualProfit.toFixed(0)}` : "待复盘"}</strong><em>${actualCostFilled ? `${actualCostFilled} 笔订单已录实际成本` : "订单结束后录入实际成本"}</em></div>
        <div class="home-pulse-item"><span>成交转化率</span><strong>${conversion.toFixed(0)}%</strong><em>${decided ? `${decided} 个已判定客户` : "成交/流失后自动计算"}</em></div>
        <div class="home-pulse-item"><span>成交重点产品</span><strong>${escapeHtml(topProduct?.[0] || "待积累")}</strong><em>${topProduct ? `成交 ${businessReportMoney(topProduct[1])}` : "记录成交后自动统计"}</em></div>
        <div class="home-pulse-item"><span>风险提前拦截</span><strong>${guardedQuotes + activeRisks}</strong><em>${guardedQuotes} 份风险报价 · ${activeRisks} 个订单风险</em></div>
        <div class="home-pulse-item"><span>本月报价客户</span><strong>${quotedCustomers.size}</strong><em>${quotes.length} 份报价仅作跟进参考</em></div>
        <div class="home-pulse-item"><span>客户确认效果图</span><strong>${confirmedVisuals}</strong><em>${visualVersions.length} 个版本已留档</em></div>
      `;
    }
    let businessReportMode = "daily";
    function businessReportTimestamp(item) {
      const raw = item?.createdAt || item?.created_at || item?.time || item?.updatedAt || item?.updated_at || "";
      const timestamp = raw ? Date.parse(raw) : NaN;
      return Number.isFinite(timestamp) ? timestamp : 0;
    }
    function businessReportMoney(value) {
      const amount = Number(value || 0);
      return `¥${(Number.isFinite(amount) ? amount : 0).toLocaleString("zh-CN", { maximumFractionDigits:0 })}`;
    }
    function setBusinessReportMode(mode) {
      businessReportMode = mode === "monthly" ? "monthly" : "daily";
      renderBusinessReport();
    }
    function renderBusinessReport() {
      const box = document.getElementById("businessReportContent");
      if (!box) return;
      const tasks = businessTasks();
      const quotes = businessQuotes();
      const profiles = businessProfiles();
      const now = new Date();
      const dayStart = startOfDay(now).getTime();
      const dayEnd = dayStart + 86400000;
      const monthStart = startOfMonth(now).getTime();
      const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime();
      const isDaily = businessReportMode !== "monthly";
      const periodStart = isDaily ? dayStart : monthStart;
      const periodEnd = isDaily ? dayEnd : monthEnd;
      const periodQuotes = quotes.filter(item => {
        const timestamp = businessReportTimestamp(item);
        return timestamp >= periodStart && timestamp < periodEnd;
      }).sort((left, right) => businessReportTimestamp(right) - businessReportTimestamp(left));
      const periodProfiles = profiles.filter(item => {
        const raw = isDaily
          ? (item?.createdAt || item?.created_at || "")
          : (item?.updatedAt || item?.updated_at || item?.createdAt || item?.created_at || "");
        const timestamp = raw ? Date.parse(raw) : NaN;
        return timestamp >= periodStart && timestamp < periodEnd;
      });
      const activeTasks = tasks.filter(task => taskPhase(task) !== "已完成");
      const overdue = activeTasks.filter(task => taskDueState(task) === "overdue");
      const dueToday = activeTasks.filter(task => taskDueState(task) === "today");
      const dueTomorrow = activeTasks.filter(task => taskDueState(task) === "tomorrow");
      const priorityTasks = [...activeTasks].sort((left, right) => {
        const score = task => (taskDueState(task) === "overdue" ? 400 : taskDueState(task) === "today" ? 300 : taskDueState(task) === "tomorrow" ? 200 : 0) + (isUrgentTask(task) ? 100 : 0);
        return score(right) - score(left) || businessReportTimestamp(right) - businessReportTimestamp(left);
      }).slice(0, 5);
      document.querySelectorAll("[data-business-report-mode]").forEach(button => {
        const active = button.dataset.businessReportMode === businessReportMode;
        button.classList.toggle("active", active);
        button.setAttribute("aria-selected", active ? "true" : "false");
      });

      if (isDaily) {
        const headline = overdue.length
          ? `今天可继续跟进 ${overdue.length} 位客户`
          : dueToday.length ? `今天有 ${dueToday.length} 条跟单需要完成`
          : periodQuotes.length ? `今天已完成 ${periodQuotes.length} 份报价`
          : "今天暂无紧急事项，可以主动跟进客户";
        const summary = `今日新增 ${periodProfiles.length} 个客户记录，生成 ${periodQuotes.length} 份报价；当前还有 ${activeTasks.length} 条进行中的跟单。`;
        const quoteRows = periodQuotes.slice(0, 5).map(item => `<div class="business-report-row">
          <div class="business-report-row-copy"><strong>${escapeHtml(item.customer || "未命名客户")} · ${escapeHtml(item.product || "定制产品")}</strong><span>${businessReportMoney(item.total)} · ${escapeHtml(new Date(businessReportTimestamp(item)).toLocaleTimeString("zh-CN", { hour:"2-digit", minute:"2-digit", hour12:false }))}</span></div>
          <button class="btn light" type="button" onclick="viewSavedQuote('${encodeURIComponent(quoteRecordKey(item))}')">查看报价</button>
        </div>`).join("") || `<div class="business-report-empty">今天还没有生成报价。完成客户核价后，这里会自动出现。</div>`;
        const taskRows = priorityTasks.map(task => {
          const dueState = taskDueState(task);
          const taskId = String(task.id || "").replace(/[^A-Za-z0-9_-]/g, "");
          const stateText = dueState === "overdue" ? "待继续跟进" : dueState === "today" ? "今天计划跟进" : dueState === "tomorrow" ? "明天跟进" : task.status || "待处理";
          return `<div class="business-report-row">
            <div class="business-report-row-copy"><strong>${escapeHtml(task.customer || task.title || "未命名客户")}</strong><span>${escapeHtml(stateText)} · ${escapeHtml(task.nextAction || "继续跟进")}</span></div>
            <button class="btn light" type="button" onclick="openTaskDetail('${taskId}')">处理</button>
          </div>`;
        }).join("") || `<div class="business-report-empty">暂无需要优先处理的跟单。</div>`;
        box.innerHTML = `<div class="business-report-hero">
          <div><span>今日经营结论</span><strong>${headline}</strong><em>${summary}</em></div>
          <div class="business-report-period"><strong>${now.toLocaleDateString("zh-CN", { month:"long", day:"numeric", weekday:"long" })}</strong><em>更新于 ${now.toLocaleTimeString("zh-CN", { hour:"2-digit", minute:"2-digit", hour12:false })}</em></div>
        </div>
        <div class="business-report-metrics">
          <div class="business-report-metric"><span>今日新报价</span><strong>${periodQuotes.length}</strong><em>已正式留档的报价数量</em></div>
          <div class="business-report-metric"><span>进行中跟单</span><strong>${activeTasks.length}</strong><em>正在推动成交或生产的任务</em></div>
          <div class="business-report-metric"><span>今天到期</span><strong>${dueToday.length}</strong><em>明天还有 ${dueTomorrow.length} 条</em></div>
          <div class="business-report-metric"><span>待继续跟进</span><strong>${overdue.length}</strong><em>${overdue.length ? "按原计划时间继续联系" : "当前均按计划推进"}</em></div>
        </div>
        <div class="business-report-grid">
          <section class="business-report-panel"><div class="business-report-panel-head"><strong>今天的报价记录</strong><span>最近 ${Math.min(5, periodQuotes.length)} 条</span></div><div class="business-report-list">${quoteRows}</div></section>
          <section class="business-report-panel"><div class="business-report-panel-head"><strong>现在优先处理</strong><span>按计划跟进和交期排序</span></div><div class="business-report-list">${taskRows}</div></section>
        </div>`;
        return;
      }

      const quotedCustomers = new Set(periodQuotes.map(item => String(item.customer || "").trim()).filter(Boolean));
      const wonCustomers = new Map();
      const lostCustomers = new Set();
      periodProfiles.forEach(item => {
        const customer = String(item.company || "").trim();
        if (!customer) return;
        if (item.dealStatus === "已成交") wonCustomers.set(customer, Number(item.actualDealTotal || 0));
        if (item.dealStatus === "已流失") lostCustomers.add(customer);
      });
      periodQuotes.forEach(item => {
        const customer = String(item.customer || "").trim();
        if (!customer) return;
        if (item.outcomeStatus === "已成交") wonCustomers.set(customer, Number(item.actualDealTotal || item.total || 0));
        if (item.outcomeStatus === "已流失") lostCustomers.add(customer);
      });
      wonCustomers.forEach((value, customer) => lostCustomers.delete(customer));
      const decidedCount = wonCustomers.size + lostCustomers.size;
      const pipelineCustomers = new Set([...quotedCustomers, ...wonCustomers.keys(), ...lostCustomers]);
      const conversion = pipelineCustomers.size ? wonCustomers.size / pipelineCustomers.size * 100 : 0;
      const decidedConversion = decidedCount ? wonCustomers.size / decidedCount * 100 : 0;
      const revenue = [...wonCustomers.values()].reduce((sum, value) => sum + Number(value || 0), 0);
      const profitQuotes = periodQuotes.filter(item => item.outcomeStatus === "已成交" && item.actualProfit != null && Number.isFinite(Number(item.actualProfit)));
      const actualProfit = profitQuotes.reduce((sum, item) => sum + Number(item.actualProfit || 0), 0);
      const wonWithoutCost = periodQuotes.filter(item => item.outcomeStatus === "已成交" && item.actualProfit == null);
      const activeRisks = activeTasks.filter(item => taskRiskText(item) !== "无明显风险");
      const productTotals = new Map();
      periodQuotes.filter(item => item.outcomeStatus === "已成交").forEach(item => productTotals.set(item.product || "未命名产品", (productTotals.get(item.product || "未命名产品") || 0) + Number(item.actualDealTotal || item.total || 0)));
      const topProduct = [...productTotals.entries()].sort((left, right) => right[1] - left[1])[0];
      const headline = wonCustomers.size
        ? (revenue ? `本月已成交 ${businessReportMoney(revenue)}` : `本月已成交 ${wonCustomers.size} 个客户，金额待补录`)
        : "本月尚未成交，先推动意向客户确认";
      const summary = `本月成交客户 ${wonCustomers.size} 个；${profitQuotes.length ? `${profitQuotes.length} 笔订单已录实际利润。` : "成交后补录实际成本，就能看到真实利润。"} 另有 ${quotedCustomers.size} 个报价客户等待跟进。`;
      const quoteRows = periodQuotes.slice(0, 6).map(item => `<div class="business-report-row">
        <div class="business-report-row-copy"><strong>${escapeHtml(item.customer || "未命名客户")} · ${escapeHtml(item.product || "定制产品")}</strong><span>${businessReportMoney(item.total)} · ${escapeHtml(item.outcomeStatus || "等待客户结果")}</span></div>
        <button class="btn light" type="button" onclick="viewSavedQuote('${encodeURIComponent(quoteRecordKey(item))}')">查看报价</button>
      </div>`).join("") || `<div class="business-report-empty">本月还没有报价记录。</div>`;
      box.innerHTML = `<div class="business-report-hero">
        <div><span>本月经营结论</span><strong>${headline}</strong><em>${summary}</em></div>
        <div class="business-report-period"><strong>${now.toLocaleDateString("zh-CN", { year:"numeric", month:"long" })}</strong><em>数据来自报价、跟单和客户档案</em></div>
      </div>
      <div class="business-report-metrics">
        <div class="business-report-metric" data-tone="success"><span>本月成交额</span><strong>${wonCustomers.size && !revenue ? "待补录" : businessReportMoney(revenue)}</strong><em>${wonCustomers.size ? "以实际成交金额为准" : "本月还没有成交记录"}</em></div>
        <div class="business-report-metric" data-tone="success"><span>本月成交客户</span><strong>${wonCustomers.size}</strong><em>已明确记录成交结果</em></div>
        <div class="business-report-metric"><span>实际订单利润</span><strong>${profitQuotes.length ? businessReportMoney(actualProfit) : "待复盘"}</strong><em>${profitQuotes.length ? `${profitQuotes.length} 笔已录成本` : "成交后补录实际成本"}</em></div>
        <div class="business-report-metric"><span>报价→成交率</span><strong>${conversion.toFixed(0)}%</strong><em>${pipelineCustomers.size ? `${wonCustomers.size} 成交 / ${pipelineCustomers.size} 个进入报价或结果阶段${decidedCount ? ` · 已决成交率 ${decidedConversion.toFixed(0)}%` : ""}` : "有报价或成交后自动计算"}</em></div>
        <div class="business-report-metric"><span>成交重点产品</span><strong>${escapeHtml(topProduct?.[0] || "待积累")}</strong><em>${topProduct ? `成交 ${businessReportMoney(topProduct[1])}` : "有成交后自动统计"}</em></div>
        <div class="business-report-metric" data-tone="${activeRisks.length ? "danger" : "success"}"><span>当前风险订单</span><strong>${activeRisks.length}</strong><em>${activeRisks.length ? "需要检查交期和资料" : "当前没有已标记风险"}</em></div>
        <div class="business-report-metric"><span>成交待补成本</span><strong>${wonWithoutCost.length}</strong><em>${wonWithoutCost.length ? "补录后显示真实利润" : "成交成本记录完整"}</em></div>
        <div class="business-report-metric"><span>本月报价客户</span><strong>${quotedCustomers.size}</strong><em>${periodQuotes.length} 份报价仅作成交跟进参考</em></div>
      </div>
      <div class="business-report-grid">
        <section class="business-report-panel"><div class="business-report-panel-head"><strong>本月报价记录</strong><span>最近 ${Math.min(6, periodQuotes.length)} 条 · 仅作成交跟进参考</span></div><div class="business-report-list">${quoteRows}</div></section>
        <section class="business-report-panel"><div class="business-report-panel-head"><strong>本月需要关注</strong><span>直接去处理</span></div><div class="business-report-actions">
          <div class="business-report-action"><div><strong>${activeRisks.length} 个风险订单</strong><span>检查交期、资料和生产异常</span></div><button class="btn light" type="button" onclick="showPage('跟单工单')">去跟单</button></div>
          <div class="business-report-action"><div><strong>${wonWithoutCost.length} 笔成交待补成本</strong><span>补录实际材料和加工费后核算利润</span></div><button class="btn light" type="button" onclick="showPage('报价系统')">去报价</button></div>
          <div class="business-report-action"><div><strong>${lostCustomers.size} 个客户已流失</strong><span>到客户档案复盘原因和报价口径</span></div><button class="btn light" type="button" onclick="showPage('客户档案')">看客户</button></div>
        </div></section>
      </div>`;
    }
    function openImageStudioAndFocus() {
      if (showPage("图片制作") === false) return;
      window.setTimeout(() => document.getElementById("aiQuestion")?.focus(), 40);
    }
    function openDashboardCustomerCreate() {
      showPage("客户档案");
      window.setTimeout(() => {
        const details = document.getElementById("customerEditorDetails");
        if (details) details.open = true;
        document.getElementById("profileCompany")?.focus();
      }, 40);
    }
    function openDashboardCustomerAction(customerCode, action = "follow") {
      const customer = decodeURIComponent(customerCode || "").trim();
      if (!customer) return;
      if (action === "quote") {
        showPage("报价系统");
        setIfExists("quoteCustomerName", customer);
        document.getElementById("quoteCustomerName")?.focus();
        return;
      }
      if (action === "customer") {
        showPage("客户档案");
        window.setTimeout(() => {
          editCustomerProfile(encodeURIComponent(customer));
          document.getElementById("profileCompany")?.focus();
        }, 80);
        return;
      }
      const task = businessTasks()
        .filter(item => String(item.customer || item.title || "").includes(customer) && taskPhase(item) !== "已完成")
        .sort((left, right) => businessReportTimestamp(right) - businessReportTimestamp(left))[0];
      if (task) openTaskDetail(task.id);
      else showPage("跟单工单");
    }
    function renderHomeCommandCenter() {
      const lead = document.getElementById("homePriorityLead");
      const metrics = document.getElementById("homeDashboardMetrics");
      const customersBox = document.getElementById("homeDashboardCustomers");
      const pipelineBox = document.getElementById("homeBusinessPipeline");
      const businessBox = document.getElementById("homeBusinessSnapshot");
      if (!lead && !metrics && !customersBox && !pipelineBox && !businessBox) return;
      const tasks = businessTasks();
      const profiles = businessProfiles();
      const quotes = businessQuotes();
      const activeTasks = tasks.filter(task => taskPhase(task) !== "已完成");
      const inquiryNeedsAction = activeTasks.filter(task => ["询盘", "待补资料", "待报价"].includes(task.status)).length;
      const inquiryQuoteCount = activeTasks.filter(task => ["询盘", "待补资料", "待报价", "已报价"].includes(task.status)).length;
      const inProduction = activeTasks.filter(task => ["生产中", "交付中"].includes(taskPhase(task)) || task.status === "打样").length;
      const waitingFollow = activeTasks.filter(task => ["已报价", "待确认"].includes(task.status)).length;
      if (lead) {
        const recommendation = waitingFollow
          ? { title:`先跟进 ${waitingFollow} 位已报价客户`, note:"报价后的及时跟进最容易影响成交", button:"开始跟进", page:"跟单工单" }
          : inquiryNeedsAction
            ? { title:`先处理 ${inquiryNeedsAction} 笔询盘和报价`, note:"整理需求、补齐资料并完成核价", button:"处理询盘/报价", page:"询盘处理" }
            : inProduction
              ? { title:`查看 ${inProduction} 笔生产中订单`, note:"确认打样、生产和交付是否按计划进行", button:"查看进度", page:"跟单工单" }
              : { title:"今天没有紧急事项", note:"可以处理新报价，或为客户制作商品图片", button:"开始制图", page:"图片制作" };
        lead.innerHTML = `<div class="home-command-lead-copy"><span>今天先做什么</span><strong>${escapeHtml(recommendation.title)}</strong><em>${escapeHtml(recommendation.note)}</em></div><button class="btn dark" type="button" onclick="showPage('${recommendation.page}')">${escapeHtml(recommendation.button)}</button>`;
      }
      if (metrics) metrics.innerHTML = [
        { label:"待跟进", value:waitingFollow, note:"报价后等待客户确认", page:"跟单工单" },
        { label:"生产中", value:inProduction, note:"打样、生产和交付中", page:"跟单工单" }
      ].map(item => `<div class="home-command-metric" role="button" tabindex="0" onclick="showPage('${item.page}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();showPage('${item.page}')}\"><span>${escapeHtml(item.label)}</span><strong>${escapeHtml(String(item.value))}</strong><em>${escapeHtml(item.note)}</em></div>`).join("");

      if (pipelineBox) {
        const stages = [
          { label:"询盘/报价", note:"整理需求并核价", value:inquiryQuoteCount, action:"showPage('询盘处理')" },
          { label:"确认/打样", note:"客户确认", value:activeTasks.filter(task => ["待确认", "打样"].includes(task.status)).length, action:"showPage('跟单工单')" },
          { label:"生产", note:"正在加工", value:activeTasks.filter(task => ["生产", "生产中"].includes(task.status) || taskPhase(task) === "生产中").length, action:"showPage('跟单工单')" },
          { label:"交付", note:"质检、发货与签收", value:activeTasks.filter(task => taskPhase(task) === "交付中").length, action:"showPage('跟单工单')" },
          { label:"回款", note:"定金、尾款与开票", value:activeTasks.filter(task => taskPhase(task) === "回款中").length, action:"showPage('跟单工单')" }
        ];
        pipelineBox.innerHTML = stages.map(item => `<button class="home-command-stage" type="button" onclick="${item.action}"><span>${escapeHtml(item.label)}</span><strong>${escapeHtml(String(item.value))}</strong><em>${escapeHtml(item.note)}</em></button>`).join("");
      }

      if (customersBox) {
        const recent = new Map();
        (state.customerSessions || []).slice().sort((left, right) => (Date.parse(right?.updatedAt || "") || 0) - (Date.parse(left?.updatedAt || "") || 0)).forEach(session => {
          const customer = String(session?.customer || "").trim();
          if (!customer || customer === "新客户" || session.archivedAt || recent.has(customer)) return;
          const display = customerSessionDisplay(session);
          recent.set(customer, { customer, status:display.title || "等待处理", action:display.action || "继续跟进", updatedAt:session.updatedAt || "" });
        });
        profiles.slice().sort((left, right) => (Date.parse(right?.updatedAt || right?.updated_at || "") || 0) - (Date.parse(left?.updatedAt || left?.updated_at || "") || 0)).forEach(profile => {
          const customer = String(profile?.company || "").trim();
          if (!customer || recent.has(customer)) return;
          recent.set(customer, { customer, status:profile.dealStatus || "未报价", action:profile.need || "查看客户资料", updatedAt:profile.updatedAt || profile.updated_at || "" });
        });
        const rows = [...recent.values()].slice(0, 3);
        customersBox.innerHTML = rows.length ? rows.map(item => {
          const code = encodeURIComponent(item.customer);
          const stateText = `${item.status || ""} ${item.action || ""}`;
          const needsName = /先补客户姓名|新客户/.test(stateText);
          const needsQuote = /未报价|待报价|询盘|补资料/.test(stateText);
          const isProducing = /打样|生产|待发货|交付/.test(stateText);
          const buttonLabel = needsName ? "补姓名" : needsQuote ? "去报价" : isProducing ? "看进度" : "去跟进";
          const buttonAction = needsName ? "customer" : needsQuote ? "quote" : "follow";
          return `<div class="home-command-customer"><div><strong>${escapeHtml(item.customer)}</strong><span>${escapeHtml(item.status)} · ${escapeHtml(item.action)}</span></div><div class="home-command-customer-actions"><button type="button" onclick="openDashboardCustomerAction('${code}','${buttonAction}')">${buttonLabel}</button></div></div>`;
        }).join("") : `<div class="home-command-empty">还没有客户记录。收到询盘后先录入客户，后续报价和跟单会自动归档。</div>`;
      }

      if (businessBox) {
        const monthStart = startOfMonth().getTime();
        const monthDeals = new Map();
        profiles.forEach(item => {
          const timestamp = Date.parse(item?.updatedAt || item?.updated_at || item?.createdAt || item?.created_at || "");
          const customer = String(item?.company || "").trim();
          if (customer && item.dealStatus === "已成交" && timestamp >= monthStart) monthDeals.set(customer, Number(item.actualDealTotal || 0));
        });
        quotes.forEach(item => {
          const customer = String(item?.customer || "").trim();
          if (customer && item.outcomeStatus === "已成交" && businessReportTimestamp(item) >= monthStart) monthDeals.set(customer, Number(item.actualDealTotal || item.total || 0));
        });
        const monthRevenue = [...monthDeals.values()].reduce((sum, value) => sum + Number(value || 0), 0);
        const value = monthDeals.size && !monthRevenue ? "待补录" : `¥${monthRevenue.toLocaleString("zh-CN", { maximumFractionDigits:0 })}`;
        const riskCount = activeTasks.filter(task => taskRiskText(task) !== "无明显风险").length;
        businessBox.innerHTML = `<div class="home-command-business">
          <div class="home-command-business-item"><span>本月成交</span><strong>${escapeHtml(value)}</strong><em>${monthDeals.size} 个客户已成交</em></div>
          <div class="home-command-business-item"><span>进行中业务</span><strong>${escapeHtml(String(activeTasks.length))}</strong><em>询盘、报价、打样和生产</em></div>
          <div class="home-command-business-item"><span>风险事项</span><strong>${escapeHtml(String(riskCount))}</strong><em>${riskCount ? "需要检查交期和资料" : "当前没有已标记风险"}</em></div>
        </div><div class="home-command-source"><span>数据来自报价、跟单和客户档案</span><button type="button" onclick="showPage('经营报表')">查看完整报表 →</button></div>`;
      }
    }
    function bossExceptionItems(tasks = businessTasks()) {
      const items = [];
      tasks.filter(task => taskPhase(task) !== "已完成").forEach(task => {
        const label = task.cloudCustomer || task.customer || task.title || "未命名订单";
        const taskId = task.id;
        const shortage = Number(task?.inventorySummary?.materials_shortage ?? task?.inventory_summary?.materials_shortage ?? 0);
        if (shortage > 0 || productionTaskHasPendingMaterial(task)) items.push({ taskId, severity:5, type:"缺料", label, desc:shortage > 0 ? `缺 ${shortage.toLocaleString("zh-CN")} 项/份材料，待采购或到货` : "采购或到货未完成" });
        const scheduleAlerts = task.scheduleAlerts || task.schedule_alerts || {};
        if (scheduleAlerts.has_conflict) items.push({ taskId, severity:5, type:"排产冲突", label, desc:(scheduleAlerts.messages || []).join("；") || "同一机台工时重叠，可能影响交期" });
        const operations = factoryOperationsForTask(task);
        const summary = factorySummaryForTask(task);
        const shipped = Number(summary.shipped_quantity || operations.warehouse.shipped_quantity || 0);
        const signed = Number(summary.signed_quantity || operations.warehouse.signed_quantity || 0);
        if (shipped > signed) items.push({ taskId, severity:4, type:"未签收", label, desc:`已发 ${shipped.toLocaleString("zh-CN")} 件，尚有 ${(shipped - signed).toLocaleString("zh-CN")} 件未签收` });
        if (Number(summary.overdue_days || 0) > 0 && Number(summary.receivable_amount || 0) > 0) items.push({ taskId, severity:5, type:"回款逾期", label, desc:`逾期 ${Number(summary.overdue_days)} 天，待收 ${factoryMoney(summary.receivable_amount)}` });
        else if (Number(summary.receivable_amount || 0) > 0 && ["已签收","待回款","已回款"].includes(task.status)) items.push({ taskId, severity:3, type:"待回款", label, desc:`剩余应收 ${factoryMoney(summary.receivable_amount)}` });
        if (Number(summary.open_after_sales || 0) > 0) items.push({ taskId, severity:5, type:"售后", label, desc:`${Number(summary.open_after_sales)} 项客诉/补货/返工待处理` });
        if (["返工","不合格"].includes(operations.quality.status)) items.push({ taskId, severity:5, type:"质量异常", label, desc:`质检结果：${operations.quality.status}` });
        if (taskDueState(task) === "overdue") items.push({ taskId, severity:4, type:"交期逾期", label, desc:task.nextAction || "订单交期已过，请立即跟进" });
      });
      return items.sort((a,b) => b.severity - a.severity || String(a.label).localeCompare(String(b.label), "zh-CN"));
    }
    function openBossExceptionTask(id) {
      if (!id) return showPage("跟单工单");
      openTaskDetail(id);
    }
    function renderDashboard() {
      const metrics = document.getElementById("dashboardMetrics");
      renderHomeCommandCenter();
      renderHomeDailyBrief();
      renderQuoteTemplateOptions();
      renderIndustryOnboarding();
      const tasks = businessTasks();
      const quotes = businessQuotes();
      const profiles = businessProfiles();
      const pendingNameCount = profiles.filter(item => isPendingCustomerName(item.company)).length;
      const pending = tasks.filter(t => taskPhase(t) !== "已完成").length;
      const riskCount = tasks.filter(t => taskRiskText(t) !== "无明显风险").length;
      const inquiryCount = tasks.filter(t => ["询盘", "待补资料"].includes(t.status)).length;
      const dueSoon = tasks.filter(t => taskPhase(t) !== "已完成" && isDueSoon(t)).length;
      const urgentCount = tasks.filter(t => taskPhase(t) !== "已完成" && isUrgentTask(t)).length;
      const confirmCount = tasks.filter(t => ["已报价", "待确认", "打样"].includes(t.status)).length;
      const bossExceptions = bossExceptionItems(tasks);
      const supplyScheduleCount = bossExceptions.filter(item => ["缺料","排产冲突"].includes(item.type)).length;
      const deliveryCount = bossExceptions.filter(item => ["未签收","交期逾期","质量异常"].includes(item.type)).length;
      const financeAfterSalesCount = bossExceptions.filter(item => ["回款逾期","待回款","售后"].includes(item.type)).length;
      const homeSecondary = document.querySelector(".home-secondary");
      const homeSecondarySummary = document.getElementById("homeSecondarySummary");
      if (homeSecondary) homeSecondary.hidden = pending === 0;
      if (homeSecondarySummary) homeSecondarySummary.textContent = `今日待办 ${pending} 项`;
      const setupGuide = document.getElementById("dashboardSetupGuide");
      if (setupGuide) {
        setupGuide.classList.remove("show");
        setupGuide.innerHTML = "";
      }
      if (metrics) {
        metrics.innerHTML = `
          <div class="ops-tile"><span>缺料/排产冲突</span><strong>${supplyScheduleCount}</strong><em>看哪些料没到、哪台机器时间撞车</em></div>
          <div class="ops-tile"><span>交期/发货/签收</span><strong>${deliveryCount}</strong><em>发货、签收和质量异常分开管</em></div>
          <div class="ops-tile"><span>回款/售后异常</span><strong>${financeAfterSalesCount}</strong><em>谁欠款、逾期几天、客诉是否处理</em></div>
        `;
      }

      const urgency = document.getElementById("dashboardUrgency");
      if (urgency) {
        urgency.innerHTML = `
          <div class="urgency-card danger"><span>高风险经营异常</span><strong>${bossExceptions.filter(item => item.severity >= 5).length}</strong><em>缺料、排产冲突、逾期应收和售后优先</em></div>
          <div class="urgency-card warn"><span>交付待闭环</span><strong>${deliveryCount}</strong><em>质检、发货、签收分阶段核对</em></div>
          <div class="urgency-card"><span>询盘/报价待推进</span><strong>${inquiryCount + confirmCount}</strong><em>询盘、报价、客户确认仍需及时处理</em></div>
        `;
      }

      const priorityList = document.getElementById("dashboardPriorityList");
      if (priorityList) {
        const priorities = [
          ...(pendingNameCount ? [{ label: "补充客户姓名", desc: `${pendingNameCount} 位新客户资料已保存，补姓名后自动合并`, action: "客户档案" }] : []),
          ...bossExceptions.slice(0, 6).map(item => ({ label:`${item.type} · ${item.label}`, desc:item.desc, taskId:item.taskId })),
          ...(!bossExceptions.length ? [{ label: "跟进询盘与报价", desc: `${inquiryCount + confirmCount} 笔生意待推进`, action: "跟单工单" }] : [])
        ];
        priorityList.innerHTML = priorities.map(item => `
          <div class="priority-item">
            <span class="priority-dot"></span>
            <span><strong>${escapeHtml(item.label)}</strong><em>${escapeHtml(item.desc)}</em></span>
            <button class="btn light" onclick="${item.taskId ? `openBossExceptionTask('${escapeHtml(item.taskId)}')` : `showPage('${item.action}')`}">处理</button>
          </div>
        `).join("");
      }

      const focusList = document.getElementById("dashboardFocusList");
      if (focusList) {
        const taskFocus = tasks
          .filter(task => taskPhase(task) !== "已完成")
          .slice(0, 3)
          .map(task => ({
            label: task.customer || task.title || "未命名客户",
            desc: `${task.status || "待处理"} · ${task.nextAction || "待安排"}`,
            action: "跟单工单"
          }));
        const profileFocus = profiles
          .filter(item => ["已报价", "客户压价", "未报价"].includes(item.dealStatus || "未报价"))
          .slice(0, 2)
          .map(item => ({
            label: item.company,
            desc: `${item.dealStatus || "未报价"} · ${item.need || "待沉淀需求"}`,
            action: "客户档案"
          }));
        const focus = [...taskFocus, ...profileFocus].slice(0, 4);
        focusList.innerHTML = focus.map(item => `
          <div class="priority-item">
            <span class="priority-dot"></span>
            <span><strong>${escapeHtml(item.label)}</strong><em>${escapeHtml(item.desc)}</em></span>
            <button class="btn light" onclick="showPage('${item.action}')">查看</button>
          </div>
        `).join("") || `<div class="muted">暂无重点客户。可以先点击“演示完整流程”查看效果。</div>`;
      }

      const body = document.getElementById("dashboardTaskBody");
      if (body) {
        const rows = tasks.slice(0, 6).map(task => `
          <tr>
            <td>${escapeHtml(task.customer || task.title || "未命名客户")}</td>
            <td><span class="status ${statusClass(task.status)}">${escapeHtml(task.status || "待处理")}</span></td>
            <td>${escapeHtml(task.nextAction || "待安排")}</td>
            <td>${escapeHtml(task.owner || "业务")}</td>
            <td>${escapeHtml(taskRiskText(task))}</td>
          </tr>
        `).join("");
        body.innerHTML = rows || `<tr><td colspan="5" class="muted">还没有跟单记录。建议先从“询盘处理”粘贴一条客户询盘，生成内部工单。</td></tr>`;
      }

      const riskBox = document.getElementById("dashboardRisk");
      const referenceReady = quoteReferencePricePackActive();
      const ruleReady = referenceReady || (parseRules(state.settings.materialRules).length && parseRules(state.settings.processRules).length && quoteLegacyRulesConfirmed());
      if (riskBox) {
        riskBox.textContent = referenceReady
          ? "已预置完整行业参考价，可直接估算。参考价不代表本厂实际成本，对外报价前请按当日采购与加工报价复核。"
          : ruleReady
          ? "报价参数已确认。正式报价前仍建议核对材料价格、工艺单价、税点和利润率；运费统一另计。"
          : "报价参数还没有配置完整。请先到“报价参数”确认材料、工艺和人工工资，再让业务员报价。";
      }

      const quoteBody = document.getElementById("dashboardQuoteBody");
      if (quoteBody) {
        const quoteRows = quotes.slice(0, 5).map(item => `
          <tr>
            <td>${escapeHtml(item.product || "未命名产品")}</td>
            <td>${Number(item.qty || 0).toLocaleString()} 个</td>
            <td>¥${Number(item.total || 0).toFixed(0)}</td>
            <td>¥${Number(item.unit || 0).toFixed(3)}</td>
          </tr>
        `).join("");
        quoteBody.innerHTML = quoteRows || `<tr><td colspan="4" class="muted">暂无报价记录。生成报价后会出现在这里。</td></tr>`;
      }

      const profileBody = document.getElementById("dashboardProfileBody");
      if (profileBody) {
        const profileRows = profiles.slice(0, 5).map(item => `
          <tr>
            <td>${escapeHtml(item.company)}</td>
            <td><span class="status">${escapeHtml(item.level)}</span></td>
            <td>${escapeHtml(item.dealStatus || "未报价")} · ${escapeHtml(item.need || "待沉淀需求")}</td>
            <td>${escapeHtml(item.updatedAt || "")}</td>
          </tr>
        `).join("");
        profileBody.innerHTML = profileRows || `<tr><td colspan="4" class="muted">暂无客户档案。从询盘处理保存客户后会出现在这里。</td></tr>`;
      }
    }
    function renderIndustryOnboarding() {
      const box = document.getElementById("industryOnboarding");
      document.querySelectorAll(".requires-industry").forEach(el => {
        el.style.display = "";
      });
      if (box) {
        box.style.display = "none";
        box.innerHTML = "";
      }
    }
    function quoteReferencePricePackActive(settings = state.settings) {
      return Boolean(window.JIANGLAI_QUOTE_REFERENCE_PRICES?.isReferenceSettings?.(settings));
    }
    function quoteReferencePriceNoticeText(settings = state.settings) {
      return settings?.referencePricePack?.notice
        || "已预置行业参考价，仅用于首次估算。对外报价前请按本厂当日采购价和加工价复核；用到哪项可直接修改哪项。";
    }
    function quoteReferenceItem(item) {
      return Boolean(window.JIANGLAI_QUOTE_REFERENCE_PRICES?.isReferenceItem?.(item));
    }
    function quoteSetupRequired() {
      return !state.settings.industrySelected
        || (!quoteLegacyRulesConfirmed() && !quoteReferencePricePackActive())
        || quoteCostConfirmationMissing(state.settings).length > 0;
    }
    function quoteCostConfirmationMissing(settings = state.settings) {
      const hasMaterialOverride = normalizeQuoteMaterialPrices(settings?.quoteMaterialPrices).some(item => Number(item.price) > 0);
      const hasOperationOverride = normalizeQuoteOperationPrices(settings?.quoteOperationPrices).some(item => Number(item.unitPrice) > 0 || Number(item.setupFee) > 0);
      const library = normalizeCostLibrary(settings?.costLibrary);
      if (library.activeVersionId) {
        const availableTypes = new Set((library.items || []).filter(item => item.active !== false && Number(item.price) > 0).map(item => item.type));
        if (hasMaterialOverride) availableTypes.add("material");
        if (hasOperationOverride) availableTypes.add("process");
        return [
          ["material", "材料价格"],
          ["process", "工艺价格"],
          ["labor", "人工工资"]
        ].filter(([type]) => !availableTypes.has(type)).map(([, label]) => label);
      }
      const missing = [];
      if (!hasMaterialOverride && !parseRules(settings?.materialRules || "").some(item => Number(item.price) > 0)) missing.push("材料价格");
      if (!hasOperationOverride && !parseRules(settings?.processRules || "").some(item => Number(item.price) > 0)) missing.push("工艺价格");
      if (!(Number(settings?.laborHourly || 0) > 0)) missing.push("人工工资");
      return missing;
    }
    function renderQuoteSetupIndustries() {
      const box = document.getElementById("quoteSetupIndustries");
      if (!box) return;
      const selected = window.__pendingQuoteCategory || state.settings.industryCategory || "包装印刷";
      box.innerHTML = Object.keys(industryCategories).map(name => `
        <button type="button" class="${name === selected ? "active" : ""}" onclick="selectQuoteSetupIndustry('${encodeURIComponent(name)}')">
          <strong>${name}${name === window.__recommendedQuoteCategory ? " · 推荐" : ""}</strong>
          <span>${industryDescriptions[name]}</span>
        </button>
      `).join("");
    }
    function openQuoteSetupPrompt(info = null, options = {}) {
      const inferredTemplate = info ? inferTemplateName(info.product || "") : state.settings.factoryType;
      const recommendedCategory = categoryForTemplate(inferredTemplate || "包装彩盒");
      window.__recommendedQuoteCategory = recommendedCategory;
      window.__pendingQuoteCategory = state.settings.industrySelected
        ? (state.settings.industryCategory || recommendedCategory)
        : recommendedCategory;
      window.__pendingQuoteAfterSetup = !!info;
      window.__pendingQuoteInlineAfterSetup = options.inline === true;
      window.__pendingQuoteSessionId = info ? String(activeCustomerSession()?.id || "") : "";
      renderQuoteSetupIndustries();
      const prompt = document.getElementById("quoteSetupPrompt");
      prompt?.classList.add("show");
      prompt?.setAttribute("aria-hidden", "false");
    }
    function closeQuoteSetupPrompt(options = {}) {
      const prompt = document.getElementById("quoteSetupPrompt");
      prompt?.classList.remove("show");
      prompt?.setAttribute("aria-hidden", "true");
      if (options.preservePending !== true) {
        window.__pendingQuoteAfterSetup = false;
        window.__pendingQuoteInlineAfterSetup = false;
        window.__pendingQuoteSessionId = "";
      }
    }
    function selectQuoteSetupIndustry(categoryCode) {
      const category = decodeURIComponent(categoryCode);
      window.__pendingQuoteCategory = industryCategories[category] ? category : "包装印刷";
      renderQuoteSetupIndustries();
    }
    function goToQuoteSetup() {
      const category = window.__pendingQuoteCategory || "包装印刷";
      setIndustryCategory(encodeURIComponent(category));
      closeQuoteSetupPrompt({ preservePending: true });
      showPage("报价参数");
    }
    function runDealDemo() {
      if (!window.__dealDemoStateSnapshot) {
        window.__dealDemoStateSnapshot = JSON.parse(JSON.stringify(state));
      }
      const stamp = new Date().toLocaleString("zh-CN", { hour12: false });
      const createdAt = new Date().toISOString();
      const customer = "星河定制客户";
      const product = "亚克力展示架";
      const size = "300x180x120";
      const qty = 500;
      const material = "3mm透明亚克力";
      const process = "丝印LOGO";
      const sampleFee = 300;
      state.settings.configured = true;
      setIfExists("inquiryCustomerName", customer);
      setIfExists("inquiryContact", "张总 / 微信待补");
      setIfExists("customerText", `客户：${product}，${size}mm，${material}，${process}，${qty}个，下周要货。问能不能做，多久交货，价格大概多少，是否可以先看效果图。`);
      setTextIfExists("customerOutput", `演示询盘已处理：

客户：${customer}
产品：${product}
规格：${size}mm
数量：${qty}个
材质：${material}
工艺：${process}

AI判断：
1. 可以先按亚克力展示架方向评估；
2. 报价前要确认图纸/设计稿、Logo位置、包装防刮和是否含税；运费统一另计；
3. 下周要货，需先查排产，建议先做效果图或样品确认；
4. 下一步已自动生成报价、效果图方案、客户确认单、打样任务和跟单待办。`);
      setIfExists("quoteProduct", product);
      setIfExists("quoteSize", size);
      setIfExists("quoteQty", String(qty));
      setIfExists("quoteMaterial", material);
      setIfExists("quoteProcess", process);
      setIfExists("quoteTaxMode", "含税");
      setIfExists("quoteFreightMode", "不含运");
      setIfExists("quoteRegion", "浙江");
      setIfExists("quoteFreight", "0");
      setIfExists("quoteSampleMode", "收费打样");
      setIfExists("quoteSample", String(sampleFee));
      setQuoteDeliveryValue("7");
      setIfExists("quotePayment", "30%定金，尾款出货前付清");
      const demoQuote = {
        time: stamp,
        createdAt,
        customer,
        product,
        qty,
        total: 8600,
        unit: 17.2,
        minAcceptTotal: 7600,
        urgentTotal: 9800,
        marginRate: 21.4,
        status: "已报价"
      };
      state.quoteHistory = [demoQuote, ...(state.quoteHistory || []).filter(item => item.customer !== customer)].slice(0, 30);
      const customerQuoteText = `客户可见报价：

客户：${customer}
产品：${product}
规格：${size} mm
数量：${qty} 个
材质：${material}
工艺：${process}
报价口径：含税，不含运费（运费另计）
打样费：¥${sampleFee.toFixed(2)}
交期：7天
付款方式：30%定金，尾款出货前付清

建议总价：¥${demoQuote.total.toFixed(2)}
建议单价：¥${demoQuote.unit.toFixed(3)} / 个

可直接发客户：
您好，按目前信息先给您预估：${product}，${size}mm，${material}，${process}，${qty}个，含税、不含运费，参考总价 ¥${demoQuote.total.toFixed(0)}，折合约 ¥${demoQuote.unit.toFixed(2)}/个。交期约 7-10 天。本报价不含运费，运费另计。`;
      const internalQuoteText = `内部报价明细：

三档报价：
1. 正常报价：¥8600，约 ¥17.20/个
2. 最低成交价：¥7600，约 ¥15.20/个
3. 加急参考：¥9800，约 ¥19.60/个

内部判断：
- 毛利率参考：21.4%
- 接单建议：利润相对健康，可先按正常价发客户，若客户诚意高可小幅议价。
- 风险提醒：亚克力易刮花，丝印位置、板材厚度、结构承重、包装防护都要确认；下周要货需查排产。`;
      setQuoteOutputs(customerQuoteText, internalQuoteText);
      setIfExists("visualText", `产品：${product}
尺寸/数量：${size}mm，${qty}个
材质/工艺：${material}，${process}
想要图片：客户确认效果图、电商主图、详情页卖点图、包装防刮说明图`);
      generateVisualPlan(false);
      generateConfirmationFromQuote(false);
      fillSampleFromQuote(false);
      const inquiryTask = {
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: `${customer} · ${product}`,
        title: `${product} 询盘补资料`,
        owner: "业务/制前",
        createdAt,
        due: "今天",
        nextFollow: "今天下午",
        status: "询盘",
        nextAction: "确认图纸/设计稿、Logo位置、包装防刮和收货地址",
        amount: 0,
        risk: "下周要货，需确认排产和包装防护",
        logs: [stamp + " 演示创建：询盘补资料"]
      };
      const quoteTask = {
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: `${customer} · ${product}`,
        title: `${product} 报价跟进`,
        owner: "业务",
        createdAt,
        due: "今天",
        nextFollow: "今天下班前",
        status: "待确认",
        nextAction: "追客户确认图纸/设计稿、工艺位置、是否先打样",
        amount: demoQuote.total,
        risk: "亚克力易刮花，需确认厚度、结构承重、丝印位置和包装防护",
        logs: [stamp + " 演示创建：报价跟进"]
      };
      const sampleTask = {
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: `${customer} · ${product}`,
        title: `${product} 打样确认`,
        owner: "业务/生产",
        createdAt,
        due: "明天",
        nextFollow: "明天上午",
        status: "打样",
        nextAction: "确认打样费、寄样地址、样品数量和客户签样标准",
        amount: sampleFee,
        risk: "打样前确认颜色、尺寸、公差、材质、丝印位置和效果图仅供方向",
        logs: [stamp + " 演示创建：打样确认"]
      };
      const demoProfile = {
        company: customer,
        contact: "张总 / 微信待补",
        level: "试单客户",
        dealStatus: "打样",
        need: product,
        preference: `${material}；${process}；关注包装防刮`,
        note: `${stamp}：完整演示已生成询盘拆解、报价 ¥8600、效果图方案、客户确认单和打样任务。`,
        createdAt,
        updatedAt: stamp
      };
      state.quoteHistory = [demoQuote, ...(state.quoteHistory || []).filter(item => item.customer !== customer)].slice(0, 30);
      state.tasks = [sampleTask, quoteTask, inquiryTask, ...(state.tasks || []).filter(item => !String(item.customer || "").includes(customer))].slice(0, 50);
      state.customerProfiles = [demoProfile, ...(state.customerProfiles || []).filter(item => item.company !== demoProfile.company)].slice(0, 50);
      setTextIfExists("sampleOutput", `已生成演示打样任务，并同步到跟单工单：

客户：${customer}
产品：${product}
打样费：¥${sampleFee}
下一步：确认打样费、寄样地址、样品数量、客户签样标准和预计寄出时间。`);
      renderQuoteHistory();
      renderTasks();
      renderCustomerProfiles();
      renderDashboard();
      showPage("图片制作");
      setIfExists("homeCustomerName", customer);
      setIfExists("aiQuestion", `客户要${qty}个${product}，${size}mm，${material}，${process}，下周要货，想先看效果图并询价。`);
      showHomeResult();
      const demoAnswer = document.getElementById("aiAnswer");
      if (demoAnswer) demoAnswer.innerHTML = `
        <div class="ai-answer-body">
          <p class="ai-answer-lead">完整接单示例已准备好，不需要关闭弹窗。</p>
          <div class="ai-answer-section"><h3>数字员工已经完成</h3><ul><li>拆解客户需求并列出待确认信息</li><li>生成客户报价与内部议价底线</li><li>准备效果图方案、确认单和打样任务</li><li>建立客户档案与后续跟单提醒</li></ul></div>
          <div class="ai-confidence-note">这是演示客户，记录不会进入正式经营统计；刷新页面后会自动清理。</div>
        </div>`;
      renderHomeNextActions(`${product} ${material} ${process} 效果图 报价 跟单`);
      notify("完整接单示例已生成，可以继续查看报价、效果图和跟单。" );
    }
    function restoreDealDemoState() {
      if (!window.__dealDemoStateSnapshot) return false;
      state = window.__dealDemoStateSnapshot;
      window.__dealDemoStateSnapshot = null;
      restoreSettings();
      renderMemberStatus();
      renderQuoteHistory();
      renderTasks();
      renderCustomerProfiles();
      renderDashboard();
      return true;
    }
    function statusClass(status) {
      if (["已回款", "已完成", "已归档"].includes(status)) return "done";
      if (["急单", "异常", "逾期"].includes(status)) return "danger";
      if (["待补资料", "待确认", "待制作", "打样", "生产", "待质检", "待发货", "已发货", "已签收", "待回款"].includes(status)) return "warn";
      return "";
    }
    function loginMember() {
      const currentPlanKey = memberPlanKey();
      const hasActivePlan = state.member.loggedIn && memberPlanRank() > 0
        && (planConfig[currentPlanKey]?.perpetual || isBuyoutPlanKey() || !isExpired(state.member.expiresAt));
      state.member = {
        ...state.member,
        loggedIn: true,
        companyName: val("memberCompany") || "演示包装厂",
        account: val("memberAccount") || "demo",
        contact: val("memberContact") || "负责人",
        plan: hasActivePlan ? state.member.plan : "永久免费报价版",
        planKey: hasActivePlan ? memberPlanKey() : "trial",
        expiresAt: hasActivePlan ? state.member.expiresAt : addDays(planConfig.trial.days),
        quotaUsed: hasActivePlan ? state.member.quotaUsed : 0,
        quotaTotal: hasActivePlan ? state.member.quotaTotal : planConfig.trial.quota,
        role: "管理员"
      };
      saveState();
      memberOutput.textContent = `账号资料已保存：${state.member.companyName}
套餐：${state.member.plan}
剩余AI点数：${Math.max(state.member.quotaTotal - state.member.quotaUsed, 0)} 点
到期：${state.member.expiresAt}`;
      renderMemberSystem();
    }
    function startMemberTrial(silent = false) {
      if (state.member.loggedIn && memberPlanRank() > planConfig.trial.rank && !isExpired(state.member.expiresAt)) {
        if (!silent) notify("当前已经是付费会员，不需要切换到永久免费报价版。" );
        return true;
      }
      activatePlan("trial", true);
      if (!silent) {
        memberOutput.textContent = "永久免费报价版已开通：基础报价可长期使用，并赠送100 AI点体验文字/图片识别。";
        renderMemberSystem();
      }
      return true;
    }
    function selectMemberPlan(planKey) {
      const plan = planConfig[planKey];
      if (!plan) return;
      if (planKey === "trial") {
        startMemberTrial();
        return;
      }
      window.__selectedAutoRenewPlan = "";
      window.__selectedMemberPlan = planKey;
      const panel = document.getElementById("memberPurchasePanel");
      panel?.classList.remove("is-hidden");
      setTextIfExists("memberPurchaseTag", isBuyoutPlanKey(planKey) ? "买断授权" : "升级会员");
      setTextIfExists("memberPurchaseTitle", `${plan.name} · ${plan.price}`);
      setTextIfExists("memberPurchaseDescription", planKey === "buyout"
          ? "当前大版本与本地核心永久使用，最多20人并赠20,000 AI点；含1年升级维护，次年维护可选¥2,980/年，AI、短信及第三方服务另计。"
        : planKey === "private_buyout"
          ? "标准私有部署¥59,800起，多系统或复杂集成¥99,800起；人数不限并赠送200,000 AI点，支持部署到本厂服务器或云账号。"
        : `${plan.painPointAnalysisSessions ? "年付赠1次一对一痛点分析；" : ""}选择微信支付或支付宝创建订单，付款成功会自动开通套餐。` );
      document.getElementById("memberPaymentMethods")?.classList.remove("is-hidden");
      const action = document.getElementById("memberPaymentAction"); if (action) { action.disabled = false; action.textContent = "创建支付订单"; }
      panel?.scrollIntoView({ behavior: "smooth", block: "center" });
    }
    function selectAiRecharge(productKey) {
      const product = aiRechargeConfig[productKey];
      if (!product) return;
      if (!isBuyoutPlanKey()) {
        notify("只有工厂买断版或私有定制版可以按0.05元/点充值。");
        return;
      }
      window.__selectedMemberPlan = productKey;
      const panel = document.getElementById("memberPurchasePanel");
      panel?.classList.remove("is-hidden");
      setTextIfExists("memberPurchaseTag", "AI点数充值");
      setTextIfExists("memberPurchaseTitle", `${product.name} · ${product.price}`);
      setTextIfExists("memberPurchaseDescription", `付款成功后立即增加${Number(product.points || 0).toLocaleString()} AI点，统一单价¥0.05/点；调用失败自动退点。` );
      document.getElementById("memberPaymentMethods")?.classList.remove("is-hidden");
      panel?.scrollIntoView({ behavior: "smooth", block: "center" });
    }
    async function startMemberPayment() {
      if (window.__selectedAutoRenewPlan) {
        const output = document.getElementById("memberPaymentOutput");
        if (output) output.textContent = "连续包月已选中。微信支付和支付宝产品正在审核，审核通过并完成签约接口配置后即可开通；当前不会产生扣款。";
        return;
      }
      const planKey = window.__selectedMemberPlan || "standard";
      const plan = planConfig[planKey] || aiRechargeConfig[planKey];
      const output = document.getElementById("memberPaymentOutput");
      if (!plan) return;
      if (planKey === "enterprise") {
        if (output) output.textContent = "企业版需要确认员工数量、数据迁移和系统对接范围，请联系销售确定合同与付款方式。";
        return copyMemberOrderInfo();
      }
      const method = document.querySelector('input[name="memberPaymentMethod"]:checked')?.value || "wechat";
      const qrBox = document.getElementById("memberPaymentQr");
      const qrImage = document.getElementById("memberPaymentQrImage");
      qrBox?.classList.add("is-hidden");
      if (qrImage) qrImage.removeAttribute("src");
      if (output) output.textContent = "正在创建支付订单…";
      try {
        if (typeof window.__createBillingOrder !== "function") throw new Error("支付订单服务尚未加载");
        const data = await window.__createBillingOrder(planKey, method);
        const order = data.order || {};
        const methodLabel = method === "alipay" ? "支付宝" : "微信支付";
        const statusLabel = order.status === "paid" ? "已付款" : "待付款";
        if (output) output.textContent = `订单号：${order.order_no || "—"}\n套餐：${plan.name} · ${plan.price}\n支付方式：${methodLabel}\n状态：${statusLabel}\n${data.message || ""}`;
        const qrDataUrl = String(data.payment?.qr_data_url || "");
        if (qrDataUrl.startsWith("data:image/png;base64,")) {
          if (qrImage) qrImage.src = qrDataUrl;
          qrBox?.classList.remove("is-hidden");
          notify("微信支付二维码已生成，请扫码付款。" );
        } else if (data.payment_url) {
          window.open(data.payment_url, "_blank", "noopener,noreferrer");
          notify("支付页面已打开，付款完成后请查询支付状态。");
        } else {
          notify(data.message || "订单已建立，等待支付通道配置。");
        }
        if (order.status === "awaiting_payment") {
          beginMemberPaymentPolling(order.order_no);
        }
      } catch (err) {
        if (output) output.textContent = `支付订单没有创建：${err.message}`;
        notify(err.message);
      }
    }
    function beginMemberPaymentPolling(orderNo) {
      if (window.__memberPaymentPollTimer) {
        window.clearInterval(window.__memberPaymentPollTimer);
      }
      let attempts = 0;
      window.__memberPaymentPollTimer = window.setInterval(async () => {
        attempts += 1;
        try {
          const data = await window.__loadBillingOrderStatus?.(orderNo);
          const order = data?.order;
          if (order?.status === "paid") {
            window.clearInterval(window.__memberPaymentPollTimer);
            window.__memberPaymentPollTimer = null;
            await refreshMemberPaymentOrders();
          } else if (order && !["awaiting_payment", "creating"].includes(order.status)) {
            window.clearInterval(window.__memberPaymentPollTimer);
            window.__memberPaymentPollTimer = null;
            await refreshMemberPaymentOrders();
          }
        } catch (_) {
          // 短暂断网时保留订单，下一轮继续查询。
        }
        if (attempts >= 60 && window.__memberPaymentPollTimer) {
          window.clearInterval(window.__memberPaymentPollTimer);
          window.__memberPaymentPollTimer = null;
        }
      }, 3000);
    }
    async function refreshMemberPaymentOrders() {
      const output = document.getElementById("memberPaymentOutput");
      try {
        if (typeof window.__loadBillingOrders !== "function") throw new Error("支付订单服务尚未加载");
        const data = await window.__loadBillingOrders();
        const order = (data.orders || [])[0];
        if (!order) {
          if (output) output.textContent = "当前账号还没有支付订单。";
          return;
        }
        const status = { awaiting_payment:"待付款", paid:"已付款并开通", closed:"已关闭", refunded:"已退款" }[order.status] || order.status;
        if (output) output.textContent = `最近订单：${order.order_no}\n金额：¥${order.amount}\n状态：${status}\n创建时间：${order.created_at}`;
        if (order.status === "paid" && typeof window.__loadMembershipUsage === "function") {
          await window.__loadMembershipUsage().catch(() => null);
          notify("付款已确认，会员状态已更新。");
        }
      } catch (err) {
        if (output) output.textContent = `查询失败：${err.message}`;
        notify(err.message);
      }
    }
    async function copyMemberOrderInfo() {
      const planKey = window.__selectedMemberPlan || "standard";
      const plan = planConfig[planKey] || aiRechargeConfig[planKey];
      let orderText = "";
      try {
        if (typeof window.__createBillingOrder !== "function") throw new Error("订单服务尚未加载");
        const data = await window.__createBillingOrder(planKey);
        if (data.order?.order_no) {
          orderText = `\n订单号：${data.order.order_no}\n支付状态：${data.order.status === "awaiting_payment" ? "待付款" : data.order.status}`;
        }
      } catch (err) {
        orderText = "\n订单状态：暂未写入服务端，请联系客服核对";
      }
      const info = `会员开通申请\n公司：${val("memberCompany") || state.member.companyName || "待填写"}\n联系人：${val("memberContact") || state.member.contact || "待填写"}\n账号：${val("memberAccount") || state.member.account || "待填写"}\n套餐：${plan.name}\n价格：${plan.price}${orderText}`;
      navigator.clipboard?.writeText(info);
      memberOutput.textContent = `${info}\n\n订单已保存并复制。支付接口未配置时，可发送给销售或客服确认付款和激活码。`;
      notify("会员开通信息已复制。" );
    }
    function focusActivationCode() {
      document.getElementById("activationCode")?.scrollIntoView({ behavior: "smooth", block: "center" });
      window.setTimeout(() => document.getElementById("activationCode")?.focus(), 180);
    }
    function requestMemberRenewal() {
      const currentKey = memberPlanKey();
      selectMemberPlan(currentKey === "none" || currentKey === "trial" ? "standard" : currentKey);
      memberOutput.textContent = "请选择续费套餐并复制开通信息；付款后输入新的激活码即可续费。";
    }
    function activatePlan(planKey, fromActivation = false) {
      const plan = planConfig[planKey];
      if (!plan) return false;
      if (planKey !== "trial" && !fromActivation) {
        selectMemberPlan(planKey);
        return false;
      }
      state.member = {
        ...state.member,
        loggedIn: true,
        companyName: state.member.companyName || val("memberCompany") || "演示包装厂",
        account: state.member.account || val("memberAccount") || "demo",
        contact: state.member.contact || val("memberContact") || "负责人",
        plan: plan.name,
        planKey,
        expiresAt: addDays(plan.days),
        quotaUsed: 0,
        quotaTotal: plan.quota,
        role: planKey === "enterprise" ? "企业管理员" : "管理员", painPointAnalysisRemaining: Number(state.member.painPointAnalysisRemaining || 0) + Number(plan.painPointAnalysisSessions || 0)
      };
      saveState();
      memberOutput.textContent = `已开通 ${plan.name}。
有效期至：${state.member.expiresAt}
本期AI点数：${plan.quota.toLocaleString()} 点
会员权限已经生效。`;
      renderMemberSystem();
      return true;
    }
    async function activateByCode() {
      const code = val("activationCode").toUpperCase();
      if (!code) {
        memberOutput.textContent = "请输入购买后获得的激活码。";
        return;
      }
      if (window.__isStaticFileMode) {
        memberOutput.textContent = "本地文件预览不能激活正式会员，请通过 http://127.0.0.1:8765/ 打开软件后再输入激活码。";
        return;
      }
      if (typeof window.__activateMembershipCode === "function") {
        try {
          await window.__activateMembershipCode(code);
          document.getElementById("activationCode").value = "";
          return;
        } catch (err) {
          memberOutput.textContent = `激活失败：${err.message}`;
          return;
        }
      }
      memberOutput.textContent = "会员服务尚未连接，请稍后重试。";
    }
    function consumeQuota(featureName, chargeLocally = true, pointCostOverride = null) {
      if (!state.member.loggedIn) startMemberTrial(true);
      const m = state.member;
      const isBuyout = isBuyoutPlanKey(memberPlanKey(m));
      if (!isBuyout && isExpired(m.expiresAt)) {
        notify("体验或会员已到期，请续费后继续使用。" );
        window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
        showPage("账号套餐");
        return false;
      }
      const requiredPlans = {
        "制前资料": "standard",
        "AI真实生图": "pro",
        "AI宝贝详情": "pro",
        "图片转矢量": "pro",
        "报价导出": "pro"
      };
      const requiredKey = requiredPlans[featureName] || "trial";
      if (memberPlanRank(m) < planConfig[requiredKey].rank) {
        window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
        showPage("账号套餐");
        selectMemberPlan(requiredKey);
        memberOutput.textContent = `${featureName}需要${planConfig[requiredKey].name}。当前套餐为${m.plan}，升级后即可继续使用。`;
        notify(`${featureName}需要升级到${planConfig[requiredKey].name}。` );
        return false;
      }
      const pointCost = Number(pointCostOverride == null ? (featurePointCost[featureName] || 0) : pointCostOverride);
      if (m.quotaUsed + pointCost > m.quotaTotal) {
        notify(isBuyout
          ? `当前AI点数不足，本功能需要${pointCost}点；买断版可按0.05元/点充值。`
          : `当前AI点数不足，本功能需要${pointCost}点，请升级套餐或续费。` );
        window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
        showPage("账号套餐");
        if (isBuyout) document.getElementById("buyoutWalletCard")?.scrollIntoView({ behavior: "smooth", block: "center" });
        return false;
      }
      if (chargeLocally && pointCost > 0) {
        m.quotaUsed += pointCost;
        saveState();
        renderMemberStatus();
        if (document.querySelector('[data-page="账号套餐"]').classList.contains("active")) renderMemberSystem();
      }
      return true;
    }

    function initNav() {
      const nav = document.getElementById("nav");
      const mainPages = [
        ["工作台", "首页"],
        ["询盘处理", "接单"],
        ["报价系统", "报价"],
        ["跟单工单", "订单"],
        ["图片制作", "制图"],
        ["生产中心", "生产"],
        ["专业经营", "经营"],
        ["外协协作", "外协"],
        ["经营报表", "报表"],
        ["更多", "更多"]
      ];
      mainPages.forEach(([name, label], i) => {
        const btn = document.createElement("button");
        btn.type = "button";
        btn.textContent = label;
        btn.dataset.page = name;
        btn.className = i === 0 ? "active" : "";
        if (i === 0) btn.setAttribute("aria-current", "page");
        btn.onclick = () => {
          if (name === "外协协作") {
            location.href = "supplier_collaboration.html";
            return;
          }
          if (name === "报价系统") {
            openQuoteWorkspace();
            return;
          }
          showPage(name);
        };
        nav.appendChild(btn);
      });
      renderPlanVisibility();
    }
    function resetAppPageScroll(name) {
      window.requestAnimationFrame(() => {
        const main = document.querySelector("main");
        const activePage = document.querySelector(`.page.active[data-page="${CSS.escape(name)}"]`);
        if (main) main.scrollTop = 0;
        if (activePage) activePage.scrollTop = 0;
        document.documentElement.scrollTop = 0;
        document.body.scrollTop = 0;
        window.scrollTo(0, 0);
      });
    }
    function showPage(name) {
      if (name === "AI效果图") {
        const opened = showPage("图片制作");
        if (opened === true) selectHomeImageMode("mockup");
        return opened;
      }
      if (name === "团队权限" && !hasEnterpriseTeamAccess()) {
        notify("多人团队协作从团队协作版开始提供；AI跟单版最多2人、团队协作版最多10人、工厂买断版最多20人，企业定制版人数不限制。" );
        window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
        showPage("账号套餐");
        return false;
      }
      if (["跟单工单", "跟单详情"].includes(name) && !hasPaidFollowupAccess()) {
        notify("客户跟单从AI跟单版开始提供，支持2人使用；多人复杂协作可升级团队协作版。" );
        window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
        showPage("账号套餐");
        return false;
      }
      if (name === "生产中心" && !hasEnterpriseTeamAccess()) {
        notify("完整生产、采购和多人权限从团队协作版开始提供，最多10人使用。" );
        window.__memberReturnPage = document.querySelector(".page.active")?.dataset.page || "工作台";
        showPage("账号套餐");
        return false;
      }
      const pagePermission = { "询盘处理":"task.view", "报价参数":"price.view", "报价系统":"quote.create", "报价结果":"quote.create", "经营报表":"price.view", "客户档案":"customer.view", "图片制作":"design.use", "AI效果图":"design.use", "制前资料":"design.use", "图片转矢量":"design.use", "刀板排版":"design.use", "跟单工单":"task.view", "跟单详情":"task.view", "生产中心":"task.view", "专业经营":"task.view", "团队权限":"task.manage", "渠道中心":"task.manage", "提醒中心":"notification.view" }[name];
      if (pagePermission && window.location.protocol !== "file:" && (!window.__currentUser || window.__formalSettingsReady !== true)) {
        notify("正在验证当前员工账号和工厂数据，请稍后再试。");
        return false;
      }
      if (pagePermission && window.__currentUser && !window.__hasPermission?.(pagePermission)) {
        notify("当前员工账号没有这个功能的权限，请联系老板分配。" );
        return false;
      }
      if (name === "报价系统") {
        const inlineWorkspace = document.getElementById("homeInlineQuoteWorkspace");
        if (inlineWorkspace && !inlineWorkspace.classList.contains("is-hidden")) {
          closeHomeInlineQuoteWorkspace({ silent: true });
        } else {
          mountQuoteWorkspace("page");
        }
      }
      const previousPageName = document.querySelector(".page.active")?.dataset.page || "";
      if (previousPageName === "图片制作" && name !== "图片制作") {
        // 只保存当前制图会话，不清除正在生成的任务或已完成结果。
        // fetch 会继续在后台等待，回到制图页时由会话状态恢复进度和图片。
        captureActiveCustomerSession();
      }
      const pageBack = document.getElementById("pageBackButton");
      const memberReturnPage = name === "账号套餐" && window.__memberReturnPage && window.__memberReturnPage !== "账号套餐"
        ? window.__memberReturnPage
        : "工作台";
      const quoteResultReturnPage = window.__quoteResultReturnPage === "图片制作" ? "图片制作" : "报价系统";
      const contextualReturnPage = name === "跟单详情"
        ? (window.__taskDetailReturnPage === "生产中心" ? "生产中心" : "跟单工单")
        : name === "报价结果" ? quoteResultReturnPage : memberReturnPage;
      pageBack?.classList.toggle("is-hidden", name === "工作台" || name === "报价结果");
      if (pageBack) {
        pageBack.textContent = name === "报价结果"
          ? "← 返回修改报价"
          : contextualReturnPage === "工作台" ? "← 返回首页" : `← 返回${pageTitles[contextualReturnPage] || contextualReturnPage}`;
        pageBack.onclick = () => {
          const target = contextualReturnPage;
          window.__memberReturnPage = "";
          if (name === "报价结果") returnToManualQuote();
          else if (target === "跟单工单") backToTaskList();
          else showPage(target);
        };
      }
      document.body.classList.toggle("clean-home", name === "图片制作");
      document.body.classList.toggle("task-detail-active", name === "跟单详情");
      document.querySelector("main")?.classList.toggle("vector-canvas-wide", ["图片转矢量", "刀板排版"].includes(name));
      const workflowNavParents = {
        "跟单详情":"跟单工单",
        "客户档案":"跟单工单",
        "确认打样":"跟单工单",
        "报价结果":"报价系统",
        "报价参数":"报价系统",
        "AI效果图":"图片制作",
        "图片转矢量":"图片制作",
        "刀板排版":"图片制作",
        "制前资料":"图片制作",
        "账号套餐":"更多",
        "团队权限":"更多",
        "渠道中心":"更多"
      };
      if (name === "跟单详情" && window.__taskDetailReturnPage === "生产中心") workflowNavParents["跟单详情"] = "生产中心";
      const activeNavPage = workflowNavParents[name] || name;
      document.querySelectorAll(".nav button").forEach(btn => {
        const active = btn.dataset.page === activeNavPage;
        btn.classList.toggle("active", active);
        if (active) btn.setAttribute("aria-current", "page");
        else btn.removeAttribute("aria-current");
      });
      document.querySelectorAll(".page").forEach(page => page.classList.toggle("active", page.dataset.page === name));
      document.getElementById("pageTitle").textContent = pageTitles[name] || name;
      document.getElementById("pageSub").textContent = pages[name];
      if (name === "工作台") {
        renderDashboard();
      }
      if (name === "图片制作") {
        restoreCustomerSession(activeCustomerSession());
      }
      if (name === "经营报表") renderBusinessReport();
      if (name === "渠道中心") loadChannelCenter();
      syncHomeConversationLayout();
      if (name === "报价系统") {
        renderCurrentQuoteWorkspace();
        ensureDirectQuoteProduct();
        setQuoteProductChooser(true);
        if (quoteCatalogSetupRequired()) {
          setQuoteCostWarning("");
        } else if (quoteSetupRequired()) {
          setQuoteCostWarning("");
        }
      }
      if (name === "制前资料") {
        const currentCustomer = (document.getElementById("homeCustomerName")?.value || currentCustomerName() || "新客户").trim() || "新客户";
        const customerInput = document.getElementById("preflightCustomer");
        if (customerInput && (!customerInput.value.trim() || customerInput.value === "新客户")) customerInput.value = currentCustomer;
        window.__loadPreflightReports?.();
      }
      if (name === "图片转矢量") {
        const currentCustomer = (document.getElementById("homeCustomerName")?.value || currentCustomerName() || "").trim();
        const vectorCustomer = document.getElementById("vectorCustomer");
        if (vectorCustomer && currentCustomer && (vectorCustomer.value === "新客户" || !vectorCustomer.value.trim())) vectorCustomer.value = currentCustomer;
      }
      if (name === "刀板排版") window.initDieLayoutWorkbench?.();
      if (name === "确认打样") fillSampleFromQuote(false);
      if (name === "跟单工单") {
        renderTasks();
        refreshSelectedTaskDetail();
        window.__loadFormalTasks?.();
      }
      if (name === "生产中心") {
        renderProductionCenter();
        Promise.all([
          Promise.resolve(window.__loadFormalTasks?.()).catch(() => null),
          loadProductionOutsourceOrders(true)
        ]).then(renderProductionCenter).catch(() => renderProductionCenter());
      }
      if (name === "专业经营") window.__loadProfessionalSuite?.();
      if (name === "跟单详情") {
        loadProductionOutsourceOrders().catch(() => null);
      }
      if (name === "客户档案") {
        const editor = document.getElementById("customerEditorDetails");
        if (editor) editor.open = false;
        renderCustomerProfiles();
        window.__loadCloudCustomers?.();
      }
      if (name === "账号套餐") {
        renderMemberSystem();
        window.__loadMembershipUsage?.();
      }
      if (name === "团队权限") window.__loadTeamManagement?.();
      if (name === "提醒中心") window.__loadNotifications?.(true);
      if (name === "图片转矢量") window.__loadVectorJobs?.();
      if (name === "更多") {
        renderBackupStatus();
        window.__renderCurrentStaff?.();
      }
      if (name === "报价参数") {
        restoreSettings();
        renderIndustryCategories();
        renderQuoteRules();
        setRuleMode(ruleMode);
        renderPriceSetupEntry();
        choosePriceSetup("");
      }
      resetAppPageScroll(name);
      return true;
    } window.showPage = showPage;

    function generateReply() {
      const { info, platform } = customerContext();
      document.getElementById("customerOutput").textContent = `识别结果：
- 产品：${info.product}
- 尺寸：${info.size}
- 数量：${info.qty}
- 材质：${info.material}
- 工艺：${info.finish}
- 使用场景：${platform}
- 风险：${info.risk.join("；")}

需要补问：
1. 是否已有最终设计稿、样品图、规格图或工艺要求；
2. 是否需要含税；本报价统一不含运费；
3. 是否需要打样，打样是否可抵大货款；
4. 期望交期和验收标准。

可直接发客户：

您好，这个需求我们可以先按${info.product}方向评估。
目前已收到：尺寸 ${info.size}，材质 ${info.material}，工艺 ${info.finish}，数量 ${info.qty}。

为了报得更准确，麻烦再确认：
1. 是否已有最终设计稿、样品图或规格要求；
2. 报价是否需要含税；运费统一另计；
3. 是否需要先打样确认颜色、工艺和成品效果；
4. 包装/交付方式和具体到货日期。

内部提醒：
- ${info.risk.join("；")}
- 黑色大面积印刷建议先确认样品，避免批量后出现色差或折痕露白争议。`;
    }
    function customerContext() {
      const text = val("customerText");
      const cached = window.__latestAiAnalysis;
      const cachedMatches = cached && window.__latestAiSourceText === text;
      const cachedList = value => Array.isArray(value) ? value.join(" + ") : (value || "待确认");
      return {
        text,
        info: cachedMatches ? {
          product: cached.product || "定制产品",
          size: cached.size || "尺寸待确认",
          qty: String(cached.qty || "数量待确认"),
          material: cachedList(cached.material),
          finish: cachedList(cached.process),
          delivery: cached.delivery || "交期待确认",
          taxShipping: cached.tax_shipping || "税运待确认",
          address: cached.address || "收货地待确认",
          sampleRequired: cached.sample_required ?? null,
          sampleMode: cached.sample_mode || "待确认",
          sampleFee: Number(cached.sample_fee || 0),
          sampleStatus: cached.sample_status || "待确认",
          customerHabits: Array.isArray(cached.customer_habits) ? cached.customer_habits : [],
          memoryInherited: !!cached.memory_inherited,
          confidence: cached.confidence || {},
          evidence: cached.evidence || {},
          customerReply: cached.customer_reply || "",
          risk: Array.isArray(cached.risk) && cached.risk.length ? cached.risk : ["报价前再核对规格、交期和文件"]
        } : { ...analyze(text), customerReply: "" },
        platform: val("contentPlatform") || "阿里巴巴/1688",
        goal: val("contentGoal") || "拿订单"
      };
    }
    function setIfExists(id, value) {
      const el = document.getElementById(id);
      if (el) el.value = value;
    }
    function setTextIfExists(id, value) {
      const el = document.getElementById(id);
      if (el) el.textContent = value;
    }
    function notify(message) {
      let toast = document.getElementById("appToast");
      if (!toast) {
        toast = document.createElement("div");
        toast.id = "appToast";
        toast.className = "app-toast";
        toast.setAttribute("role", "status");
        document.body.appendChild(toast);
      }
      toast.textContent = message;
      toast.classList.add("show");
      window.clearTimeout(notify.timer);
      notify.timer = window.setTimeout(() => {
        toast.classList.remove("show");
        toast.textContent = "";
      }, 2600);
    }
    function setQuoteIndustryWarning(message = "", targetCategory = "", targetTemplate = "") {
      const warning = document.getElementById("quoteIndustryWarning");
      if (!warning) return;
      window.__quoteIndustryTarget = message && targetCategory
        ? { category: targetCategory, template: targetTemplate || (industryCategories[targetCategory] || [])[0] || "" }
        : null;
      if (message) {
        const canSwitch = targetCategory && targetCategory !== currentIndustryCategory();
        const targetProfile = state.settings.quoteProfiles?.[targetCategory];
        const targetReady = Boolean(targetProfile && quoteLegacyRulesConfirmed(targetProfile));
        warning.innerHTML = `<div class="quote-industry-warning-copy"><strong>价格表不匹配，已停止报价</strong><span>${escapeHtml(message)}</span></div>${canSwitch ? `<button class="btn light" type="button" onclick="switchQuoteIndustryAndKeepDraft()">${targetReady ? `切换到${escapeHtml(targetCategory)}价格表并继续` : `切换到${escapeHtml(targetCategory)}并补充成本`}</button>` : ""}`;
      } else {
        warning.textContent = "";
      }
      warning.classList.toggle("is-hidden", !message);
    }
    function switchQuoteIndustryAndKeepDraft() {
      const target = window.__quoteIndustryTarget;
      if (!target?.category || !industryCategories[target.category]) return;
      const ids = [
        "quoteAiSource", "quoteCustomerName", "quoteProduct", "quoteSize", "quoteQty", "quoteMaterial", "quoteProcess",
        "quoteDelivery", "quotePayment", "quoteLevel", "quoteTaxMode", "quoteRegion", "quoteFreightMode", "quoteFreight",
        "quoteSampleMode", "quoteSample"
      ];
      const draft = Object.fromEntries(ids.map(id => [id, document.getElementById(id)?.value ?? ""]));
      const uncertainFields = [...(window.__quoteUncertainFields || [])];
      const uncertainSource = window.__quoteUncertainSource || "ai";
      activateIndustryQuoteProfile(target.category, target.template);
      saveState();
      restoreSettings();
      renderIndustryCategories();
      renderQuoteTemplateOptions();
      if ([...document.getElementById("quoteTemplate")?.options || []].some(option => option.value === target.template)) {
        setIfExists("quoteTemplate", target.template);
      }
      Object.entries(draft).forEach(([id, value]) => setIfExists(id, value));
      window.__quoteUncertainFields = uncertainFields;
      window.__quoteUncertainSource = uncertainSource;
      renderIndustryQuoteCostFields();
      renderQuoteRules();
      renderQuoteGuide();
      setQuoteIndustryWarning("");
      const ready = quoteLegacyRulesConfirmed();
      setQuoteCostWarning(ready
        ? `已切换到“${target.category}”价格表，当前客户资料完整保留。请核对材料、工艺和人工工资后继续报价。`
        : `已切换到“${target.category}”，当前客户资料完整保留。本单用到哪项成本就补哪项，其他材料和工艺可以以后用到时再填。`);
      notify(ready ? `已切换到${target.category}价格表，询盘内容没有丢失。` : `询盘已保留，请先确认${target.category}成本。`);
    }
    function setQuoteView(view) {
      document.querySelectorAll("#quoteViewTabs button").forEach(btn => {
        btn.classList.toggle("active", (view === "customer" && btn.textContent.includes("客户")) || (view === "internal" && btn.textContent.includes("内部")));
      });
      document.getElementById("quoteCustomerOutput")?.classList.toggle("active", view === "customer");
      document.getElementById("quoteInternalOutput")?.classList.toggle("active", view === "internal");
    }
    function setQuoteOutputs(customerText, internalText = "") {
      setTextIfExists("quoteCustomerOutput", customerText);
      setTextIfExists("quoteInternalOutput", internalText || "内部底价会在生成报价后显示。默认不要发给客户。");
      setTextIfExists("quoteOutput", `${customerText}\n\n内部报价明细：\n\n${internalText}`.trim());
      setQuoteView("customer");
    }
    function markQuoteDirty() {
      setQuoteCostWarning("");
      scheduleQuoteLiveDock();
      const output = document.getElementById("quoteCustomerOutput")?.textContent || "";
      if (!output || output.includes("点击左侧") || output.includes("参数已修改")) return;
      setQuoteOutputs(
        "报价参数已修改，请点击左侧“生成报价单”重新计算。旧报价已经作废，不能继续复制或发送。",
        "报价参数已修改，请重新生成内部底价。"
      );
      window.__activeQuoteRecordKey = "";
      const tiers = document.getElementById("quoteTierCards");
      if (tiers) tiers.innerHTML = "";
      const decision = document.getElementById("quoteDecisionSummary");
      if (decision) decision.innerHTML = "";
      const trust = document.getElementById("quoteTrustSummary");
      if (trust) trust.innerHTML = "";
      renderQuoteAccuracySummary(null); renderQuoteMarketComparison(null);
      const approval = document.getElementById("quoteApprovalPanel");
      if (approval) {
        approval.classList.add("is-pending");
        approval.innerHTML = "<div><strong>报价参数已经修改</strong><span>请重新生成新版本，旧价格不能继续发送。</span></div>";
      }
    }
    function updateQuoteSourceNote(info = null) {
      const note = document.getElementById("quoteSourceNote");
      if (!info) {
        if (note) note.textContent = "手动选择产品，并填写数量、材料、工艺和报价条件。";
        setQuoteIndustryWarning("");
        return;
      }
      if (note) note.textContent = `已从首页聊天带入：${info.product} / ${info.qty} / ${info.material} / ${info.finish}。请人工核对后生成报价。`;
    }
    function toggleQuoteAdvanced() {
      const box = document.getElementById("quoteAdvanced");
      if (box) box.classList.toggle("is-hidden");
      renderIndustryQuoteCostFields();
    }
    function quoteGuideStepFields(step) {
      const sizeValid = value => /^\s*\d+(?:\.\d+)?\s*[xX×*]\s*\d+(?:\.\d+)?(?:\s*[xX×*]\s*\d+(?:\.\d+)?)?\s*(?:mm|cm|m)?\s*$/i.test(value);
      const groups = {
        1: [
          { id: "quoteProduct", label: "产品名称", valid: value => value.length >= 2 && !value.includes("待确认") },
          { id: "quoteSize", label: "尺寸", valid: sizeValid },
          { id: "quoteQty", label: "数量", valid: value => Number(String(value).replace(/[^\d.]/g, "")) > 0 }
        ],
        2: [
          { id: "quoteMaterial", label: "材质", valid: value => value.length >= 2 && !value.includes("待确认") },
          { id: "quoteProcess", label: "工艺", valid: value => value.length >= 2 && !value.includes("待确认") }
        ],
        3: [
          { id: "quoteDelivery", label: "交期", valid: value => Number(normalizeQuoteDeliveryDays(value)) > 0 },
          { id: "quotePayment", label: "付款方式", valid: value => value.length >= 2 && !value.includes("待确认") }
        ]
      };
      return groups[step] || [];
    }
    function validateQuoteGuideStep(step) {
      const fields = quoteGuideStepFields(step);
      fields.forEach(item => document.getElementById(item.id)?.classList.remove("quote-field-error"));
      const missing = fields.filter(item => !item.valid(val(item.id)));
      if (!missing.length) return true;
      revealQuoteMissingFields(missing);
      notify(`这一步还需要填写：${missing.map(item => item.label).join("、")}`);
      return false;
    }
    function updateQuoteAiFileName() {
      const files = Array.from(document.getElementById("quoteAiFiles")?.files || []);
      const label = document.getElementById("quoteAiFileName");
      if (label) label.textContent = files.length ? `已选择 ${files.length} 个文件：${files.map(file => file.name).join("、")}` : "也可以只粘贴文字";
    }
    function clearQuoteAiFileSelection() {
      const input = document.getElementById("quoteAiFiles");
      if (input) input.value = "";
      updateQuoteAiFileName();
      window.__quoteAiRecognizedSignature = "";
    }
    function resetFreshQuoteTransientState() {
      Object.keys(quoteCatalogDrafts).forEach(key => delete quoteCatalogDrafts[key]);
      quoteCatalogCategoryId = "";
      quoteCatalogProductId = "";
      quoteCatalogSearchText = "";
      window.__quoteUncertainFields = [];
      window.__quoteUncertainSource = "ai";
      window.__quoteShowAllFields = false;
      window.__quoteMaterialCostSignature = "";
      window.__quoteIndustryTarget = null;
      clearQuoteAiFileSelection();
      setQuoteIndustryWarning("");
      setQuoteCostWarning("");
      const status = document.getElementById("quoteAiRecognitionStatus");
      if (status) {
        status.textContent = "";
        status.classList.add("is-hidden");
      }
      const summary = document.getElementById("quoteAiRecognitionSummary");
      if (summary) {
        summary.innerHTML = "";
        summary.classList.add("is-hidden");
      }
      const uncertainPanel = document.getElementById("quoteAiUncertainConfirm");
      uncertainPanel?.classList.add("is-hidden");
      const confirmed = document.getElementById("quoteAiConfirmed");
      if (confirmed) confirmed.checked = false;
    }
    function quoteAiInputSignature(source, files) {
      return JSON.stringify([
        source || "",
        Array.from(files || []).map(file => [file.name, file.size, file.lastModified])
      ]);
    }
    function quoteAnalysisText(value, fallback = "待确认") {
      if (Array.isArray(value)) return value.filter(Boolean).join("、") || fallback;
      const text = String(value || "").trim();
      return text || fallback;
    }
    function quoteAnalysisIsPending(value) {
      const text = String(value || "").trim();
      return !text || /待.*(?:确认|识别)|未识别|需要补充/.test(text);
    }
    function setQuoteAiUncertainty(analysis = {}) {
      const panel = document.getElementById("quoteAiUncertainConfirm");
      const checkbox = document.getElementById("quoteAiConfirmed");
      const text = document.getElementById("quoteAiUncertainText");
      const title = document.getElementById("quoteAiUncertainTitle");
      if (!panel) return;
      const confidence = analysis.confidence || {};
      const labels = { product: "产品", size: "尺寸", qty: "数量", material: "材质", process: "工艺", delivery: "交期", tax_shipping: "税费", sample: "打样" };
      const uncertain = Object.keys(labels)
        .filter(key => confidence[key] && confidence[key] !== "high")
        .map(key => labels[key]);
      const inherited = Object.keys(labels).filter(key => confidence[key] === "memory").map(key => labels[key]);
      panel.classList.toggle("is-hidden", !uncertain.length);
      if (checkbox) checkbox.checked = false;
      if (title) title.textContent = inherited.length ? "请确认沿用的客户历史资料" : "请确认AI推测字段";
      if (text) text.textContent = uncertain.length
        ? `${uncertain.join("、")}没有在本次原话中逐项重申${inherited.length ? "，已按客户明确要求从上一单带入" : ""}。请人工核对并勾选后再报价。`
        : "";
      window.__quoteUncertainFields = uncertain;
      window.__quoteUncertainSource = inherited.length ? "memory" : "ai";
      renderQuoteMissingFirst();
    }
    function normalizeQuoteDeliveryDays(value = "") {
      const text = String(value ?? "").trim();
      if (!text || /(?:待确认|待补充|未知|未识别|按.+确认)/.test(text)) return "";
      const numbers = [...text.matchAll(/\d+(?:\.\d+)?/g)].map(match => Number(match[0])).filter(number => number > 0);
      if (!numbers.length) return "";
      const conservativeValue = Math.max(...numbers);
      const days = /(?:周|星期)/.test(text) ? conservativeValue * 7 : conservativeValue;
      return String(Math.max(1, Math.ceil(days)));
    }
    function setQuoteDeliveryValue(value = "") {
      setIfExists("quoteDelivery", normalizeQuoteDeliveryDays(value));
    }
    function quoteDeliveryText(value = val("quoteDelivery")) {
      const days = normalizeQuoteDeliveryDays(value);
      return days ? `${days}天` : "待确认";
    }
    function quoteMissingFieldDefinitions() {
      return [
        { id:"quoteCustomerName", label:"客户姓名", missing:() => !val("quoteCustomerName") || isPendingCustomerName(val("quoteCustomerName")) || val("quoteCustomerName") === "新客户" },
        { id:"quoteProduct", label:"产品名称", missing:() => !val("quoteProduct") || ["定制产品","待确认"].includes(val("quoteProduct")) },
        { id:"quoteSize", label:"尺寸", missing:() => !val("quoteSize") || val("quoteSize").includes("待确认") },
        { id:"quoteQty", label:"数量", missing:() => !(Number(val("quoteQty").replace(/[^\d.]/g, "")) > 0) },
        { id:"quoteMaterial", label:"材质", missing:() => !val("quoteMaterial") || val("quoteMaterial").includes("待确认") },
        { id:"quoteProcess", label:"工艺", missing:() => !val("quoteProcess") || val("quoteProcess").includes("待确认") },
        { id:"quoteDelivery", label:"交期", missing:() => !(Number(normalizeQuoteDeliveryDays(val("quoteDelivery"))) > 0) },
        { id:"quoteTaxMode", label:"税费", missing:() => val("quoteTaxMode") === "待确认" },
        { id:"quoteSampleMode", label:"打样", missing:() => val("quoteSampleMode") === "待确认" }
      ];
    }
    function renderQuoteMissingFirst() {
      const viewport = document.getElementById("quotePageViewport");
      if (!viewport) return;
      const definitions = quoteMissingFieldDefinitions();
      const missing = definitions.filter(item => item.missing());
      const uncertainLabels = document.getElementById("quoteAiConfirmed")?.checked ? new Set() : new Set(window.__quoteUncertainFields || []);
      const inferred = definitions.filter(item => !item.missing() && uncertainLabels.has(item.label));
      definitions.forEach(item => {
        const row = document.getElementById(item.id)?.closest(".row");
        row?.classList.toggle("quote-field-missing", missing.some(entry => entry.id === item.id));
        row?.classList.toggle("quote-field-inferred", inferred.some(entry => entry.id === item.id));
      });
      viewport.classList.remove("quote-focus-mode");
    }
    function syncManualQuoteCertainty(changedId = "", options = {}) {
      const unresolved = new Set(window.__quoteUncertainFields || []);
      const changed = {
        quoteProduct: ["产品", () => val("quoteProduct") && !val("quoteProduct").includes("待确认")],
        quoteSize: ["尺寸", () => val("quoteSize") && !val("quoteSize").includes("待确认")],
        quoteQty: ["数量", () => Number(val("quoteQty").replace(/[^\d.]/g, "")) > 0],
        quoteMaterial: ["材质", () => val("quoteMaterial") && !val("quoteMaterial").includes("待确认")],
        quoteProcess: ["工艺", () => val("quoteProcess") && !val("quoteProcess").includes("待确认")],
        quoteDelivery: ["交期", () => Number(normalizeQuoteDeliveryDays(val("quoteDelivery"))) > 0],
        quoteTaxMode: ["税费", () => val("quoteTaxMode") !== "待确认"],
        quoteSampleMode: ["打样", () => val("quoteSampleMode") !== "待确认"]
      }[changedId];
      if (changed?.[1]?.()) unresolved.delete(changed[0]);
      window.__quoteUncertainFields = [...unresolved];
      const panel = document.getElementById("quoteAiUncertainConfirm");
      const checkbox = document.getElementById("quoteAiConfirmed");
      const text = document.getElementById("quoteAiUncertainText");
      panel?.classList.toggle("is-hidden", !unresolved.size);
      if (!unresolved.size && checkbox) checkbox.checked = true;
      if (text && unresolved.size) text.textContent = `${[...unresolved].join("、")}仍需人工核对；已经手动填写的项目无需重复确认。`;
      if (options.renderMissing !== false) renderQuoteMissingFirst();
    }
    function applyQuoteRecognizedAnalysis(analysis, fileCount = 0) {
      window.__quoteShowAllFields = false;
      const product = quoteAnalysisText(analysis.product, "定制产品");
      const size = quoteAnalysisText(analysis.size);
      const qty = quoteAnalysisText(analysis.qty);
      const material = quoteAnalysisText(analysis.material);
      const process = quoteAnalysisText(analysis.process || analysis.finish);
      const delivery = quoteAnalysisText(analysis.delivery, "");
      const taxShipping = quoteAnalysisText(analysis.tax_shipping || analysis.taxShipping, "");
      const address = quoteAnalysisText(analysis.address, "");
      const detectedProfile = quoteBusinessProfileForProduct(product);
      const inferredTemplate = detectedProfile.template;
      const allowedTemplates = currentCategoryTemplates();
      if (allowedTemplates.includes(inferredTemplate)) {
        setIfExists("quoteTemplate", inferredTemplate);
        setQuoteIndustryWarning("");
      } else {
        const detectedCategory = detectedProfile.category;
        setQuoteIndustryWarning(`AI识别到这是“${detectedCategory}”需求，当前价格表属于“${currentIndustryCategory()}”。`, detectedCategory, inferredTemplate);
      }
      // 每次识别都先清空本单字段，避免切换客户后沿用上一单的地区、交期或打样费。
      setIfExists("quoteDelivery", "");
      setIfExists("quoteRegion", "");
      setIfExists("quoteFreight", "0");
      setIfExists("quoteSample", "0");
      setIfExists("quoteProduct", quoteAnalysisIsPending(product) ? "" : product);
      setIfExists("quoteSize", quoteAnalysisIsPending(size) ? "" : size);
      setIfExists("quoteQty", quoteAnalysisIsPending(qty) ? "" : qty.replace(/[^\d.]/g, ""));
      setIfExists("quoteMaterial", quoteAnalysisIsPending(material) ? "" : material);
      setIfExists("quoteProcess", quoteAnalysisIsPending(process) ? "" : process.replace(/\s*[+、]\s*/g, ", "));
      applyQuoteCatalogRecognizedValues({ ...analysis, product, size, material, process });
      if (!quoteAnalysisIsPending(delivery)) setQuoteDeliveryValue(delivery);
      setIfExists("quoteTaxMode", "待确认");
      setIfExists("quoteFreightMode", "不含运");
      if (taxShipping.includes("不含税")) setIfExists("quoteTaxMode", "不含税");
      else if (taxShipping.includes("含税")) setIfExists("quoteTaxMode", "含税");
      if (!quoteAnalysisIsPending(address)) setIfExists("quoteRegion", address);
      setIfExists("quoteSampleMode", analysis.sample_mode || (analysis.sample_required === true
        ? "需要打样，费用待确认"
        : analysis.sample_required === false
          ? "不需要打样"
          : "待确认"));
      if (Number(analysis.sample_fee || 0) > 0) setIfExists("quoteSample", String(analysis.sample_fee));
      const paymentHabit = (analysis.customer_habits || []).find(item => String(item).startsWith("付款习惯："));
      if (paymentHabit) {
        const payment = String(paymentHabit).replace(/^付款习惯：/, "").trim();
        if (payment) setIfExists("quotePayment", /尾款|余款/.test(payment) ? payment : `${payment}，尾款出货前付清`);
      }
      setQuoteAiUncertainty(analysis);
      const summary = document.getElementById("quoteAiRecognitionSummary");
      if (summary) {
        const items = [
          ["产品", product], ["尺寸", size], ["数量", qty],
          ["材质", material], ["工艺", process],
          ["税运", taxShipping || "待确认"],
          ["打样", analysis.sample_status || "待确认"],
          ["附件", fileCount ? `${fileCount} 个已识别` : "未上传"]
        ];
        summary.innerHTML = items.map(item => `<div><span>${escapeHtml(item[0])}</span><strong>${escapeHtml(item[1])}</strong></div>`).join("");
        summary.classList.remove("is-hidden");
      }
      const status = document.getElementById("quoteAiRecognitionStatus");
      const missingCore = [size, qty, material, process].filter(quoteAnalysisIsPending);
      if (status) {
        const privacyPrefix = analysis.privacy?.enabled ? "隐私保护完成，" : "";
        status.textContent = missingCore.length
          ? `${privacyPrefix}已识别部分信息。没有看清的项目会留空，请在下方补充后再生成报价。`
          : `${privacyPrefix}产品、尺寸、数量、材质和工艺已自动填入。请在下方核对，无误后直接生成报价。`;
        status.classList.remove("is-hidden");
      }
      setTextIfExists("quoteSourceNote", "AI已从客户文字或图片中提取报价信息；请在下方直接核对或修改。" );
      renderQuoteReviewSummary();
      return missingCore.length === 0;
    }
    async function quoteAiRecognize(autoContinue = false) {
      const source = (document.getElementById("quoteAiSource")?.value || "").trim();
      const files = Array.from(document.getElementById("quoteAiFiles")?.files || []);
      const status = document.getElementById("quoteAiRecognitionStatus");
      const button = document.getElementById("quoteAiRecognizeButton");
      if (!source && !files.length) {
        document.getElementById("quoteManualDetails")?.setAttribute("open", "");
        notify("请粘贴客户原话、上传图片，或者展开手动填写。");
        return false;
      }
      if (status) {
        status.textContent = files.length
          ? "正在保护客户隐私、清理图片信息并识别需求…"
          : "正在隐藏联系方式和详细地址，再整理客户需求…";
        status.classList.remove("is-hidden");
      }
      if (button) {
        button.disabled = true;
        button.textContent = "正在识别…";
      }
      try {
        let analysis;
        if (window.__isStaticFileMode) {
          if (!source) throw new Error("当前是本地文件预览，图片识别需要从软件服务地址打开；也可以先补充客户文字。");
          const local = analyze(source);
          analysis = { ...local, process: local.finish, missing: [] };
          if (files.length && status) status.textContent = "当前本地预览已按文字识别；图片内容需要从软件服务地址打开后识别。";
        } else {
          await window.__quoteAiEnsureFormalLogin();
          const fileIds = files.length ? await window.__quoteAiUploadFiles("quoteAiFiles", "quoteAiFileName") : [];
          const payloadText = source || "请识别客户上传的聊天截图或产品资料，提取产品、尺寸、数量、材质、工艺、交期、税运和收货地址。";
          const data = await window.__quoteAiRequestAnalysis({ text: payloadText, file_ids: fileIds });
          analysis = data?.analysis;
          if (!analysis) throw new Error("没有识别到客户需求，请补充一句文字说明后重试。");
        }
        if (source) setIfExists("customerText", source);
        window.__latestAiAnalysis = analysis;
        window.__latestAiSourceText = source;
        applyQuoteRecognizedAnalysis(analysis, files.length);
        if (files.length) clearQuoteAiFileSelection();
        window.__quoteAiRecognizedSignature = quoteAiInputSignature(source, []);
        if (autoContinue && validateQuoteGuideStep(1)) {
          quoteGuideDirection = "next";
          quoteGuideStep = 2;
          renderQuoteGuide();
        }
        return true;
      } catch (err) {
        if (status) {
          status.textContent = `识别提醒：${err.message}`;
          status.classList.remove("is-hidden");
        }
        document.getElementById("quoteManualDetails")?.setAttribute("open", "");
        notify("这次没有识别完整，请补充文字或手动修改。" );
        return false;
      } finally {
        if (button) {
          button.disabled = false;
          button.textContent = "AI识别并填入";
        }
      }
    }
    function renderQuoteReviewSummary(options = {}) {
      const box = document.getElementById("quoteReviewSummary");
      if (!box) return;
      const items = [
        ["产品与规格", `${val("quoteProduct") || "未填写"} · ${val("quoteSize") || "未填写"}`],
        ["数量", `${val("quoteQty") || "未填写"} 个`],
        ["材质", val("quoteMaterial") || "未填写"],
        ["工艺", val("quoteProcess") || "未填写"],
        ["产品参数", quoteCatalogConfigurationText() || "手动填写"],
        ["报价口径", `${val("quoteTaxMode") || "待确认"} · 不含运费`],
        ["打样", `${val("quoteSampleMode") || "待确认"}${val("quoteSampleMode") === "收费打样" ? ` · ¥${num("quoteSample", 0).toFixed(2)}` : ""}`],
        ["交付", `${quoteDeliveryText()} · ${val("quotePayment") || "未填写"}`]
      ];
      box.innerHTML = items.map(item => `<div><span>${escapeHtml(item[0])}</span><strong>${escapeHtml(item[1])}</strong></div>`).join("");
      renderQuoteMaterialCostCheck();
      if (options.renderMissing !== false) renderQuoteMissingFirst();
    }
    function currentQuoteMaterialRule() {
      const material = val("quoteMaterial");
      const override = findQuoteMaterialPrice(material);
      if (override && override.unit === "元/个" && Number(override.price) > 0) {
        return { name: override.name || material, price: Number(override.price), source: "本厂快捷价" };
      }
      return quoteLegacyRulesConfirmed()
        ? findMatchingRule(parseRules(state.settings?.materialRules || ""), material)
        : null;
    }
    function quoteSpecMaterialKey() {
      return [
        currentIndustryCategory(), val("quoteTemplate"), val("quoteMaterial"), val("quoteSize")
      ].map(normalizedQuoteValue).join("|");
    }
    function quoteSpecMaterialCost() {
      const library = state.settings?.specMaterialCosts;
      if (!library || typeof library !== "object" || Array.isArray(library)) return null;
      const item = library[quoteSpecMaterialKey()];
      if (!item || !(Number(item.price) > 0)) return null;
      const costRulesConfirmedAt = Date.parse(state.settings.costRulesConfirmedAt || "") || 0;
      const specConfirmedAt = Date.parse(item.confirmedAt || "") || 0;
      return specConfirmedAt >= costRulesConfirmedAt ? item : null;
    }
    function rememberQuoteSpecMaterialCost(price) {
      const value = Number(price || 0);
      if (!(value > 0) || !val("quoteMaterial") || !val("quoteSize")) return;
      state.settings.specMaterialCosts = state.settings.specMaterialCosts && typeof state.settings.specMaterialCosts === "object" && !Array.isArray(state.settings.specMaterialCosts)
        ? state.settings.specMaterialCosts
        : {};
      const key = quoteSpecMaterialKey();
      state.settings.specMaterialCosts[key] = {
        price: value,
        category: currentIndustryCategory(),
        template: val("quoteTemplate"),
        material: val("quoteMaterial"),
        size: val("quoteSize"),
        confirmedAt: new Date().toISOString()
      };
      const entries = Object.entries(state.settings.specMaterialCosts)
        .sort((a, b) => Date.parse(b[1]?.confirmedAt || "") - Date.parse(a[1]?.confirmedAt || ""))
        .slice(0, 200);
      state.settings.specMaterialCosts = Object.fromEntries(entries);
      window.__saveSettingsCloud?.(state.settings);
    }
    function normalizedQuoteMaterialPurchaseUnit(unit) {
      const text = String(unit || "").replace(/\s/g, "").toLowerCase();
      if (text.includes("吨") || text.includes("ton")) return "元/吨";
      if (text.includes("kg") || text.includes("千克") || text.includes("公斤")) return "元/kg";
      if (text.includes("㎡") || text.includes("m²") || text.includes("m2") || text.includes("平方")) return "元/㎡";
      if (text.includes("米") || text.includes("/m")) return "元/米";
      if (text.includes("张") || text.includes("sheet")) return "元/张";
      if (text.includes("小时") || text.includes("hour")) return "元/小时";
      return "元/个";
    }
    function quoteMaterialPurchaseKind(unit) {
      const normalized = normalizedQuoteMaterialPurchaseUnit(unit);
      if (normalized === "元/吨" || normalized === "元/kg") return "weight";
      if (normalized === "元/㎡") return "area";
      if (normalized === "元/米") return "meter";
      if (normalized === "元/张") return "sheet";
      return "piece";
    }
    function quoteMaterialFormulaFamily() {
      const entry = quoteCatalogEntry();
      return window.JIANGLAI_QUOTE_FORMULA?.profileRule(entry?.profile || "")?.family || "manual";
    }
    function quoteMaterialIsPaperLike(value = val("quoteMaterial")) {
      return /(纸|铜版|白卡|黑卡|灰板|牛皮|瓦楞|特种纸|\d+(?:\.\d+)?\s*g)/i.test(String(value || ""));
    }
    function quoteMaterialConversionBasis(kind = quoteMaterialPurchaseKind(val("quoteMaterialPurchaseUnit"))) {
      const family = quoteMaterialFormulaFamily();
      if (["book", "multipart", "bom"].includes(family)) return "unsupported";
      if (kind === "weight") {
        if (["sheet", "dieline"].includes(family)) return quoteMaterialIsPaperLike() ? "dimensions" : "unsupported";
        if (family === "manual") return quoteMaterialIsPaperLike() ? "dimensions" : "usage";
        return "usage";
      }
      if (kind === "area") return ["sheet", "dieline", "area", "roll"].includes(family) ? "dimensions" : "usage";
      return "usage";
    }
    function quoteMaterialPurchaseSource() {
      const material = val("quoteMaterial");
      const override = findQuoteMaterialPrice(material);
      if (override && Number(override.price) > 0) {
        return {
          price: Number(override.price), unit: normalizedQuoteMaterialPurchaseUnit(override.unit),
          name: override.name || material, spec: "", item: { ...override, type: "material", source: "catalog_override" }, legacy: false
        };
      }
      const item = findActiveCostItem(material, ["material"]);
      if (item && Number(item.price) > 0) {
        return {
          price: Number(item.price),
          unit: normalizedQuoteMaterialPurchaseUnit(item.unit),
          name: item.name || material,
          spec: item.spec || "",
          item,
          legacy: false
        };
      }
      const rule = currentQuoteMaterialRule();
      if (rule && Number(rule.price) > 0) {
        return { price: Number(rule.price), unit: "元/个", name: rule.name || material, spec: "", item: null, legacy: true };
      }
      const looksWeightPriced = quoteMaterialIsPaperLike(material);
      return { price: null, unit: looksWeightPriced ? "元/吨" : "元/个", name: material, spec: "", item: null, legacy: false };
    }
    function quoteMaterialAutomaticFlatSize() {
      const result = quoteAutomaticFlatSizeResult();
      return result?.widthMm && result?.heightMm ? `${quoteCatalogSetupNumber(result.widthMm)}×${quoteCatalogSetupNumber(result.heightMm)}` : "";
    }
    function quoteMaterialGsmFromSelection() {
      const source = quoteMaterialPurchaseSource();
      return String(`${val("quoteMaterial")} ${source.name || ""} ${source.spec || ""}`).match(/(\d+(?:\.\d+)?)\s*(?:g|克)/i)?.[1] || "";
    }
    function quoteMaterialConversion(options = {}) {
      const strict = Boolean(options.strict);
      const price = num("quoteMaterialPurchasePrice", 0);
      const unit = normalizedQuoteMaterialPurchaseUnit(val("quoteMaterialPurchaseUnit"));
      const kind = quoteMaterialPurchaseKind(unit);
      const basis = quoteMaterialConversionBasis(kind);
      const qty = num("quoteQty", 0);
      const utilization = rateNum("quoteMaterialUtilization", 0);
      const wasteRate = rateNum("quoteMaterialWasteRate", 0);
      const fail = error => ({ ok: false, error, unit, kind });
      if (!(price > 0)) return fail("请填写本厂当前材料采购价");
      if (!(utilization > 0 && utilization <= 1)) return fail("排版/裁切利用率必须大于0且不超过100%");
      if (!(wasteRate >= 0 && wasteRate < 1)) return fail("生产损耗率必须大于或等于0且小于100%");
      if (strict && !(qty > 0)) return fail("请先填写本单数量");
      if (basis === "unsupported") {
        const family = quoteMaterialFormulaFamily();
        const reason = family === "book" ? "书刊需要分别计算封面、内页和页数" : family === "roll" ? "卷材需要计算排数和走料米数" : family === "multipart" ? "多联产品需要分别计算各联材料" : family === "bom" ? "该产品需要按BOM分项" : "当前产品不能只按一层材料面积";
        return fail(`${reason}，不能用通用吨价公式直接报价；请切换专属/行业公式或使用本厂已确认的按件成本`);
      }
      const engine = window.JIANGLAI_QUOTE_FORMULA?.calculateMaterialPurchaseCost;
      if (typeof engine !== "function") return fail("材料换算引擎未加载，请刷新后重试");
      const dimensionsText = val("quoteMaterialFlatSize");
      const gsm = num("quoteMaterialGsm", 0);
      const usagePerPiece = num("quoteMaterialUsage", kind === "piece" ? 1 : 0);
      if (basis === "dimensions" && !dimensionsText) return fail("请填写单件展开尺寸");
      if (kind === "weight" && basis === "dimensions" && !(gsm > 0)) return fail("纸张按吨或公斤计价时必须填写材料克重");
      if (basis === "usage" && !(usagePerPiece > 0)) {
        return fail(`按${unit.replace("元/", "")}计价时必须填写单件材料用量`);
      }
      try {
        const dimensions = basis === "dimensions"
          ? window.JIANGLAI_QUOTE_FORMULA.parseDimensions(dimensionsText, 2)
          : null;
        const calculation = engine({
          kind,
          basis,
          purchaseUnit: unit,
          purchasePrice: price,
          quantity: qty > 0 ? qty : 1,
          utilizationRate: utilization,
          wasteRate,
          widthMm: dimensions?.[0],
          heightMm: dimensions?.[1],
          gsm: kind === "weight" && basis === "dimensions" ? gsm : undefined,
          usagePerPiece: basis === "usage" ? usagePerPiece : undefined
        });
        const formulaFamily = window.JIANGLAI_QUOTE_FORMULA?.profileRule(quoteCatalogEntry()?.profile || "")?.family || "manual";
        const shouldUseSheetImposition = qty > 0 && kind === "weight" && basis === "dimensions"
          && ["sheet", "dieline"].includes(formulaFamily);
        let sheetImposition = null;
        if (shouldUseSheetImposition) {
          const recommendation = window.JIANGLAI_QUOTE_COST_DEFAULTS?.recommendPressSheet({
            flatSize:`${dimensions[0]}×${dimensions[1]}`,
            quantity:qty,
            gsm,
            materialPricePerKg:calculation.pricePerUsageUnit,
            setupWasteSheets:Number(window.JIANGLAI_QUOTE_COST_DEFAULTS?.publicPrintReferences?.setupWasteSheets || 100),
            printWasteRate:wasteRate * 100,
            finishingWasteRate:0
          });
          if (!recommendation) return fail("当前展开尺寸无法排入可用上机纸，请核对刀线或选择实际机台");
          const totalUsage = recommendation.widthMm / 1000 * recommendation.heightMm / 1000
            * recommendation.productionSheets * gsm / 1000;
          const totalCost = totalUsage * calculation.pricePerUsageUnit;
          sheetImposition = {
            sheetSize:`${recommendation.widthMm}×${recommendation.heightMm}`,
            piecesPerSheet:recommendation.piecesPerSheet,
            netSheets:recommendation.netSheets,
            productionSheets:recommendation.productionSheets,
            totalUsage,
            grossUsagePerPiece:totalUsage / qty,
            totalCost,
            unitCost:totalCost / qty,
            effectiveUtilization:calculation.netUsagePerPiece * qty / totalUsage
          };
        }
        return {
          ok: true,
          price,
          unit,
          kind,
          basis,
          qty,
          dimensions,
          gsm,
          utilization,
          wasteRate,
          netUsage: calculation.netUsagePerPiece,
          grossUsage: sheetImposition?.grossUsagePerPiece ?? calculation.grossUsagePerPiece,
          totalUsage: sheetImposition?.totalUsage ?? (qty > 0 ? calculation.totalUsage : calculation.grossUsagePerPiece),
          usageUnit: calculation.usageUnit,
          unitCost: sheetImposition?.unitCost ?? calculation.materialUnitCost,
          totalCost: sheetImposition?.totalCost ?? (qty > 0 ? calculation.materialCost : calculation.materialUnitCost),
          pricePerUsageUnit: calculation.pricePerUsageUnit,
          calculationMethod:sheetImposition ? "sheet_imposition" : "utilization_conversion",
          sheetImposition,
          source: quoteMaterialPurchaseSource()
        };
      } catch (error) {
        return fail(error.message || "单件展开尺寸格式不正确");
      }
    }
    function updateQuoteMaterialConversionPreview() {
      const resultBox = document.getElementById("quoteMaterialConversionResult");
      if (!resultBox) return null;
      const calculation = quoteMaterialConversion();
      resultBox.classList.toggle("is-error", !calculation.ok);
      if (!calculation.ok) {
        resultBox.innerHTML = `<strong>还不能换算：</strong>${escapeHtml(calculation.error)}。材料和客户资料都已保留，补齐后会立即重算。`;
        setIfExists("quoteMaterialUnitCost", "");
        return calculation;
      }
      const qtyText = calculation.qty > 0 ? `${calculation.qty.toLocaleString()}个` : "待填写数量";
      const priceText = calculation.unit === "元/吨"
        ? `¥${calculation.price.toFixed(2)}/吨（内部换算 ¥${calculation.pricePerUsageUnit.toFixed(4)}/kg）`
        : `¥${calculation.price.toFixed(4)}/${calculation.unit.replace("元/", "")}`;
      const standardBasis = calculation.sheetImposition
        ? `实际拼版：${calculation.sheetImposition.sheetSize}mm，${calculation.sheetImposition.piecesPerSheet}拼，净需${calculation.sheetImposition.netSheets}张，含起机与损耗共${calculation.sheetImposition.productionSheets}张；不再使用统一90%利用率代替拼版`
        : calculation.basis === "dimensions" && calculation.kind === "weight"
        ? "克重换算口径：GB/T 451.2 / ISO 536；价格、利用率和损耗：本厂价格表"
        : "计价口径：本厂采购单位与实际用量";
      resultBox.innerHTML = `<strong>${calculation.sheetImposition ? "拼版换算" : "自动换算"}：</strong>${escapeHtml(priceText)}；单件净用量 ${calculation.netUsage.toFixed(6)}${escapeHtml(calculation.usageUnit)}，${calculation.sheetImposition ? "按上机纸、拼版、起机和损耗换算后" : "计入利用率和损耗后"} ${calculation.grossUsage.toFixed(6)}${escapeHtml(calculation.usageUnit)}/个；${escapeHtml(qtyText)}预计用料 ${calculation.totalUsage.toFixed(3)}${escapeHtml(calculation.usageUnit)}，材料总成本 ¥${calculation.totalCost.toFixed(2)}，折合 ¥${calculation.unitCost.toFixed(4)}/个。<small>${escapeHtml(standardBasis)}</small>`;
      setIfExists("quoteMaterialUnitCost", calculation.unitCost.toFixed(6));
      return calculation;
    }
    function quoteMaterialCostDraft() {
      return {
        costMode: val("quoteCostMode") || "material",
        purchasePrice: val("quoteMaterialPurchasePrice"),
        purchaseUnit: val("quoteMaterialPurchaseUnit"),
        flatSize: val("quoteMaterialFlatSize"),
        gsm: val("quoteMaterialGsm"),
        usage: val("quoteMaterialUsage"),
        utilization: val("quoteMaterialUtilization"),
        wasteRate: val("quoteMaterialWasteRate"),
        unitCost: val("quoteMaterialUnitCost"),
        confirmed: Boolean(document.getElementById("quoteMaterialCostConfirmed")?.checked)
      };
    }
    function restoreQuoteMaterialCostDraft(draft) {
      if (!draft || typeof draft !== "object") return false;
      setIfExists("quoteCostMode", draft.costMode || "material");
      renderIndustryQuoteCostFields();
      setIfExists("quoteMaterialPurchasePrice", draft.purchasePrice ?? "");
      setIfExists("quoteMaterialPurchaseUnit", draft.purchaseUnit || "元/吨");
      setIfExists("quoteMaterialFlatSize", draft.flatSize ?? "");
      setIfExists("quoteMaterialGsm", draft.gsm ?? "");
      setIfExists("quoteMaterialUsage", draft.usage ?? "");
      setIfExists("quoteMaterialUtilization", draft.utilization ?? "");
      setIfExists("quoteMaterialWasteRate", draft.wasteRate ?? "");
      setIfExists("quoteMaterialUnitCost", draft.unitCost ?? "");
      const confirmation = document.getElementById("quoteMaterialCostConfirmed");
      if (confirmation) confirmation.checked = draft.confirmed === true;
      updateQuoteMaterialConversionPreview();
      return true;
    }
    function renderQuoteMaterialCostCheck(forceReset = false) {
      const panel = document.getElementById("quoteMaterialCostCheck");
      const input = document.getElementById("quoteMaterialUnitCost");
      const confirmation = document.getElementById("quoteMaterialCostConfirmed");
      const hint = document.getElementById("quoteMaterialCostHint");
      const purchaseGrid = document.getElementById("quoteMaterialPurchaseGrid");
      const resultBox = document.getElementById("quoteMaterialConversionResult");
      const confirmText = document.getElementById("quoteMaterialConfirmText");
      if (!panel || !input || !confirmation || !hint) return;
      const costMode = document.getElementById("quoteCostMode")?.value || "material";
      panel.classList.toggle("is-hidden", !["material", "unit"].includes(costMode));
      if (!["material", "unit"].includes(costMode)) return;
      const size = val("quoteSize").trim();
      const material = val("quoteMaterial").trim();
      const product = val("quoteProduct").trim();
      const selectionIncomplete = !product || /待确认|定制产品/.test(product)
        || !size || /待确认|当前规格/.test(size)
        || !material || /待确认|当前材料/.test(material);
      if (selectionIncomplete) {
        window.__quoteMaterialCostSignature = "";
        confirmation.checked = false;
        setIfExists("quoteMaterialUnitCost", "");
        purchaseGrid?.classList.add("is-hidden");
        hint.textContent = "先选择产品、材料和尺寸，系统再显示与本单准确匹配的采购价和成本换算。";
        if (resultBox) {
          resultBox.classList.remove("is-error");
          resultBox.innerHTML = "<strong>等待选择：</strong>还没有确定材料和尺寸，不会提前套用历史成本。";
        }
        if (confirmText) confirmText.textContent = "选择产品、材料和尺寸后，再核对本单采购价、利用率和生产损耗。";
        return;
      }
      if (costMode === "unit") {
        const savedSpec = quoteSpecMaterialCost();
        const rule = currentQuoteMaterialRule();
        const unitPrice = Number(savedSpec?.price || rule?.price || 0);
        const signature = `unit|${normalizeRuleText(material)}|${String(size).trim().toLowerCase()}|${unitPrice}`;
        if (forceReset || window.__quoteMaterialCostSignature !== signature) confirmation.checked = false;
        window.__quoteMaterialCostSignature = signature;
        purchaseGrid?.classList.add("is-hidden");
        setIfExists("quoteMaterialUnitCost", unitPrice > 0 ? unitPrice.toFixed(6) : "");
        hint.textContent = unitPrice > 0
          ? `历史兼容价格：${savedSpec ? `${savedSpec.material || material} · ${savedSpec.size || size}` : rule?.name || material}，¥${unitPrice.toFixed(4)}/个。这个价格不会再叠加利用率或损耗。`
          : `历史价格表里还没有“${material}”的已确认按件成本；请切回原料采购价换算，或先在价格表中补齐。`;
        if (resultBox) {
          resultBox.classList.toggle("is-error", !(unitPrice > 0));
          resultBox.innerHTML = unitPrice > 0
            ? `<strong>历史按件成本：</strong>¥${unitPrice.toFixed(4)}/个。仅用于兼容旧报价，新的纸张采购价建议维护为元/吨。`
            : `<strong>还不能报价：</strong>没有找到已确认的历史按件材料成本。`;
        }
        if (confirmText) confirmText.textContent = "我已确认这条历史按件材料成本仍适用于当前产品和尺寸。";
        return;
      }
      purchaseGrid?.classList.remove("is-hidden");
      if (confirmText) confirmText.textContent = "我已核对本厂当前采购价、实际用量、材料利用率和生产损耗，可以用于本次报价。";
      const source = quoteMaterialPurchaseSource();
      const automaticFlat = quoteAutomaticFlatSizeResult();
      const signature = `material|${normalizeRuleText(material)}|${String(size).trim().toLowerCase()}|${val("quoteTemplate") || ""}|${source.price || ""}|${source.unit}`;
      if (forceReset || window.__quoteMaterialCostSignature !== signature) {
        window.__quoteMaterialCostSignature = signature;
        setIfExists("quoteMaterialPurchasePrice", source.price === null ? "" : String(source.price));
        setIfExists("quoteMaterialPurchaseUnit", source.unit);
        setIfExists("quoteMaterialFlatSize", quoteMaterialAutomaticFlatSize());
        setIfExists("quoteMaterialGsm", quoteMaterialGsmFromSelection());
        setIfExists("quoteMaterialUsage", "1");
        const purchaseKind = quoteMaterialPurchaseKind(source.unit);
        const directUnit = ["piece", "sheet"].includes(purchaseKind);
        setIfExists("quoteMaterialUtilization", directUnit ? "100%" : `${rateText(Number(state.settings.utilizationRate ?? .9))}%`);
        setIfExists("quoteMaterialWasteRate", directUnit ? "0%" : `${rateText(Number(state.settings.loss ?? .05))}%`);
        confirmation.checked = false;
      }
      const kind = quoteMaterialPurchaseKind(val("quoteMaterialPurchaseUnit"));
      const basis = quoteMaterialConversionBasis(kind);
      document.querySelectorAll(".quote-material-size-field").forEach(node => node.classList.toggle("is-hidden", basis !== "dimensions"));
      document.querySelectorAll(".quote-material-gsm-field").forEach(node => node.classList.toggle("is-hidden", !(basis === "dimensions" && kind === "weight")));
      document.querySelectorAll(".quote-material-usage-field").forEach(node => node.classList.toggle("is-hidden", basis !== "usage"));
      setTextIfExists("quoteMaterialUsageLabel", kind === "weight" ? "单件净用量（kg）*" : kind === "area" ? "单件净用量（㎡）*" : kind === "meter" ? "单件净用量（米）*" : kind === "sheet" ? "单件分摊用量（张）*" : "单件净用量（个）*");
      hint.textContent = source.price !== null
        ? `${source.legacy ? "历史按件价" : "已从本厂价格表匹配"}：${source.name}${source.spec ? ` ${source.spec}` : ""}，¥${Number(source.price).toFixed(3)}/${source.unit.replace("元/", "")}。当前规格 ${size}，下方会按实际用量换算。`
        : `本厂价格表还没有“${material}”的采购价。请按实际采购单位填写；纸张通常填元/吨，系统不会把吨价误当成元/个。`;
      if (basis === "dimensions") {
        hint.textContent += automaticFlat?.status === "manual_required"
          ? ` 当前结构无法安全推算展开尺寸，请按刀线填写；系统不会拿成品长宽高冒充展开尺寸。`
          : ` 展开尺寸已由${automaticFlat?.estimated ? "产品结构模板估算" : "成品二维尺寸"}生成，正式生产前可按刀线修正。`;
      }
      updateQuoteMaterialConversionPreview();
    }
    function renderQuoteGuide() {
      const inline = isQuoteWorkspaceMountedAtHome();
      setTextIfExists("quoteGuideKicker", inline ? "AI整理报价" : "直接报价");
      setTextIfExists("quoteGuideTitle", inline ? "核对AI整理的报价信息" : "新建报价");
      setTextIfExists("quoteGuideRequired", inline ? "AI已整理，仍需厂家核对" : "所有字段由厂家核对");
      const prev = document.getElementById("quoteGuidePrev");
      const next = document.getElementById("quoteGuideNext");
      const generate = document.getElementById("quoteGuideGenerate");
      prev?.classList.add("is-hidden");
      next?.classList.add("is-hidden");
      generate?.classList.remove("is-hidden");
      renderQuoteReviewSummary();
      renderIndustryQuoteCostFields();
    }
    function revealQuoteResult(options = {}) {
      const card = document.getElementById("quoteResultCard");
      if (!card) return false;
      const returnPage = options.returnPage
        ? options.returnPage === "图片制作" ? "图片制作" : "报价系统"
        : isQuoteWorkspaceMountedAtHome() ? "图片制作" : "报价系统";
      window.__quoteResultReturnPage = returnPage;
      if (options.captureDraft !== false) window.__quoteResultDraft = captureQuoteWorkspaceSnapshot();
      if (showPage("报价结果") !== true) return false;
      if (quoteResultAttentionTimer) window.clearTimeout(quoteResultAttentionTimer);
      card.classList.remove("is-hidden");
      card.classList.remove("is-new-result");
      void card.offsetWidth;
      card.classList.add("is-new-result");
      window.requestAnimationFrame(() => {
        card.focus({ preventScroll: true });
        window.scrollTo(0, 0);
      });
      quoteResultAttentionTimer = window.setTimeout(() => {
        card.classList.remove("is-new-result");
        quoteResultAttentionTimer = 0;
      }, 2200);
      notify("报价已生成，已打开独立价格结果页。");
      return true;
    }
    function returnToManualQuote() {
      const returnPage = window.__quoteResultReturnPage === "图片制作" ? "图片制作" : "报价系统";
      const restoreDraft = () => {
        restoreQuoteWorkspaceSnapshot(window.__quoteResultDraft);
        document.getElementById("quoteCustomerName")?.focus({ preventScroll: true });
      };
      if (returnPage === "图片制作") {
        showPage("图片制作");
        window.requestAnimationFrame(() => {
          openHomeInlineQuoteWorkspace();
          window.requestAnimationFrame(restoreDraft);
        });
        return;
      }
      mountQuoteWorkspace("page");
      showPage("报价系统");
      window.requestAnimationFrame(restoreDraft);
    }
    async function quoteGuideNextStep() {
      if (quoteGuideStep === 1) {
        const source = (document.getElementById("quoteAiSource")?.value || "").trim();
        const files = document.getElementById("quoteAiFiles")?.files || [];
        const currentSignature = quoteAiInputSignature(source, files);
        if ((source || files.length) && currentSignature !== window.__quoteAiRecognizedSignature) {
          await quoteAiRecognize(true);
          return;
        }
        if (!validateQuoteGuideStep(1)) {
          document.getElementById("quoteManualDetails")?.setAttribute("open", "");
          notify("请先粘贴客户原话、上传图片，或手动补充产品、尺寸和数量。");
          return;
        }
      }
      if (!validateQuoteGuideStep(quoteGuideStep)) return;
      quoteGuideDirection = "next";
      quoteGuideStep = Math.min(4, quoteGuideStep + 1);
      renderQuoteGuide();
      document.querySelector(".quote-guide-card")?.scrollIntoView({ behavior: "smooth", block: "start" });
    }
    function quoteGuidePreviousStep() {
      quoteGuideDirection = "prev";
      quoteGuideStep = Math.max(1, quoteGuideStep - 1);
      renderQuoteGuide();
      document.querySelector(".quote-guide-card")?.scrollIntoView({ behavior: "smooth", block: "start" });
    }
    function askQuickQuestion(text) {
      setIfExists("aiQuestion", text);
      askFactoryAi();
    }
    function autoGrowHomeQuestion(input) {
      if (!input) return;
      const inConversation = document.querySelector(".gpt-home")?.classList.contains("has-conversation");
      const compact = window.matchMedia("(max-width: 980px)").matches;
      const minHeight = inConversation ? 46 : compact ? 64 : 64;
      const maxHeight = inConversation ? 140 : compact ? 190 : 250;
      input.style.height = "auto";
      input.style.height = `${Math.min(Math.max(input.scrollHeight, minHeight), maxHeight)}px`;
    }
    function homeRetainedFileIds() {
      return Array.isArray(window.__lastRecognizedHomeFileIds)
        ? window.__lastRecognizedHomeFileIds.filter(id => Number(id) > 0)
        : [];
    }
    function customerFacingAttachmentName(value = "") {
      const name = String(value || "").trim();
      if (!name) return "";
      if (/^(?:AI生成-|ai-image-|image_generation_|generated[-_])/i.test(name)) return "上一张效果图";
      if (/^上一张AI效果图$/i.test(name)) return "上一张效果图";
      return name;
    }
    function homeRetainedFileNames() {
      return Array.isArray(window.__homeRetainedFileNames)
        ? window.__homeRetainedFileNames.map(customerFacingAttachmentName).filter(Boolean)
        : [];
    }
    function attachmentNameLooksLikeImage(value = "") {
      const name = String(value || "").trim().toLowerCase();
      if (!name) return true;
      if (/\.(?:xlsx?|csv|pdf|docx?|pptx?|txt|zip|rar|7z|ai|cdr|psd|svg|dwg|dxf)$/i.test(name)) return false;
      return true;
    }
    function recoverHomeRetainedFilesFromConversation() {
      const currentBubble = document.getElementById("homeUserMessage");
      const currentAnswer = document.getElementById("aiAnswer");
      const historyTurns = Array.from(document.querySelectorAll(
        "#homeChatHistory .chat-user-bubble, #homeChatHistory .chat-assistant-content"
      )).reverse();
      const scopes = [currentBubble, currentAnswer, ...historyTurns].filter(Boolean);
      for (const scope of scopes) {
        const buttons = Array.from(scope.querySelectorAll(
          ".chat-user-attachment[data-user-file-id], .generated-image-open[data-user-file-id]"
        ));
        const records = buttons.map(button => ({
          id: Number(button.dataset.userFileId || 0),
          name: String(button.dataset.imageName || "").trim()
        })).filter(record => Number.isSafeInteger(record.id) && record.id > 0);
        if (!records.length) continue;
        window.__lastRecognizedHomeFileIds = records.map(record => record.id);
        window.__homeRetainedFileNames = records.map((record, index) => record.name || `历史附件 ${index + 1}`);
        const session = activeCustomerSession?.();
        if (session) {
          session.retainedFileIds = [...window.__lastRecognizedHomeFileIds];
          session.retainedFileNames = [...window.__homeRetainedFileNames];
          session.updatedAt = new Date().toISOString();
        }
        renderHomeRetainedFileContext();
        saveState();
        return [...window.__lastRecognizedHomeFileIds];
      }
      return [];
    }
    function renderHomeRetainedFileContext() {
      const box = document.getElementById("homeRetainedFile");
      const text = document.getElementById("homeRetainedFileText");
      if (!box || !text) return;
      const retainedIds = homeRetainedFileIds();
      const retainedNames = homeRetainedFileNames();
      const hasNewFiles = homeFileNames().length > 0;
      const textOnlyMode = normalizeHomeImageMode(window.__selectedHomeCapability) === "text_image";
      box.classList.toggle("is-hidden", textOnlyMode || !retainedIds.length || hasNewFiles);
      if (!retainedIds.length) return;
      const label = retainedNames.length
        ? retainedNames.join("、")
        : `${retainedIds.length} 个已识别附件`;
      text.textContent = `可继续参考：${label}（只有提到这份附件时才会带上）`;
    }
    function forgetHomeConversationFiles(silent = false) {
      window.__lastRecognizedHomeFileIds = [];
      window.__homeRetainedFileNames = [];
      renderHomeRetainedFileContext();
      captureActiveCustomerSession?.();
      if (!silent) notify("已取消引用上一份附件，下一条消息不会再带上它。");
    }
    function syncHomeComposerClearance() {
      const home = document.querySelector(".gpt-home");
      if (document.getElementById("homeWorkbench")?.classList.contains("image-studio-only")) {
        home?.style.removeProperty("--home-composer-height");
        return;
      }
      const shell = document.querySelector(".gpt-home.has-conversation .gpt-shell");
      if (!home || !shell) return;
      const height = Math.ceil(shell.getBoundingClientRect().height);
      if (height > 0) home.style.setProperty("--home-composer-height", `${height}px`);
    }
    function syncHomeConversationLayout() {
      const home = document.querySelector(".gpt-home");
      const homePageActive = document.querySelector('[data-page="图片制作"]')?.classList.contains("active") === true;
      const imageStudio = document.getElementById("homeWorkbench")?.classList.contains("image-studio-only") === true;
      const active = homePageActive && !imageStudio && home?.classList.contains("has-conversation") === true;
      document.body.classList.toggle("home-conversation-active", active);
      if (active) syncHomeComposerClearance();
    }
    function scrollHomeReplyAboveComposer(target, behavior = "smooth") {
      if (!target) return;
      window.requestAnimationFrame(() => {
        if (document.getElementById("homeWorkbench")?.classList.contains("image-studio-only")) {
          scrollImageStudioToTop(behavior);
          return;
        }
        syncHomeConversationLayout();
        syncHomeComposerClearance();
        const scroller = document.querySelector(".gpt-home.has-conversation .gpt-result");
        if (scroller && !homeConversationMayAutoFollow(scroller)) return;
        const shell = document.querySelector(".gpt-home.has-conversation .gpt-shell");
        const header = document.querySelector("header, .topbar, .app-header");
        const scrollerRect = scroller?.getBoundingClientRect();
        const topLimit = scrollerRect
          ? Math.ceil(scrollerRect.top) + 18
          : Math.max(92, Math.ceil(header?.getBoundingClientRect().bottom || 0) + 18);
        const bottomLimit = Math.min(
          Math.floor(shell?.getBoundingClientRect().top || window.innerHeight) - 24,
          Math.floor(scrollerRect?.bottom || window.innerHeight) - 18
        );
        const rect = target.getBoundingClientRect();
        const availableHeight = Math.max(120, bottomLimit - topLimit);
        let delta = 0;
        if (rect.height > availableHeight) delta = rect.top - topLimit;
        else if (rect.bottom > bottomLimit) delta = rect.bottom - bottomLimit;
        else if (rect.top < topLimit) delta = rect.top - topLimit;
        if (Math.abs(delta) > 2) {
          if (scroller) scroller.scrollBy({ top: delta, behavior });
          else window.scrollBy({ top: delta, behavior });
        }
      });
    }
    function scrollImageStudioToTop(behavior = "auto") {
      if (!document.getElementById("homeWorkbench")?.classList.contains("image-studio-only")) return;
      const options = { top: 0, left: 0, behavior };
      window.scrollTo(options);
      const candidates = [
        document.scrollingElement,
        document.documentElement,
        document.body,
        document.querySelector("main"),
        document.querySelector(".app-main"),
        document.querySelector(".main-content"),
        document.querySelector(".content"),
        document.querySelector(".page.active")
      ];
      [...new Set(candidates.filter(Boolean))].forEach(scroller => {
        if (typeof scroller.scrollTo === "function") scroller.scrollTo(options);
        else scroller.scrollTop = 0;
      });
    }
    function initHomeComposerClearance() {
      const shell = document.querySelector(".gpt-shell");
      const home = document.querySelector(".gpt-home");
      if (!shell) return;
      if (!window.__homeComposerResizeObserver && "ResizeObserver" in window) {
        window.__homeComposerResizeObserver = new ResizeObserver(syncHomeComposerClearance);
        window.__homeComposerResizeObserver.observe(shell);
      }
      if (!window.__homeComposerResizeReady) {
        window.__homeComposerResizeReady = true;
        window.addEventListener("resize", syncHomeConversationLayout, { passive: true });
      }
      if (home && !window.__homeConversationClassObserver && "MutationObserver" in window) {
        window.__homeConversationClassObserver = new MutationObserver(syncHomeConversationLayout);
        window.__homeConversationClassObserver.observe(home, { attributes: true, attributeFilter: ["class"] });
      }
      syncHomeConversationLayout();
    }
    const HOME_IMAGE_MODEL_CATALOG = {
      jianglai_tiangong: {
        name: "将莱·天工 Pro",
        resolutions: { "2K": { label: "2K 高清", points: 30 } }
      },
      jianglai_zhenjing: {
        name: "将莱·臻境",
        resolutions: {
          "1K": { label: "1K 普通", points: 20 },
          "2K": { label: "2K 专业高清", points: 30 },
          "4K": { label: "4K 超清", points: 35 }
        }
      },
      jianglai_liuguang: {
        name: "将莱·流光",
        resolutions: { "2K": { label: "2K 普通", points: 20 } }
      }
    };
    function homeImageGenerationRequestOptions() {
      const modelAlias = val("homeImageModel") || "jianglai_tiangong";
      const model = HOME_IMAGE_MODEL_CATALOG[modelAlias] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
      const requestedResolution = val("homeImageQuality") || "2K";
      const resolution = model.resolutions[requestedResolution] ? requestedResolution : Object.keys(model.resolutions)[0];
      return { model_alias: modelAlias, resolution };
    }
    function selectedHomeImagePointCost() {
      const options = homeImageGenerationRequestOptions();
      const model = HOME_IMAGE_MODEL_CATALOG[options.model_alias] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
      return Number(model.resolutions[options.resolution]?.points || 30);
    }
    function syncHomeImageStatus(config, model) {
      const statusHint = String(config?.status || "").replace(/^当前：[^·]+·\s*/, "");
      setTextIfExists("homeImageModeStatusLabel", config?.label || "图片生成");
      setTextIfExists("homeImageModeStatusModel", model?.name || "将莱图片引擎");
      setTextIfExists("homeImageModeStatusHint", statusHint);
    }
    function syncHomeImagePointCost() {
      const mode = normalizeHomeImageMode(window.__selectedHomeCapability);
      const options = homeImageGenerationRequestOptions();
      const model = HOME_IMAGE_MODEL_CATALOG[options.model_alias] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
      const resolution = model.resolutions[options.resolution];
      const count = mode === "detail_image" ? Math.max(1, Number(val("homeImageDetailCount") || 8)) : 1;
      const total = resolution.points * count;
      const cost = document.getElementById("homeImagePointCost");
      if (cost) cost.innerHTML = mode === "detail_image"
        ? `整套预计 ${count} 张 · <strong>${total} AI点</strong>（${resolution.points}点/张）`
        : `本次生成 1 张 · <strong>${resolution.points} AI点</strong>`;
      const config = HOME_IMAGE_MODE_CONFIG[mode] || HOME_IMAGE_MODE_CONFIG.main_image;
      syncHomeImageStatus(config, model);
    }
    function syncHomeImageModelOptions() {
      const modelAlias = val("homeImageModel") || "jianglai_tiangong";
      const model = HOME_IMAGE_MODEL_CATALOG[modelAlias] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
      const quality = document.getElementById("homeImageQuality");
      if (!quality) return;
      const previous = quality.value;
      quality.innerHTML = Object.entries(model.resolutions)
        .map(([value, item]) => `<option value="${value}">${item.label}</option>`).join("");
      quality.value = model.resolutions[previous] ? previous : Object.keys(model.resolutions)[0];
      syncHomeImagePointCost();
    }
    const HOME_IMAGE_MODE_CONFIG = {
      main_image: {
        label: "主图生成",
        action: "生成主图",
        requirement: "主图要求",
        placeholder: "上传产品图，再说明背景、角度、画面比例和平台要求…",
        status: "当前：主图生成 · 上传产品图，生成干净醒目的电商主图",
        upload: "点击或拖入产品图",
        uploadTitle: "产品图",
        uploadSubtitle: "上传清晰的产品图片或设计稿",
        uploadGuide: "最多6张，建议只上传必要视角或SKU图",
        resultHint: "上传产品图并填写要求后，点击生成主图"
      },
      detail_image: {
        label: "详情图生成",
        action: "规划详情图",
        requirement: "详情图要求",
        placeholder: "写产品名称、材质、尺寸、核心卖点、发布平台和需要几张；可上传产品图…",
        status: "当前：详情图生成 · 先确认张数和结构，再生成多张详情图",
        upload: "点击或拖入产品图",
        uploadTitle: "产品图",
        uploadSubtitle: "上传必要的产品视角或SKU图",
        uploadGuide: "最多6张，图片不是越多越好",
        resultHint: "选择张数并填写卖点后，先规划整套详情图"
      },
      ad_image: {
        label: "广告图生成",
        action: "生成广告图",
        requirement: "广告图要求",
        placeholder: "上传产品图，再说明活动主题、宣传重点、画面比例和想要的广告风格…",
        status: "当前：广告图生成 · 保留产品本身，制作推广海报或活动画面",
        upload: "点击或拖入产品素材",
        uploadTitle: "产品与品牌素材",
        uploadSubtitle: "上传产品图、Logo或版式参考",
        uploadGuide: "最多6张，优先上传产品图与品牌素材",
        resultHint: "上传素材并填写活动要求后，点击生成广告图"
      },
      text_image: {
        label: "文字生图",
        action: "生成图片",
        requirement: "画面描述",
        placeholder: "描述想生成的画面，例如：奶油色背景上的亚克力冰箱贴，电商摄影风格…",
        status: "当前：文字生图 · 只使用本次输入的文字描述",
        upload: "文字生图无需上传图片",
        uploadTitle: "无需产品图",
        uploadSubtitle: "只使用本次填写的画面描述",
        uploadGuide: "直接填写下方的主体、背景与风格",
        resultHint: "写清主体、背景和风格后，点击生成图片"
      },
      mockup: {
        label: "效果图生成",
        action: "生成效果图",
        requirement: "效果图要求",
        placeholder: "上传产品图或设计稿，再说明材质、摆放位置、背景和想要的效果…",
        status: "当前：效果图生成 · 以你上传的产品图或设计稿为准",
        upload: "点击或拖入设计稿",
        uploadTitle: "设计稿与参考图",
        uploadSubtitle: "上传产品图、设计稿或场景参考",
        uploadGuide: "最多6张，优先上传正面设计稿",
        resultHint: "上传设计稿并说明材质与场景后，点击生成效果图"
      }
    };
    const HOME_DEFAULT_PLACEHOLDER = HOME_IMAGE_MODE_CONFIG.main_image.placeholder;
    const HOME_DEFAULT_PROMPT_HINT = "制图工作台集中处理商品图片。选择类型、上传素材、填写要求，完成后直接下载。";
    function normalizeHomeImageMode(value = "") {
      const legacy = { visual: "mockup", detail: "detail_image", detail_package: "detail_image" };
      const mode = legacy[String(value || "").trim()] || String(value || "").trim();
      return HOME_IMAGE_MODE_CONFIG[mode] ? mode : "main_image";
    }
    function syncHomeCapabilityUi(type = "text_image") {
      const mode = normalizeHomeImageMode(type);
      const config = HOME_IMAGE_MODE_CONFIG[mode];
      const input = document.getElementById("aiQuestion");
      const hint = document.getElementById("homeIntentHint");
      const send = document.getElementById("homeCreationSubmitButton");
      const upload = document.querySelector('label[for="homeFiles"]');
      const uploadCard = document.getElementById("homeUploadCard");
      const uploadInput = document.getElementById("homeFiles");
      const fileName = document.getElementById("homeFileName");
      const promptAssist = document.getElementById("homePromptAssistButton");
      const mockupPurpose = document.getElementById("homeMockupPurposeField");
      document.querySelectorAll("[data-home-image-mode]").forEach(button => {
        const active = button.dataset.homeImageMode === mode;
        button.classList.toggle("active", active);
        button.setAttribute("aria-selected", active ? "true" : "false");
      });
      window.__selectedHomeCapability = mode;
      if (input) input.placeholder = config.placeholder;
      const selectedModel = HOME_IMAGE_MODEL_CATALOG[val("homeImageModel")] || HOME_IMAGE_MODEL_CATALOG.jianglai_tiangong;
      syncHomeImageStatus(config, selectedModel);
      setTextIfExists("homeRequirementLabel", config.requirement);
      setTextIfExists("homeUploadTitle", config.uploadTitle);
      setTextIfExists("homeUploadSubtitle", config.uploadSubtitle);
      setTextIfExists("homeUploadGuide", config.uploadGuide);
      setTextIfExists("homeResultModeTitle", `${config.label}结果`);
      setTextIfExists("homeResultModeHint", config.resultHint);
      document.getElementById("homeImageDetailCountField")?.classList.toggle("is-hidden", mode !== "detail_image");
      promptAssist?.classList.toggle("is-hidden", mode !== "mockup");
      mockupPurpose?.classList.toggle("is-hidden", mode !== "mockup");
      if (hint) {
        hint.textContent = "";
        hint.classList.add("is-hidden");
      }
      if (send) {
        const action = config.action;
        send.textContent = action;
        send.title = action;
        send.setAttribute("aria-label", action);
      }
      if (upload) {
        const disabled = mode === "text_image";
        upload.classList.toggle("is-disabled", disabled);
        uploadCard?.classList.toggle("is-disabled", disabled);
        if (uploadInput) uploadInput.disabled = disabled;
        upload.setAttribute("aria-disabled", disabled ? "true" : "false");
        upload.setAttribute("aria-label", config.upload);
        upload.title = config.upload;
      }
      if (fileName && !homeFileNames().length && !(activeCustomerSession()?.pendingFileIds || []).length) fileName.textContent = config.upload;
      setTextIfExists("homePromptHint", HOME_DEFAULT_PROMPT_HINT);
      syncHomeImagePointCost();
    }

    function homeImageStudioOptionSummary(mode = "main_image") {
      const platform = val("homeTargetPlatform") || "智能匹配";
      const ratio = val("homeImageRatio") || "1:1 方图";
      const quality = document.getElementById("homeImageQuality")?.selectedOptions?.[0]?.textContent || "2K 高清";
      const detailCount = mode === "detail_image" ? `；生成数量：${val("homeImageDetailCount") || "8"}张` : "";
      const mockupPurpose = mode === "mockup" ? `效果图用途：${val("homeMockupPurpose") || "客户确认图"}；` : "";
      return `${mockupPurpose}目标平台：${platform}；尺寸比例：${ratio}；清晰度：${quality}${detailCount}`;
    }
    function resetHomeCapabilitySelection() {
      window.__selectedHomeCapability = "main_image";
      syncHomeCapabilityUi("main_image");
    }
    function selectHomeImageMode(mode = "text_image") {
      const normalized = normalizeHomeImageMode(mode);
      const previousMode = normalizeHomeImageMode(window.__selectedHomeCapability || "main_image");
      if (previousMode !== normalized) {
        clearHomeGeneratedResult({ clearInputs: true, silent: true, reason: "mode_change" });
      }
      window.__selectedHomeCapability = normalized;
      syncHomeCapabilityUi(normalized);
      syncActiveCustomerSessionDraft();
      document.getElementById("aiQuestion")?.focus();
    }
    async function enhanceHomeImageRequirement() {
      const input = document.getElementById("aiQuestion");
      const button = document.getElementById("homePromptAssistButton");
      const customer = String(document.getElementById("homeCustomerName")?.value || currentCustomerName() || "新客户").trim();
      const prompt = imagePromptWithoutCustomerIdentity(input?.value || "", customer);
      if (!prompt) {
        notify("请先写一句产品、材质或场景要求，AI再帮你补完整。");
        input?.focus();
        return false;
      }
      if (input && input.value.trim() !== prompt) {
        input.value = prompt;
        autoGrowHomeQuestion(input);
      }
      const originalLabel = button?.textContent || "AI帮写要求";
      if (button) {
        button.disabled = true;
        button.textContent = "正在整理…";
      }
      try {
        await ensureFormalLogin();
        const data = await api("/api/image/prompt-enhance", {
          method: "POST",
          body: JSON.stringify({
            prompt,
            purpose: val("homeMockupPurpose") || "客户确认图",
            platform: val("homeTargetPlatform") || "智能匹配",
            ratio: val("homeImageRatio") || "1:1 方图",
            customer,
            has_references: Boolean(homeFileNames().length || homeRetainedFileIds().length)
          })
        });
        if (!String(data?.prompt || "").trim()) throw new Error("没有返回整理结果");
        input.value = String(data.prompt).trim();
        autoGrowHomeQuestion(input);
        syncActiveCustomerSessionDraft();
        const hint = document.getElementById("homeIntentHint");
        if (hint) {
          hint.textContent = data.used_ai
            ? "已按产品结构、材质、工艺、场景和禁止项补全要求；生成前仍请核对。"
            : "已按工厂效果图规则补全要求；生成前仍请核对。";
          hint.classList.remove("is-hidden");
        }
        notify("效果图要求已整理，请核对后再点击生成。");
        return true;
      } catch (error) {
        notify(`暂时没有整理成功：${String(error?.message || "请稍后重试")}`);
        return false;
      } finally {
        if (button) {
          button.disabled = false;
          button.textContent = originalLabel;
        }
      }
    }
    function normalizeHomeAnalysisMode(value = "") {
      return String(value || "").trim().toLowerCase() === "deep" ? "deep" : "standard";
    }
    function applyHomeAnalysisMode(value = "standard", options = {}) {
      let mode = normalizeHomeAnalysisMode(value);
      // The integration status is loaded asynchronously after customer
      // sessions are restored.  `undefined` means "not checked yet", not
      // "verification failed"; downgrading here would overwrite a customer's
      // persisted Deep mode during every cold start.
      if (mode === "deep" && window.__deepseekReasoningVerified === false) mode = "standard";
      const session = options.session || activeCustomerSession?.();
      window.__homeAnalysisMode = mode;
      if (session) session.analysisMode = mode;
      const button = document.getElementById("homeDeepAnalysisButton");
      if (button) {
        button.classList.toggle("active", mode === "deep");
        button.setAttribute("aria-pressed", mode === "deep" ? "true" : "false");
      }
      if (options.persist !== false && session) saveState();
      return mode;
    }
    function resetHomeDeepAnalysisSelection(message = "", notifyUser = false) {
      const wasDeep = normalizeHomeAnalysisMode(window.__homeAnalysisMode) === "deep";
      applyHomeAnalysisMode("standard");
      if (message && wasDeep) {
        const hint = document.getElementById("homeIntentHint");
        if (hint) {
          hint.textContent = message;
          hint.classList.remove("is-hidden");
        }
        if (notifyUser) notify(message);
      }
      return wasDeep;
    }
    function startDeepAnalysis() {
      const input = document.getElementById("aiQuestion");
      if (!input) return;
      if (!window.__deepseekReasoningVerified) {
        notify("DeepSeek Pro 尚未验证。请先到“更多 → 系统状态”接入 DeepSeek 官方 Key。");
        return;
      }
      const selectedImages = Array.from(document.getElementById("homeFiles")?.files || [])
        .some(file => String(file?.type || "").toLowerCase().startsWith("image/"));
      const pendingImages = (Array.isArray(activeCustomerSession()?.pendingFileRecords) ? activeCustomerSession().pendingFileRecords : [])
        .some(file => String(file?.mimeType || "").toLowerCase().startsWith("image/"));
      const retainedIds = homeRetainedFileIds();
      const referencesPreviousImage = Boolean(
        retainedHomeUserAttachmentItems(retainedIds).some(item => item.imageLike)
        && window.__shouldReuseHomeRetainedFiles?.(input.value, retainedIds)
      );
      if (selectedImages || pendingImages || referencesPreviousImage) {
        notify("DeepSeek 只处理文字。请先发送图片完成识别，再对识别结果使用深度分析。");
        return;
      }
      applyHomeAnalysisMode("deep");
      const hint = document.getElementById("homeIntentHint");
      if (hint) {
        hint.textContent = "已选择 DeepSeek Pro 深度分析：适合报价复核、利润风险、复杂排产和合同风险；图片仍由视觉模型识别。";
        hint.classList.remove("is-hidden");
      }
      syncActiveCustomerSessionDraft();
      autoGrowHomeQuestion(input);
      input.focus();
    }
    function executeHomeCapability(type) {
      const compatibilityMode = type === "visual" ? "mockup" : type === "detail" ? "detail_image" : "text_image";
      return selectHomeImageMode(compatibilityMode);
    }
    function showHomeCreationRequirement(title, detail, actions = "") {
      if (typeof window.showHomeInlineVisualMessage === "function") return window.showHomeInlineVisualMessage(title, detail, actions);
      showHomeResult();
      setTextIfExists("aiAnswer", `${title} ${detail}`);
    }
    function submitHomeComposer() {
      const mode = normalizeHomeImageMode(window.__selectedHomeCapability);
      scrollImageStudioToTop();
      const prompt = val("aiQuestion").trim();
      const optionSummary = homeImageStudioOptionSummary(mode);
      const studioPrompt = prompt ? `${prompt}\n${optionSummary}` : optionSummary;
      const session = activeCustomerSession();
      const pendingCount = homeFileNames().length + Number(session?.pendingFileIds?.length || 0);
      const hasCustomerContent = !!val("aiQuestion") || homeFileNames().length > 0;
      if (hasCustomerContent) ensurePendingCustomerIdentity();
      if (mode === "text_image") {
        if (pendingCount) {
          showHomeCreationRequirement(
            "文字生图只读取文字，不会偷偷使用附件。",
            "请移除输入框里的附件，或切换到“效果图生成”。",
            `<button class="btn dark" type="button" onclick="selectHomeImageMode('mockup')">切换到效果图生成</button>`
          );
          return;
        }
        if (prompt.replace(/(请|帮我|给我|生成|制作|做一张|图片|一下|一张)/g, "").trim().length < 6) {
          showHomeCreationRequirement("画面描述还不够。", "至少写清主体、背景和风格，例如：白色桌面上的金属钥匙扣，柔和自然光，电商摄影风格。");
          document.getElementById("aiQuestion")?.focus();
          return;
        }
        return window.generateHomeVisualInline?.(studioPrompt, { generationMode: "text_image", referenceFileIds: [], retainedFileIds: [] });
      }
      if (mode === "detail_image") {
        if (session?.detailPackageJob && prompt && typeof window.handleHomeDetailPackageFollowUp === "function" && window.handleHomeDetailPackageFollowUp(prompt, session)) return;
        if (typeof window.prepareHomeDetailPackageFromComposer === "function") {
          return window.prepareHomeDetailPackageFromComposer(studioPrompt, window.__latestAiAnalysis || null);
        }
        return homeGoDetailPackage();
      }
      const hasReference = pendingCount > 0 || homeRetainedFileIds().length > 0;
      if (!hasReference) {
        const modeName = HOME_IMAGE_MODE_CONFIG[mode]?.label || "图片生成";
        showHomeCreationRequirement(
          `${modeName}需要一张参考图。`,
          mode === "ad_image"
            ? "请上传产品图或品牌素材，再写清活动主题、宣传重点和广告风格。"
            : mode === "main_image"
              ? "请上传产品图或设计稿，再写清背景、角度、比例和发布平台。"
              : "请上传产品图、设计稿或图案素材，再写清要放到什么材质和场景里。",
          `<button class="btn dark" type="button" onclick="document.getElementById('homeFiles')?.click()">上传参考图</button>`
        );
        return;
      }
      return window.generateHomeVisualInline?.(studioPrompt, { generationMode: mode });
    }
    function useHomeChatSuggestion(text = "") {
      const input = document.getElementById("aiQuestion");
      if (!input) return;
      selectHomeImageMode("text_image");
      input.value = text;
      autoGrowHomeQuestion(input);
      syncActiveCustomerSessionDraft();
      input.focus();
    }
    function chooseHomeCapability(type) {
      const compatibilityMode = type === "visual" ? "mockup" : type === "detail" ? "detail_package" : "text_image";
      return selectHomeImageMode(compatibilityMode);
    }
    function prepareHomeUploadPurpose(type) {
      const input = document.getElementById("aiQuestion");
      if (!input || !homeFileNames().length) return;
      const prompts = {
        analyze: "请先判断图片是什么类型。只有确认是客户产品、聊天或订单资料时才整理需求；如果是人物、风景或无关图片，请如实说明，不要套用报价模板。",
        visual: "请根据上传的产品参考图制作效果图，保持产品结构、比例和Logo位置，不要擅自改变产品。",
        detail: "请根据上传的产品资料整理宝贝详情页，先识别产品和卖点，不确定的内容请标为待确认。",
        task: "请把上传的客户资料整理成跟单事项，提取当前状态、下一步、负责人和截止时间；看不清的内容不要猜。"
      };
      input.value = prompts[type] || prompts.analyze;
      autoGrowHomeQuestion(input);
      if (type === "visual") return startHomeInquiry();
      if (type === "detail") return homeGoDetail();
      if (type === "task") return startHomeInquiry();
      window.__selectedHomeCapability = "analyze";
      syncHomeCapabilityUi("");
      return startHomeInquiry();
    }
    function clearHomeDemoTimers() {
      (window.__homeDemoTimers || []).forEach(timer => window.clearTimeout(timer));
      window.__homeDemoTimers = [];
    }
    const HOME_DEMO_SEEN_KEY = "jianglai_home_demo_seen_v2";
    function syncHomeDemoEntry() {
      const teaser = document.getElementById("homeDemoTeaser");
      const helpButton = document.getElementById("homeHelpButton");
      const seen = localStorage.getItem(HOME_DEMO_SEEN_KEY) === "1";
      document.getElementById("homeWorkbench")?.classList.toggle("is-experienced", seen);
      teaser?.classList.toggle("is-hidden", seen);
      if (helpButton) {
        helpButton.textContent = seen ? "？ 使用帮助" : "第一次使用？看30秒示例";
        helpButton.classList.toggle("is-first-use", !seen);
      }
    }
    function rememberHomeDemoSeen() {
      localStorage.setItem(HOME_DEMO_SEEN_KEY, "1");
      syncHomeDemoEntry();
    }
    function dismissHomeDemo() {
      rememberHomeDemoSeen();
      closeHomeDemo();
      notify("新手示例已收起，需要时可点右上角“使用帮助”。");
    }
    function startHomeDemo() {
      clearHomeDemoTimers();
      rememberHomeDemoSeen();
      const panel = document.getElementById("homeDemoPanel");
      const result = document.getElementById("homeDemoResult");
      const steps = Array.from(document.querySelectorAll(".home-demo-step"));
      if (!panel || !steps.length) return;
      panel.classList.remove("is-hidden");
      panel.setAttribute("aria-hidden", "false");
      result?.classList.add("is-hidden");
      steps.forEach(step => step.classList.remove("is-active", "is-done"));
      const activate = index => {
        steps.forEach((step, stepIndex) => {
          step.classList.toggle("is-done", stepIndex < index);
          step.classList.toggle("is-active", stepIndex === index);
        });
      };
      activate(0);
      [1, 2, 3].forEach((index, offset) => {
        window.__homeDemoTimers.push(window.setTimeout(() => activate(index), 650 * (offset + 1)));
      });
      window.__homeDemoTimers.push(window.setTimeout(() => {
        steps.forEach(step => step.classList.remove("is-active"));
        steps.forEach(step => step.classList.add("is-done"));
        result?.classList.remove("is-hidden");
      }, 2700));
      panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
    }
    function closeHomeDemo() {
      clearHomeDemoTimers();
      const panel = document.getElementById("homeDemoPanel");
      panel?.classList.add("is-hidden");
      panel?.setAttribute("aria-hidden", "true");
    }
    function useMyCustomerMessage() {
      restoreDealDemoState();
      closeHomeDemo();
      const input = document.getElementById("aiQuestion");
      if (!input) return;
      input.value = "";
      resetHomeCapabilitySelection();
      input.placeholder = HOME_IMAGE_MODE_CONFIG.text_image.placeholder;
      setTextIfExists("homePromptHint", HOME_DEFAULT_PROMPT_HINT);
      autoGrowHomeQuestion(input);
      input.focus();
      document.querySelector(".gpt-composer")?.scrollIntoView({ behavior: "smooth", block: "center" });
    }
    function hideHomeNextActions() {
      document.getElementById("aiNextActions")?.classList.add("is-hidden");
    }
    function initHomeExperience() {
      syncHomeDemoEntry();
      initHomeComposerClearance();
      syncHomeCapabilityUi(window.__selectedHomeCapability || "main_image");
      const input = document.getElementById("aiQuestion");
      if (input && !input.dataset.keyboardReady) {
        input.dataset.keyboardReady = "1";
        input.addEventListener("compositionstart", () => {
          window.__homeImeComposing = true;
          window.__homeImeCommitGuardUntil = 0;
        });
        input.addEventListener("compositionend", () => {
          window.__homeImeComposing = false;
          window.__homeImeCommitGuardUntil = performance.now() + 180;
        });
        input.addEventListener("beforeinput", event => {
          if (event.isComposing || String(event.inputType || "").includes("Composition")) {
            window.__homeImeComposing = true;
          }
        });
        input.addEventListener("blur", () => {
          window.__homeImeComposing = false;
          window.__homeImeCommitGuardUntil = 0;
        });
        input.addEventListener("keydown", event => {
          if (event.key !== "Enter" || event.shiftKey) return;
          if (normalizeHomeImageMode(window.__selectedHomeCapability) === "mockup") return;
          const imeIsChoosing = event.isComposing
            || window.__homeImeComposing === true
            || event.keyCode === 229
            || event.which === 229;
          if (imeIsChoosing) return;
          if (performance.now() < Number(window.__homeImeCommitGuardUntil || 0)) {
            event.preventDefault();
            return;
          }
          if (event.repeat) {
            event.preventDefault();
            return;
          }
          event.preventDefault();
          submitHomeComposer();
        });
      }
      const creationButton = document.getElementById("homeCreationSubmitButton");
      if (creationButton && !creationButton.dataset.keyboardGuardReady) {
        creationButton.dataset.keyboardGuardReady = "1";
        creationButton.addEventListener("keydown", event => {
          if (event.key === "Enter" && normalizeHomeImageMode(window.__selectedHomeCapability) === "mockup") {
            event.preventDefault();
            event.stopPropagation();
          }
        });
      }
    }
    function homeFileNames() {
      const files = Array.from(document.getElementById("homeFiles")?.files || []);
      return files.map(file => file.name);
    }
    function homeUserAttachmentMarkup(items = [], label = "附件") {
      const normalized = (Array.isArray(items) ? items : []).filter(item => item && (item.name || item.id || item.src));
      if (!normalized.length) return "";
      return `<div class="chat-user-attachments" data-user-attachments>${normalized.map((item, index) => {
        const name = String(item.name || `附件 ${index + 1}`);
        const fileId = Number(item.id || 0);
        const fileKey = String(item.key || "");
        const mimeType = String(item.mimeType || "");
        const imageLike = item.imageLike !== false;
        const src = String(item.src || "");
        return `<button class="chat-user-attachment${imageLike ? "" : " is-not-image"}" type="button" data-user-file-id="${fileId > 0 ? fileId : ""}" data-user-file-key="${escapeHtml(fileKey)}" data-attachment-mime="${escapeHtml(mimeType)}" data-attachment-index="${index}" data-image-name="${escapeHtml(name)}" data-image-url="${escapeHtml(src)}"${imageLike ? "" : " disabled"} aria-label="${imageLike ? "放大查看" : "附件"}${escapeHtml(name)}">${src ? `<img src="${escapeHtml(src)}" alt="${escapeHtml(name)}">` : `<span class="chat-user-attachment-placeholder">${imageLike ? "图片加载中" : "文件附件"}</span>`}<span class="chat-user-attachment-number">${imageLike ? "图片" : "附件"} ${index + 1}</span><span class="chat-user-attachment-name" title="${escapeHtml(name)}">${escapeHtml(name)}</span></button>`;
      }).join("")}</div><span class="chat-user-files">${escapeHtml(label)}：${escapeHtml(normalized.map(item => item.name || "附件").join("、"))}</span>`;
    }
    function currentHomeUserAttachmentItems() {
      const files = Array.from(document.getElementById("homeFiles")?.files || []);
      return files.map((file, index) => ({
        id: 0,
        name: file.name,
        key: [file.name || "", Number(file.size || 0), Number(file.lastModified || 0)].join("|"),
        mimeType: normalizedUploadMime(file),
        src: homeFilePreviewUrls[index] || "",
        imageLike: file.type.startsWith("image/")
      }));
    }
    function retainedHomeUserAttachmentItems(selectedIds = homeRetainedFileIds()) {
      const selected = new Set((Array.isArray(selectedIds) ? selectedIds : []).map(Number));
      const ids = homeRetainedFileIds();
      const names = homeRetainedFileNames();
      return ids
        .map((id, index) => {
          const name = names[index] || `历史附件 ${index + 1}`;
          return { id, name, src: "", imageLike: attachmentNameLooksLikeImage(name) };
        })
        .filter(item => selected.has(Number(item.id)));
    }
    function fileKind(file) {
      const name = file.name.toLowerCase();
      if (file.type.startsWith("image/")) return "图片/聊天截图";
      if (file.type.startsWith("video/")) return "视频";
      if (name.endsWith(".pdf")) return "PDF";
      if (/\.(ai|cdr|svg|psd)$/.test(name)) return "设计源文件";
      if (/\.(dwg|dxf)$/.test(name)) return "图纸文件";
      if (/\.(xls|xlsx)$/.test(name)) return "表格文件";
      return "客户文件";
    }
    function normalizedUploadMime(file) {
      const explicit = String(file?.type || "").trim().toLowerCase();
      if (explicit && explicit !== "application/octet-stream" && explicit !== "application/zip") return explicit;
      const name = String(file?.name || "").toLowerCase();
      if (name.endsWith(".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
      if (name.endsWith(".csv")) return "text/csv";
      if (name.endsWith(".pdf")) return "application/pdf";
      if (name.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
      return explicit || "application/octet-stream";
    }
    function fileSizeText(size) {
      if (!size) return "大小未知";
      if (size < 1024 * 1024) return `${Math.max(1, Math.round(size / 1024))}KB`;
      return `${(size / 1024 / 1024).toFixed(1)}MB`;
    }
    function archiveHomeTurn() {
      const history = document.getElementById("homeChatHistory");
      const row = document.getElementById("homeUserRow");
      const bubble = document.getElementById("homeUserMessage");
      const answer = document.getElementById("aiAnswer");
      if (!history || !row || row.classList.contains("is-hidden") || !bubble?.textContent.trim() || !answer) return;
      if (answer.querySelector(".ai-loading")) return;
      history.insertAdjacentHTML("beforeend", `
        <div class="chat-user-row"><div class="chat-user-bubble">${bubble.innerHTML}</div></div>
        <div class="chat-assistant-row">
          <div class="chat-assistant-avatar" aria-hidden="true"><img src="assets/报价跟单助手-logo.png" alt=""></div>
          <div class="chat-assistant-content">
            <div class="chat-assistant-name">将莱数字员工</div>
            <div class="output">${answer.innerHTML}</div>
          </div>
        </div>
      `);
    }
    function beginHomeConversation() {
      const home = document.querySelector(".gpt-home");
      const input = document.getElementById("aiQuestion");
      const row = document.getElementById("homeUserRow");
      const bubble = document.getElementById("homeUserMessage");
      const typedText = (input?.value || "").trim();
      const files = homeFileNames();
      if (typedText && !window.__homePromptPrepared) window.__activeHomePrompt = typedText;
      if (typedText && !window.__homeVisiblePrompt) window.__homeVisiblePrompt = typedText;
      if (!window.__activeHomePrompt && files.length) {
        window.__activeHomePrompt = "请先客观描述附件是什么，再根据我的内容判断应该进行普通问答、看图、客户需求整理、报价、做图还是跟单。不确定的地方直接说明，不要猜测，也不要强行套用报价模板。";
        window.__homeVisiblePrompt = "请看一下这些附件";
      }
      const source = String(window.__homeVisiblePrompt || window.__activeHomePrompt || "").trim();
      if (!source && !files.length) return false;
      const availableRetainedIds = homeRetainedFileIds();
      const retainedIds = files.length || !window.__shouldReuseHomeRetainedFiles?.(source, availableRetainedIds)
        ? []
        : availableRetainedIds;
      const retainedFiles = retainedIds.length ? homeRetainedFileNames() : [];
      if (bubble) {
        const attachmentItems = files.length ? currentHomeUserAttachmentItems() : retainedHomeUserAttachmentItems(retainedIds);
        const attachmentLabel = files.length ? "附件" : "继续参考";
        bubble.innerHTML = `${escapeHtml(source)}${attachmentItems.length ? homeUserAttachmentMarkup(attachmentItems, attachmentLabel) : retainedFiles.length ? `<span class="chat-user-files">继续参考：${escapeHtml(retainedFiles.join("、"))}</span>` : ""}`;
        window.__homeCurrentTurnFileIds = attachmentItems.map(item => Number(item.id || 0)).filter(Boolean);
        window.__homeCurrentTurnFileNames = attachmentItems.map(item => item.name);
      }
      row?.classList.remove("is-hidden");
      home?.classList.add("has-conversation");
      window.requestAnimationFrame(syncHomeComposerClearance);
      if (typedText && input) {
        input.value = "";
        input.style.height = "auto";
      }
      window.__homePromptPrepared = false;
      return true;
    }
    function homeSourceText(defaultText = "客户上传了图片/视频/图纸文件，需要先识别需求并确认报价资料。") {
      const text = val("aiQuestion") || String(window.__activeHomePrompt || "").trim();
      const files = homeFileNames();
      if (text) return text;
      const visibleMessage = (document.getElementById("homeUserMessage")?.textContent || "")
        .replace(/附件：.*$/s, "")
        .trim();
      if (visibleMessage) return visibleMessage;
      if (files.length) return `${defaultText}\n客户文件：${files.join("、")}`;
      return "";
    }
    function syncHomeToInquiry(defaultText) {
      const source = homeSourceText(defaultText);
      if (source) setIfExists("customerText", source);
      const homeCustomer = (document.getElementById("homeCustomerName")?.value || "").trim();
      if (homeCustomer) setIfExists("inquiryCustomerName", homeCustomer);
      return source;
    }
    let homeFilePreviewUrls = [];
    let homeFilesBeforePicker = [];
    let activeHomeFilePreviewIndex = -1;
    let activeGeneratedImagePreview = null;
    let homeFilePreviewRequestGeneration = 0;
    function homeFileSelectionKey(file) {
      return [file?.name || "", Number(file?.size || 0), Number(file?.lastModified || 0), file?.type || ""].join("|");
    }
    function snapshotHomeFilesBeforePick() {
      homeFilesBeforePicker = Array.from(document.getElementById("homeFiles")?.files || []);
    }
    function appendHomeFileSelection() {
      const input = document.getElementById("homeFiles");
      if (!input) return;
      const pickedFiles = Array.from(input.files || []);
      const mergedFiles = [];
      const seen = new Set();
      [...homeFilesBeforePicker, ...pickedFiles].forEach(file => {
        const key = homeFileSelectionKey(file);
        if (!file || seen.has(key)) return;
        seen.add(key);
        mergedFiles.push(file);
      });
      const acceptedFiles = mergedFiles.slice(0, 6);
      if (mergedFiles.length > 6) notify("最多上传6张图片，已保留前6张。");
      homeFilesBeforePicker = [];
      if (typeof DataTransfer !== "undefined") {
        const transfer = new DataTransfer();
        acceptedFiles.forEach(file => transfer.items.add(file));
        input.files = transfer.files;
      }
      if (acceptedFiles.some(file => String(file?.type || "").toLowerCase().startsWith("image/"))) {
        resetHomeDeepAnalysisSelection(
          "图片会先由视觉模型识别；识别完成后可以再对文字结果使用深度分析。",
          true,
        );
      }
      updateHomeFileName();
    }
    function homeFileCanBeUploaded(file) {
      if (!file) return false;
      const mimeType = String(file.type || "").toLowerCase();
      const extension = String(file.name || "").toLowerCase().match(/\.[a-z0-9]+$/)?.[0] || "";
      return mimeType.startsWith("image/") || [".ai", ".cdr", ".psd", ".svg"].includes(extension);
    }
    function handleHomeFileDrag(event, entering) {
      event?.preventDefault?.();
      event?.stopPropagation?.();
      const card = document.getElementById("homeUploadCard");
      if (!card || card.classList.contains("is-disabled")) return;
      if (!entering && event?.relatedTarget && event.currentTarget?.contains?.(event.relatedTarget)) return;
      card.classList.toggle("is-dragging", Boolean(entering));
      if (event?.dataTransfer) event.dataTransfer.dropEffect = "copy";
    }
    function handleHomeFileDrop(event) {
      event?.preventDefault?.();
      event?.stopPropagation?.();
      const card = document.getElementById("homeUploadCard");
      card?.classList.remove("is-dragging");
      if (!card || card.classList.contains("is-disabled")) return;
      const input = document.getElementById("homeFiles");
      const droppedFiles = Array.from(event?.dataTransfer?.files || []);
      const acceptedFiles = droppedFiles.filter(homeFileCanBeUploaded);
      if (!acceptedFiles.length) {
        if (droppedFiles.length) notify("这里只支持图片、AI、CDR、PSD 或 SVG 文件。");
        return;
      }
      if (acceptedFiles.length !== droppedFiles.length) notify("已忽略不支持的文件，只添加图片或设计稿。");
      if (!input || typeof DataTransfer === "undefined") return notify("当前环境暂不支持拖入，请点击上传区域选择文件。");
      snapshotHomeFilesBeforePick();
      const transfer = new DataTransfer();
      acceptedFiles.forEach(file => transfer.items.add(file));
      input.files = transfer.files;
      appendHomeFileSelection();
    }
    // RESTORED_CHAT_SANITIZER_BEGIN
    // RESTORED_IMAGE_URL_POLICY_BEGIN
    function safeRestoredImageUrl(value = "", allowAsset = false, baseHref = null) {
      const raw = String(value || "").trim();
      if (!raw) return "";
      const base = baseHref || (typeof window !== "undefined" ? window.location.href : "http://127.0.0.1/");
      try {
        const target = new URL(raw, base);
        const baseUrl = new URL(base);
        if (target.origin !== baseUrl.origin) return "";
        const path = target.pathname;
        const decodedPath = decodeURIComponent(path);
        if (/^\/generated\/[^/\\\u0000-\u001f\u007f?#]+$/.test(decodedPath)) return path;
        if (/^\/api\/files\/[1-9]\d*\/content$/.test(decodedPath)) return path;
        if (allowAsset && /^\/assets\/[^/\\\u0000-\u001f\u007f?#]+$/.test(decodedPath)) return path;
      } catch (_) {}
      return "";
    }
    // RESTORED_IMAGE_URL_POLICY_END
    const restoredChatAllowedTags = new Set([
      "div", "span", "p", "br", "strong", "b", "em", "i", "small",
      "ul", "ol", "li", "dl", "dt", "dd", "details", "summary",
      "button", "img", "h1", "h2", "h3", "h4", "pre", "code", "section"
    ]);
    const restoredChatDropTags = new Set([
      "script", "style", "svg", "math", "iframe", "object", "embed", "form",
      "input", "textarea", "select", "option", "template", "noscript", "meta",
      "link", "base", "audio", "video", "source", "canvas"
    ]);
    const restoredChatAllowedClasses = new Set([
      "chat-user-attachments", "chat-user-attachment", "chat-user-attachment-placeholder",
      "chat-user-attachment-number", "chat-user-attachment-name", "chat-user-files",
      "chat-user-row", "chat-user-bubble", "chat-assistant-row", "chat-assistant-avatar",
      "chat-assistant-content", "chat-assistant-name", "output", "ai-answer-body",
      "ai-answer-lead", "ai-answer-section", "ai-confidence-note", "ai-facts", "muted",
      "actions", "btn", "light", "dark", "is-not-image", "home-inline-visual",
      "home-inline-visual-meta", "generated-image-panel", "generated-image-open",
      "generated-image-open-label", "generated-image-download", "visual-version-item",
      "visual-version-actions", "locked", "historical-service-note", "stale-image-setup-note",
      "historical-service-actions"
    ]);
    function restoredChatAttributeText(value, limit = 300) {
      return String(value || "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, limit);
    }
    function restoredChatActionDescriptor(source) {
      const handler = String(source?.getAttribute?.("onclick") || "")
        .trim()
        .replace(/\s+/g, "")
        .replace(/;+$/, "");
      const actions = new Map([
        ["document.getElementById('aiQuestion')?.focus()", "focus_question"],
        ["document.getElementById('homeFiles')?.click()", "choose_files"],
        ["generateHomeVisualInline()", "generate_visual"],
        ["generateRealVisual()", "generate_visual"],
        ["homeGoQuote()", "go_quote"],
        ["homeGoVisual()", "go_visual"],
        ["homeGoDetail()", "go_detail"],
        ["homeGoVector()", "go_vector"],
        ["homeCreateTask()", "create_task"],
        ["copyHomeAnswer()", "copy_answer"],
        ["openFormalOps()", "open_ops"],
        ["showHuoshenImageConfig()", "open_image_config"],
        ["openFormalOps();showHuoshenImageConfig()", "open_image_config"],
        ["showSeedreamImageConfig()", "open_image_config"],
        ["openFormalOps();showSeedreamImageConfig()", "open_image_config"],
        ["removeAllHomeFiles()", "remove_files"],
        ["retryTemporaryImageGeneration()", "retry_temporary_image"],
        ["window.retryStaleImageSetupFromHistory?.()", "retry_stale_image"]
      ]);
      if (actions.has(handler)) return { action: actions.get(handler), argument: "" };
      const versionAction = handler.match(/^(markVisualVersionSent|lockVisualVersion|prepareVisualRevision)\((['"])([A-Za-z0-9._:-]{1,96})\2\)$/);
      if (!versionAction) return null;
      const action = versionAction[1] === "markVisualVersionSent"
        ? "mark_visual_sent"
        : versionAction[1] === "lockVisualVersion"
          ? "lock_visual_version"
          : "revise_visual_version";
      return { action, argument: versionAction[3] };
    }
    function sanitizeRestoredChatHtml(html = "") {
      if (!String(html).trim()) return "";
      const input = document.createElement("template");
      const output = document.createElement("template");
      input.innerHTML = String(html);
      const copyNode = (node, parent) => {
        if (node.nodeType === 3) {
          parent.appendChild(document.createTextNode(node.textContent || ""));
          return;
        }
        if (node.nodeType !== 1) return;
        const source = node;
        const tag = String(source.tagName || "").toLowerCase();
        if (source.namespaceURI && source.namespaceURI !== "http://www.w3.org/1999/xhtml") return;
        if (restoredChatDropTags.has(tag) || source.hasAttribute("data-stale-image-setup-recovery")) return;
        if (!restoredChatAllowedTags.has(tag)) {
          Array.from(source.childNodes).forEach(child => copyNode(child, parent));
          return;
        }

        const classes = Array.from(source.classList || [])
          .filter(token => restoredChatAllowedClasses.has(token))
          .slice(0, 32);
        const safeImageUrl = safeRestoredImageUrl(source.getAttribute("data-image-url") || "", false);
        const safeImageSource = safeRestoredImageUrl(source.getAttribute("src") || "", true);
        const safeAuthenticatedSource = safeRestoredImageUrl(source.getAttribute("data-auth-image-src") || "", false);
        const rawFileId = source.getAttribute("data-user-file-id") || "";
        const fileId = /^[1-9]\d{0,14}$/.test(rawFileId) ? rawFileId : "";
        const restoredAction = tag === "button" ? restoredChatActionDescriptor(source) : null;
        if (tag === "img" && !safeImageSource && !safeAuthenticatedSource) return;
        if (tag === "button") {
          const recognized = classes.some(name => ["generated-image-open", "generated-image-download", "chat-user-attachment"].includes(name))
            || Boolean(safeImageUrl && /下载/.test(source.textContent || ""))
            || Boolean(fileId)
            || Boolean(restoredAction);
          if (!recognized) return;
        }

        const clean = document.createElement(tag);
        if (classes.length) clean.className = classes.join(" ");
        const title = restoredChatAttributeText(source.getAttribute("title"));
        const ariaLabel = restoredChatAttributeText(source.getAttribute("aria-label"));
        if (title) clean.setAttribute("title", title);
        if (ariaLabel) clean.setAttribute("aria-label", ariaLabel);
        if (["true", "false"].includes(source.getAttribute("aria-hidden"))) {
          clean.setAttribute("aria-hidden", source.getAttribute("aria-hidden"));
        }
        if (["status", "button"].includes(source.getAttribute("role"))) clean.setAttribute("role", source.getAttribute("role"));
        if (tag === "details" && source.hasAttribute("open")) clean.setAttribute("open", "");
        if (tag === "img") {
          if (safeImageSource) clean.setAttribute("src", safeImageSource);
          if (safeAuthenticatedSource) clean.setAttribute("data-auth-image-src", safeAuthenticatedSource);
          clean.setAttribute("alt", restoredChatAttributeText(source.getAttribute("alt")));
        }
        if (tag === "button") {
          clean.type = "button";
          if (source.hasAttribute("disabled")) clean.disabled = true;
          if (safeImageUrl) clean.setAttribute("data-image-url", safeImageUrl);
          const imageName = restoredChatAttributeText(source.getAttribute("data-image-name"));
          if (imageName) clean.setAttribute("data-image-name", imageName);
          if (fileId) clean.setAttribute("data-user-file-id", fileId);
          const fileKey = restoredChatAttributeText(source.getAttribute("data-user-file-key"), 500);
          if (fileKey) clean.setAttribute("data-user-file-key", fileKey);
          const attachmentIndex = source.getAttribute("data-attachment-index") || "";
          if (/^\d{1,4}$/.test(attachmentIndex)) clean.setAttribute("data-attachment-index", attachmentIndex);
          if (restoredAction) {
            clean.setAttribute("data-restored-action", restoredAction.action);
            if (restoredAction.argument) clean.setAttribute("data-restored-action-argument", restoredAction.argument);
          }
        }
        if (source.hasAttribute("data-user-attachments")) clean.setAttribute("data-user-attachments", "");
        if (source.getAttribute("data-stale-image-setup-record") === "true") {
          clean.setAttribute("data-stale-image-setup-record", "true");
        }
        parent.appendChild(clean);
        if (tag !== "img" && tag !== "br") {
          Array.from(source.childNodes).forEach(child => copyNode(child, clean));
        }
      };
      Array.from(input.content.childNodes).forEach(node => copyNode(node, output.content));
      return output.innerHTML;
    }
    // RESTORED_CHAT_SANITIZER_END
    function generatedImageDownloadUrl(url, filename = "将莱数字员工效果图.png") {
      try {
        const target = new URL(String(url || ""), window.location.href);
        target.searchParams.set("download", "1");
        target.searchParams.set("name", String(filename || "将莱数字员工效果图.png"));
        return target.href;
      } catch (_) {
        return String(url || "");
      }
    }
    const generatedImageBlobCache = new Map();
    const generatedImageBlobUrls = new Set();
    function generatedImageNeedsAuthorization(url = "") {
      try {
        const target = new URL(String(url || ""), window.location.href);
        return target.origin === window.location.origin
          && (/^\/generated\//.test(target.pathname) || /^\/api\/files\/\d+\/content$/.test(target.pathname));
      } catch (_) {
        return false;
      }
    }
    function currentGeneratedImageAuthToken() {
      try {
        const runtimeToken = typeof window.__currentFormalApiToken === "function"
          ? window.__currentFormalApiToken()
          : "";
        if (runtimeToken) return String(runtimeToken);
      } catch (_) {}
      try {
        return String(window.localStorage?.getItem?.("formal_api_token") || "");
      } catch (_) {
        return "";
      }
    }
    async function authenticatedGeneratedImageBlobUrl(url) {
      const source = String(url || "");
      if (!generatedImageNeedsAuthorization(source)) return source;
      const token = currentGeneratedImageAuthToken();
      if (!token) throw new Error("请先登录后查看图片");
      const cacheKey = `${token}|${source}`;
      if (generatedImageBlobCache.has(cacheKey)) return generatedImageBlobCache.get(cacheKey);
      const response = await fetch(source, {
        headers: { Authorization: `Bearer ${token}` },
        cache: "no-store"
      });
      if (!response.ok) throw new Error("图片读取失败，请重新生成后再试。");
      const blobUrl = URL.createObjectURL(await response.blob());
      generatedImageBlobCache.set(cacheKey, blobUrl);
      generatedImageBlobUrls.add(blobUrl);
      while (generatedImageBlobCache.size > 20) {
        const oldestKey = generatedImageBlobCache.keys().next().value;
        const oldestUrl = generatedImageBlobCache.get(oldestKey);
        generatedImageBlobCache.delete(oldestKey);
        generatedImageBlobUrls.delete(oldestUrl);
        URL.revokeObjectURL(oldestUrl);
      }
      return blobUrl;
    }
    async function hydrateGeneratedImagePreviews(root = document) {
      const scope = root?.querySelectorAll ? root : document;
      const images = Array.from(scope.querySelectorAll("img[data-auth-image-src]"));
      await Promise.all(images.map(async image => {
        const source = image.dataset.authImageSrc || "";
        if (!source || image.dataset.authImageHydrating === "true") return;
        image.dataset.authImageHydrating = "true";
        try {
          image.src = await authenticatedGeneratedImageBlobUrl(source);
          image.dataset.authImageReady = "true";
        } catch (_) {
          image.alt = "图片暂时无法读取";
        } finally {
          image.dataset.authImageHydrating = "false";
        }
      }));
    }
    function generatedImagePreviewMarkup(url, filename = "将莱数字员工效果图.png", alt = "AI生成的效果图", fileId = 0) {
      if (!url) return "";
      const imageSource = generatedImageNeedsAuthorization(url)
        ? `data-auth-image-src="${escapeHtml(url)}"`
        : `src="${escapeHtml(url)}"`;
      const durableFileId = Number(fileId || 0);
      const fileIdAttribute = Number.isSafeInteger(durableFileId) && durableFileId > 0
        ? ` data-user-file-id="${durableFileId}"`
        : "";
      window.setTimeout(() => hydrateGeneratedImagePreviews(document), 0);
      return `<button class="generated-image-open" type="button" data-image-url="${escapeHtml(url)}" data-image-name="${escapeHtml(filename)}"${fileIdAttribute} aria-label="放大查看${escapeHtml(filename)}"><img ${imageSource} alt="${escapeHtml(alt)}"><span class="generated-image-open-label">点击放大</span></button>`;
    }
    function generatedImageDownloadButton(url, filename = "将莱数字员工效果图.png", label = "下载原图", className = "btn dark", clearAfterDownload = false) {
      if (!url) return "";
      const clearAttribute = clearAfterDownload ? ' data-clear-result-after-download="true"' : "";
      return `<button class="${escapeHtml(`${className} generated-image-download`)}" type="button" data-image-url="${escapeHtml(url)}" data-image-name="${escapeHtml(filename)}"${clearAttribute}>${escapeHtml(label)}</button>`;
    }
    async function downloadGeneratedImage(url, filename = "将莱数字员工效果图.png") {
      let href = generatedImageDownloadUrl(url, filename);
      if (!href) {
        notify("图片地址不存在，请重新生成后再试。" );
        return false;
      }
      try {
        if (generatedImageNeedsAuthorization(url)) href = await authenticatedGeneratedImageBlobUrl(url);
      } catch (err) {
        notify(err.message);
        return false;
      }
      const link = document.createElement("a");
      link.href = href;
      link.download = String(filename || "将莱数字员工效果图.png");
      link.rel = "noopener";
      document.body.appendChild(link);
      link.click();
      link.remove();
      return true;
    }
    async function openGeneratedImagePreview(url, filename = "将莱数字员工效果图.png") {
      if (!url) return;
      const requestGeneration = ++homeFilePreviewRequestGeneration;
      activeGeneratedImagePreview = { url: String(url), filename: String(filename || "将莱数字员工效果图.png") };
      activeHomeFilePreviewIndex = -1;
      setTextIfExists("homeFilePreviewCount", "生成图片预览");
      setTextIfExists("homeFilePreviewName", activeGeneratedImagePreview.filename);
      const mask = document.getElementById("homeFileLightbox");
      const stage = mask?.querySelector(".home-file-lightbox-stage");
      mask?.classList.add("show");
      mask?.setAttribute("aria-hidden", "false");
      stage?.classList.add("is-loading");
      document.body.classList.add("home-file-preview-open");
      const image = document.getElementById("homeFilePreviewImage");
      if (image) {
        image.removeAttribute("src");
        try {
          const imageUrl = await authenticatedGeneratedImageBlobUrl(activeGeneratedImagePreview.url);
          if (requestGeneration !== homeFilePreviewRequestGeneration || !mask?.classList.contains("show")) return;
          image.src = imageUrl;
        } catch (err) {
          if (requestGeneration !== homeFilePreviewRequestGeneration) return;
          closeHomeFilePreview();
          notify(err.message);
          return;
        } finally {
          if (requestGeneration === homeFilePreviewRequestGeneration) stage?.classList.remove("is-loading");
        }
        image.alt = `预览：${activeGeneratedImagePreview.filename}`;
      }
      document.getElementById("homeFilePreviewPrev")?.classList.add("is-hidden");
      document.getElementById("homeFilePreviewNext")?.classList.add("is-hidden");
      document.getElementById("homeFilePreviewDownload")?.classList.remove("is-hidden");
      document.querySelector(".home-file-lightbox-close")?.focus();
    }
    function generatedImageFilename(url, fallback = "将莱数字员工效果图.png") {
      try {
        const name = decodeURIComponent(new URL(String(url || ""), window.location.href).pathname.split("/").pop() || "").trim();
        return name || fallback;
      } catch (_) {
        return fallback;
      }
    }
    function isGeneratedImageEngineLabel(value = "") {
      const text = String(value || "").trim();
      if (!text) return false;
      if (/^(AI生图模型|智能文案|本机模板)$/i.test(text)) return true;
      if (/^(火神AI|火山方舟|阿里云百炼|OpenAI官方)(?:\s+.+)?$/i.test(text)) return true;
      return /^(?:gpt|qwen|doubao|wan|dreamina|seedream|stable[-_]?diffusion|sdxl|midjourney|gemini)[a-z0-9._\/-]*$/i.test(text);
    }
    function publicGeneratedImageMetaLine(value = "") {
      const parts = String(value || "").split(/\s*·\s*/).map(item => item.trim()).filter(Boolean);
      while (parts.length && isGeneratedImageEngineLabel(parts[0])) parts.shift();
      while (parts.length && isGeneratedImageEngineLabel(parts[parts.length - 1])) parts.pop();
      return parts.join(" · ");
    }
    function removeGeneratedImageEngineLabels(root) {
      root?.querySelectorAll?.(".home-inline-visual-meta").forEach(meta => {
        const lineBreak = meta.querySelector("br");
        if (!lineBreak) {
          meta.textContent = publicGeneratedImageMetaLine(meta.textContent) || "效果图已保存";
          return;
        }
        const beforeBreak = [];
        for (let node = meta.firstChild; node && node !== lineBreak; node = node.nextSibling) {
          beforeBreak.push(node.textContent || "");
        }
        let node = meta.firstChild;
        while (node && node !== lineBreak) {
          const next = node.nextSibling;
          node.remove();
          node = next;
        }
        meta.insertBefore(document.createTextNode(publicGeneratedImageMetaLine(beforeBreak.join("")) || "效果图已生成"), lineBreak);
      });
    }
    function upgradeGeneratedImageHtml(html = "") {
      if (!String(html).trim()) return "";
      const template = document.createElement("template");
      template.innerHTML = String(html);
      removeGeneratedImageEngineLabels(template.content);
      template.content.querySelectorAll(".home-inline-visual, .generated-image-panel, .visual-version-item").forEach(panel => {
        const image = panel.querySelector('img[src*="/generated/"], img[data-auth-image-src*="/generated/"], img[alt*="生成的效果图"], img[alt*="生成的产品图"], img[alt*="历史生成效果图"]');
        if (!image) return;
        const existingTrigger = image.closest(".generated-image-open");
        const url = existingTrigger?.dataset.imageUrl || image.dataset.authImageSrc || image.getAttribute("src") || "";
        if (!url) return;
        const filename = existingTrigger?.dataset.imageName || generatedImageFilename(url);
        if (generatedImageNeedsAuthorization(url)) {
          image.removeAttribute("src");
          image.dataset.authImageSrc = url;
        }
        if (!existingTrigger) {
          const trigger = document.createElement("button");
          trigger.className = "generated-image-open";
          trigger.type = "button";
          trigger.dataset.imageUrl = url;
          trigger.dataset.imageName = filename;
          trigger.setAttribute("aria-label", `放大查看${filename}`);
          image.replaceWith(trigger);
          trigger.appendChild(image);
          const label = document.createElement("span");
          label.className = "generated-image-open-label";
          label.textContent = "点击放大";
          trigger.appendChild(label);
        }
        let actions = panel.querySelector(":scope > .actions, .visual-version-actions");
        if (!actions) {
          actions = document.createElement("div");
          actions.className = "actions";
          panel.appendChild(actions);
        }
        const oldDownload = Array.from(actions.querySelectorAll("a, button")).find(item => /下载(图片|原图|最终稿)?/.test(item.textContent || ""));
        if (oldDownload && !oldDownload.classList.contains("generated-image-download")) {
          const holder = document.createElement("template");
          holder.innerHTML = generatedImageDownloadButton(url, filename);
          oldDownload.replaceWith(holder.content.firstElementChild);
        } else if (!oldDownload) {
          actions.insertAdjacentHTML("afterbegin", generatedImageDownloadButton(url, filename));
        }
      });
      window.setTimeout(() => hydrateGeneratedImagePreviews(document), 0);
      return template.innerHTML;
    }
    document.addEventListener("click", event => {
      const trigger = event.target.closest?.(".generated-image-open");
      if (!trigger) return;
      event.preventDefault();
      openGeneratedImagePreview(trigger.dataset.imageUrl, trigger.dataset.imageName);
    });
    document.addEventListener("click", async event => {
      const trigger = event.target.closest?.(".generated-image-download");
      if (!trigger) return;
      event.preventDefault();
      const downloaded = await downloadGeneratedImage(trigger.dataset.imageUrl, trigger.dataset.imageName);
      if (downloaded && trigger.dataset.clearResultAfterDownload === "true") {
        window.setTimeout(() => clearHomeGeneratedResult({ clearInputs: true, reason: "download" }), 120);
      }
    });
    document.addEventListener("click", event => {
      const trigger = event.target.closest?.("[data-restored-action]");
      if (!trigger || trigger.disabled) return;
      event.preventDefault();
      const argument = String(trigger.dataset.restoredActionArgument || "");
      switch (trigger.dataset.restoredAction) {
        case "focus_question": document.getElementById("aiQuestion")?.focus(); break;
        case "choose_files": document.getElementById("homeFiles")?.click(); break;
        case "generate_visual": window.generateHomeVisualInline?.(); break;
        case "go_quote": window.homeGoQuote?.(); break;
        case "go_visual": window.homeGoVisual?.(); break;
        case "go_detail": window.homeGoDetail?.(); break;
        case "go_vector": window.homeGoVector?.(); break;
        case "create_task": window.homeCreateTask?.(); break;
        case "copy_answer": window.copyHomeAnswer?.(); break;
        case "open_ops": window.openFormalOps?.(); break;
        case "open_image_config": window.openFormalOps?.(); window.showHuoshenImageConfig?.(); break;
        case "remove_files": window.removeAllHomeFiles?.(); break;
        case "retry_temporary_image": window.retryTemporaryImageGeneration?.(); break;
        case "retry_stale_image": window.retryStaleImageSetupFromHistory?.(); break;
        case "mark_visual_sent": window.markVisualVersionSent?.(argument); break;
        case "lock_visual_version": window.lockVisualVersion?.(argument); break;
        case "revise_visual_version": window.prepareVisualRevision?.(argument); break;
        default: break;
      }
    });
    document.addEventListener("click", event => {
      const trigger = event.target.closest?.(".chat-user-attachment");
      if (!trigger || trigger.disabled) return;
      const url = trigger.dataset.imageUrl || trigger.querySelector("img")?.src || "";
      if (!url) return;
      event.preventDefault();
      openGeneratedImagePreview(url, trigger.dataset.imageName || "客户上传图片");
    });
    function downloadActiveGeneratedImage() {
      if (!activeGeneratedImagePreview) return;
      downloadGeneratedImage(activeGeneratedImagePreview.url, activeGeneratedImagePreview.filename);
    }
    function clearHomeFilePreviewUrls() {
      closeHomeFilePreview();
      homeFilePreviewUrls.forEach(url => { if (url) URL.revokeObjectURL(url); });
      homeFilePreviewUrls = [];
      activeHomeFilePreviewIndex = -1;
    }
    function homePreviewImageIndexes() {
      return Array.from(document.getElementById("homeFiles")?.files || [])
        .map((file, index) => file.type.startsWith("image/") ? index : -1)
        .filter(index => index >= 0);
    }
    function renderHomeFilePreviewLightbox() {
      const files = Array.from(document.getElementById("homeFiles")?.files || []);
      const file = files[activeHomeFilePreviewIndex];
      const url = homeFilePreviewUrls[activeHomeFilePreviewIndex];
      if (!file || !url) return closeHomeFilePreview();
      const imageIndexes = homePreviewImageIndexes();
      const imagePosition = Math.max(0, imageIndexes.indexOf(activeHomeFilePreviewIndex));
      setTextIfExists("homeFilePreviewCount", `第 ${imagePosition + 1} 张，共 ${imageIndexes.length} 张`);
      setTextIfExists("homeFilePreviewName", file.name);
      const image = document.getElementById("homeFilePreviewImage");
      if (image) {
        image.src = url;
        image.alt = `预览：${file.name}`;
      }
      const prev = document.getElementById("homeFilePreviewPrev");
      const next = document.getElementById("homeFilePreviewNext");
      if (prev) prev.disabled = imageIndexes.length < 2;
      if (next) next.disabled = imageIndexes.length < 2;
      prev?.classList.remove("is-hidden");
      next?.classList.remove("is-hidden");
      document.getElementById("homeFilePreviewDownload")?.classList.add("is-hidden");
    }
    function openHomeFilePreview(index) {
      if (!homeFilePreviewUrls[index]) return;
      activeGeneratedImagePreview = null;
      activeHomeFilePreviewIndex = index;
      const mask = document.getElementById("homeFileLightbox");
      mask?.classList.add("show");
      mask?.setAttribute("aria-hidden", "false");
      document.body.classList.add("home-file-preview-open");
      renderHomeFilePreviewLightbox();
      document.querySelector(".home-file-lightbox-close")?.focus();
    }
    function closeHomeFilePreview() {
      homeFilePreviewRequestGeneration += 1;
      const mask = document.getElementById("homeFileLightbox");
      mask?.classList.remove("show");
      mask?.setAttribute("aria-hidden", "true");
      mask?.querySelector(".home-file-lightbox-stage")?.classList.remove("is-loading");
      document.body.classList.remove("home-file-preview-open");
      const image = document.getElementById("homeFilePreviewImage");
      if (image) image.removeAttribute("src");
      activeGeneratedImagePreview = null;
    }
    function moveHomeFilePreview(direction) {
      if (activeGeneratedImagePreview) return;
      const imageIndexes = homePreviewImageIndexes();
      if (imageIndexes.length < 2) return;
      const position = Math.max(0, imageIndexes.indexOf(activeHomeFilePreviewIndex));
      activeHomeFilePreviewIndex = imageIndexes[(position + direction + imageIndexes.length) % imageIndexes.length];
      renderHomeFilePreviewLightbox();
    }
    document.addEventListener("keydown", event => {
      if (!document.getElementById("homeFileLightbox")?.classList.contains("show")) return;
      if (event.key === "Escape") closeHomeFilePreview();
      if (event.key === "ArrowLeft") moveHomeFilePreview(-1);
      if (event.key === "ArrowRight") moveHomeFilePreview(1);
    });
    function restoredHomePendingFileCard(record, index) {
      const id = Number(record?.id || 0);
      const name = String(record?.name || `已上传附件 ${index + 1}`);
      const mimeType = String(record?.mimeType || "");
      const imageLike = mimeType.startsWith("image/") || (!mimeType && attachmentNameLooksLikeImage(name));
      return `
        <div class="file-card${imageLike ? " has-preview" : ""}">
          <button class="file-card-remove" type="button" aria-label="移除 ${escapeHtml(name)}" title="移除这个附件" onclick="removeRestoredHomePendingFile(${id})">×</button>
          <button class="chat-user-attachment${imageLike ? "" : " is-not-image"}" type="button" data-user-file-id="${id}" data-image-name="${escapeHtml(name)}" data-attachment-mime="${escapeHtml(mimeType)}"${imageLike ? "" : " disabled"} aria-label="${imageLike ? "放大查看" : "附件"}${escapeHtml(name)}">
            <span class="chat-user-attachment-placeholder">${imageLike ? "图片加载中" : "文件附件"}</span>
            <span class="chat-user-attachment-number">${imageLike ? "图片" : "附件"} ${index + 1}</span>
            <span class="chat-user-attachment-name" title="${escapeHtml(name)}">${escapeHtml(name)}</span>
          </button>
          <div class="file-card-meta"><strong title="${escapeHtml(name)}">${index + 1}. ${escapeHtml(name)}</strong><span>已安全保存 · 可直接发送</span></div>
        </div>`;
    }
    async function removeRestoredHomePendingFile(fileId) {
      if (typeof window.__removeRestoredHomePendingFile !== "function") return notify("附件服务正在连接，请稍后重试。");
      await window.__removeRestoredHomePendingFile(Number(fileId || 0));
    }
    function updateHomeFileName() {
      const files = Array.from(document.getElementById("homeFiles")?.files || []);
      const names = files.map(file => file.name);
      const fileKeys = new Set(files.map(file => [file.name || "", Number(file.size || 0), Number(file.lastModified || 0)].join("|")));
      const restoredRecords = (Array.isArray(activeCustomerSession()?.pendingFileRecords) ? activeCustomerSession().pendingFileRecords : [])
        .filter(record => Number(record?.id || 0) > 0 && !fileKeys.has(String(record?.key || "")));
      const totalFileCount = files.length + restoredRecords.length;
      const label = document.getElementById("homeFileName");
      const uploadCount = document.getElementById("homeUploadCount");
      const preview = document.getElementById("homeFilePreview");
      const privacyNotice = document.querySelector("#homePrivacyNotice span");
      const riskyImage = files.some(file => /截图|聊天|微信|合同|身份证|银行卡|发票|订单|报价单|screenshot|wechat|chat|contract|invoice|idcard/i.test(file.name));
      if (privacyNotice) {
        privacyNotice.textContent = riskyImage
          ? "检测到可能含隐私的截图/文件：中转站模式会自动拦截，请粘贴文字或先打码后上传"
          : "隐私保护已开启：调用AI前自动隐藏联系方式与详细地址，并清理图片元数据";
      }
      if (label) {
        const mode = normalizeHomeImageMode(window.__selectedHomeCapability);
        label.textContent = totalFileCount
          ? `已上传 ${Math.min(totalFileCount, 6)} 张图片`
          : HOME_IMAGE_MODE_CONFIG[mode]?.upload || "上传图片";
      }
      if (uploadCount) uploadCount.textContent = `${Math.min(totalFileCount, 6)}/6`;
      clearHomeFilePreviewUrls();
      homeFilePreviewUrls = files.map(file => file.type.startsWith("image/") ? URL.createObjectURL(file) : "");
      if (preview) {
        preview.innerHTML = `${restoredRecords.map((record, index) => restoredHomePendingFileCard(record, index)).join("")}${files.map((file, index) => `
          <div class="file-card${homeFilePreviewUrls[index] ? " has-preview" : ""}">
            <button class="file-card-remove" type="button" aria-label="移除 ${escapeHtml(file.name)}" title="移除这张图片" onclick="removeHomeFile(${index})">×</button>
            ${homeFilePreviewUrls[index] ? `
              <button class="file-card-preview" type="button" aria-label="放大预览第 ${restoredRecords.length + index + 1} 张图片：${escapeHtml(file.name)}" onclick="openHomeFilePreview(${index})">
                <img src="${escapeHtml(homeFilePreviewUrls[index])}" alt="第 ${restoredRecords.length + index + 1} 张图片：${escapeHtml(file.name)}">
                <span class="file-card-number">图片 ${restoredRecords.length + index + 1}</span>
                <span class="file-card-open">点击放大</span>
              </button>
            ` : `<div class="file-card-placeholder">${escapeHtml(fileKind(file))}</div>`}
            <div class="file-card-meta">
              <strong title="${escapeHtml(file.name)}">${restoredRecords.length + index + 1}. ${escapeHtml(file.name)}</strong>
              <span>${escapeHtml(fileKind(file))} · ${escapeHtml(fileSizeText(file.size))}</span>
            </div>
          </div>
        `).join("")}`;
        window.setTimeout(() => window.__hydrateHomeUserAttachmentThumbs?.(preview), 0);
      }
      if (totalFileCount) {
        ensurePendingCustomerIdentity();
        setTextIfExists("homeIntentHint", `已添加 ${Math.min(totalFileCount, 6)} 张素材，可继续填写制作要求。`);
        document.getElementById("homeIntentHint")?.classList.remove("is-hidden");
      } else {
        document.getElementById("homeIntentHint")?.classList.add("is-hidden");
      }
      renderHomeRetainedFileContext();
      window.requestAnimationFrame(syncHomeComposerClearance);
    }
    function homeFlowAction(action) {
      const input = document.getElementById("aiQuestion");
      document.querySelectorAll(".home-flow button").forEach(btn => {
        btn.classList.toggle("active", btn.dataset.flow === action);
      });
      const text = val("aiQuestion");
      const files = homeFileNames();
      if (action === "message") {
        input?.focus();
        setTextIfExists("aiAnswer", "把客户微信、询盘内容、图片视频说明粘进来，也可以先上传客户发来的图片、视频、Logo或图纸文件。");
        return;
      }
      if (!text && !files.length) {
        setTextIfExists("aiAnswer", "先粘贴客户消息，或上传客户发来的图片、视频、Logo、图纸文件，再点这里处理。");
        input?.focus();
        return;
      }
      if (action === "parse") {
        syncHomeToInquiry();
        showPage("询盘处理");
        generateReply();
        return;
      }
      if (action === "quote") {
        homeGoQuote();
        return;
      }
      if (action === "visual") {
        syncHomeToInquiry("客户发来了图片/视频/Logo，帮我做产品图和宝贝详情。");
        loadVisualFromInquiry();
        return;
      }
      if (action === "task") {
        syncHomeToInquiry("客户发来了文件，帮我生成跟单任务，提醒后续报价、做图、打样和确认。");
        homeCreateTask();
      }
    }
    function renderHomeAiReport(info, missing, files) {
      const answer = document.getElementById("aiAnswer");
      if (!answer) return;
      showHomeResult();
      const fileText = files.length ? files.join("、") : "未上传文件";
      const reply = `您好，这个需求我们可以先按 ${info.product} 方向评估。为了给您报准确价格，麻烦确认${missing.slice(0, 5).join("、")}。如果有设计稿、样品图或参考图，也可以一起发我。`;
      answer.innerHTML = `
        <div class="ai-report">
          <div class="ai-report-card">
            <span>客户要做什么</span>
            <strong>${escapeHtml(info.product)}</strong>
            <p>${escapeHtml(info.size)} · ${escapeHtml(info.qty)}<br>${escapeHtml(info.material)} · ${escapeHtml(info.finish)}</p>
          </div>
          <div class="ai-report-card">
            <span>客户发的文件</span>
            <strong>${files.length ? "已收到" : "没有上传"}</strong>
            <p>${escapeHtml(fileText)}</p>
          </div>
          <div class="ai-report-card">
            <span>还要问客户什么</span>
            <ol>${missing.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ol>
          </div>
          <div class="ai-report-card">
            <span>注意点</span>
            <p>${escapeHtml(info.risk.length ? info.risk.join("；") : "暂无明显风险")}</p>
          </div>
          <div class="ai-report-card full">
            <span>可以直接发给客户</span>
            <p>${escapeHtml(reply)}</p>
          </div>
          <div class="ai-report-card full">
            <span>下一步</span>
            <p>资料够了就去报价；客户想看样子就做产品图；已经报价过就生成跟单提醒。</p>
          </div>
        </div>
      `.trim();
    }
    function startHomeInquiry() {
      const files = homeFileNames();
      const pendingText = val("aiQuestion");
      const pendingCustomer = val("homeCustomerName");
      if (window.__dealDemoStateSnapshot) {
        restoreDealDemoState();
        setIfExists("aiQuestion", pendingText);
        setIfExists("homeCustomerName", pendingCustomer || "新客户");
      }
      const text = val("aiQuestion");
      if (!text && !files.length) {
        showHomeResult();
        setTextIfExists("aiAnswer", "先把客户发来的聊天内容粘进来，或者上传客户发来的截图、产品图、Logo、图纸文件。想先体验可以点“看一个例子”。");
        return;
      }
      if (isCasualAiQuestion(text, files)) {
        renderCasualAiReply(text);
        return;
      }
      const sourceText = text || "客户上传了图片/视频/图纸文件，需要先识别需求并确认报价资料。";
      setIfExists("customerText", sourceText);
      const info = analyze(sourceText);
      const missing = [
        info.size.includes("待确认") ? "尺寸" : "",
        info.qty.includes("待确认") ? "数量" : "",
        info.material.includes("待确认") ? "材质" : "",
        info.finish.includes("待确认") ? "工艺/Logo位置" : "",
        "是否含税",
        "是否需要打样"
      ].filter(Boolean);
      renderHomeAiReport(info, missing, files);
      renderHomeNextActions(`${sourceText} ${files.join(" ")}`);
      captureActiveCustomerSession(true);
      renderCustomerWorkbench();
    }
    function renderHomeNextActions(text = val("aiQuestion"), scrollIntoView = true) {
      const box = document.getElementById("aiNextActions");
      if (!box) return;
      const imageNeed = hasAny(text, ["效果图", "产品图", "主图", "图片", "海报", "做图"]);
      const detailNeed = hasAny(text, ["详情页", "宝贝详情", "详情", "卖点"]);
      const vectorNeed = hasAny(text, ["矢量", "CDR", "cdr", "转图", "转文件", "AI文件", "雕刻"]);
      const quoteNeed = hasAny(text, ["报价", "报个价", "价格", "多少钱", "单价", "成本"]);
      const hasMissing = String(text || "").includes("待确认");
      const configured = !quoteSetupRequired();
      const primaryIntent = vectorNeed && !quoteNeed ? "vector" : detailNeed && !quoteNeed ? "detail" : imageNeed && !quoteNeed ? "visual" : "quote";
      const nextLabel = primaryIntent === "visual"
        ? "下一步：生成客户确认效果图"
        : primaryIntent === "detail"
          ? "下一步：制作宝贝详情"
          : primaryIntent === "vector"
            ? "下一步：图片转矢量"
            : configured
              ? (hasMissing ? "下一步：补齐资料并生成报价" : "下一步：核对并生成报价")
              : "下一步：按本单补齐成本并报价";
      const customerName = val("homeCustomerName") || "新客户";
      const savedText = customerName === "新客户"
        ? "当前内容已保留；填写客户名称后会建立长期客户记忆。"
        : `${customerName} 的资料和本次聊天已保存。`;
      const ruleText = primaryIntent === "quote"
        ? (configured
          ? `报价会使用本厂已确认价格表${state.settings.quoteRulesUpdatedAt ? `（${new Date(state.settings.quoteRulesUpdatedAt).toLocaleDateString("zh-CN")}更新）` : ""}。`
          : "不要求先填完整价格表；进入报价后只补本单实际使用的材料、印刷和工艺费用。")
        : "系统已按客户原话选择更合适的处理流程，之后仍可继续报价或跟单。";
      box.classList.remove("is-hidden");
      box.querySelector("details")?.removeAttribute("open");
      setTextIfExists("homeNextTitle", nextLabel);
      setTextIfExists("homeNextTrust", `${savedText}${ruleText}`);
      setTextIfExists("homeActionQuote", nextLabel);
      const quoteAction = document.getElementById("homeActionQuote");
      if (quoteAction) {
        quoteAction.dataset.source = homeSourceText("") || (document.getElementById("homeUserMessage")?.textContent || "").trim();
        quoteAction.dataset.primaryIntent = primaryIntent;
        quoteAction.onclick = primaryIntent === "visual" ? homeGoVisual : primaryIntent === "detail" ? homeGoDetail : primaryIntent === "vector" ? homeGoVector : homeGoQuote;
      }
      const visual = document.getElementById("homeActionVisual");
      const detail = document.getElementById("homeActionDetail");
      const vector = document.getElementById("homeActionVector");
      if (visual) visual.className = imageNeed ? "btn" : "btn light";
      if (detail) detail.className = detailNeed ? "btn" : "btn light";
      vector?.classList.toggle("is-hidden", !vectorNeed);
      if (scrollIntoView) window.setTimeout(() => scrollHomeReplyAboveComposer(box), 60);
    }
    function startNewCustomerConversation(silent = false, preserveComposer = false) {
      closeHomeDemo();
      window.__homeConversationContext = "";
      window.__latestAiSourceText = "";
      window.__latestAiAnalysis = null;
      window.__activeHomePrompt = "";
      window.__homeVisiblePrompt = "";
      window.__homePromptPrepared = false;
      window.__homeIsFollowUp = false;
      window.__homeCurrentTurnFileIds = [];
      window.__homeCurrentTurnFileNames = [];
      applyHomeAnalysisMode("standard", { persist: false });
      window.__revokeHomeUserAttachmentBlobUrls?.();
      forgetHomeConversationFiles(true);
      resetHomeCapabilitySelection();
      window.__resetConnectedAnalysisCache?.();
      const home = document.querySelector(".gpt-home");
      const history = document.getElementById("homeChatHistory");
      const row = document.getElementById("homeUserRow");
      const bubble = document.getElementById("homeUserMessage");
      const answer = document.getElementById("aiAnswer");
      const input = document.getElementById("aiQuestion");
      const files = document.getElementById("homeFiles");
      if (history) history.innerHTML = "";
      row?.classList.add("is-hidden");
      if (bubble) bubble.innerHTML = "";
      if (answer) answer.textContent = "生成结果会直接显示在这里。";
      hideHomeNextActions();
      if (input && !preserveComposer) {
        input.value = "";
        input.style.height = "auto";
        input.focus();
      }
      if (files && !preserveComposer) files.value = "";
      if (!preserveComposer) {
        clearHomeFilePreviewUrls();
        setTextIfExists("homeFileName", "上传图片、表格、文档、视频或图纸");
        document.getElementById("homeFilePreview")?.replaceChildren();
      }
      document.getElementById("homeResult")?.classList.add("is-empty");
      home?.classList.remove("has-conversation");
      if (!silent) notify("已开始新询盘，可以粘贴新的客户消息。");
    }
    function selectHomeCustomer() {
      window.__selectHomeCustomerConnected?.();
    }
    function scheduleHomeCustomerMemory() {
      window.clearTimeout(window.__homeCustomerMemoryTimer);
      if (activeCustomerSession()?.renameFrom) {
        const status = document.getElementById("homeMemoryStatus");
        if (status) status.textContent = "姓名填写完成后按回车或点一下空白处，系统会合并原资料";
        return;
      }
      window.__homeCustomerMemoryTimer = window.setTimeout(() => selectHomeCustomer(), 450);
    }
    function startNewCustomerMemory() {
      restoreDealDemoState();
      createCustomerSession();
    }
    function showHomeResult() {
      document.getElementById("homeResult")?.classList.remove("is-empty");
      beginHomeConversation();
    }
    function homeHasGeneratedResult() {
      const answer = document.getElementById("aiAnswer");
      return Boolean(answer?.querySelector(
        ".home-inline-visual, [data-home-detail-package-result], .detail-package, .generated-image-panel"
      ));
    }
    function clearHomeGeneratedResult(options = {}) {
      const session = activeCustomerSession();
      if (session && homeSessionIsProcessing(session)) return false;
      const hadResult = homeHasGeneratedResult()
        || /(?:home-inline-visual|data-home-detail-package-result|generated-image-panel)/.test(String(session?.answerHtml || ""));
      if (!hadResult && options.force !== true) return false;
      closeHomeFilePreview();
      window.__clearHomeDetailPackageResult?.(session);
      const answer = document.getElementById("aiAnswer");
      if (answer) answer.textContent = "生成结果会直接显示在这里。";
      document.getElementById("homeResult")?.classList.add("is-empty");
      document.querySelector(".gpt-home")?.classList.remove("has-conversation");
      hideHomeNextActions();
      if (session) {
        session.answerHtml = "";
        session.resultEmpty = true;
        session.hasConversation = false;
        session.hasUnreadResult = false;
        session.retainedFileIds = [];
        session.retainedFileNames = [];
        session.lastVisualPrompt = "";
        session.lastVisualUserPrompt = "";
        session.updatedAt = new Date().toISOString();
      }
      window.__lastHomeVisualPrompt = "";
      window.__lastHomeVisualUserPrompt = "";
      window.__lastRecognizedHomeFileIds = [];
      window.__homeRetainedFileNames = [];
      latestVisualFileId = null;
      renderHomeRetainedFileContext();
      if (options.clearInputs === true) {
        const input = document.getElementById("aiQuestion");
        if (input) {
          input.value = "";
          input.style.height = "auto";
        }
        const files = document.getElementById("homeFiles");
        if (files) files.value = "";
        if (session) {
          session.draft = "";
          session.pendingFileIds = [];
          session.pendingFileRecords = [];
          session.uploadFailures = [];
        }
        uploadedFileIds = [];
        uploadedHomeFileRecords = [];
        window.__homeUploadFailures = [];
        latestUploadPromise = null;
        clearHomeFilePreviewUrls();
        document.getElementById("homeFilePreview")?.replaceChildren();
        const config = HOME_IMAGE_MODE_CONFIG[normalizeHomeImageMode(window.__selectedHomeCapability)] || HOME_IMAGE_MODE_CONFIG.main_image;
        setTextIfExists("homeFileName", config.upload);
        document.getElementById("homeIntentHint")?.classList.add("is-hidden");
        window.requestAnimationFrame(syncHomeComposerClearance);
      }
      saveState();
      if (options.silent !== true) {
        notify(options.reason === "download"
          ? "图片已下载，本次生成结果和临时素材已清理。"
          : "本次生成结果已清理，可以开始下一张。"
        );
      }
      return true;
    }
    function activeHomeBusinessSource(session = activeCustomerSession()) {
      return String(
        session?.businessSourceText
        || window.__latestAiSourceText
        || session?.draft
        || (!["等待客户消息", "历史客户档案"].includes(session?.lastMessage) ? session?.lastMessage : "")
        || ""
      ).trim();
    }
    function setQuoteBusinessContext(origin = "home", task = null, sourceText = "") {
      const session = activeCustomerSession();
      const linkedTask = task || homeChatTaskForSession(session);
      const customer = String(
        linkedTask?.cloudCustomer
        || String(linkedTask?.customer || "").split(" · ")[0]
        || session?.customer
        || document.getElementById("homeCustomerName")?.value
        || "新客户"
      ).trim() || "新客户";
      window.__quoteBusinessContext = {
        origin,
        customer,
        sessionId: session?.id || "",
        caseSource: linkedTask?.source || homeChatTaskSource(session),
        taskId: linkedTask?.id || "",
        taskServerId: Number(linkedTask?.serverId || session?.followupTaskId || 0),
        sourceText: String(sourceText || activeHomeBusinessSource(session) || "").trim()
      };
      window.__quoteIntakeExpanded = false;
      return window.__quoteBusinessContext;
    }
    function quoteBusinessContext() {
      return window.__quoteBusinessContext && typeof window.__quoteBusinessContext === "object"
        ? window.__quoteBusinessContext
        : {};
    }
    function quoteWorkspaceIsInline() {
      return document.getElementById("quoteWorkspaceRoot")?.parentElement?.id === "homeInlineQuoteMount";
    }
    function setHomeInlineQuoteLayout(open) {
      const home = document.querySelector(".gpt-home");
      if (!home) return;
      home.classList.toggle("quote-workspace-open", open === true);
      window.requestAnimationFrame(() => {
        syncHomeConversationLayout();
        syncHomeComposerClearance();
      });
    }
    function copyQuoteDraftValue(value, fallback = {}) {
      try {
        return JSON.parse(JSON.stringify(value ?? fallback));
      } catch (_) {
        return JSON.parse(JSON.stringify(fallback));
      }
    }
    function captureQuoteWorkspaceSnapshot() {
      const root = document.getElementById("quoteWorkspaceRoot");
      if (!root) return null;
      const fields = {};
      root.querySelectorAll("input[id], select[id], textarea[id], details[id]").forEach(control => {
        if (control.tagName === "DETAILS") {
          fields[control.id] = { kind: "details", open: control.open === true };
          return;
        }
        if (String(control.type || "").toLowerCase() === "file") return;
        fields[control.id] = {
          kind: "field",
          value: String(control.value ?? ""),
          checked: Boolean(control.checked),
          checkable: ["checkbox", "radio"].includes(String(control.type || "").toLowerCase())
        };
      });
      return {
        fields,
        // 材料采购价和确认状态会被报价页重新渲染。单独保留一份，
        // 返回修改时在页面结构稳定后再恢复，避免被默认价格覆盖。
        materialCostDraft: quoteMaterialCostDraft(),
        materialCostSignature: String(window.__quoteMaterialCostSignature || ""),
        capturedAt: new Date().toISOString()
      };
    }
    function restoreQuoteWorkspaceSnapshot(snapshot) {
      if (!snapshot?.fields || typeof snapshot.fields !== "object") return false;
      Object.entries(snapshot.fields).forEach(([id, saved]) => {
        const control = document.getElementById(id);
        if (!control || !saved) return;
        if (saved.kind === "details") {
          control.open = saved.open === true;
          return;
        }
        if (String(control.type || "").toLowerCase() === "file") return;
        control.value = String(saved.value ?? "");
        if (saved.checkable === true) control.checked = saved.checked === true;
      });
      window.__quoteMaterialCostSignature = String(snapshot.materialCostSignature || window.__quoteMaterialCostSignature || "");
      updateQuoteMaterialConversionPreview();
      // 报价摘要通常会顺带刷新材料核价区。返回结果页时先只恢复摘要，
      // 再写回采购价、用量和确认勾选，确保厂家不用重复填写。
      renderQuoteReviewSummary({ renderMaterialCost: false });
      restoreQuoteMaterialCostDraft(snapshot.materialCostDraft);
      renderQuoteMissingFirst();
      return true;
    }
    let quoteLiveDockTimer = 0;
    function quoteManualAdjustments() {
      const productionSheets = String(document.getElementById("quotePrintProductionSheetsOverride")?.value || "").trim();
      const plateCount = String(document.getElementById("quotePrintPlateCountOverride")?.value || "").trim();
      const reason = String(document.getElementById("quoteManualAdjustmentReason")?.value || "").trim().slice(0, 160);
      return {
        productionSheets,
        plateCount,
        reason,
        active:Boolean(productionSheets || plateCount)
      };
    }
    function quoteReusableDraftSnapshot() {
      return normalizeQuoteDraftSnapshot({
        product:val("quoteProduct"),
        size:val("quoteSize"),
        material:val("quoteMaterial"),
        process:val("quoteProcess"),
        qty:val("quoteQty"),
        template:val("quoteTemplate"),
        costMode:val("quoteCostMode"),
        delivery:val("quoteDelivery"),
        payment:val("quotePayment"),
        taxMode:val("quoteTaxMode"),
        sampleMode:val("quoteSampleMode"),
        sample:val("quoteSample"),
        customerLevel:val("quoteLevel"),
        productConfiguration:quoteCatalogConfigurationSnapshot(),
        materialCostDraft:quoteMaterialCostDraft(),
        manualAdjustments:quoteManualAdjustments()
      });
    }
    function quoteSavedTemplates() {
      state.settings.quoteSavedTemplates = normalizeQuoteSavedTemplates(state.settings.quoteSavedTemplates);
      return state.settings.quoteSavedTemplates;
    }
    function renderQuoteSavedTemplates(preferredId = "") {
      const select = document.getElementById("quoteSavedTemplateSelect");
      if (!select) return;
      const templates = quoteSavedTemplates();
      const previous = preferredId || select.value;
      select.innerHTML = templates.length
        ? templates.map(item => `<option value="${escapeHtml(item.id)}">${escapeHtml(item.name)}</option>`).join("")
        : `<option value="">还没有已保存方案</option>`;
      if (previous && templates.some(item => item.id === previous)) select.value = previous;
    }
    function saveCurrentQuoteTemplate() {
      const snapshot = quoteReusableDraftSnapshot();
      if (!snapshot?.product) {
        notify("请先选择报价产品，再保存整套方案。");
        return false;
      }
      const input = document.getElementById("quoteSavedTemplateName");
      const defaultName = [snapshot.product, snapshot.material].filter(Boolean).join("·").slice(0, 40);
      const name = String(input?.value || defaultName || "常用报价方案").trim().slice(0, 40);
      if (!name) {
        input?.focus();
        notify("请填写方案名称。");
        return false;
      }
      const templates = quoteSavedTemplates();
      const existing = templates.find(item => item.name === name);
      if (existing && !confirm(`“${name}”已存在，是否用当前参数覆盖？`)) return false;
      const now = new Date().toISOString();
      const record = {
        id:existing?.id || `quote-template-${Date.now()}`,
        name,
        snapshot,
        createdAt:existing?.createdAt || now,
        updatedAt:now
      };
      state.settings.quoteSavedTemplates = normalizeQuoteSavedTemplates(existing
        ? templates.map(item => item.id === existing.id ? record : item)
        : [record, ...templates]);
      saveState();
      window.__saveSettingsCloud?.(state.settings);
      renderQuoteSavedTemplates(record.id);
      if (input) input.value = name;
      notify(`已保存“${name}”，下次可一键带入整套参数。`);
      return true;
    }
    function applyQuoteReusableDraft(snapshot) {
      const draft = normalizeQuoteDraftSnapshot(snapshot);
      if (!draft) return false;
      if (draft.productConfiguration) restoreQuoteCatalogConfiguration(draft.productConfiguration);
      setIfExists("quoteProduct", draft.product);
      setIfExists("quoteSize", draft.size);
      setIfExists("quoteMaterial", draft.material);
      setIfExists("quoteProcess", draft.process);
      setIfExists("quoteQty", draft.qty);
      setIfExists("quoteTemplate", draft.template);
      setIfExists("quoteCostMode", draft.costMode || quoteRecommendedCostMode());
      setQuoteDeliveryValue(draft.delivery || state.settings.delivery || "7");
      setIfExists("quotePayment", draft.payment || state.settings.payment);
      setIfExists("quoteTaxMode", draft.taxMode || "待确认");
      setIfExists("quoteSampleMode", draft.sampleMode || "待确认");
      setIfExists("quoteSample", draft.sample || "0");
      setIfExists("quoteLevel", draft.customerLevel || "普通客户");
      renderIndustryQuoteCostFields();
      renderQuoteMaterialCostCheck(true);
      if (draft.materialCostDraft) restoreQuoteMaterialCostDraft(draft.materialCostDraft);
      setIfExists("quotePrintProductionSheetsOverride", draft.manualAdjustments?.productionSheets || "");
      setIfExists("quotePrintPlateCountOverride", draft.manualAdjustments?.plateCount || "");
      setIfExists("quoteManualAdjustmentReason", draft.manualAdjustments?.reason || "");
      renderQuoteQuantityPresets();
      renderQuoteReviewSummary();
      setQuoteProductChooser(false);
      quoteGuideHasResult = false;
      window.__activeQuoteRecordKey = "";
      markQuoteDirty();
      renderQuoteLiveDock();
      return true;
    }
    function applySelectedQuoteTemplate() {
      const id = String(document.getElementById("quoteSavedTemplateSelect")?.value || "");
      const template = quoteSavedTemplates().find(item => item.id === id);
      if (!template) {
        notify("请先选择一个已保存报价方案。");
        return false;
      }
      if (!applyQuoteReusableDraft(template.snapshot)) return false;
      const nameInput = document.getElementById("quoteSavedTemplateName");
      if (nameInput) nameInput.value = template.name;
      notify(`已带入“${template.name}”；客户名称未被覆盖，请核对本单数量和当前价格。`);
      return true;
    }
    function deleteSelectedQuoteTemplate() {
      const id = String(document.getElementById("quoteSavedTemplateSelect")?.value || "");
      const template = quoteSavedTemplates().find(item => item.id === id);
      if (!template) return notify("还没有可删除的报价方案。");
      if (!confirm(`确定删除“${template.name}”吗？\n\n只删除这个参数方案，不影响历史报价和价格表。`)) return false;
      state.settings.quoteSavedTemplates = quoteSavedTemplates().filter(item => item.id !== id);
      saveState();
      window.__saveSettingsCloud?.(state.settings);
      renderQuoteSavedTemplates();
      notify(`已删除“${template.name}”。`);
      return true;
    }
    function quoteLiveMoney(value) {
      return Number.isFinite(Number(value)) ? `¥${Number(value).toFixed(2)}` : "—";
    }
    function quoteLiveSelectedPrices() {
      const configuration = quoteCatalogConfigurationSnapshot();
      const profile = quoteCatalogProfile();
      const rows = [];
      if (configuration && profile) {
        profile.groups.filter(quoteCatalogGroupIsMaterial).forEach(group => {
          quoteCatalogSelectedLabels(group, quoteCatalogDraft()).forEach(label => {
            const price = quoteCatalogMaterialPriceSnapshot(label);
            rows.push({ name:label, configured:price.price !== null, value:quoteCatalogOptionPriceText(group, label), source:price.source || "" });
          });
        });
      }
      quoteCatalogSelectedOperationRequirements().forEach(item => {
        const group = profile?.groups?.find(candidate => quoteCatalogGroupPriceKind(candidate) === item.kind && quoteCatalogSelectedLabels(candidate, quoteCatalogDraft()).some(label => canonicalQuoteOperationLabel(label) === item.name));
        rows.push({ name:item.name, configured:item.configured, value:group ? quoteCatalogOptionPriceText(group, item.name) : (item.configured ? "已设价" : "待改价"), source:item.source || "" });
      });
      return rows.slice(0, 12);
    }
    function renderQuoteLiveDock(formalRecord = null) {
      const dock = document.getElementById("quoteLiveDock");
      if (!dock) return;
      const qty = Math.max(0, Number(val("quoteQty") || 0));
      const product = val("quoteProduct") || quoteCatalogEntry()?.name || "待选产品";
      const material = val("quoteMaterial") || "材料待确认";
      const process = val("quoteProcess") || "工艺待确认";
      const costMode = val("quoteCostMode") || "material";
      const manual = quoteManualAdjustments();
      const adjustmentPanel = document.getElementById("quoteManualAdjustments");
      adjustmentPanel?.classList.toggle("is-hidden", costMode !== "printing");
      adjustmentPanel?.classList.toggle("has-adjustment", manual.active);
      const reasonInput = document.getElementById("quoteManualAdjustmentReason");
      reasonInput?.classList.toggle("quote-field-error", manual.active && !manual.reason);
      setTextIfExists("quoteLiveDockTitle", quoteCatalogProfileIsPrintable() ? "印刷快速报价" : "当前报价看板");
      const current = document.getElementById("quoteLiveCurrent");
      if (current) current.innerHTML = `<strong>${escapeHtml(product)}</strong><br>${escapeHtml(material)} · ${qty > 0 ? `${qty.toLocaleString()}个` : "数量待填"}<br><span>${escapeHtml(process)}</span>`;
      let materialCost = NaN;
      let processCost = NaN;
      let estimatedCost = NaN;
      let estimateError = "";
      if (qty > 0) {
        if (costMode === "printing") {
          const breakdown = printingQuoteCostBreakdown(qty);
          if (breakdown.ok) {
            materialCost = Number(breakdown.materialCost || 0);
            processCost = Number(breakdown.printingCost || 0) + Number(breakdown.operationCost || 0)
              + Number(breakdown.laborCost || 0) + Number(breakdown.machineCost || 0) + Number(breakdown.extraDirectCost || 0);
            estimatedCost = materialCost + processCost;
          } else estimateError = breakdown.error || "印刷参数待补齐";
        } else if (costMode === "industry") {
          const breakdown = industryQuoteCostBreakdown(qty);
          if (breakdown.ok) {
            materialCost = Number(breakdown.materialCost || 0);
            processCost = Number(breakdown.operationCost || 0) + Number(breakdown.laborCost || 0)
              + Number(breakdown.machineCost || 0) + Number(breakdown.extraDirectCost || 0);
            estimatedCost = materialCost + processCost;
          } else estimateError = breakdown.error || "行业公式参数待补齐";
        } else {
          const materialCalculation = costMode === "unit"
            ? { ok:Number(val("quoteMaterialUnitCost") || 0) > 0, totalCost:Number(val("quoteMaterialUnitCost") || 0) * qty, error:"按件材料成本待核对" }
            : quoteMaterialConversion();
          if (materialCalculation.ok) materialCost = Number(materialCalculation.totalCost || 0);
          else estimateError = materialCalculation.error || "材料成本待补齐";
          const operationCalculations = quoteCatalogSelectedOperationRequirements().map(item => quoteOperationPriceCalculation(item, qty));
          const configured = operationCalculations.filter(item => item.ok);
          processCost = configured.reduce((sum, item) => sum + Number(item.total || 0), 0);
          if (Number.isFinite(materialCost)) estimatedCost = materialCost + processCost;
        }
      }
      setTextIfExists("quoteLiveMaterialCost", Number.isFinite(materialCost) ? quoteLiveMoney(materialCost) : "待补数据");
      setTextIfExists("quoteLiveProcessCost", Number.isFinite(processCost) ? quoteLiveMoney(processCost) : "待补数据");
      setTextIfExists("quoteLiveEstimatedCost", Number.isFinite(estimatedCost) ? quoteLiveMoney(estimatedCost) : "待补数据");
      const active = formalRecord || currentDraftQuoteRecord();
      setTextIfExists("quoteLiveFormalPrice", active ? `${quoteLiveMoney(active.total)} · ${quoteLiveMoney(active.unit)}/个` : "尚未生成");
      const prices = quoteLiveSelectedPrices();
      const priceList = document.getElementById("quoteLivePriceList");
      if (priceList) priceList.innerHTML = prices.length ? prices.map(item => `<div class="quote-live-price-item${item.configured ? "" : " is-missing"}"><span>${escapeHtml(item.name)}<em>${escapeHtml(item.source || (item.configured ? "本厂价格" : "尚未设价"))}</em></span><strong>${escapeHtml(item.value || (item.configured ? "已设价" : "待改价"))}</strong></div>`).join("") : "已选材料、印刷和工艺的当前单价会显示在这里。";
      const status = document.getElementById("quoteLiveDockStatus");
      const ready = qty > 0 && Number.isFinite(estimatedCost) && (!manual.active || Boolean(manual.reason));
      if (status) {
        status.textContent = ready ? "可正式核价" : manual.active && !manual.reason ? "请填修正原因" : "待补参数";
        status.classList.toggle("is-ready", ready);
        status.classList.toggle("is-warning", !ready && Boolean(estimateError || manual.active));
      }
      setTextIfExists("quoteLiveFootnote", estimateError
        ? `当前还不能完整核价：${estimateError}。已填内容不会丢失。`
        : "实时金额只用于核对成本；税费、毛利、客户协议价和审批规则在正式生成时统一处理。");
    }
    function scheduleQuoteLiveDock() {
      window.clearTimeout(quoteLiveDockTimer);
      quoteLiveDockTimer = window.setTimeout(() => renderQuoteLiveDock(), 100);
    }
    function initializeQuoteExpressWorkspace() {
      renderQuoteSavedTemplates();
      const root = document.getElementById("quoteWorkspaceRoot");
      if (!root || root.dataset.expressBound === "1") {
        renderQuoteLiveDock();
        return;
      }
      root.dataset.expressBound = "1";
      const update = event => {
        const id = String(event.target?.id || "");
        if (["quotePrintProductionSheetsOverride", "quotePrintPlateCountOverride", "quoteManualAdjustmentReason"].includes(id)) markQuoteDirty();
        scheduleQuoteLiveDock();
      };
      root.addEventListener("input", update);
      root.addEventListener("change", update);
      renderQuoteLiveDock();
    }
    function quoteSessionAttachmentSignature(session = null) {
      session = session || activeCustomerSession();
      if (!session) return "";
      const records = (Array.isArray(session.userFileRecords) ? session.userFileRecords : []).map(item => [
        Number(item?.id || 0), String(item?.key || ""), String(item?.name || ""), String(item?.mimeType || "")
      ]);
      const userIds = (Array.isArray(session.userFileIds) ? session.userFileIds : []).map(Number).filter(Boolean);
      const userNames = (Array.isArray(session.userFileNames) ? session.userFileNames : []).map(String);
      const retainedIds = (Array.isArray(session.retainedFileIds) ? session.retainedFileIds : []).map(Number).filter(Boolean);
      const retainedNames = (Array.isArray(session.retainedFileNames) ? session.retainedFileNames : []).map(String);
      if (!records.length && !userIds.length && !userNames.length && !retainedIds.length && !retainedNames.length) return "";
      return JSON.stringify({ records, userIds, userNames, retainedIds, retainedNames });
    }
    function captureHomeInlineQuoteDraft(session = activeCustomerSession()) {
      const workspace = document.getElementById("homeInlineQuoteWorkspace");
      const root = document.getElementById("quoteWorkspaceRoot");
      if (!session || !workspace || !root || workspace.classList.contains("is-hidden") || !quoteWorkspaceIsInline()) return false;
      if (String(workspace.dataset.sessionId || "") !== String(session.id || "")) return false;
      const fields = {};
      root.querySelectorAll("input[id], select[id], textarea[id], details[id]").forEach(control => {
        if (control.tagName === "DETAILS") {
          fields[control.id] = { kind: "details", open: Boolean(control.open) };
          return;
        }
        if (String(control.type || "").toLowerCase() === "file") return;
        fields[control.id] = {
          kind: "field",
          value: String(control.value ?? ""),
          checked: Boolean(control.checked),
          checkable: ["checkbox", "radio"].includes(String(control.type || "").toLowerCase())
        };
      });
      const context = quoteBusinessContext();
      session.quoteDraft = {
        sourceText: String(context.sourceText || activeHomeBusinessSource(session) || "").trim(),
        fields,
        guideStep: quoteGuideStep,
        guideHasResult: quoteGuideHasResult,
        guideDirection: quoteGuideDirection,
        catalogCategoryId: quoteCatalogCategoryId,
        catalogProductId: quoteCatalogProductId,
        catalogSearchText: quoteCatalogSearchText,
        catalogDrafts: copyQuoteDraftValue(quoteCatalogDrafts),
        attachmentSignature: quoteSessionAttachmentSignature(session),
        industryCategory: currentIndustryCategory(),
        factoryType: String(state.settings.factoryType || ""),
        uncertainFields: copyQuoteDraftValue(window.__quoteUncertainFields, []),
        uncertainSource: String(window.__quoteUncertainSource || "ai"),
        showAllFields: window.__quoteShowAllFields === true,
        industryTarget: copyQuoteDraftValue(window.__quoteIndustryTarget, null),
        pendingQuoteCosts: copyQuoteDraftValue(window.__pendingQuoteCosts, []),
        missingCostsVisible: !document.getElementById("quoteMissingCostsPanel")?.classList.contains("is-hidden"),
        businessContext: copyQuoteDraftValue(context, {}),
        quoteIntakeExpanded: Boolean(window.__quoteIntakeExpanded),
        activeQuoteRecordKey: String(window.__activeQuoteRecordKey || ""),
        capturedAt: new Date().toISOString()
      };
      return true;
    }
    function restoreHomeInlineQuoteDraft(session = activeCustomerSession()) {
      const draft = session?.quoteDraft;
      const workspace = document.getElementById("homeInlineQuoteWorkspace");
      if (!draft || !workspace) return false;
      const draftCategory = industryCategories[draft.industryCategory]
        ? draft.industryCategory
        : currentIndustryCategory();
      const draftTemplates = industryCategories[draftCategory] || industryCategories["包装印刷"];
      const draftTemplate = draftTemplates.includes(draft.factoryType) ? draft.factoryType : draftTemplates[0];
      activateIndustryQuoteProfile(draftCategory, draftTemplate);
      saveState();
      renderIndustryCategories();
      quoteGuideStep = Math.max(1, Math.min(4, Number(draft.guideStep || 1)));
      quoteGuideHasResult = draft.guideHasResult === true;
      quoteGuideDirection = draft.guideDirection === "prev" ? "prev" : "next";
      quoteCatalogCategoryId = String(draft.catalogCategoryId || "");
      quoteCatalogProductId = String(draft.catalogProductId || "");
      quoteCatalogSearchText = String(draft.catalogSearchText || "");
      Object.keys(quoteCatalogDrafts).forEach(key => delete quoteCatalogDrafts[key]);
      Object.assign(quoteCatalogDrafts, copyQuoteDraftValue(draft.catalogDrafts, {}));
      const restoredCatalogEntry = quoteCatalogEntry(quoteCatalogProductId);
      if (restoredCatalogEntry) {
        window.JIANGLAI_QUOTE_RECOMMENDER?.apply({
          product: restoredCatalogEntry,
          profile: quoteCatalogProfile(restoredCatalogEntry),
          draft: quoteCatalogDraft(restoredCatalogEntry.id)
        });
      }
      window.__quoteBusinessContext = copyQuoteDraftValue(draft.businessContext, {});
      window.__quoteBusinessContext.customer = session.customer || window.__quoteBusinessContext.customer || "新客户";
      window.__quoteBusinessContext.sessionId = session.id || "";
      window.__quoteIntakeExpanded = draft.quoteIntakeExpanded === true;
      window.__activeQuoteRecordKey = String(draft.activeQuoteRecordKey || "");
      window.__quoteUncertainFields = copyQuoteDraftValue(draft.uncertainFields, []);
      window.__quoteUncertainSource = String(draft.uncertainSource || "ai");
      window.__quoteShowAllFields = draft.showAllFields === true;
      window.__quoteMaterialCostSignature = "";
      window.__quoteIndustryTarget = null;
      setQuoteIndustryWarning("");
      setQuoteCostWarning("");
      clearQuoteAiFileSelection();
      showHomeResult();
      mountQuoteWorkspace("home");
      workspace.classList.remove("is-hidden");
      workspace.dataset.sessionId = String(session.id || "");
      setHomeInlineQuoteLayout(true);
      renderCurrentQuoteWorkspace();
      const pendingQuoteCosts = copyQuoteDraftValue(draft.pendingQuoteCosts, []);
      if (draft.missingCostsVisible === true && pendingQuoteCosts.length) {
        const missingMaterial = pendingQuoteCosts.find(item => item?.type === "material")?.name || "";
        const missingProcesses = pendingQuoteCosts.filter(item => item?.type === "process").map(item => ({ name: item.name, label: item.displayName || item.name, kind: item.kind })).filter(item => item.name);
        showMissingQuoteCosts(missingMaterial, missingProcesses, { focus: false });
      } else {
        hideMissingQuoteCosts();
      }
      const restoreDraftFields = () => Object.entries(draft.fields || {}).forEach(([id, saved]) => {
          const control = document.getElementById(id);
          if (!control || !saved) return;
          if (saved.kind === "details") {
            control.open = saved.open === true;
            return;
          }
          if (String(control.type || "").toLowerCase() === "file") return;
          control.value = String(saved.value ?? "");
          if (saved.checkable === true) control.checked = saved.checked === true;
        });
      // 第一遍先恢复成本模式等稳定字段，再创建该行业/模式专属的动态成本控件。
      restoreDraftFields();
      renderIndustryQuoteCostFields();
      // 第二遍把 BOM、印刷展开尺寸、机时等刚创建的动态字段完整写回。
      restoreDraftFields();
      setIfExists("quoteCustomerName", session.customer || "新客户");
      const draftIndustryTarget = copyQuoteDraftValue(draft.industryTarget, null);
      if (draftIndustryTarget?.category && industryCategories[draftIndustryTarget.category]) {
        setQuoteIndustryWarning(
          `AI识别到这是“${draftIndustryTarget.category}”需求，但当前使用的是“${currentIndustryCategory()}”价格表。`,
          draftIndustryTarget.category,
          draftIndustryTarget.template || ""
        );
      }
      renderQuoteReviewSummary();
      renderQuoteMissingFirst();
      if (window.__activeQuoteRecordKey) restoreExactSavedQuoteResult();
      else renderQuoteGuide();
      syncHomeConversationLayout();
      scrollHomeReplyAboveComposer(workspace);
      return true;
    }
    function mountQuoteWorkspace(target = "page") {
      const root = document.getElementById("quoteWorkspaceRoot");
      const mount = document.getElementById(target === "home" ? "homeInlineQuoteMount" : "quotePageWorkspaceMount");
      if (!root || !mount || root.parentElement === mount) return;
      mount.appendChild(root);
    }
    function isQuoteWorkspaceMountedAtHome() {
      const root = document.getElementById("quoteWorkspaceRoot");
      const homeMount = document.getElementById("homeInlineQuoteMount");
      return Boolean(root && homeMount && root.parentElement === homeMount);
    }
    function renderCurrentQuoteWorkspace() {
      renderQuoteTemplateOptions();
      renderQuoteHistory();
      updateQuoteSourceNote();
      renderIndustryQuoteCostFields();
      renderQuoteGuide();
      renderQuoteCustomerMemory();
      renderQuoteLinkedContext();
      renderQuoteCatalogSetup();
    }
    function openHomeInlineQuoteWorkspace() {
      const workspace = document.getElementById("homeInlineQuoteWorkspace");
      if (!workspace) return;
      showHomeResult();
      mountQuoteWorkspace("home");
      workspace.classList.remove("is-hidden");
      workspace.dataset.sessionId = String(activeCustomerSession()?.id || "");
      setHomeInlineQuoteLayout(true);
      renderCurrentQuoteWorkspace();
      syncHomeConversationLayout();
      scrollHomeReplyAboveComposer(workspace);
    }
    function closeHomeInlineQuoteWorkspace(options = {}) {
      const workspace = document.getElementById("homeInlineQuoteWorkspace");
      if (!workspace) return;
      if (options.skipCapture !== true) {
        captureHomeInlineQuoteDraft(activeCustomerSession());
        saveState();
      }
      workspace.classList.add("is-hidden");
      delete workspace.dataset.sessionId;
      setHomeInlineQuoteLayout(false);
      clearQuoteAiFileSelection();
      mountQuoteWorkspace("page");
      syncHomeConversationLayout();
      if (!options.silent) scrollHomeReplyAboveComposer(document.querySelector(".chat-assistant-row"));
    }
    function syncHomeInlineQuoteWorkspaceSession(session) {
      const workspace = document.getElementById("homeInlineQuoteWorkspace");
      if (!workspace || workspace.classList.contains("is-hidden")) return;
      if (String(workspace.dataset.sessionId || "") !== String(session?.id || "")) {
        closeHomeInlineQuoteWorkspace({ silent: true, skipCapture: true });
      }
    }
    function renderQuoteLinkedContext() {
      const box = document.getElementById("quoteLinkedContext");
      if (!box) return;
      const context = quoteBusinessContext();
      const linked = isQuoteWorkspaceMountedAtHome()
        && ["home", "task"].includes(context.origin)
        && Boolean(context.customer);
      box.classList.toggle("is-hidden", !linked);
      if (!linked) return;
      const sourceSummary = String(context.sourceText || val("quoteAiSource") || "客户需求已带入").replace(/\s+/g, " ").slice(0, 90);
      box.innerHTML = `<div><strong>AI已从首页整理：${escapeHtml(context.customer)} · 同一笔生意</strong><span>${escapeHtml(sourceSummary)}${sourceSummary.length >= 90 ? "…" : ""} · 请在下方人工核对参数</span></div>`;
    }
    function openQuoteWorkspace() {
      window.__quoteBusinessContext = null;
      showPage("报价系统");
    }
    function homeGoQuote() {
      const source = homeSourceText("") || document.getElementById("homeActionQuote")?.dataset?.source || "";
      if (!source && !homeFileNames().length) {
        showHomeResult();
        setTextIfExists("aiAnswer", "先把客户发来的聊天内容粘进来，或者上传截图、产品图、Logo、图纸。AI 看完以后再去报价会更准确。");
        document.getElementById("aiQuestion")?.focus();
        return;
      }
      const session = activeCustomerSession();
      const normalizedSource = String(source || "").trim();
      const attachmentSignature = quoteSessionAttachmentSignature(session);
      if (
        session?.quoteDraft
        && String(session.quoteDraft.sourceText || "").trim() === normalizedSource
        && String(session.quoteDraft.attachmentSignature || "") === attachmentSignature
        && restoreHomeInlineQuoteDraft(session)
      ) {
        notify("已恢复这位客户上次未完成的报价，已填内容没有丢失。");
        return;
      }
      setQuoteBusinessContext("home", null, source);
      setIfExists("customerText", source || "客户发来了图片/文件，需要先确认报价资料并生成报价。");
      setIfExists("inquiryCustomerName", (document.getElementById("homeCustomerName")?.value || "新客户").trim() || "新客户");
      try {
        applyInquiryToQuote({ inline: true });
      } catch (error) {
        showHomeResult();
        setTextIfExists("aiAnswer", "客户原话已经保留，但报价工作台暂时没有展开。请原地重试，不需要重新输入。" );
        notify("报价工作台暂时没有展开，请原地重试。" );
      }
    }
    function homeGoVisual() {
      const source = homeSourceText("") || document.getElementById("homeActionQuote")?.dataset?.source || "";
      if (!source && !homeFileNames().length && !homeRetainedFileIds().length) {
        showHomeResult();
        setTextIfExists("aiAnswer", "请在这里上传参考图，或者直接写清楚产品、画面和风格要求。填好后仍在首页生成，不会跳到其他页面。");
        document.getElementById("aiQuestion")?.focus();
        return;
      }
      if (source) setIfExists("customerText", source);
      if (typeof window.generateHomeVisualInline === "function") return window.generateHomeVisualInline(source, { generationMode: "mockup" });
      showHomeResult();
      setTextIfExists("aiAnswer", "效果图服务正在连接，请稍后在当前对话重试，不需要进入其他页面。");
    }
    function homeGoDetail() {
      syncHomeToInquiry("客户发来了图片/文件，需要整理宝贝详情页。");
      showPage("询盘处理");
      generateDetailPage();
    }
    function homeGoDetailPackage() {
      const source = homeSourceText("客户发来了产品图片/资料，需要生成淘宝或1688整套图文详情页。");
      if (!source && !homeFileNames().length && !homeRetainedFileIds().length) {
        showHomeResult();
        setTextIfExists("aiAnswer", "先上传产品图，并写清产品名称、尺寸、材质、工艺和准备发布的平台，然后再点“一键详情”。");
        document.getElementById("aiQuestion")?.focus();
        return;
      }
      if (typeof window.prepareHomeDetailPackageFromComposer === "function") {
        return window.prepareHomeDetailPackageFromComposer(source, window.__latestAiAnalysis || null);
      }
      if (typeof window.prepareHomeDetailPackage === "function") {
        return window.prepareHomeDetailPackage(source, window.__latestAiAnalysis || null, homeRetainedFileIds());
      }
      syncHomeToInquiry("客户发来了产品图片/资料，需要生成淘宝或1688整套图文详情页。");
      setIfExists("contentGoal", "做宝贝详情");
      showPage("询盘处理");
    }
    function homeGoVector() {
      fillVectorFromHome();
      showPage("图片转矢量");
    }
    function homeChatTaskSource(session = activeCustomerSession()) {
      return session?.id ? `home_chat:${session.id}` : "";
    }
    const customerTaskStageOrder = ["询盘", "待补资料", "待报价", "已报价", "待确认", "打样", "生产", "待质检", "待发货", "已发货", "已签收", "待回款", "已回款", "已完成", "已归档"];
    function customerTaskStageRank(status = "") {
      const index = customerTaskStageOrder.indexOf(String(status || ""));
      return index < 0 ? 0 : index + 1;
    }
    function customerTasksByName(customer = "") {
      const name = String(customer || "").trim();
      if (!name) return [];
      return businessTasks().filter(task => (task.cloudCustomer || String(task.customer || "").split(" · ")[0]) === name);
    }
    function preferredCustomerTask(tasks = []) {
      return [...tasks].sort((left, right) => {
        const stageDelta = customerTaskStageRank(right?.status) - customerTaskStageRank(left?.status);
        if (stageDelta) return stageDelta;
        return (Date.parse(right?.updatedAt || right?.createdAt || "") || 0) - (Date.parse(left?.updatedAt || left?.createdAt || "") || 0);
      })[0] || null;
    }
    function homeChatTaskForSession(session = activeCustomerSession()) {
      if (!session) return null;
      const source = homeChatTaskSource(session);
      const direct = businessTasks().find(task =>
        (source && task.source === source)
        || (Number(session.followupTaskId || 0) > 0 && Number(task.serverId || 0) === Number(session.followupTaskId))
      );
      if (direct) return direct;
      // A customer may have several products or repeat orders. Once this
      // session contains its own business conversation, never borrow another
      // session's task merely because the customer name is the same.
      if (session.hasConversation || session.businessSourceText || session.businessAnalysis) return null;
      const customer = String(session.customer || "").trim();
      if (!customer || ["新客户", "未命名客户"].includes(customer) || isPendingCustomerName(customer)) return null;
      return preferredCustomerTask(customerTasksByName(customer));
    }
    function viewHomeChatTask() {
      const task = homeChatTaskForSession();
      if (task) openTaskDetail(task.id);
      else showPage("跟单工单");
    }
    function normalizedHomeChatActionStatus(message = "", savedTask = null) {
      const value = String(message || "").trim();
      const legacySavedStatuses = new Set([
        "已加入跟单列表，不会重复创建",
        "已保存到跟单列表，点击按钮即可查看"
      ]);
      if (!legacySavedStatuses.has(value)) return value;
      return savedTask
        ? "已自动记录，报价和生产进度都会更新到这里"
        : "原进度记录暂未找到，发出新消息后会自动重新建立";
    }
    function renderHomeChatRecordActions() {
      const session = activeCustomerSession();
      const savedTask = homeChatTaskForSession(session);
      const saveButton = document.getElementById("homeSaveChatTaskButton");
      const clearButton = document.getElementById("homeClearTodayChatButton");
      const status = document.getElementById("homeChatRecordStatus");
      if (saveButton && saveButton.dataset.busy !== "true") {
        saveButton.textContent = savedTask
          ? "查看生意进度"
          : session?.chatAutoRecordPending
            ? "正在自动记录…"
            : session?.hasConversation
              ? "记录生意进度"
              : "查看生意进度";
        saveButton.classList.toggle("is-saved", !!savedTask);
        saveButton.title = savedTask ? "查看这笔生意当前的下一步" : "发出客户消息后会自动建立进度记录";
        saveButton.disabled = Boolean(session?.chatAutoRecordPending || (!session?.hasConversation && !savedTask));
      }
      if (clearButton && clearButton.dataset.busy !== "true") clearButton.disabled = false;
      if (session && savedTask?.serverId) session.followupTaskId = Number(savedTask.serverId);
      if (status) {
        status.textContent = normalizedHomeChatActionStatus(session?.chatActionStatus, savedTask)
          || (savedTask
            ? "已自动记录，报价和生产进度都会更新到这里"
            : session?.chatAutoRecordPending
              ? "正在自动建立生意进度，不需要重复保存"
              : session?.hasConversation
                ? "这笔历史沟通还没有进度记录，可点击补建；新消息会自动记录"
              : "发出客户消息后会自动记录；清空聊天不会删除客户资料");
      }
    }
    async function saveCurrentHomeChatToTask() {
      const session = activeCustomerSession();
      if (!session) return;
      if (homeChatTaskForSession(session)) return viewHomeChatTask();
      const sourceText = String(session.businessSourceText || window.__latestAiSourceText || session.lastMessage || "").trim();
      if (!session.hasConversation && !sourceText) {
        notify("请先发送一条客户消息，聊完后再保存到跟单。未发送的草稿不会被当成跟单。" );
        document.getElementById("aiQuestion")?.focus();
        return;
      }
      if (typeof window.__saveHomeChatTaskConnected !== "function") {
        notify("跟单服务正在连接，请稍后再点一次。" );
        return;
      }
      const button = document.getElementById("homeSaveChatTaskButton");
      const status = document.getElementById("homeChatRecordStatus");
      if (button?.dataset.busy === "true") return;
      if (button) {
        button.dataset.busy = "true";
        button.disabled = true;
        button.textContent = "正在保存…";
      }
      if (status) status.textContent = "正在建立生意进度，当前页面不会跳走";
      try {
        await window.__saveHomeChatTaskConnected(session);
      } catch (err) {
        if (status) status.textContent = `保存失败：${err.message}`;
        notify(`保存跟单失败：${err.message}`);
      } finally {
        if (button) {
          button.dataset.busy = "false";
          button.disabled = false;
        }
        renderHomeChatRecordActions();
      }
    }
    async function clearCurrentHomeTodayChat() {
      const session = activeCustomerSession();
      if (!session) return;
      const customer = String(session.customer || currentCustomerName() || "当前客户").trim() || "当前客户";
      if (!confirm(`确定清空“${customer}”今天的聊天内容吗？\n\n只会删除今天的聊天；客户档案、报价、跟单和已上传文件都会保留。输入框里尚未发送的草稿也会保留。`)) return;
      if (typeof window.__clearHomeTodayChatConnected !== "function") {
        notify("聊天管理服务正在连接，请稍后再试。" );
        return;
      }
      const button = document.getElementById("homeClearTodayChatButton");
      const status = document.getElementById("homeChatRecordStatus");
      if (button?.dataset.busy === "true") return;
      if (button) {
        button.dataset.busy = "true";
        button.disabled = true;
        button.textContent = "正在清空…";
      }
      if (status) status.textContent = "正在清空今天聊天，其他客户资料不会受影响";
      try {
        await window.__clearHomeTodayChatConnected(session);
      } catch (err) {
        if (status) status.textContent = `清空失败：${err.message}`;
        notify(`清空今天聊天失败：${err.message}`);
      } finally {
        if (button) {
          button.dataset.busy = "false";
          button.textContent = "清空今天聊天";
          button.disabled = false;
        }
        renderHomeChatRecordActions();
      }
    }
    function homeCreateTask() {
      return saveCurrentHomeChatToTask();
    }
    function copyHomeAnswer() {
      const output = document.getElementById("aiAnswer")?.textContent || "";
      navigator.clipboard?.writeText(output);
      alert("AI结果已复制。");
    }
    function askFactoryAi() {
      const files = homeFileNames();
      const question = val("aiQuestion") || (files.length ? "客户上传了图片/视频/图纸文件，帮我判断下一步。" : "");
      const answer = document.getElementById("aiAnswer");
      if (!answer) return;
      showHomeResult();
      if (!question) {
        answer.textContent = "把客户问题、产品要求、报价疑问或工艺风险写上来，AI 会先帮你拆下一步。";
        return;
      }
      if (isCasualAiQuestion(question, files)) {
        renderCasualAiReply(question);
        return;
      }
      const info = analyze(question);
      const actions = [];
      if (hasAny(question, ["报价", "多少钱", "价格", "单价", "算价格"])) actions.push("先确认尺寸、数量、材质、工艺、税费和交期，再进入“报价”生成报价单；运费统一另计。");
      if (hasAny(question, ["怎么回", "回复", "回客户", "客户"])) actions.push("可以先这样回：我们可以先按这个需求评估，麻烦确认尺寸、数量、材质、工艺、是否含税和期望交期。本报价不含运费。");
      if (hasAny(question, ["压价", "便宜", "优惠", "降价"])) actions.push("客户压价时，不要直接降价。先问数量是否增加、是否可调整材质/工艺/税运口径，再给阶梯价或最低可接范围。");
      if (hasAny(question, ["效果图", "图片", "主图", "详情", "海报", "做图"])) actions.push("这是图片需求。点“发送给 AI”会自动带到效果图页面，也可以在客户需求结果里点“下一步：做效果图”。");
      if (hasAny(question, ["矢量", "CDR", "cdr", "Logo", "logo", "转文件", "雕刻"])) actions.push("这是制前文件处理需求，可以在“制图 > 图片转矢量/CDR”上传客户Logo或图片，转换成 CDR/SVG/AI/PDF 文件。");
      if (hasAny(question, ["详情页", "宝贝详情", "卖点", "标题"])) actions.push("详情页建议先写：产品用途、材质工艺、尺寸说明、定制流程、打样说明、下单须知和风险提醒。");
      if (hasAny(question, ["值不值得接", "要不要接", "能不能接", "该不该接", "看看风险"])) actions.push("先看四点：资料是否齐全、交期是否紧、利润是否够、工艺风险是否可控。资料不全或急单高风险，先补资料再报价。");
      if (hasAny(question, ["缺什么", "补资料", "资料不全"])) actions.push("报价前通常要补：尺寸、数量、材质、工艺/Logo位置、是否含税、设计稿/样品图、是否打样和期望交期；运费统一另计。");
      if (hasAny(question, ["打样", "样品", "确认样"])) actions.push("打样前要确认打样费、是否抵扣、样品数量、寄样地址、签样标准和预计寄出时间。");
      if (hasAny(question, ["交期", "多久", "加急", "下周"])) actions.push("交期要从资料齐全、定金到账、样品/效果图确认后开始算；急单要先查排产，不要直接承诺。");
      if (hasAny(question, ["付款", "定金", "尾款", "账期"])) actions.push("付款建议写清定金比例、尾款时间、是否开票以及未付款前是否排产；运费不计入报价。");
      if (hasAny(question, ["跟单", "生成跟单", "建工单"])) actions.push("可以把这条客户需求生成跟单任务，后面继续追报价、打样、生产、发货和回款。");
      if (hasAny(question, ["生产注意", "车间", "生产", "制前"])) actions.push("生产前要确认图纸尺寸、公差、材料批次、工艺位置、包装方式、质检标准和交期节点，特殊工艺建议先做首件确认。");
      if (hasAny(question, ["图纸", "设计稿", "工艺", "风险", "能不能做"])) actions.push("建议进入“制图 > 制前检查”，先核图纸、材料、公差、表面处理和交期风险。");
      const nextActions = actions.length ? actions : ["如果这是客户完整需求，点“发送给 AI”；如果只是临时问题，可以继续直接问AI。"];
      answer.innerHTML = `
        <div class="ai-report">
          <div class="ai-report-card">
            <span>业务判断报告</span>
            <strong>${escapeHtml(info.product)}</strong>
            <p>${escapeHtml(info.size)}，${escapeHtml(info.qty)}<br>${escapeHtml(info.material)}，${escapeHtml(info.finish)}</p>
          </div>
          <div class="ai-report-card">
            <span>风险提醒</span>
            <p>${escapeHtml(info.risk.join("；"))}</p>
          </div>
          <div class="ai-report-card">
            <span>文件状态</span>
            <p>${escapeHtml(files.length ? files.join("、") : "未上传文件")}</p>
          </div>
          <div class="ai-report-card">
            <span>建议动作</span>
            <ol>${nextActions.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ol>
          </div>
        </div>
      `.trim();
      hideHomeNextActions();
    }
    function setRuleMode(mode) {
      ruleMode = mode === "advanced" ? "advanced" : "simple";
      document.getElementById("ruleSimpleBtn")?.classList.toggle("active", ruleMode === "simple");
      document.getElementById("ruleAdvancedBtn")?.classList.toggle("active", ruleMode === "advanced");
      document.querySelectorAll(".advanced-settings").forEach(el => el.classList.toggle("is-hidden", ruleMode === "simple"));
      renderRuleGuide();
      renderQuoteRules();
    }
    function choosePriceSetup(method) {
      priceSetupMethod = ["import", "manual"].includes(method) ? method : "";
      document.getElementById("priceSetupChoice")?.classList.toggle("is-hidden", Boolean(priceSetupMethod));
      document.getElementById("priceConfidenceStrip")?.classList.toggle("is-hidden", Boolean(priceSetupMethod));
      document.getElementById("priceImportCard")?.classList.toggle("is-hidden", priceSetupMethod !== "import");
      document.getElementById("priceImportBack")?.classList.toggle("is-hidden", priceSetupMethod !== "import");
      document.getElementById("priceManualSetup")?.classList.toggle("is-hidden", priceSetupMethod !== "manual");
      document.getElementById("priceManualBack")?.classList.toggle("is-hidden", priceSetupMethod !== "manual");
      if (priceSetupMethod === "manual") {
        if (state.settings.configured) ruleGuidePage = 5;
        renderRuleGuide();
      }
      renderCostLibrary();
      const target = priceSetupMethod === "import"
        ? document.getElementById("priceImportCard")
        : priceSetupMethod === "manual"
          ? document.getElementById("ruleGuideHeader")
          : document.getElementById("priceSetupChoice");
      window.setTimeout(() => target?.scrollIntoView({ behavior: "smooth", block: "start" }), 40);
    }
    function renderPriceQuickSearch() {
      const box = document.getElementById("priceQuickResults");
      if (!box) return;
      const query = (document.getElementById("priceQuickSearch")?.value || "").trim().toLowerCase();
      const items = [
        ...parseRules(state.settings.materialRules).map(item => ({ ...item, kind:"material", type:"材料" })),
        ...parseRules(state.settings.processRules).map(item => ({ ...item, kind:"process", type:"工艺" })),
        { name:"完全人工成本", price:Number(state.settings.laborHourly || 0), kind:"labor", type:"人工工资" }
      ].filter(item => !query || item.name.toLowerCase().includes(query)).slice(0, query ? 12 : 6);
      box.innerHTML = items.map((item, index) => `<div class="price-quick-row"><span>${escapeHtml(item.name)}<em>${item.type} · 单个成本</em></span><input id="priceQuickValue${index}" inputmode="decimal" value="${Number(item.price).toFixed(3)}" aria-label="${escapeHtml(item.name)}单个成本"><button class="btn light price-quick-save" type="button" data-price-kind="${escapeHtml(item.kind)}" data-price-name="${escapeHtml(item.name)}" data-price-input="priceQuickValue${index}">保存</button></div>`).join("") || `<div class="muted" style="padding:10px">没有找到这个材料或工艺，可以进入“修改当前价格表”新增。</div>`;
      box.querySelectorAll(".price-quick-save").forEach(button => {
        button.addEventListener("click", () => savePriceQuickItem(
          button.dataset.priceKind || "",
          encodeURIComponent(button.dataset.priceName || ""),
          button.dataset.priceInput || ""
        ));
      });
    }
    function savePriceQuickItem(kind, nameCode, inputId) {
      const name = decodeURIComponent(nameCode);
      const price = Number((document.getElementById(inputId)?.value || "").replace(/[^\d.]/g, ""));
      if (!(price > 0)) return notify("单个成本必须大于0。");
      if (kind === "labor") {
        setIfExists("ruleLaborHourly", price);
        const confirmation = document.getElementById("ruleCostConfirmation");
        if (confirmation) confirmation.checked = true;
        if (saveQuoteRules() === false) return;
        renderPriceSetupEntry();
        renderPriceQuickSearch();
        return notify(`完全人工成本已更新为 ¥${price.toFixed(2)}/小时。`);
      }
      const key = kind === "material" ? "materialRules" : "processRules";
      const rules = parseRules(state.settings[key]);
      const target = rules.find(item => item.name === name);
      if (!target) return notify("没有找到这条价格，请刷新后重试。");
      target.price = price;
      const updatedRules = rules.map(item => `${item.name}=${Number(item.price).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}`).join("\n");
      setIfExists(kind === "material" ? "ruleMaterials" : "ruleProcesses", updatedRules);
      const confirmation = document.getElementById("ruleCostConfirmation");
      if (confirmation) confirmation.checked = true;
      const saved = saveQuoteRules();
      if (!saved) return;
      renderPriceSetupEntry();
      renderPriceQuickSearch();
      notify(`${name}已更新为 ¥${price.toFixed(3)}/个，下一次报价自动使用。`);
    }
    function renderPriceSetupEntry() {
      const configured = Boolean(quoteLegacyRulesConfirmed() || ensureCostLibrary().activeVersionId);
      document.getElementById("priceQuickEdit")?.classList.toggle("is-hidden", !configured);
      if (!configured) {
        setTextIfExists("priceSetupKicker", "先选择一种设置方式");
        setTextIfExists("priceSetupTitle", "你的工厂现在有现成价格表吗？");
        setTextIfExists("priceSetupDescription", "可以先录常用价格，也可以报价时按本单补齐；未用到的材料、工艺和工资以后再填。运费统一另计。");
        setTextIfExists("priceImportOptionTitle", "有价格表，直接导入");
        setTextIfExists("priceImportOptionDescription", "上传 Excel 或 CSV，系统自动识别材料、工艺和人工工资。");
        setTextIfExists("priceImportOptionBadge", "推荐 · 最快完成");
        setTextIfExists("priceManualOptionTitle", "没有价格表，手动设置");
        setTextIfExists("priceManualOptionDescription", "跟着5页引导，确认主营业务、材料、工艺和人工工资。");
        setTextIfExists("priceManualOptionBadge", "适合第一次整理成本");
        return;
      }
      if (quoteReferencePricePackActive()) {
        document.getElementById("priceQuickEdit")?.classList.add("is-hidden");
        const count = Number(state.settings.referencePricePack?.itemCount || ensureCostLibrary().items.length || 0);
        setTextIfExists("priceSetupKicker", "新账号已自动配好参考价");
        setTextIfExists("priceSetupTitle", `已预置 ${count} 项材料、印刷、工艺、人工和设备参考价`);
        setTextIfExists("priceSetupDescription", "可以直接估算，不需要第一次把价格全填完。参考价不代表本厂实际成本；报价时用到哪项，就在当前产品中直接修改哪项，修改后本厂价永久优先。");
        setTextIfExists("priceImportOptionTitle", "导入本厂现有价格表");
        setTextIfExists("priceImportOptionDescription", "一次批量替换参考价，适合已有 Excel 或 CSV 价格表的工厂。");
        setTextIfExists("priceImportOptionBadge", "可选 · 批量更新");
        setTextIfExists("priceManualOptionTitle", "查看全部参考价");
        setTextIfExists("priceManualOptionDescription", "按材料、工艺、人工或设备搜索，可逐项改成本厂价。");
        setTextIfExists("priceManualOptionBadge", "用到哪项改哪项");
        return;
      }
      renderPriceQuickSearch();
      const updated = state.settings.quoteRulesUpdatedAt
        ? new Date(state.settings.quoteRulesUpdatedAt).toLocaleString("zh-CN", { hour12: false })
        : "未记录";
      setTextIfExists("priceSetupKicker", "价格表已经在使用");
      setTextIfExists("priceSetupTitle", "今天要修改哪一种价格？");
      setTextIfExists("priceSetupDescription", `当前价格表最后确认：${updated}。修改后必须重新保存，才会用于下一次正式报价。`);
      setTextIfExists("priceImportOptionTitle", "导入新版价格表");
      setTextIfExists("priceImportOptionDescription", "用新的 Excel 或 CSV 更新材料、工艺和人工工资，导入后再核对保存。");
      setTextIfExists("priceImportOptionBadge", "适合批量更新");
      setTextIfExists("priceManualOptionTitle", "修改当前价格表");
      setTextIfExists("priceManualOptionDescription", "直接修改材料、加工费、人工工资、利润、税费或交期。");
      setTextIfExists("priceManualOptionBadge", "适合少量调整");
    }
    function renderIndustryCalibrationGuide() {
      const box = document.getElementById("industryCalibrationGuide");
      if (!box) return;
      const strategy = currentQuoteIndustryStrategy();
      const saved = state.settings.industryCalibration;
      const category = currentIndustryCategory();
      const calibrationKey = `${category}::${strategy.id}`;
      const savedForCurrentStrategy = Boolean(saved?.confirmedAt && saved?.strategyId === strategy.id && saved?.category === category);
      if (box.dataset.calibrationKey === calibrationKey && box.querySelector("#ruleIndustryCalibrationConfirmed")) return;
      box.dataset.calibrationKey = calibrationKey;
      box.innerHTML = `
        <div class="member-note">
          <strong>${escapeHtml(strategy.label)} · 首次校准</strong>
          <div class="field-help">请用本厂最近的采购单、工资和设备记录逐项核对；这里只确认价格基础，订单尺寸、厚度、工艺数量仍在每次报价时填写。</div>
          <ol>${strategy.calibration.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ol>
          <label class="rule-cost-confirmation"><input id="ruleIndustryCalibrationConfirmed" type="checkbox" ${savedForCurrentStrategy ? "checked" : ""} onchange="renderRuleGuideReview()"><span>我已按本厂真实数据核对以上项目，并理解不同产品不能套用其他行业公式。</span></label>
          ${savedForCurrentStrategy ? `<div class="field-help">上次确认：${escapeHtml(new Date(saved.confirmedAt).toLocaleString("zh-CN", { hour12: false }))}</div>` : ""}
        </div>
      `;
    }
    function validateRuleGuidePage(page) {
      const fail = (message, id) => {
        document.getElementById(id)?.focus();
        document.getElementById(id)?.classList.add("quote-field-error");
        notify(message);
        return false;
      };
      ["ruleProductType", "ruleDefaultMaterial", "ruleMaterials", "rulePrintBase", "ruleProcesses", "ruleLaborHourly"].forEach(id => document.getElementById(id)?.classList.remove("quote-field-error"));
      if (page === 1) {
        if (!val("ruleProductType")) return fail("请先填写主营品类。", "ruleProductType");
        if (!val("ruleDefaultMaterial")) return fail("请先填写默认材料。", "ruleDefaultMaterial");
      }
      if (page === 2) {
        const materials = parseRules(val("ruleMaterials"));
        if (!materials.length || materials.some(item => !(item.price > 0))) {
          return fail("请按“材料名称=单个成本”填写至少一种真实材料成本。", "ruleMaterials");
        }
      }
      if (page === 3) {
        const processes = parseRules(val("ruleProcesses"));
        if (!(num("rulePrintBase", 0) > 0)) return fail("请填写每张订单都会发生的基础加工费。", "rulePrintBase");
        if (!processes.length || processes.some(item => !(item.price > 0))) {
          return fail("请按“工艺名称=单个加工费”填写至少一种常用工艺。", "ruleProcesses");
        }
      }
      if (page === 4 && !(num("ruleLaborHourly", 0) > 0)) {
        return fail("请填写本厂核对后的完全人工成本，金额必须大于 0。", "ruleLaborHourly");
      }
      if (page === 4) {
        renderIndustryCalibrationGuide();
        if (!document.getElementById("ruleIndustryCalibrationConfirmed")?.checked) {
          return fail(`请先完成“${currentQuoteIndustryStrategy().label}”首次校准确认。`, "ruleIndustryCalibrationConfirmed");
        }
      }
      return true;
    }
    function renderRuleGuideReview() {
      const box = document.getElementById("ruleGuideReview");
      if (!box) return;
      const materials = parseRules(val("ruleMaterials"));
      const processes = parseRules(val("ruleProcesses"));
      const items = [
        ["主营业务", `${val("ruleProductType") || "未填写"} · 默认 ${val("ruleDefaultMaterial") || "未填写"}`],
        ["材料成本", `${materials.length} 条 · ${materials.slice(0, 3).map(item => item.name).join("、") || "未填写"}`],
        ["加工费用", `基础 ¥${num("rulePrintBase", 0).toFixed(2)} · 单位 ¥${num("rulePrintUnit", 0).toFixed(3)}`],
        ["常用工艺", `${processes.length} 条 · ${processes.slice(0, 3).map(item => item.name).join("、") || "未填写"}`],
        ["人工工资", `完全人工成本 ¥${num("ruleLaborHourly", 0).toFixed(2)}/小时`],
        ["行业校准", `${currentQuoteIndustryStrategy().label} · ${document.getElementById("ruleIndustryCalibrationConfirmed")?.checked ? "已核对" : "待核对"}`]
      ];
      const currentMatchesSaved = String(state.settings.materialRules || "") === val("ruleMaterials")
        && String(state.settings.processRules || "") === val("ruleProcesses")
        && String(state.settings.print ?? "") === String(num("rulePrintBase", 180))
        && String(state.settings.printUnit ?? "") === String(num("rulePrintUnit", .12))
        && String(state.settings.laborHourly ?? "") === String(num("ruleLaborHourly", 0));
      const confirmed = Boolean(document.getElementById("ruleCostConfirmation")?.checked || (state.settings.costRulesConfirmedAt && currentMatchesSaved));
      items.push(["成本确认", confirmed ? "已人工核对真实成本" : "待人工确认，不能正式报价"]);
      box.innerHTML = items.map(item => `<div><span>${escapeHtml(item[0])}</span><strong>${escapeHtml(item[1])}</strong></div>`).join("");
    }
    function renderRuleGuide() {
      const simple = ruleMode === "simple";
      const header = document.getElementById("ruleGuideHeader");
      const nav = document.getElementById("ruleGuideNav");
      const layout = document.getElementById("ruleGuideLayout");
      const leftCard = document.getElementById("ruleGuideLeftCard");
      const rightCard = document.getElementById("ruleGuideRightCard");
      const advancedGrid = document.getElementById("ruleAdvancedGrid");
      header?.classList.toggle("is-hidden", !simple);
      nav?.classList.toggle("is-hidden", !simple);
      advancedGrid?.classList.toggle("is-hidden", simple);
      renderIndustryCalibrationGuide();
      layout?.classList.toggle("turn-prev", ruleGuideDirection === "prev");
      layout?.classList.toggle("turn-next", ruleGuideDirection !== "prev");
      const pages = [...document.querySelectorAll("[data-rule-guide-step]")];
      pages.forEach(page => page.classList.toggle("active", simple && Number(page.dataset.ruleGuideStep) === ruleGuidePage));
      if (simple) {
        leftCard?.classList.toggle("is-hidden", ruleGuidePage > 2);
        rightCard?.classList.toggle("is-hidden", ruleGuidePage <= 2);
      } else {
        leftCard?.classList.remove("is-hidden");
        rightCard?.classList.remove("is-hidden");
      }
      if (!simple) return;
      const progress = [...document.querySelectorAll("#ruleGuideProgress span")];
      progress.forEach((item, index) => {
        const page = index + 1;
        item.classList.toggle("active", page === ruleGuidePage);
        item.classList.toggle("done", page < ruleGuidePage);
        const marker = item.querySelector("i");
        if (marker) marker.textContent = page < ruleGuidePage ? "✓" : String(page);
      });
      const titles = ["先选择工厂主营业务", "填写常用材料成本", "填写基础加工和工艺费", "确认本厂人工工资", "核对并保存价格表"];
      const hints = [
        "选择最常接的行业和产品，系统会自动带入对应的报价模板。",
        "先录入3—5种最常用材料，格式为“材料名称=单个成品分摊成本”。",
        "填写订单固定加工费、单位加工费，以及覆膜、模切、车缝等常用工艺费。",
        "填写本厂每小时完全人工成本，工资、社保、福利和加班支出都应计入。",
        "确认这些数据来自你的工厂。保存后即可开始报价，高级参数可以以后再补。"
      ];
      const nextLabels = ["下一页：材料成本", "下一页：加工费用", "下一页：人工工资", "下一页：核对保存"];
      setTextIfExists("ruleGuideKicker", `第 ${ruleGuidePage} 页，共 5 页`);
      setTextIfExists("ruleGuideTitle", titles[ruleGuidePage - 1]);
      setTextIfExists("ruleGuideHint", hints[ruleGuidePage - 1]);
      const prev = document.getElementById("ruleGuidePrev");
      const next = document.getElementById("ruleGuideNext");
      const save = document.getElementById("ruleGuideSave");
      const saveQuote = document.getElementById("ruleGuideSaveQuote");
      prev?.classList.toggle("is-hidden", ruleGuidePage === 1);
      next?.classList.toggle("is-hidden", ruleGuidePage === 5);
      save?.classList.toggle("is-hidden", ruleGuidePage !== 5);
      saveQuote?.classList.toggle("is-hidden", ruleGuidePage !== 5);
      if (next) next.textContent = nextLabels[ruleGuidePage - 1] || "下一页";
      if (ruleGuidePage === 5) {
        renderQuoteRules();
        renderRuleGuideReview();
      }
    }
    function ruleGuideNextPage() {
      if (!validateRuleGuidePage(ruleGuidePage)) return;
      ruleGuideDirection = "next";
      ruleGuidePage = Math.min(5, ruleGuidePage + 1);
      renderRuleGuide();
      document.getElementById("ruleGuideHeader")?.scrollIntoView({ behavior: "smooth", block: "start" });
    }
    function ruleGuidePreviousPage() {
      ruleGuideDirection = "prev";
      ruleGuidePage = Math.max(1, ruleGuidePage - 1);
      renderRuleGuide();
      document.getElementById("ruleGuideHeader")?.scrollIntoView({ behavior: "smooth", block: "start" });
    }
    function ruleGuideJumpTo(page) {
      ruleGuideDirection = page < ruleGuidePage ? "prev" : "next";
      ruleGuidePage = Math.max(1, Math.min(5, Number(page) || 1));
      renderRuleGuide();
      document.getElementById("ruleGuideHeader")?.scrollIntoView({ behavior: "smooth", block: "start" });
    }
    function saveQuoteRulesFromGuide(goQuote) {
      for (let page = 1; page <= 4; page += 1) {
        if (validateRuleGuidePage(page)) continue;
        ruleGuidePage = page;
        ruleGuideDirection = "prev";
        renderRuleGuide();
        return;
      }
      if (goQuote) applyRulesToQuote();
      else if (saveQuoteRules() !== false) {
        renderRuleGuideReview();
        notify("基础价格表已保存，可以开始报价或继续设置高级参数。");
      }
    }
    function parseCapacityRows(text) {
      return String(text || "").split(/\n+/).map(line => {
        const trimmed = line.trim();
        if (!trimmed) return null;
        const [industry, rest = ""] = trimmed.split("=");
        const parts = rest.split(/[，,\/]/).map(item => item.trim()).filter(Boolean);
        return {
          industry: (industry || "未命名业务").trim(),
          scope: parts.slice(0, 2).join(" / ") || rest.trim() || "待补充",
          note: parts.slice(2).join(" / ") || "待补充"
        };
      }).filter(Boolean);
    }
    function renderCapacityTable(text) {
      const body = document.getElementById("capacityTableBody");
      if (!body) return;
      const rows = parseCapacityRows(text);
      body.innerHTML = rows.map(row => `
        <tr>
          <td>${row.industry}</td>
          <td>${row.scope}</td>
          <td>${row.note}</td>
        </tr>
      `).join("") || `<tr><td colspan="3" class="muted">先填写能力范围，系统会自动整理成表格。</td></tr>`;
    }
    function categoryForTemplate(name) {
      return Object.keys(industryCategories).find(category => industryCategories[category].includes(name)) || "包装印刷";
    }
    function currentIndustryCategory() {
      const saved = state.settings.industryCategory || categoryForTemplate(state.settings.factoryType);
      if (industryCategories[saved] && industryCategories[saved].includes(state.settings.factoryType)) return saved;
      return categoryForTemplate(state.settings.factoryType);
    }
    function currentCategoryTemplates() {
      return industryCategories[currentIndustryCategory()] || industryCategories["包装印刷"];
    }
    function currentQuoteIndustryStrategy() {
      return quoteIndustryEngine.resolveStrategy(currentIndustryCategory(), state.settings.factoryType);
    }
    function currentQuoteIndustryValues() {
      return Object.fromEntries(quoteIndustryEngine.fieldIds.map(id => [id, document.getElementById(id)?.value || ""]));
    }
    function renderIndustryCostModelGuide() {
      const box = document.getElementById("industryCostModelGuide");
      if (!box) return;
      const category = currentIndustryCategory();
      const strategy = currentQuoteIndustryStrategy();
      box.innerHTML = `<strong>${escapeHtml(category)} · ${escapeHtml(strategy.label)}</strong><br>${escapeHtml(strategy.formula)}<br><span class="muted">核算依据：${escapeHtml(strategy.source)}。不同产品使用不同字段和公式；材料价、工资、机时费和利用率只允许使用本厂确认数据。</span>`;
    }
    function renderIndustryQuoteCostFields() {
      const box = document.getElementById("quoteIndustryCostFields");
      const mode = document.getElementById("quoteCostMode")?.value || "material";
      renderQuoteMaterialCostCheck();
      if (!box) return;
      box.classList.toggle("is-hidden", mode === "unit" || mode === "material");
      if (mode === "unit" || mode === "material") return;
      const previous = Object.fromEntries([...new Set([
        ...quoteIndustryEngine.fieldIds,
        "quoteUnitUsage", "quoteMaterialUnitPrice", "quoteUtilizationRate", "quoteLaborMinutes", "quoteMachineMinutes",
        "quoteBomCost", "quoteSmtPoints", "quoteSmtPointPrice", "quoteTestCost", "quotePrintRoute", "quoteSupplierName", "quoteSupplierQuotedAt", "quoteSupplierTotalCost", "quotePrintFlatSize",
        "quotePrintSheetSize", "quotePrintSheetFormat", "quotePrintSheetStandard", "quotePrintGsm", "quotePrintSideCount", "quotePrintColorCount", "quotePrintSetupWaste",
        "quotePrintWasteRate", "quoteFinishingWasteRate", "quotePrintSetupFee", "quotePlateFee", "quotePrintRatePerImpression",
        "quotePrintMinimumCharge", "quotePrintSetupMinutes", "quotePressSheetsPerHour", "quoteOperatorCount",
        "quotePrintGripper", "quotePrintSideMargin", "quotePrintGap", "quotePrintAllowRotate",
        "quoteLaminationSheetFormat", "quoteLaminationSheetStandard", "quoteLaminationSize",
        "quoteLaminationFilmType", "quoteLaminationSideCount", "quoteLaminationQuantity",
        ...(window.JIANGLAI_QUOTE_AUTO_ROUTER?.rollFieldIds || [])
      ])].map(id => [id, document.getElementById(id)?.value || ""]));
      const category = currentIndustryCategory();
      const s = state.settings;
      if (mode === "printing") {
        const engine = window.JIANGLAI_QUOTE_FORMULA;
        const entry = quoteCatalogEntry();
        const profile = entry?.profile || "";
        const formula = engine?.profileRule(profile) || { family: "manual", routes: ["fixed"] };
        const sheetReady = ["sheet", "dieline"].includes(formula.family);
        const bookReady = formula.family === "book" && profile === "book";
        const automaticFlatResult = quoteAutomaticFlatSizeResult();
        const automaticFlatSize = automaticFlatResult?.widthMm && automaticFlatResult?.heightMm
          ? `${quoteCatalogSetupNumber(automaticFlatResult.widthMm)}×${quoteCatalogSetupNumber(automaticFlatResult.heightMm)}` : "";
        const gsmMatch = String(val("quoteMaterial") || "").match(/(\d+(?:\.\d+)?)\s*g/i);
        const configuration = quoteCatalogConfigurationSnapshot();
        const groupMap = Object.fromEntries((configuration?.groups || []).map(group => [group.id, group.values || []]));
        const sideText = (groupMap.print_side || []).join(" ");
        const colorText = (groupMap.print_color || []).join(" ");
        const defaultSides = sideText.includes("双面") ? "2" : "1";
        const defaultColors = /四色.*专色|专色.*四色/.test(colorText) ? "5"
          : colorText.includes("四色") ? "4"
          : colorText.includes("双色") ? "2"
          : "1";
        const routeOptions = Object.values(engine?.routes || {}).filter(route => (formula.routes || []).includes(route.id));
        const matchedMaterialCost = findActiveCostItem(val("quoteMaterial"), ["material"]), matchedMaterialPricePerKg = costItemPricePerKg(matchedMaterialCost), selectedPrintTier = quoteCatalogSelectedOperationPrices().find(item => item.kind === "print" && item.printSheetTierPricing) || null, publicDefaults = window.JIANGLAI_QUOTE_COST_DEFAULTS?.recommendPrintingInputs({ flatSize:previous.quotePrintFlatSize || automaticFlatSize, quantity:num("quoteQty", 1000), sideCount:Number(defaultSides), gsm:Number(previous.quotePrintGsm || gsmMatch?.[1] || 0), materialPricePerKg:Number(previous.quoteMaterialUnitPrice || matchedMaterialPricePerKg || 0), tierPricing:selectedPrintTier?.printSheetTierPricing }) || {};
        if (formula.family === "roll") {
          box.innerHTML = window.JIANGLAI_QUOTE_AUTO_ROUTER?.renderRoll({ previous, entryName:entry?.name, size:val("quoteSize"), quantity:num("quoteQty", 1000), materialName:val("quoteMaterial"), materialSource:quoteMaterialPurchaseSource(), configuration, settings:s }) || `<div class="member-note">卷材自动公式没有加载，请刷新后重试。</div>`;
          return;
        }
        if (bookReady) {
          const report = window.JIANGLAI_BOOK_QUOTE?.estimate(quoteBookPricingContext());
          const status = report?.ok ? (report.complete ? `当前系统基础报价约 ¥${Number(report.estimatedUnitPrice || 0).toFixed(2)}/本；生成报价时会再叠加所选封面工艺。` : `已算部分约 ¥${Number(report.estimatedUnitPrice || 0).toFixed(2)}/本；还缺 ${report.missing.join("、")}。`) : (report?.error || "选择封面、内页、尺寸、页数和印刷后显示价格。");
          box.innerHTML = `<div class="member-note" style="margin:8px 0 10px"><strong>${escapeHtml(entry?.name || "书刊")} · 封面/内页分项计价</strong><br>${escapeHtml(status)}<br><span class="muted">系统按本厂纸张吨价、胶印开数价和装订价，分别计算封面与内页；每个页数按钮已显示当前数量下的对应价格。上机纸按大度全开 889×1194mm 核算，所有单价仍来自本厂价格表。</span></div>`;
          return;
        }
        if (!sheetReady) {
          const automaticMode = quoteRecommendedCostMode(entry);
          if (automaticMode !== "printing") {
            setIfExists("quoteCostMode", automaticMode);
            renderIndustryQuoteCostFields();
            return;
          }
          box.innerHTML = `<div class="member-note"><strong>${escapeHtml(entry?.name || "当前产品")} · 自动公式路由</strong><br>系统正在载入对应生产公式，请刷新后原地重试。</div>`;
          return;
        }
        box.innerHTML = `
          <div class="member-note" style="margin:8px 0 10px"><strong>${escapeHtml(entry?.name || "当前产品")} · ${formula.family === "dieline" ? "展开刀版" : "平张拼版"}规则</strong><br>
            系统独立计算拼版数、净印张、起机损耗、印刷/后工损耗、纸张重量、印版和印次。公开设备参数与行业损耗只作预填起点，本厂已保存的纸价和开数价始终优先。运费不计入报价。</div>
          <div class="mini-grid">
            <div><label class="field-label">生产路线</label><select id="quotePrintRoute">${routeOptions.map(route => `<option value="${escapeHtml(route.id)}" ${route.id === (previous.quotePrintRoute || "offset") ? "selected" : ""}>${escapeHtml(route.label)}</option>`).join("")}</select></div><div><label class="field-label">外发供应商</label><input id="quoteSupplierName" value="${escapeHtml(previous.quoteSupplierName)}" placeholder="选择外发成品时填写"></div><div><label class="field-label">供应商报价日期</label><input id="quoteSupplierQuotedAt" type="date" value="${escapeHtml(previous.quoteSupplierQuotedAt)}"></div><div><label class="field-label">外发实际采购总成本（元，不含运）</label><input id="quoteSupplierTotalCost" inputmode="decimal" value="${escapeHtml(previous.quoteSupplierTotalCost)}" placeholder="同规格、同数量、含全部工艺"><div class="field-help">只在“外发成品”路线使用；不会再叠加本厂纸张、印工、人工和设备费</div></div>
            <div><label class="field-label">${formula.family === "dieline" ? "刀版展开尺寸" : "单个展开尺寸"}（系统生成，可修正）</label><input id="quotePrintFlatSize" value="${escapeHtml(previous.quotePrintFlatSize || automaticFlatSize)}" placeholder="${automaticFlatSize ? "系统已生成，按刀线可修正" : "请按刀线填写宽×高"}"><div class="field-help">${escapeHtml(automaticFlatResult?.note || "正式生产前请按刀线核对")}</div></div>
            <div><label class="field-label">上机纸尺寸（mm）</label><input id="quotePrintSheetSize" value="${escapeHtml(previous.quotePrintSheetSize || publicDefaults.sheetSize)}" placeholder="系统按刀版与已配置开数价自动推荐"><div class="field-help">${publicDefaults.sheet ? `已比较可用开数：推荐 ${publicDefaults.sheet.widthMm}×${publicDefaults.sheet.heightMm}mm，${publicDefaults.sheet.piecesPerSheet}拼，预计 ${publicDefaults.sheet.productionSheets} 张` : "刀版超过已配置开数时才需要人工选择设备"}</div></div>
            <div><label class="field-label">上机纸系列</label><select id="quotePrintSheetStandard"><option value="auto" ${!["regular","large"].includes(previous.quotePrintSheetStandard || publicDefaults.printSheetStandard) ? "selected" : ""}>按上机尺寸自动判断</option><option value="regular" ${(previous.quotePrintSheetStandard || publicDefaults.printSheetStandard) === "regular" ? "selected" : ""}>正度 787×1092mm</option><option value="large" ${(previous.quotePrintSheetStandard || publicDefaults.printSheetStandard) === "large" ? "selected" : ""}>大度 889×1194mm</option></select></div>
            <div><label class="field-label">印刷上机开数</label><select id="quotePrintSheetFormat"><option value="auto" ${!["full_open","two_open","four_open","eight_open"].includes(previous.quotePrintSheetFormat || publicDefaults.printSheetFormat) ? "selected" : ""}>按上机纸尺寸自动推荐</option><option value="full_open" ${(previous.quotePrintSheetFormat || publicDefaults.printSheetFormat) === "full_open" ? "selected" : ""}>全开</option><option value="two_open" ${(previous.quotePrintSheetFormat || publicDefaults.printSheetFormat) === "two_open" ? "selected" : ""}>对开</option><option value="four_open" ${(previous.quotePrintSheetFormat || publicDefaults.printSheetFormat) === "four_open" ? "selected" : ""}>四开</option><option value="eight_open" ${(previous.quotePrintSheetFormat || publicDefaults.printSheetFormat) === "eight_open" ? "selected" : ""}>八开</option></select><div class="field-help">系统只会从本厂已配置价格的开数中比较并选择，不会套用未保存的档位</div></div>
            <div><label class="field-label">咬口（mm）</label><input id="quotePrintGripper" inputmode="decimal" value="${escapeHtml(previous.quotePrintGripper || publicDefaults.gripperMm)}" placeholder="按本厂设备填写，可填0"></div>
            <div><label class="field-label">侧规/安全边（mm）</label><input id="quotePrintSideMargin" inputmode="decimal" value="${escapeHtml(previous.quotePrintSideMargin || publicDefaults.sideMarginMm)}" placeholder="按本厂设备填写，可填0"></div>
            <div><label class="field-label">拼版间隙（mm）</label><input id="quotePrintGap" inputmode="decimal" value="${escapeHtml(previous.quotePrintGap || publicDefaults.gapMm)}" placeholder="按成品工艺填写，可填0"></div>
            <div><label class="field-label">材料克重（g/㎡）</label><input id="quotePrintGsm" inputmode="decimal" value="${escapeHtml(previous.quotePrintGsm || gsmMatch?.[1] || "")}" placeholder="按本厂实际材料"></div>
            <div><label class="field-label">材料采购价（元/kg）</label><input id="quoteMaterialUnitPrice" inputmode="decimal" value="${escapeHtml(previous.quoteMaterialUnitPrice || (matchedMaterialPricePerKg ?? ""))}" placeholder="等待厂家提供"><div class="field-help">${matchedMaterialPricePerKg !== null ? `已从成本库匹配：${escapeHtml(matchedMaterialCost.name)} ${escapeHtml(matchedMaterialCost.spec)}，${escapeHtml(matchedMaterialCost.unit)} ¥${Number(matchedMaterialCost.price).toFixed(3)}` : "当前材料未匹配到元/吨或元/kg价格，请手动填写"}</div></div>
            <div><label class="field-label">印刷面数</label><input id="quotePrintSideCount" inputmode="numeric" value="${escapeHtml(previous.quotePrintSideCount || defaultSides)}"></div>
            <div><label class="field-label">每面色数</label><input id="quotePrintColorCount" inputmode="numeric" value="${escapeHtml(previous.quotePrintColorCount || defaultColors)}"></div>
            <div><label class="field-label">起机固定损耗（张）</label><input id="quotePrintSetupWaste" inputmode="numeric" value="${escapeHtml(previous.quotePrintSetupWaste || publicDefaults.setupWasteSheets)}" placeholder="公开参考起点，可按本厂实绩覆盖"></div>
            <div><label class="field-label">印刷损耗率%</label><input id="quotePrintWasteRate" inputmode="decimal" value="${escapeHtml(previous.quotePrintWasteRate || publicDefaults.printWasteRate)}" placeholder="公开参考起点，可按本厂实绩覆盖"></div>
            <div><label class="field-label">后工损耗率%</label><input id="quoteFinishingWasteRate" inputmode="decimal" value="${escapeHtml(previous.quoteFinishingWasteRate || publicDefaults.finishingWasteRate)}" placeholder="公开参考起点，可按本厂实绩覆盖"></div>
            <div><label class="field-label">印刷开机费（元/单）</label><input id="quotePrintSetupFee" inputmode="decimal" value="${escapeHtml(previous.quotePrintSetupFee || publicDefaults.printSetupFee)}" placeholder="等待厂家提供"></div>
            <div><label class="field-label">印版费（元/版）</label><input id="quotePlateFee" inputmode="decimal" value="${escapeHtml(previous.quotePlateFee || publicDefaults.plateFee)}" placeholder="数码可填0"><div class="field-help">${publicDefaults.allInPrintPrice ? "当前开数价按整项印工使用，默认不重复加收印版/开机费；若本厂开数价不含版费，请在此改为真实版费" : "网络版价波动大，必须采用本厂真实费用"}</div></div>
            <div><label class="field-label">备用统一印工（元/印次）</label><input id="quotePrintRatePerImpression" inputmode="decimal" value="${escapeHtml(previous.quotePrintRatePerImpression || publicDefaults.printRatePerImpression)}" placeholder="未设置开数价时使用"><div class="field-help">已配置当前颜色的开数价时，本项不会重复计费</div></div>
            <div><label class="field-label">备用统一最低收费（元）</label><input id="quotePrintMinimumCharge" inputmode="decimal" value="${escapeHtml(previous.quotePrintMinimumCharge || publicDefaults.printMinimumCharge)}" placeholder="没有可填0"></div>
            <div><label class="field-label">开机准备时间（分钟）</label><input id="quotePrintSetupMinutes" inputmode="decimal" value="${escapeHtml(previous.quotePrintSetupMinutes || publicDefaults.setupMinutes)}" placeholder="等待厂家提供"></div>
            <div><label class="field-label">设备速度（张/小时）</label><input id="quotePressSheetsPerHour" inputmode="decimal" value="${escapeHtml(previous.quotePressSheetsPerHour || publicDefaults.pressSheetsPerHour)}" placeholder="按实际设备填写"></div>
            <div><label class="field-label">操作人数</label><input id="quoteOperatorCount" inputmode="numeric" value="${escapeHtml(previous.quoteOperatorCount || publicDefaults.operatorCount || "1")}"></div>
            <div><label class="field-label">允许旋转拼版</label><select id="quotePrintAllowRotate"><option value="yes" ${(previous.quotePrintAllowRotate || publicDefaults.allowRotate) !== "no" ? "selected" : ""}>允许（无纸纹/方向限制）</option><option value="no" ${(previous.quotePrintAllowRotate || publicDefaults.allowRotate) === "no" ? "selected" : ""}>不允许（有纸纹/方向要求）</option></select></div>
            <div><label class="field-label">覆膜纸张系列</label><select id="quoteLaminationSheetStandard"><option value="auto" ${!["regular","large"].includes(previous.quoteLaminationSheetStandard) ? "selected" : ""}>按上机纸自动判断</option><option value="regular" ${previous.quoteLaminationSheetStandard === "regular" ? "selected" : ""}>正度 787×1092mm</option><option value="large" ${previous.quoteLaminationSheetStandard === "large" ? "selected" : ""}>大度 889×1194mm</option></select></div>
            <div><label class="field-label">覆膜开数</label><select id="quoteLaminationSheetFormat"><option value="auto" ${!["full_open","two_open","four_open","eight_open","sixteen_open","thirty_two_open"].includes(previous.quoteLaminationSheetFormat) ? "selected" : ""}>按实际尺寸自动判断</option><option value="full_open" ${previous.quoteLaminationSheetFormat === "full_open" ? "selected" : ""}>全开</option><option value="two_open" ${previous.quoteLaminationSheetFormat === "two_open" ? "selected" : ""}>对开</option><option value="four_open" ${previous.quoteLaminationSheetFormat === "four_open" ? "selected" : ""}>四开</option><option value="eight_open" ${previous.quoteLaminationSheetFormat === "eight_open" ? "selected" : ""}>八开</option><option value="sixteen_open" ${previous.quoteLaminationSheetFormat === "sixteen_open" ? "selected" : ""}>十六开</option><option value="thirty_two_open" ${previous.quoteLaminationSheetFormat === "thirty_two_open" ? "selected" : ""}>三十二开</option></select><div class="field-help">非标准拼版请按实际尺寸人工核价</div></div>
            <div><label class="field-label">实际覆膜上机尺寸（mm）</label><input id="quoteLaminationSize" value="${escapeHtml(previous.quoteLaminationSize)}" placeholder="留空自动使用上机纸尺寸"><div class="field-help">应与下面的覆膜张数使用同一上机口径；外发裁小后覆膜时再手动修改</div></div>
            <div><label class="field-label">膜类型</label><select id="quoteLaminationFilmType"><option value="auto" ${!["gloss","matte","soft_touch","scratch_resistant","laser","other"].includes(previous.quoteLaminationFilmType) ? "selected" : ""}>跟随所选工艺</option><option value="gloss" ${previous.quoteLaminationFilmType === "gloss" ? "selected" : ""}>亮膜/光膜</option><option value="matte" ${previous.quoteLaminationFilmType === "matte" ? "selected" : ""}>哑膜</option><option value="soft_touch" ${previous.quoteLaminationFilmType === "soft_touch" ? "selected" : ""}>触感膜</option><option value="scratch_resistant" ${previous.quoteLaminationFilmType === "scratch_resistant" ? "selected" : ""}>防刮膜</option><option value="laser" ${previous.quoteLaminationFilmType === "laser" ? "selected" : ""}>镭射膜</option><option value="other" ${previous.quoteLaminationFilmType === "other" ? "selected" : ""}>其他膜</option></select></div>
            <div><label class="field-label">覆膜面数</label><select id="quoteLaminationSideCount"><option value="1" ${previous.quoteLaminationSideCount !== "2" ? "selected" : ""}>单面</option><option value="2" ${previous.quoteLaminationSideCount === "2" ? "selected" : ""}>双面（自动按2面计费）</option></select></div>
            <div><label class="field-label">实际覆膜数量（张）</label><input id="quoteLaminationQuantity" inputmode="numeric" value="${escapeHtml(previous.quoteLaminationQuantity)}" placeholder="留空则使用生产印张数"><div class="field-help">可填写外发覆膜或补数后的实际张数</div></div>
          </div>
          <div class="field-help">公开参考预填：海德堡 XL75 叼口 8–10mm、标准速度 15,000张/小时；Adobe 常用出血 3mm，因此相邻拼版预留 6mm。起机100张、印刷5%、后工2%是公开行业参考起点，保存过的本厂数据始终优先。</div><div class="field-help">系统会按产品结构生成或估算展开尺寸；盒、袋、信封正式生产前仍应按刀线核对。未知三维结构不会拿成品长宽高直接代替。</div>
          <div class="field-help">当前总参数：人工 ¥${Number(s.laborHourly || 0).toFixed(2)}/小时，设备 ¥${Number(s.machineHourly || 0).toFixed(2)}/小时。选择本厂开数印工价时人工和设备已包含、不重复计算；自算机时才从成本库匹配对应岗位与设备。</div>
        `;
        return;
      }
      const strategy = currentQuoteIndustryStrategy();
      const dynamicFields = strategy.fields.map(field => {
        const fallback = field.id === "quoteUtilizationRate"
          ? rateText(s.utilizationRate ?? .90)
          : field.defaultValue;
        const value = previous[field.id] || fallback || "";
        const required = field.required ? " <span aria-hidden=\"true\">*</span>" : "";
        const help = field.help ? `<div class="field-help">${escapeHtml(field.help)}</div>` : "";
        return `<div><label class="field-label" for="${escapeHtml(field.id)}">${escapeHtml(field.label)}${required}</label><input id="${escapeHtml(field.id)}" inputmode="${escapeHtml(field.inputMode)}" value="${escapeHtml(value)}" placeholder="${escapeHtml(field.placeholder)}">${help}</div>`;
      }).join("");
      box.innerHTML = `
        <div class="member-note" style="margin:8px 0 10px"><strong>${escapeHtml(category)} · ${escapeHtml(strategy.label)}</strong><br>${escapeHtml(strategy.formula)}<br><span class="muted">${escapeHtml(strategy.source)}</span></div>
        <div class="mini-grid">${dynamicFields}</div>
        <div class="field-help">当前总参数：人工 ¥${Number(s.laborHourly || 0).toFixed(2)}/小时，设备 ¥${Number(s.machineHourly || 0).toFixed(2)}/小时。未单独设置时按启用成本库匹配岗位和设备并在报价快照留痕；订单必填参数缺失仍会停止报价。</div>
      `;
    }

    function quoteRollPricingContext(qty) {
      const configuration = quoteCatalogConfigurationSnapshot();
      const selectedOverrides = quoteCatalogSelectedOperationPrices().filter(item => item.kind === "process").map(item => ({
        id:`quote-roll-operation-${normalizeRuleText(item.name)}`,
        name:item.name,
        basis:item.basis,
        chargeMode:item.chargeMode,
        unitRate:Number(item.chargeMode === "package_plus_excess" ? item.excessUnitPrice ?? item.unitPrice ?? 0 : item.unitPrice || 0),
        setupFee:Number(item.setupFee || 0),
        minimumCharge:Number(item.minimumCharge || 0),
        packageQuantity:Number(item.includedQuantity || 0),
        packageFee:Number(item.minimumCharge || 0),
        usagePerPiece:Number(item.usagePerPiece || 0)
      }));
      const operations = [
        ...costLibraryOperationsForQuote(val("quoteProcess")).filter(item =>
          !selectedOverrides.some(override => costOperationNamesOverlap(item.name, override.name))
        ),
        ...selectedOverrides
      ];
      const materialSource = quoteMaterialPurchaseSource();
      const operationIds = new Set(operations.map(item => item.id));
      const costLibrary = ensureCostLibrary();
      return {
        previous:Object.fromEntries((window.JIANGLAI_QUOTE_AUTO_ROUTER?.rollFieldIds || []).map(id => [id, document.getElementById(id)?.value || ""])),
        values:Object.fromEntries((window.JIANGLAI_QUOTE_AUTO_ROUTER?.rollFieldIds || []).map(id => [id, document.getElementById(id)?.value || ""])),
        entryName:quoteCatalogEntry()?.name,
        size:val("quoteSize"),
        quantity:qty,
        materialName:val("quoteMaterial"),
        materialSource,
        configuration,
        settings:state.settings,
        operations,
        usedCostItems:activeCostLibraryItems().filter(item => item.id === materialSource.item?.id || operationIds.has(item.id)),
        costLibrary
      };
    }

    function printingQuoteCostBreakdown(qty) {
      const engine = window.JIANGLAI_QUOTE_FORMULA;
      if (!engine) return { ok: false, error: "精细印刷公式没有加载，请刷新后重试。" };
      const entry = quoteCatalogEntry();
      const formula = engine.profileRule(entry?.profile || "");
      if (formula.family === "book" && entry?.profile === "book") {
        return window.JIANGLAI_BOOK_QUOTE?.breakdown(quoteBookPricingContext()) || { ok:false, error:"书刊页数计价模块没有加载，请刷新后重试。" };
      }
      if (formula.family === "roll") {
        return window.JIANGLAI_QUOTE_AUTO_ROUTER?.breakdownRoll(quoteRollPricingContext(qty)) || { ok:false, error:"卷材自动公式没有加载，请刷新后重试。" };
      }
      if (!["sheet", "dieline"].includes(formula.family)) {
        return { ok: false, error: `当前“${entry?.name || "产品"}”的自动公式路由尚未载入，请刷新后原地重试。` };
      }
      try {
        const route = val("quotePrintRoute") || "offset";
        if (route === "outsourced") { const costLibrary = ensureCostLibrary(), formulaInput = { route, quantity:qty, supplierTotalCost:requiredNumber("quoteSupplierTotalCost", "外发实际采购总成本"), supplierName:val("quoteSupplierName"), supplierQuotedAt:val("quoteSupplierQuotedAt"), overheadRate:0, targetGrossMargin:0, taxRate:0, taxIncluded:false }, result = engine.calculateSheetQuote(formulaInput); return { ok:true, category:"包装印刷", materialCost:0, materialUnitCost:0, printingCost:0, operationCost:0, operationItems:[], extraDirectCost:result.costs.outsource, laborCost:0, machineCost:0, materialLabel:"供应商成品采购价已包含材料、印刷和所报工艺", extraLabel:"外发成品采购", basis:`外发成品：${result.supplier.name || "供应商待补"} · ${result.supplier.quotedAt || "报价日期待补"} · 不含运；本厂成本不重复叠加。`, formulaInput, formulaResult:result, libraryOperationIds:[], libraryOperationNames:[], usedCostItems:[], hourlyRates:{ labor:{rate:0,source:"outsourced"}, machine:{rate:0,source:"outsourced"}, allInPrintPrice:true }, costVersionId:costLibrary.activeVersionId || "", costVersionEffectiveAt:costLibrary.effectiveAt || "" }; }
        const [pieceWidthMm, pieceHeightMm] = requiredDimensions(engine, "quotePrintFlatSize", "单个展开尺寸"), [sheetWidthMm, sheetHeightMm] = requiredDimensions(engine, "quotePrintSheetSize", "上机纸尺寸");
        const selectedGroups = Object.fromEntries((quoteCatalogConfigurationSnapshot()?.groups || []).map(group => [group.id, group.values || []]));
        const printingEnabled = !(selectedGroups.print_color || []).some(quoteCatalogOperationIsNoOp);
        const selectedPrintPrices = quoteCatalogSelectedOperationPrices().filter(item => item.kind === "print" && item.printSheetTierPricing);
        if (selectedPrintPrices.length > 1) throw new Error("一次报价只能选择一种印刷颜色价格规则，请核对印刷颜色");
        const selectedPrintPrice = selectedPrintPrices[0] || null;
        const printTierOperation = selectedPrintPrice ? {
          id:`quote-print-${normalizeRuleText(selectedPrintPrice.name)}`,
          name:selectedPrintPrice.name,
          setupFee:Number(selectedPrintPrice.setupFee || 0),
          printSheetTierPricing:selectedPrintPrice.printSheetTierPricing
        } : null;
        const selectedOperationOverrides = quoteCatalogSelectedOperationPrices()
          .filter(item => item.kind === "process")
          .map(item => ({
            id: `quote-operation-${normalizeRuleText(item.name)}`,
            name: item.name,
            basis: item.laminationTierPricing ? "sheet" : item.basis,
            chargeMode: item.chargeMode,
            unitRate: item.chargeMode === "package_plus_excess"
              ? Number(item.excessUnitPrice ?? item.unitPrice ?? 0)
              : Number(item.unitPrice || 0),
            setupFee: Number(item.setupFee || 0),
            minimumCharge: Number(item.minimumCharge || 0),
            packageQuantity: Number(item.includedQuantity || 0),
            packageFee: Number(item.minimumCharge || 0),
            usagePerPiece: Number(item.usagePerPiece || 0),
            laminationTierPricing: item.laminationTierPricing || null
          }));
        const operations = [
          ...costLibraryOperationsForQuote(val("quoteProcess")).filter(item =>
            !selectedOperationOverrides.some(override => costOperationNamesOverlap(item.name, override.name))
          ),
          ...selectedOperationOverrides
        ];
        const hasTierLamination = operations.some(item => item?.laminationTierPricing);
        const laminationSizeText = String(val("quoteLaminationSize") || val("quotePrintSheetSize") || `${sheetWidthMm}×${sheetHeightMm}`).trim();
        const [laminationWidthMm, laminationHeightMm] = hasTierLamination
          ? engine.parseDimensions(laminationSizeText, 2) : [pieceWidthMm, pieceHeightMm];
        const laminationQuantityText = String(val("quoteLaminationQuantity") || "").trim();
        const manualAdjustments = quoteManualAdjustments();
        if (manualAdjustments.active && !manualAdjustments.reason) {
          const error = new Error("使用人工修正生产参数时必须填写修正原因");
          error.fieldId = "quoteManualAdjustmentReason";
          throw error;
        }
        const operationIds = new Set(operations.map(item => item.id)), matchedMaterialItem = findActiveCostItem(val("quoteMaterial"), ["material"]);
        const hourlyRates = window.JIANGLAI_QUOTE_COST_DEFAULTS?.resolvePrintingRates({ explicitLaborRate:state.settings.laborHourly, explicitMachineRate:state.settings.machineHourly, items:activeCostLibraryItems(), route, sheetWidthMm, sheetHeightMm, allInPrintPrice:Boolean(printTierOperation) }) || { labor:{rate:Number(state.settings.laborHourly || 0)}, machine:{rate:Number(state.settings.machineHourly || 0)} };
        const laborHourly = Number(hourlyRates.labor?.rate || 0), machineHourly = Number(hourlyRates.machine?.rate || 0);
        if (!printTierOperation && !(laborHourly > 0)) throw new Error("价格表还没有填写完全人工成本（元/小时）");
        if (!printTierOperation && !(machineHourly > 0)) throw new Error("价格表还没有填写设备机时费（元/小时）");
        const hourlyItemIds = new Set([hourlyRates.labor?.item?.id, hourlyRates.machine?.item?.id].filter(Boolean)), usedCostItems = activeCostLibraryItems().filter(item => operationIds.has(item.id) || item.id === matchedMaterialItem?.id || hourlyItemIds.has(item.id)), costLibrary = ensureCostLibrary();
        const formulaInput = {
          route,
          printingEnabled,
          quantity: qty,
          pieceWidthMm,
          pieceHeightMm,
          sheetWidthMm,
          sheetHeightMm,
          gripperMm: requiredNumber("quotePrintGripper", "咬口", { allowZero: true }),
          sideMarginMm: requiredNumber("quotePrintSideMargin", "侧规/安全边", { allowZero: true }),
          gapMm: requiredNumber("quotePrintGap", "拼版间隙", { allowZero: true }),
          allowRotate: val("quotePrintAllowRotate") !== "no",
          gsm: requiredNumber("quotePrintGsm", "材料克重"),
          materialPricePerKg: requiredNumber("quoteMaterialUnitPrice", "材料采购价"),
          sideCount: requiredNumber("quotePrintSideCount", "印刷面数", { integer: true }),
          colorCount: requiredNumber("quotePrintColorCount", "每面色数", { integer: true }),
          productionSheetsOverride: manualAdjustments.productionSheets
            ? requiredNumber("quotePrintProductionSheetsOverride", "实际生产印张", { integer: true }) : undefined,
          plateCount: manualAdjustments.plateCount
            ? requiredNumber("quotePrintPlateCountOverride", "实际印版/色令数", { integer: true }) : undefined,
          manualAdjustmentReason: manualAdjustments.active ? manualAdjustments.reason : "",
          setupWasteSheets: requiredNumber("quotePrintSetupWaste", "起机固定损耗", { allowZero: true, integer: true }),
          printWasteRate: requiredNumber("quotePrintWasteRate", "印刷损耗率", { allowZero: true, rate: true }),
          finishingWasteRate: requiredNumber("quoteFinishingWasteRate", "后工损耗率", { allowZero: true, rate: true }),
          printSetupFee: !printingEnabled || printTierOperation ? 0 : requiredNumber("quotePrintSetupFee", "印刷开机费", { allowZero: true }),
          plateFee: !printingEnabled || route === "digital" ? 0 : requiredNumber("quotePlateFee", "印版费", { allowZero: true }),
          printRatePerImpression: !printingEnabled || printTierOperation ? 0 : requiredNumber("quotePrintRatePerImpression", "印工", { allowZero: true }),
          printMinimumCharge: !printingEnabled || printTierOperation ? 0 : requiredNumber("quotePrintMinimumCharge", "印刷最低收费", { allowZero: true }),
          printTierOperation: printingEnabled ? printTierOperation : null,
          printSheetFormat: val("quotePrintSheetFormat") || "auto",
          printSheetStandard: val("quotePrintSheetStandard") || "auto",
          setupMinutes: printingEnabled ? requiredNumber("quotePrintSetupMinutes", "开机准备时间", { allowZero: true }) : 0,
          pressSheetsPerHour: printingEnabled ? requiredNumber("quotePressSheetsPerHour", "设备速度") : 1,
          operatorCount: printingEnabled ? requiredNumber("quoteOperatorCount", "操作人数", { integer: true }) : 1,
          laborHourly,
          machineHourly,
          overheadRate: 0,
          targetGrossMargin: 0,
          taxRate: 0,
          taxIncluded: false,
          operations,
          laminationWidthMm,
          laminationHeightMm,
          laminationSheetFormat: val("quoteLaminationSheetFormat") || "auto",
          laminationSheetStandard: val("quoteLaminationSheetStandard") || "auto",
          laminationSideCount: hasTierLamination ? requiredNumber("quoteLaminationSideCount", "覆膜面数", { integer: true }) : 1,
          laminationQuantity: laminationQuantityText
            ? requiredNumber("quoteLaminationQuantity", "实际覆膜数量", { integer: true }) : undefined,
          laminationFilmType: val("quoteLaminationFilmType") || "auto"
        };
        const result = engine.calculateSheetQuote(formulaInput);
        const routeLabel = engine.routes[result.route]?.label || result.route;
        return {
          ok: true,
          category: "包装印刷",
          materialCost: result.costs.material,
          materialUnitCost: result.costs.material / Math.max(result.quantity, 1),
          printingCost: result.costs.printing,
          operationCost: result.costs.operations,
          operationItems: result.operationItems,
          extraDirectCost: Number(result.costs.outsource || 0),
          laborCost: result.costs.labor,
          machineCost: result.costs.machine,
          materialLabel: `${result.layout.count}拼；净印张 ${result.sheets.netSheets}，生产印张 ${result.sheets.productionSheets}${result.sheets.manuallyAdjusted ? `（系统原算 ${result.sheets.automaticProductionSheets}，已人工修正）` : ""}；纸重 ${result.paper.weightKg.toFixed(3)}kg`,
          extraLabel: result.outsourced ? "外发成品采购" : "",
          basis: `${routeLabel}：${result.layout.rotated ? "旋转" : "正常"}排版 ${result.layout.across}×${result.layout.down}；${result.printSheetFormat?.label ? `采用${result.printSheetFormat.label}价，` : ""}${result.plateCount}版，${result.impressions}印次。材料损耗只计入生产印张，不放大开机费与人工。${manualAdjustments.active ? ` 人工修正：${manualAdjustments.productionSheets ? `生产印张 ${manualAdjustments.productionSheets}` : ""}${manualAdjustments.productionSheets && manualAdjustments.plateCount ? "、" : ""}${manualAdjustments.plateCount ? `印版/色令 ${manualAdjustments.plateCount}` : ""}；原因：${manualAdjustments.reason}。` : ""}`,
          formulaInput,
          formulaResult: result,
          manualAdjustments,
          libraryOperationIds: operations.map(item => item.id),
          libraryOperationNames: operations.map(item => item.name),
          usedCostItems, hourlyRates,
          costVersionId: costLibrary.activeVersionId || "",
          costVersionEffectiveAt: costLibrary.effectiveAt || ""
        };
      } catch (error) {
        return { ok: false, error: `精细印刷公式还缺数据：${error.message}`, focusFieldId:error.fieldId || "" };
      }
    }
    function focusQuoteFormulaField(id) {
      revealQuoteMissingFields([{ id }]);
    }
    function revealQuoteMissingFields(fields = []) {
      const elements = fields.map(item => document.getElementById(item?.id || "")).filter(Boolean);
      elements.forEach(field => {
        let details = field.closest("details");
        while (details) {
          details.open = true;
          details = details.parentElement?.closest("details") || null;
        }
        field.classList.add("quote-field-error");
        field.setAttribute("aria-invalid", "true");
        const clearError = () => {
          field.classList.remove("quote-field-error");
          field.removeAttribute("aria-invalid");
        };
        const primaryEvent = field.matches("select,input[type=checkbox],input[type=radio]") ? "change" : "input";
        field.addEventListener(primaryEvent, clearError, { once:true });
        if (primaryEvent !== "change") field.addEventListener("change", clearError, { once:true });
      });
      const first = elements[0];
      if (first) window.setTimeout(() => {
        first.scrollIntoView({ behavior:"smooth", block:"center" });
        first.focus({ preventScroll:true });
      }, 0);
    }
    function renderIndustryCategories() {
      const box = document.getElementById("industryCategoryStrip");
      if (!box) return;
      const current = currentIndustryCategory();
      const templates = currentCategoryTemplates();
      box.innerHTML = Object.keys(industryCategories).map(name => `
        <button class="industry-chip ${name === current ? "active" : ""}" onclick="setIndustryCategory('${encodeURIComponent(name)}')">
          ${name}<span>${industryDescriptions[name]}</span>
        </button>
      `).join("");
      const hint = document.getElementById("industryTemplateHint");
      if (hint) {
        hint.textContent = `当前大类：${current}。可用模板：${templates.join("、")}。更多行业放在模板库里，不挤占首页。`;
      }
    }
    function applyTemplateToQuoteFields(name, updateOutput = true) {
      const template = productTemplates[name] || productTemplates["通用加工"];
      setIfExists("quoteTemplate", name);
      setIfExists("quoteProduct", template.product);
      setIfExists("quoteMaterial", template.material);
      setIfExists("quoteProcess", template.process);
      setQuoteDeliveryValue(template.delivery);
      if (updateOutput) {
        setQuoteOutputs(`已切换报价模板：${name}

已带入：
- 产品：${template.product}
- 材质：${template.material}
- 工艺：${template.process}
- 交期：${quoteDeliveryText(template.delivery)}

下一步：填写尺寸、数量、税运和收货地区后生成报价。`);
      }
    }
    function setIndustryCategory(categoryCode) {
      const category = decodeURIComponent(categoryCode);
      const safeCategory = industryCategories[category] ? category : "包装印刷";
      const firstTemplate = industryCategories[safeCategory][0];
      activateIndustryQuoteProfile(safeCategory, firstTemplate);
      saveState();
      restoreSettings();
      renderIndustryCategories();
      renderQuoteTemplateOptions();
      applyTemplateToQuoteFields(firstTemplate, false);
      renderIndustryQuoteCostFields();
      renderQuoteRules();
      renderDashboard();
    }
    function chooseIndustryAndStart(categoryCode) {
      setIndustryCategory(categoryCode);
      saveState();
      renderDashboard();
    }
    function currentCustomerName() {
      const quotePageActive = document.querySelector('[data-page="报价系统"]')?.classList.contains("active");
      if (quotePageActive && val("quoteCustomerName")) return val("quoteCustomerName");
      const quoteResultActive = document.querySelector('[data-page="报价结果"]')?.classList.contains("active");
      if (quoteResultActive) {
        const resultCustomer = activeQuoteRecord()?.customer || val("quoteCustomerName");
        if (resultCustomer) return resultCustomer;
      }
      const homePageActive = document.querySelector('[data-page="图片制作"]')?.classList.contains("active");
      if (homePageActive && val("homeCustomerName")) return val("homeCustomerName");
      return val("inquiryCustomerName") || state.member.companyName || "新客户";
    }
    function isPendingCustomerName(name) {
      const value = String(name || "").trim();
      return value.startsWith("待命名客户·") || /^新客户\s+\d+$/.test(value);
    }
    function customerSessionId() {
      return `session-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
    }
    function nextPendingCustomerName() {
      const allNames = [
        ...(state.customerSessions || []).map(item => item.customer),
        ...(state.customerProfiles || []).map(item => item.company)
      ];
      const used = allNames
        .map(name => String(name || "").trim().match(/^新客户\s+(\d+)$/) || String(name || "").trim().match(/^待命名客户·\d{4}-(\d+)$/))
        .filter(Boolean)
        .map(match => Number(match[1] || 0));
      const next = (used.length ? Math.max(...used) : 0) + 1;
      return `新客户 ${next}`;
    }
    function migrateLegacyPendingCustomerNames() {
      const legacyPattern = /^待命名客户·\d{4}-(\d+)$/;
      const sources = [
        ...(state.customerSessions || []).map(item => item?.customer),
        ...(state.customerProfiles || []).map(item => item?.company),
        ...(state.quoteHistory || []).map(item => item?.customer),
        ...(state.tasks || []).map(item => item?.cloudCustomer || String(item?.customer || "").split(" · ")[0])
      ].filter(Boolean);
      const used = new Set(sources.map(name => String(name).match(/^新客户\s+(\d+)$/)).filter(Boolean).map(match => Number(match[1])));
      const mapping = new Map();
      [...new Set(sources.filter(name => legacyPattern.test(String(name))))].forEach(name => {
        let number = Number(String(name).match(legacyPattern)?.[1] || 1);
        while (used.has(number)) number += 1;
        used.add(number);
        mapping.set(String(name), `新客户 ${number}`);
      });
      if (!mapping.size) return;
      const rename = name => mapping.get(String(name || "")) || name;
      (state.customerSessions || []).forEach(item => {
        item.customer = rename(item.customer);
        if (item.renameFrom) item.renameFrom = rename(item.renameFrom);
      });
      (state.customerProfiles || []).forEach(item => { item.company = rename(item.company); });
      (state.quoteHistory || []).forEach(item => { item.customer = rename(item.customer); });
      (state.tasks || []).forEach(item => {
        item.cloudCustomer = rename(item.cloudCustomer);
        const parts = String(item.customer || "").split(" · ");
        if (mapping.has(parts[0])) item.customer = [rename(parts[0]), ...parts.slice(1)].join(" · ");
      });
    }
    function customerSessionStatus(customer, session = null) {
      const relatedSession = session || (state.customerSessions || []).find(item => item.customer === customer && item.id === state.activeCustomerSessionId);
      if (!customer || ["新客户", "未命名客户"].includes(customer)) {
        if (isStructuredBusinessAnalysis(relatedSession?.businessAnalysis)) {
          return Array.isArray(relatedSession.businessAnalysis.missing) && relatedSession.businessAnalysis.missing.length ? "待补资料" : "待报价";
        }
        return "待消息";
      }
      if (isPendingCustomerName(customer)) return "待补姓名";
      const task = preferredCustomerTask(customerTasksByName(customer));
      const quotes = businessQuotes().filter(item => item.customer === customer);
      const latestQuote = quotes[0] || null;
      const profile = (state.customerProfiles || []).find(item => item.company === customer);
      if (task?.status && ["生产中", "已完成"].includes(taskPhase(task))) return task.status;
      if (latestQuote?.outcomeStatus === "已成交" || profile?.dealStatus === "已成交") return task?.status === "生产中" ? "生产" : "已成交";
      if (latestQuote?.outcomeStatus === "已流失" || profile?.dealStatus === "已流失") return "已流失";
      if (latestQuote?.outcomeStatus === "客户压价" || profile?.dealStatus === "客户压价") return "客户压价";
      if (task?.status && customerTaskStageRank(task.status) >= customerTaskStageRank("待确认")) return task.status;
      if (task?.status && (!quotes.length || customerTaskStageRank(task.status) >= customerTaskStageRank("已报价"))) return task.status;
      if (quotes.length) return "已报价";
      if (task?.status) return task.status;
      return profile?.dealStatus || "待整理";
    }
    function homeSessionIsProcessing(session) {
      return session?.processingStatus === "processing" && Boolean(session.processingRequestId);
    }
    function homeSessionIsForeground(session) {
      return Boolean(
        session?.id
        && session.id === state.activeCustomerSessionId
        && document.querySelector('.page.active')?.dataset.page === "图片制作"
      );
    }
    function homeSessionById(sessionId) {
      return ensureCustomerSessions().find(item => item.id === sessionId) || null;
    }
    function homeVisualProcessingHtml(session) {
      const mode = String(session?.lastImageCreationMode || "mockup");
      const presentation = {
        main_image: ["主图", "正在根据产品图生成电商主图…"],
        ad_image: ["广告图", "正在根据产品图制作广告画面…"],
        text_image: ["图片", "正在把文字描述生成图片…"],
        detail_image: ["效果图", "正在根据参考图生成产品效果图…"],
        mockup: ["效果图", "正在根据参考图生成产品效果图…"]
      }[mode] || ["图片", "正在生成图片…"];
      const startedAt = new Date(session?.processingStartedAt || 0).getTime();
      const elapsed = startedAt > 0 ? Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) : 0;
      const progressMessage = String(session?.processingProgressMessage || presentation[1]);
      return `
        <div class="ai-answer-body">
          <div class="ai-loading" id="homeVisualThinkingState" role="status" aria-live="polite" aria-label="正在首页生成${escapeHtml(presentation[0])}">
            <span class="ai-loading-spinner" aria-hidden="true"><i></i><i></i><i></i></span>
            <span class="ai-loading-copy"><strong id="homeVisualThinkingText">${escapeHtml(progressMessage)}</strong><em><span class="ai-elapsed-time" id="homeVisualThinkingElapsed">已等待 ${elapsed} 秒</span><span class="ai-elapsed-note">完成后图片会直接显示在这里</span></em></span>
          </div>
        </div>
      `.trim();
    }
    window.__homeVisualProcessingHtml = homeVisualProcessingHtml;
    function beginHomeSessionJob(session, kind, prompt = "") {
      if (!session || homeSessionIsProcessing(session)) return "";
      const requestId = `home-job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
      session.processingStatus = "processing";
      session.processingKind = ["visual", "detail_package"].includes(kind) ? kind : "analysis";
      session.processingRequestId = requestId;
      session.processingStartedAt = new Date().toISOString();
      session.processingPrompt = String(prompt || "").slice(0, 2000);
      session.processingProgressMessage = "";
      session.hasUnreadResult = false;
      session.updatedAt = new Date().toISOString();
      saveState();
      return requestId;
    }
    function finishHomeSessionJob(session, requestId, status = "completed") {
      if (!session || session.processingRequestId !== requestId) return false;
      const isForeground = homeSessionIsForeground(session);
      session.processingStatus = isForeground ? "" : (status === "failed" ? "failed" : "completed");
      session.processingKind = "";
      session.processingRequestId = "";
      session.processingStartedAt = "";
      session.processingProgressMessage = "";
      session.hasUnreadResult = !isForeground;
      session.updatedAt = new Date().toISOString();
      saveState();
      return true;
    }
    function syncHomeSessionProcessingUi(session = activeCustomerSession()) {
      const sendButton = document.querySelector(".gpt-send");
      if (!sendButton) return;
      const processing = homeSessionIsProcessing(session);
      const rawMode = String(window.__selectedHomeCapability || session?.mode || "main_image");
      const legacyMode = rawMode === "detail" || rawMode === "detail_package" ? "detail_image" : rawMode === "visual" ? "mockup" : rawMode;
      const action = ({
        main_image: "生成主图",
        detail_image: "规划详情图",
        ad_image: "生成广告图",
        text_image: "生成图片",
        mockup: "生成效果图"
      })[legacyMode] || "生成图片";
      sendButton.disabled = processing;
      sendButton.textContent = processing ? "生成中…" : action;
      sendButton.setAttribute("aria-label", processing ? "图片正在生成" : action);
      sendButton.title = processing ? "图片正在生成" : action;
    }
    function notifyHomeSessionCompleted(session, failed = false) {
      if (!session || homeSessionIsForeground(session)) return;
      const message = failed
        ? `${session.customer} 处理未完成，可切回查看并重试。`
        : `${session.customer} 已处理完成，点击左侧会话查看结果。`;
      notify(message);
      try {
        if (Notification.permission === "granted") {
          new Notification("将莱数字员工", { body: message, icon: "assets/报价跟单助手-logo.png" });
        }
      } catch (_) {}
    }
    function isStructuredBusinessAnalysis(analysis) {
      if (!analysis || typeof analysis !== "object") return false;
      if (["casual_chat", "direct_answer", "service_unavailable", "image_unavailable", "image_other", "image_qa"].includes(analysis.intent)) return false;
      const product = String(analysis.product || "").trim();
      if (product && !["定制产品", "待确认"].includes(product) && !product.includes("待确认")) return true;
      return ["size", "qty", "material", "process"].some(field => {
        const value = Array.isArray(analysis[field]) ? analysis[field].join("、") : String(analysis[field] || "");
        return value && !value.includes("待确认");
      });
    }
    function customerSessionDisplay(session) {
      if (!session) return { title:"新客户", action:"等待客户消息" };
      const text = String(session.draft || session.lastMessage || "").trim();
      const info = session.businessAnalysis || (text ? analyze(text) : null);
      const customer = String(session.customer || "").trim();
      const task = preferredCustomerTask(customerTasksByName(customer));
      const quote = businessQuotes().find(item => item.customer === customer);
      const profile = (state.customerProfiles || []).find(item => item.company === customer);
      const taskProduct = String(task?.customer || "").split(" · ").slice(1).join(" · ") || "";
      const product = quote?.product || taskProduct || profile?.need || (info?.product && info.product !== "定制产品" ? info.product : "");
      const status = customerSessionStatus(customer, session);
      const workflowActions = {
        "待消息":"等待客户消息", "待补姓名":"先补客户姓名", "询盘":"待整理需求", "待整理":"待整理需求", "待整理询盘":"待整理需求",
        "待补资料":"待补报价资料", "未报价":"待报价", "待报价":"待报价", "已报价":"等待客户反馈", "客户压价":"继续议价",
        "待确认":"等待客户确认", "打样":"跟进打样", "已成交":"准备生产", "生产":"查看生产进度", "生产中":"查看生产进度",
        "已发货":"跟进签收", "已回款":"订单已回款", "已完成":"订单已完成", "已归档":"订单已归档", "已流失":"客户已流失"
      };
      if (workflowActions[status]) return { title: product || session.customer || "新客户", action: workflowActions[status] };
      let action = "待整理需求";
      if (hasAny(text, ["效果图", "产品图", "主图", "做图"])) action = "待做效果图";
      else if (hasAny(text, ["材质待确认", "确认材质", "材质"] ) && (!info?.material || info.material.includes("待确认"))) action = "待确认材质";
      else if (hasAny(text, ["报价", "价格", "多少钱"])) action = "待报价";
      else if (info && product) action = Array.isArray(info.missing) && info.missing.length ? "待补报价资料" : "待报价";
      else if (hasAny(text, ["打样", "样品"]) && !hasAny(text, ["不需要打样", "无需打样", "不用打样", "不打样"])) action = "待确认打样";
      else if (!text) action = "等待客户消息";
      return { title: product ? product : session.customer || "新客户", action };
    }
    function ensureCustomerSessions() {
      state.customerSessions = Array.isArray(state.customerSessions)
        ? state.customerSessions.filter(item => item && item.id && item.customer)
        : [];
      migrateLegacyPendingCustomerNames();
      (state.customerProfiles || []).filter(profile => !isInternalDemoRecord({ customer: profile?.company })).forEach((profile, index) => {
        if (!profile?.company || profile.company === "新客户") return;
        if (state.customerSessions.some(item => item.customer === profile.company)) return;
        state.customerSessions.push({
          id: `profile-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 5)}`,
          customer: profile.company,
          draft: "",
          lastMessage: profile.need || "历史客户档案",
          updatedAt: profile.updatedAt || profile.createdAt || new Date().toISOString(),
          mode: "", analysisMode: "standard", lastRequestedAnalysisMode: "standard",
          resultEmpty: true,
          hasConversation: false
        });
      });
      let openSessions = state.customerSessions.filter(item => !item.archivedAt);
      if (!openSessions.length) {
        const blankSession = {
          id: customerSessionId(), customer: "新客户", draft: "", lastMessage: "等待客户消息",
          updatedAt: new Date().toISOString(), mode: "", analysisMode: "standard",
          lastRequestedAnalysisMode: "standard", resultEmpty: true, hasConversation: false
        };
        state.customerSessions.push(blankSession);
        openSessions = [blankSession];
      }
      if (!openSessions.some(item => item.id === state.activeCustomerSessionId)) {
        state.activeCustomerSessionId = openSessions[0].id;
      }
      return openSessions;
    }
    function activeCustomerSession() {
      const openSessions = ensureCustomerSessions();
      return openSessions.find(item => item.id === state.activeCustomerSessionId) || openSessions[0];
    }
    function isStaleImageSetupFailureText(text = "") {
      return /(图片服务还差一步|图片服务还差[“\"']?图片分组|图片\s*Key\s*尚未通过|图片Key未通过|尚未通过\s*gpt-image-2\s*权限|gpt-image-2\s*权限验证|当前火神AI图片Key.*未通过)/i.test(String(text || ""));
    }
    function isHistoricalServiceFailureText(text = "") {
      return isStaleImageSetupFailureText(text)
        || /(视觉服务还没连接|通用AI接口.*没有连接|当前还差AI服务|ModelNotOpen|生图失败（?404|接口尚未连接|配置视觉模型后)/i.test(String(text || ""));
    }
    function staleImageSetupRecoveryInnerHtml() {
      const configured = window.__imageGenerationConfigured === true;
      const verification = String(window.__imageGenerationVerificationStatus || "unknown");
      const retryAllowed = window.__imageGenerationRetryAllowed !== false;
      const canRetry = configured && retryAllowed && verification !== "invalid";
      const statusText = canRetry
        ? verification === "verified"
          ? "当前图片服务已通过实际生图验证。下面这条只是旧版失败记录，不代表现在仍失败。"
          : "当前图片服务配置已保留，可以重新生成验证。下面这条只是旧版失败记录，不代表当前状态。"
        : configured
          ? "当前图片服务正在读取最新状态；旧版失败记录不会覆盖现在的配置。"
          : window.__imageGenerationConfigured === false
            ? "当前图片服务需要平台检查；旧版记录本身不作为当前状态。"
            : "正在读取当前图片服务状态；旧版失败记录不代表现在仍失败。";
      const primaryAction = canRetry || window.__imageGenerationConfigured !== false
        ? `<button class="btn dark" type="button" onclick="window.retryStaleImageSetupFromHistory?.()">${verification === "verified" ? "按原要求重新生成" : "重新检测并原地重试"}</button>`
        : `<button class="btn dark" type="button" onclick="openFormalOps()">查看服务状态</button>`;
      return `<strong>当前状态以实时检测为准</strong><span>${escapeHtml(statusText)}</span><div class="historical-service-actions">${primaryAction}<button class="btn light" type="button" onclick="openFormalOps()">查看当前图片服务</button></div>`;
    }
    function staleImageSetupRecoveryMarkup() {
      return `<div class="stale-image-setup-recovery" data-stale-image-setup-recovery>${staleImageSetupRecoveryInnerHtml()}</div>`;
    }
    function refreshStaleImageSetupRecoveryUi() {
      document.querySelectorAll("[data-stale-image-setup-recovery]").forEach(box => {
        box.innerHTML = staleImageSetupRecoveryInnerHtml();
      });
    }
    window.refreshStaleImageSetupRecoveryUi = refreshStaleImageSetupRecoveryUi;
    function compactStaleImageSetupAnswerHtml(html = "") {
      if (!String(html).trim() || !isStaleImageSetupFailureText(html)) return html;
      const template = document.createElement("template");
      template.innerHTML = html;
      if (template.content.querySelector('[data-stale-image-setup-record="true"]')) {
        if (!template.content.querySelector("[data-stale-image-setup-recovery]")) {
          const target = template.content.querySelector(".ai-answer-body") || template.content;
          const recovery = document.createElement("template");
          recovery.innerHTML = staleImageSetupRecoveryMarkup();
          target.appendChild(recovery.content);
        }
        return template.innerHTML;
      }
      template.content.querySelectorAll(".actions, .historical-service-actions, [data-stale-image-setup-recovery]").forEach(item => item.remove());
      const original = template.innerHTML;
      return `<div class="ai-answer-body"><details class="historical-service-note stale-image-setup-note" data-stale-image-setup-record="true"><summary>旧版图片服务异常记录（不代表当前状态）</summary><div>${original}</div></details>${staleImageSetupRecoveryMarkup()}</div>`;
    }
    function compactHistoricalServiceMessages(html = "") {
      if (!String(html).trim()) return "";
      const template = document.createElement("template");
      template.innerHTML = html;
      template.content.querySelectorAll(".chat-assistant-row").forEach(row => {
        if (!isHistoricalServiceFailureText(row.textContent)) return;
        const output = row.querySelector(".output");
        if (!output) return;
        const staleImageSetup = isStaleImageSetupFailureText(row.textContent);
        if (row.querySelector(".historical-service-note")) {
          if (staleImageSetup && !row.querySelector("[data-stale-image-setup-recovery]")) {
            output.insertAdjacentHTML("beforeend", staleImageSetupRecoveryMarkup());
          }
          return;
        }
        const originalTemplate = document.createElement("template");
        originalTemplate.innerHTML = output.innerHTML;
        originalTemplate.content.querySelectorAll(".actions, .historical-service-actions, [data-stale-image-setup-recovery]").forEach(item => item.remove());
        output.innerHTML = `<details class="historical-service-note${staleImageSetup ? " stale-image-setup-note" : ""}"${staleImageSetup ? ' data-stale-image-setup-record="true"' : ""}><summary>${staleImageSetup ? "旧版图片服务异常记录（不代表当前状态）" : "当时的服务异常记录（已折叠）"}</summary><div>${originalTemplate.innerHTML}</div></details>`;
      });
      return template.innerHTML;
    }
    function restoreSessionUserAttachmentMarkup(html = "", fileIds = [], fileNames = [], fileRecords = []) {
      const ids = (Array.isArray(fileIds) ? fileIds : []).map(Number).filter(id => id > 0);
      if (!ids.length) return html;
      if (String(html).includes("data-user-attachments")) {
        const template = document.createElement("template");
        template.innerHTML = html;
        const buttons = Array.from(template.content.querySelectorAll(".chat-user-attachment"));
        const recordsByKey = new Map((Array.isArray(fileRecords) ? fileRecords : [])
          .filter(record => record?.key && Number(record?.id || 0) > 0)
          .map(record => [String(record.key), record]));
        buttons.forEach(button => {
          const record = recordsByKey.get(String(button.dataset.userFileKey || ""));
          if (!record) return;
          button.dataset.userFileId = String(record.id);
          if (record.name) button.dataset.imageName = record.name;
          if (record.mimeType) button.dataset.attachmentMime = String(record.mimeType);
          if (record.mimeType && !String(record.mimeType).startsWith("image/")) {
            button.classList.add("is-not-image");
            button.dataset.persistentThumb = "not-image";
            button.disabled = true;
          }
        });
        if (!recordsByKey.size && buttons.length === ids.length) {
          buttons.forEach((button, index) => {
            button.dataset.userFileId = String(ids[index]);
            if (fileNames[index]) button.dataset.imageName = fileNames[index];
            if (!attachmentNameLooksLikeImage(fileNames[index])) {
              button.classList.add("is-not-image");
              button.dataset.persistentThumb = "not-image";
              button.disabled = true;
            }
          });
        }
        return template.innerHTML;
      }
      return `${html}${homeUserAttachmentMarkup(ids.map((id, index) => {
        const name = fileNames[index] || `历史附件 ${index + 1}`;
        return { id, name, imageLike: attachmentNameLooksLikeImage(name) };
      }), "附件")}`;
    }
    function homeConversationScroller() {
      const scroller = document.querySelector(".gpt-home.has-conversation .gpt-result") || document.getElementById("homeResult");
      trackHomeConversationScroller(scroller);
      return scroller;
    }
    let homeConversationScrollGeneration = 0;
    let homeConversationScrollTracker = null;
    function homeConversationIsNearLatest(scroller, threshold = 96) {
      if (!scroller) return true;
      const remaining = Number(scroller.scrollHeight || 0)
        - Number(scroller.clientHeight || 0)
        - Number(scroller.scrollTop || 0);
      return remaining <= threshold;
    }
    function trackHomeConversationScroller(scroller) {
      if (!scroller || homeConversationScrollTracker?.scroller === scroller) return;
      homeConversationScrollTracker?.cleanup?.();
      const tracker = { scroller, userAway: false, cleanup: null };
      const markUserAway = () => {
        if (tracker.userAway) return;
        tracker.userAway = true;
        homeConversationScrollGeneration += 1;
      };
      const onWheel = event => {
        if (Number(event?.deltaY || 0) < 0) markUserAway();
      };
      let lastTouchY = null;
      const onTouchStart = event => {
        lastTouchY = Number(event?.touches?.[0]?.clientY);
      };
      const onTouchMove = event => {
        const currentY = Number(event?.touches?.[0]?.clientY);
        if (Number.isFinite(currentY) && Number.isFinite(lastTouchY) && currentY > lastTouchY + 2) markUserAway();
        if (Number.isFinite(currentY)) lastTouchY = currentY;
      };
      const onPointerDown = event => {
        const right = Number(scroller.getBoundingClientRect?.().right || 0);
        if (right && Number(event?.clientX || 0) >= right - 20) markUserAway();
      };
      const onKeyDown = event => {
        if (["ArrowUp", "PageUp", "Home"].includes(event?.key) || (event?.key === " " && event?.shiftKey)) markUserAway();
      };
      const onScroll = () => {
        if (homeConversationIsNearLatest(scroller)) tracker.userAway = false;
      };
      scroller.addEventListener?.("wheel", onWheel, { passive: true });
      scroller.addEventListener?.("touchstart", onTouchStart, { passive: true });
      scroller.addEventListener?.("touchmove", onTouchMove, { passive: true });
      scroller.addEventListener?.("pointerdown", onPointerDown, { passive: true });
      scroller.addEventListener?.("scroll", onScroll, { passive: true });
      window.addEventListener?.("keydown", onKeyDown);
      tracker.cleanup = () => {
        scroller.removeEventListener?.("wheel", onWheel);
        scroller.removeEventListener?.("touchstart", onTouchStart);
        scroller.removeEventListener?.("touchmove", onTouchMove);
        scroller.removeEventListener?.("pointerdown", onPointerDown);
        scroller.removeEventListener?.("scroll", onScroll);
        window.removeEventListener?.("keydown", onKeyDown);
      };
      homeConversationScrollTracker = tracker;
    }
    function resetHomeConversationScrollFollow(scroller = homeConversationScroller()) {
      if (!scroller) return;
      trackHomeConversationScroller(scroller);
      if (homeConversationScrollTracker?.scroller === scroller) homeConversationScrollTracker.userAway = false;
      homeConversationScrollGeneration += 1;
    }
    function homeConversationMayAutoFollow(scroller) {
      if (!scroller) return false;
      trackHomeConversationScroller(scroller);
      return homeConversationScrollTracker?.scroller !== scroller || homeConversationScrollTracker.userAway !== true;
    }
    function setHomeConversationScrollTop(scroller, top) {
      if (!scroller) return;
      const previousBehavior = scroller.style?.scrollBehavior || "";
      if (scroller.style) scroller.style.scrollBehavior = "auto";
      scroller.scrollTop = Math.max(0, Number(top || 0));
      window.requestAnimationFrame(() => {
        if (scroller.isConnected !== false && scroller.style) scroller.style.scrollBehavior = previousBehavior;
      });
    }
    function scrollHomeConversationToLatest(force = false) {
      const initialScroller = homeConversationScroller();
      if (!initialScroller) return;
      if (force) resetHomeConversationScrollFollow(initialScroller);
      if (!homeConversationMayAutoFollow(initialScroller)) return;
      const generation = ++homeConversationScrollGeneration;
      const sessionId = String(state.activeCustomerSessionId || "");
      const settleAtLatest = () => {
        if (generation !== homeConversationScrollGeneration) return;
        if (sessionId !== String(state.activeCustomerSessionId || "")) return;
        const conversationScroller = homeConversationScroller();
        if (conversationScroller && homeConversationMayAutoFollow(conversationScroller)) {
          setHomeConversationScrollTop(conversationScroller, Number(conversationScroller.scrollHeight || 0));
        }
      };
      settleAtLatest();
      window.requestAnimationFrame(() => {
        settleAtLatest();
        const conversationScroller = homeConversationScroller();
        conversationScroller?.querySelectorAll("img").forEach((image) => {
          if (!image.complete) image.addEventListener("load", settleAtLatest, { once: true });
        });
        window.requestAnimationFrame(settleAtLatest);
      });
      // 历史缩略图和云端记录可能在切换后的数秒内继续改变内容高度。
      // 只在当前客户会话内做有限次数的收尾校正；下一次切换会让旧任务立即失效。
      [120, 500, 1400, 3000].forEach(delay => window.setTimeout?.(settleAtLatest, delay));
    }
    function captureActiveCustomerSession(touch = false, customerOverride = null) {
      const session = activeCustomerSession();
      if (!session) return;
      const customerSource = customerOverride == null
        ? (document.getElementById("homeCustomerName")?.value || session.customer || "新客户")
        : customerOverride;
      const customer = String(customerSource || "新客户").trim() || "新客户";
      const draft = document.getElementById("aiQuestion")?.value || "";
      session.customer = customer;
      session.draft = draft;
      session.mode = String(window.__selectedHomeCapability || "");
      session.analysisMode = normalizeHomeAnalysisMode(window.__homeAnalysisMode);
      session.chatHistoryHtml = document.getElementById("homeChatHistory")?.innerHTML || "";
      session.userMessageHtml = document.getElementById("homeUserMessage")?.innerHTML || "";
      session.userFileIds = [...(window.__homeCurrentTurnFileIds || [])];
      session.userFileNames = [...(window.__homeCurrentTurnFileNames || [])];
      session.userFileRecords = Array.from(document.getElementById("homeUserMessage")?.querySelectorAll(".chat-user-attachment[data-user-file-id]") || [])
        .map(button => ({
          id: Number(button.dataset.userFileId || 0),
          key: String(button.dataset.userFileKey || ""),
          name: String(button.dataset.imageName || ""),
          mimeType: String(button.dataset.attachmentMime || "")
        }))
        .filter(record => record.id > 0);
      session.userHidden = document.getElementById("homeUserRow")?.classList.contains("is-hidden") !== false;
      session.answerHtml = document.getElementById("aiAnswer")?.innerHTML || "";
      session.nextActionsHtml = document.getElementById("aiNextActions")?.innerHTML || "";
      session.nextActionsHidden = document.getElementById("aiNextActions")?.classList.contains("is-hidden") !== false;
      session.resultEmpty = document.getElementById("homeResult")?.classList.contains("is-empty") !== false;
      session.hasConversation = document.querySelector(".gpt-home")?.classList.contains("has-conversation") === true;
      const conversationScroller = homeConversationScroller();
      session.chatScrollTop = Number(conversationScroller?.scrollTop || 0);
      if (isStructuredBusinessAnalysis(window.__latestAiAnalysis)) {
        session.businessAnalysis = window.__latestAiAnalysis;
      }
      session.retainedFileIds = homeRetainedFileIds();
      session.retainedFileNames = homeRetainedFileNames();
      window.__captureHomeSessionUploadState?.(session);
      if (typeof captureHomeInlineQuoteDraft === "function") {
        captureHomeInlineQuoteDraft(session);
      }
      if (draft) session.lastMessage = draft.slice(0, 80);
      if (touch) session.updatedAt = new Date().toISOString();
      saveState();
    }
    function restoreCustomerSession(session) {
      if (!session) return;
      if (typeof syncHomeInlineQuoteWorkspaceSession === "function") {
        syncHomeInlineQuoteWorkspaceSession(session);
      }
      closeHomeDemo();
      setIfExists("homeCustomerName", session.customer || "新客户");
      setIfExists("inquiryCustomerName", session.customer || "新客户");
      setIfExists("aiQuestion", session.draft || "");
      window.__selectedHomeCustomerName = session.customer || "新客户";
      window.__selectedHomeCapability = session.mode || "";
      applyHomeAnalysisMode(session.analysisMode || "standard", { session, persist: false });
      window.__latestAiAnalysis = session.businessAnalysis || null;
      window.__latestAiSourceText = session.businessSourceText || "";
      syncHomeCapabilityUi(session.mode || "");
      const history = document.getElementById("homeChatHistory");
      const bubble = document.getElementById("homeUserMessage");
      const row = document.getElementById("homeUserRow");
      const answer = document.getElementById("aiAnswer");
      const nextActions = document.getElementById("aiNextActions");
      const restoredUserFileIds = Array.isArray(session.userFileIds) ? session.userFileIds : [];
      const restoredUserFileNames = Array.isArray(session.userFileNames) ? session.userFileNames : [];
      const restoredUserFileRecords = Array.isArray(session.userFileRecords) ? session.userFileRecords : [];
      const shouldRestoreNextActions = session.nextActionsHidden === false && session.hasConversation === true;
      const sanitizedHistoryHtml = sanitizeRestoredChatHtml(session.chatHistoryHtml || "");
      const restoredHistoryHtml = upgradeGeneratedImageHtml(compactHistoricalServiceMessages(sanitizedHistoryHtml));
      if (history) history.innerHTML = restoredHistoryHtml;
      session.chatHistoryHtml = restoredHistoryHtml;
      const sanitizedUserMessageHtml = sanitizeRestoredChatHtml(session.userMessageHtml || "");
      const restoredUserMessageHtml = restoreSessionUserAttachmentMarkup(sanitizedUserMessageHtml, restoredUserFileIds, restoredUserFileNames, restoredUserFileRecords);
      if (bubble) bubble.innerHTML = restoredUserMessageHtml;
      session.userMessageHtml = restoredUserMessageHtml;
      window.__homeCurrentTurnFileIds = [...restoredUserFileIds];
      window.__homeCurrentTurnFileNames = [...restoredUserFileNames];
      row?.classList.toggle("is-hidden", session.userHidden !== false);
      const sanitizedAnswerHtml = sanitizeRestoredChatHtml(session.answerHtml || "");
      const restoredAnswerCandidate = upgradeGeneratedImageHtml(compactStaleImageSetupAnswerHtml(sanitizedAnswerHtml));
      let restoredAnswerHtml = restoredAnswerCandidate === "结果会显示在这里。你可以先粘贴客户消息，也可以直接上传客户截图或产品图。"
        ? ""
        : restoredAnswerCandidate;
      const restoringVisualProgress = homeSessionIsProcessing(session) && session.processingKind === "visual";
      if (restoringVisualProgress) {
        restoredAnswerHtml = homeVisualProcessingHtml(session);
        session.resultEmpty = false;
        session.hasConversation = true;
      }
      const imageStudioOnly = document.getElementById("homeWorkbench")?.classList.contains("image-studio-only") === true;
      const studioHasGeneratedResult = /(generated-image|home-inline-visual|detail-package|detail-page)/.test(restoredAnswerHtml);
      const studioHasActiveTask = studioHasGeneratedResult
        || (session?.processingStatus === "processing" && Boolean(session.processingRequestId));
      const visibleAnswerHtml = imageStudioOnly && !studioHasActiveTask ? "" : restoredAnswerHtml;
      if (imageStudioOnly && history) history.innerHTML = "";
      if (imageStudioOnly) row?.classList.add("is-hidden");
      if (answer) answer.innerHTML = visibleAnswerHtml || "生成结果会直接显示在这里。";
      session.answerHtml = restoredAnswerHtml;
      session.nextActionsHtml = "";
      nextActions?.classList.add("is-hidden");
      session.nextActionsHidden = true;
      document.getElementById("homeResult")?.classList.toggle("is-empty", session.resultEmpty !== false);
      document.querySelector(".gpt-home")?.classList.toggle("has-conversation", session.hasConversation === true);
      window.__lastRecognizedHomeFileIds = Array.isArray(session.retainedFileIds) ? [...session.retainedFileIds] : [];
      window.__homeRetainedFileNames = Array.isArray(session.retainedFileNames) ? [...session.retainedFileNames] : [];
      window.__applyHomeSessionUploadState?.(session);
      const input = document.getElementById("aiQuestion");
      autoGrowHomeQuestion(input);
      const files = document.getElementById("homeFiles");
      if (files) files.value = "";
      clearHomeFilePreviewUrls();
      window.updateHomeFileName?.();
      renderHomeRetainedFileContext();
      window.requestAnimationFrame(syncHomeComposerClearance);
      window.requestAnimationFrame(() => {
        const hydration = window.__hydrateHomeUserAttachmentThumbs?.(document.querySelector(".gpt-home"), { reset: true });
        scrollHomeConversationToLatest(true);
        Promise.resolve(hydration).catch(() => null).finally(scrollHomeConversationToLatest);
      });
      if (nextActions && shouldRestoreNextActions) {
        renderHomeNextActions((bubble?.textContent || session.draft || "").trim(), false);
        session.nextActionsHtml = nextActions.innerHTML;
        session.nextActionsHidden = false;
      }
      window.__applyPendingHomeSessionResult?.(session);
      if (["completed", "failed"].includes(session.processingStatus) && session.hasUnreadResult) {
        session.processingStatus = "";
        session.hasUnreadResult = false;
      }
      syncHomeSessionProcessingUi(session);
      saveState();
      window.__selectHomeCustomerConnected?.();
    }
    function switchCustomerSession(sessionCode) {
      const id = decodeURIComponent(sessionCode);
      captureActiveCustomerSession();
      const session = ensureCustomerSessions().find(item => item.id === id);
      if (!session) return;
      state.activeCustomerSessionId = session.id;
      saveState();
      restoreCustomerSession(session);
      toggleMobileCustomerRail(false);
      renderCustomerWorkbench();
    }
    function createCustomerSession() {
      captureActiveCustomerSession();
      const current = activeCustomerSession();
      const reuseEmpty = current && current.customer === "新客户" && !current.draft && !current.hasConversation;
      const session = reuseEmpty ? current : {
        id: customerSessionId(), customer: "新客户", draft: "", lastMessage: "等待客户消息",
        updatedAt: new Date().toISOString(), mode: "", analysisMode: "standard",
        lastRequestedAnalysisMode: "standard", resultEmpty: true, hasConversation: false,
        userHidden: true, nextActionsHidden: true
      };
      session.customer = nextPendingCustomerName();
      session.updatedAt = new Date().toISOString();
      if (!reuseEmpty) state.customerSessions.unshift(session);
      state.activeCustomerSessionId = session.id;
      saveState();
      restoreCustomerSession(session);
      toggleMobileCustomerRail(false);
      renderCustomerWorkbench();
      document.getElementById("aiQuestion")?.focus();
      notify(`已建立 ${session.customer}，消息会独立保存，姓名可稍后补充。`);
    }
    function ensurePendingCustomerIdentity() {
      const input = document.getElementById("homeCustomerName");
      const current = (input?.value || "").trim();
      if (current && !["新客户", "未命名客户"].includes(current)) return current;
      const name = nextPendingCustomerName();
      if (input) input.value = name;
      setIfExists("inquiryCustomerName", name);
      const session = activeCustomerSession();
      if (session) {
        session.customer = name;
        session.updatedAt = new Date().toISOString();
      }
      window.__selectedHomeCustomerName = name;
      const status = document.getElementById("homeMemoryStatus");
      if (status) {
        status.textContent = `已暂存为 ${name}，以后可以补姓名或直接删除`;
        status.classList.add("is-saved");
      }
      saveState();
      renderCustomerWorkbench();
      return name;
    }
    function findExistingCustomerSuggestion(text) {
      const source = String(text || "").trim();
      const current = activeCustomerSession()?.customer || "新客户";
      if (!source || (!isPendingCustomerName(current) && !["新客户", "未命名客户"].includes(current))) return null;
      const scored = businessProfiles()
        .filter(profile => profile.company && !isPendingCustomerName(profile.company))
        .map(profile => {
          let score = 0;
          let reason = "历史需求相似";
          const company = String(profile.company || "").trim();
          const contactTokens = String(profile.contact || "")
            .split(/[、,，/\s]+/)
            .map(item => item.trim())
            .filter(item => item.length >= 2 && !["未填写", "联系人未填"].includes(item));
          if (company.length >= 2 && source.includes(company)) {
            score = 120;
            reason = "消息中出现了客户名称";
          } else if (contactTokens.some(token => source.includes(token))) {
            score = 100;
            reason = "消息中出现了历史联系人";
          } else {
            const need = String(profile.need || "").trim();
            const preferenceTokens = String(profile.preference || "")
              .split(/[；;、,，/\s]+/)
              .map(item => item.trim())
              .filter(item => item.length >= 2 && !item.includes("待沉淀"));
            if (need.length >= 2 && source.includes(need)) score += 32;
            score += Math.min(2, preferenceTokens.filter(token => source.includes(token)).length) * 18;
          }
          return { profile, score, reason };
        })
        .filter(item => item.score >= 60)
        .sort((a, b) => b.score - a.score);
      if (!scored.length) return null;
      if (scored[1] && scored[0].score < 90 && scored[0].score - scored[1].score < 20) return null;
      return scored[0];
    }
    function renderExistingCustomerSuggestion() {
      const box = document.getElementById("homeCustomerMatch");
      if (!box) return;
      const session = activeCustomerSession();
      const suggestion = findExistingCustomerSuggestion(session?.draft || "");
      if (!suggestion || session?.dismissedSuggestion === suggestion.profile.company) {
        window.__homeCustomerSuggestion = null;
        box.classList.add("is-hidden");
        box.innerHTML = "";
        return;
      }
      window.__homeCustomerSuggestion = suggestion.profile;
      box.innerHTML = `<span>可能是老客户：<strong>${escapeHtml(suggestion.profile.company)}</strong>${suggestion.profile.contact ? ` · ${escapeHtml(suggestion.profile.contact)}` : ""}<br>${escapeHtml(suggestion.reason)}，确认后会读取历史报价和聊天记忆。</span><button class="primary" type="button" onclick="linkSuggestedCustomer()">关联此客户</button><button type="button" onclick="dismissSuggestedCustomer()">不是</button>`;
      box.classList.remove("is-hidden");
    }
    function linkSuggestedCustomer() {
      const profile = window.__homeCustomerSuggestion;
      const session = activeCustomerSession();
      if (!profile || !session) return;
      const previous = session.customer;
      const pendingStored = (state.customerProfiles || []).some(item => item.company === previous);
      session.customer = profile.company;
      session.dismissedSuggestion = "";
      if (pendingStored && isPendingCustomerName(previous)) session.renameFrom = previous;
      else delete session.renameFrom;
      setIfExists("homeCustomerName", profile.company);
      setIfExists("inquiryCustomerName", profile.company);
      saveState();
      renderCustomerWorkbench();
      selectHomeCustomer();
      notify(`已关联 ${profile.company}，后续会沿用该客户的历史记忆。`);
    }
    function dismissSuggestedCustomer() {
      const session = activeCustomerSession();
      if (session && window.__homeCustomerSuggestion?.company) session.dismissedSuggestion = window.__homeCustomerSuggestion.company;
      window.__homeCustomerSuggestion = null;
      saveState();
      renderExistingCustomerSuggestion();
    }
    function syncActiveCustomerSessionDraft() {
      const session = activeCustomerSession();
      if (!session) return;
      const draft = document.getElementById("aiQuestion")?.value || "";
      session.draft = draft;
      session.analysisMode = normalizeHomeAnalysisMode(window.__homeAnalysisMode);
      if (draft) session.lastMessage = draft.slice(0, 80);
      session.updatedAt = new Date().toISOString();
      saveState();
      renderCustomerWorkbench();
    }
    function syncActiveCustomerSessionName() {
      const name = (document.getElementById("homeCustomerName")?.value || "").trim() || "新客户";
      // Typing is only a preview. Persisting every keystroke used to rename
      // the active session before the datalist choice was committed, so an
      // existing customer could appear twice and partial names were saved.
      setIfExists("inquiryCustomerName", name);
    }
    function syncQuoteCustomerToActiveSession(customer) {
      const name = String(customer || "").trim();
      if (!name || ["新客户", "未命名客户"].includes(name) || isPendingCustomerName(name)) return false;
      const session = activeCustomerSession();
      if (!session) return false;
      const previous = String(session.customer || "新客户").trim() || "新客户";
      if (previous !== name && (isPendingCustomerName(previous) || ["新客户", "未命名客户"].includes(previous))) {
        session.renameFrom = previous;
      }
      session.customer = name;
      session.updatedAt = new Date().toISOString();
      setIfExists("homeCustomerName", name);
      setIfExists("inquiryCustomerName", name);
      window.__selectedHomeCustomerName = name;
      renderCustomerWorkbench();
      return previous !== name;
    }
    function toggleMobileCustomerRail(open) {
      document.getElementById("homeWorkbench")?.classList.toggle("mobile-conversations-open", !!open);
    }
    function sortedCustomerSessionsForRail(sessions = ensureCustomerSessions()) {
      const query = (document.getElementById("homeSessionSearch")?.value || "").trim().toLowerCase();
      return [...sessions]
        .filter(item => !query || `${item.customer} ${item.lastMessage || ""} ${customerSessionStatus(item.customer, item)}`.toLowerCase().includes(query))
        .sort((a, b) => (Date.parse(b.updatedAt || "") || 0) - (Date.parse(a.updatedAt || "") || 0));
    }
    async function deletePendingCustomerSession(sessionCode, event) {
      event?.preventDefault?.();
      event?.stopPropagation?.();
      const id = decodeURIComponent(sessionCode);
      let session = ensureCustomerSessions().find(item => item.id === id);
      if (!session) return;
      if (homeSessionIsProcessing(session)) {
        notify(`${session.customer} 正在处理中。可以先处理其他客户，完成后再关闭该会话。`);
        return;
      }
      const wasActive = state.activeCustomerSessionId === id;
      const visibleBefore = sortedCustomerSessionsForRail();
      const currentVisibleIndex = visibleBefore.findIndex(item => item.id === id);
      if (wasActive) {
        captureActiveCustomerSession();
        session = (state.customerSessions || []).find(item => item.id === id) || session;
      }
      const hasCloudProfile = (state.customerProfiles || []).some(item => item.company === session.customer);
      const hasBusinessRecord = businessQuotes().some(quote => quote.customer === session.customer)
        || businessTasks().some(task => (task.cloudCustomer || String(task.customer || "").split(" · ")[0]) === session.customer);
      const hasPersistentHistory = Boolean(
        session.hasConversation
        || String(session.chatHistoryHtml || "").trim()
        || String(session.userMessageHtml || "").trim()
      );
      const customerName = String(session.customer || "新客户").trim() || "新客户";
      const isFormalSession = !isPendingCustomerName(customerName)
        && !["新客户", "未命名客户"].includes(customerName)
        && (hasCloudProfile || hasBusinessRecord || hasPersistentHistory);
      if (isFormalSession) {
        if (!confirm(`关闭“${customerName}”的当前会话吗？\n\n只会从左侧会话列表移除；客户档案、历史聊天、报价、跟单和文件全部保留。以后可从客户档案或跟单重新打开。`)) return;
        session.archivedAt = new Date().toISOString();
        if (wasActive) {
          const remainingVisible = visibleBefore.filter(item => item.id !== id && !item.archivedAt);
          const nextSession = remainingVisible[Math.min(Math.max(currentVisibleIndex, 0), remainingVisible.length - 1)]
            || remainingVisible[Math.max(currentVisibleIndex - 1, 0)]
            || ensureCustomerSessions()[0];
          state.activeCustomerSessionId = nextSession?.id || "";
        }
        saveState();
        if (wasActive) restoreCustomerSession(activeCustomerSession());
        renderCustomerProfiles();
        renderDashboard();
        renderCustomerWorkbench();
        notify(`已关闭“${customerName}”的当前会话；客户资料和全部历史记录仍保留。` );
        return;
      }
      if (!confirm(`确定彻底删除临时会话“${customerName}”吗？\n\n聊天、草稿和关联上传文件都会删除，且无法恢复。`)) return;
      const requiresServerDelete = hasCloudProfile || hasBusinessRecord || hasPersistentHistory;
      if (requiresServerDelete && typeof window.__deleteCustomerCloud !== "function") {
        notify("临时会话删除服务尚未连接，为避免记录重新出现，本次没有删除。" );
        return;
      }
      if (requiresServerDelete) {
        try { await window.__deleteCustomerCloud(session.customer); }
        catch (err) { return notify(`删除失败：${err.message}`); }
      }
      state.customerProfiles = (state.customerProfiles || []).filter(item => item.company !== session.customer);
      state.quoteHistory = (state.quoteHistory || []).filter(item => item.customer !== session.customer);
      state.tasks = (state.tasks || []).filter(item => (item.cloudCustomer || String(item.customer || "").split(" · ")[0]) !== session.customer);
      state.customerSessions = state.customerSessions.filter(item => item.id !== id);
      if (wasActive) {
        const remainingVisible = visibleBefore.filter(item => item.id !== id && !item.archivedAt);
        state.activeCustomerSessionId = (remainingVisible[Math.min(Math.max(currentVisibleIndex, 0), remainingVisible.length - 1)]
          || remainingVisible[Math.max(currentVisibleIndex - 1, 0)]
          || ensureCustomerSessions()[0])?.id || "";
      }
      saveState();
      if (wasActive) restoreCustomerSession(activeCustomerSession());
      renderCustomerProfiles();
      renderDashboard();
      renderCustomerWorkbench();
      notify("临时客户会话已彻底删除。" );
    }
    function renderCustomerWorkbench() {
      const sessions = ensureCustomerSessions();
      const active = activeCustomerSession();
      const workbench = document.getElementById("homeWorkbench");
      workbench?.classList.toggle("has-multiple", sessions.length > 1);
      workbench?.classList.toggle("has-draft", Boolean(active?.draft?.trim()));
      const filtered = sortedCustomerSessionsForRail(sessions);
      const processingCount = sessions.filter(homeSessionIsProcessing).length;
      setTextIfExists("homeSessionCount", `${sessions.length}个进行中${processingCount ? ` · ${processingCount}个处理中` : ""}`);
      setTextIfExists("homeMobileCurrentCustomer", `当前客户：${active?.customer || "新客户"}`);
      const list = document.getElementById("homeSessionList");
      if (list) {
        list.innerHTML = filtered.map(item => {
          const pending = isPendingCustomerName(item.customer);
          const display = customerSessionDisplay(item);
          const hasProfile = (state.customerProfiles || []).some(profile => profile.company === item.customer);
          const hasBusinessRecord = businessQuotes().some(quote => quote.customer === item.customer) || businessTasks().some(task => (task.cloudCustomer || String(task.customer || "").split(" · ")[0]) === item.customer);
          const hasSessionHistory = Boolean(
            item.hasConversation
            || String(item.chatHistoryHtml || "").trim()
            || String(item.userMessageHtml || "").trim()
            || (item.lastMessage && !["等待客户消息", "历史客户档案"].includes(item.lastMessage))
          );
          // 只隐藏完全空白的初始“新客户”；正式客户、选中客户和有历史的会话统一显示删除入口。
          const isActiveSession = item.id === state.activeCustomerSessionId;
          const canDeleteSession = isActiveSession || pending || hasProfile || hasBusinessRecord || hasSessionHistory;
          const closesFormalSession = !pending && !["新客户", "未命名客户"].includes(item.customer) && (hasProfile || hasBusinessRecord || hasSessionHistory);
          const status = customerSessionStatus(item.customer, item);
          const processingBadge = homeSessionIsProcessing(item)
            ? { label:"处理中", className:"processing" }
            : item.processingStatus === "completed" && item.hasUnreadResult
              ? { label:"已完成", className:"completed" }
              : item.processingStatus === "failed" && item.hasUnreadResult
                ? { label:"处理失败", className:"failed" }
                : null;
          const sessionTitle = pending
            ? display.title
            : display.title && display.title !== item.customer
              ? `${item.customer} · ${display.title}`
              : item.customer;
          return `<div class="home-session-item ${isActiveSession ? "active" : ""}" role="button" tabindex="0" data-session-id="${escapeHtml(item.id)}">
            <span><strong>${escapeHtml(sessionTitle)}</strong><span>${escapeHtml(processingBadge?.className === "processing" ? "数字员工正在后台处理" : display.action)} · ${escapeHtml(item.lastMessage || "等待客户消息")}</span></span>
            ${processingBadge ? `<span class="home-session-badge ${processingBadge.className}">${escapeHtml(processingBadge.label)}</span>` : pending ? `<span class="home-session-badge">${escapeHtml(item.customer)} · 待补姓名</span>` : `<span class="home-session-badge">${escapeHtml(status)}</span>`}
            <em>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString("zh-CN", { month:"numeric", day:"numeric", hour:"2-digit", minute:"2-digit", hour12:false }) : "刚刚")}</em>
            ${canDeleteSession ? `<button class="home-session-delete" type="button" title="${closesFormalSession ? "关闭并移出当前会话列表" : "删除临时会话"}" aria-label="${closesFormalSession ? "关闭" : "删除"} ${escapeHtml(item.customer)}">×</button>` : ""}
          </div>`;
        }).join("") || `<div class="muted" style="padding:16px">没有找到对应客户。</div>`;
        list.querySelectorAll(".home-session-item").forEach(row => {
          const sessionCode = () => encodeURIComponent(row.dataset.sessionId || "");
          row.addEventListener("click", event => {
            if (event.target.closest(".home-session-delete")) return;
            switchCustomerSession(sessionCode());
          });
          row.addEventListener("keydown", event => {
            if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) {
              event.preventDefault();
              switchCustomerSession(sessionCode());
            }
          });
          const deleteButton = row.querySelector(".home-session-delete");
          deleteButton?.addEventListener("pointerdown", event => event.stopPropagation());
          deleteButton?.addEventListener("click", event => deletePendingCustomerSession(sessionCode(), event));
        });
      }
      const customer = active?.customer || "新客户";
      const profile = (state.customerProfiles || []).find(item => item.company === customer);
      const tasks = customerTasksByName(customer);
      const quotes = businessQuotes().filter(item => item.customer === customer);
      const visuals = (state.visualVersions || []).filter(item => item.customer === customer);
      const rememberedMessage = ["等待客户消息", "历史客户档案"].includes(active?.lastMessage) ? "" : (active?.lastMessage || "");
      const sourceText = active?.draft || rememberedMessage;
      const info = active?.businessAnalysis || (sourceText ? analyze(sourceText) : null);
      const needReady = Boolean(
        info
        && info.product
        && !["定制产品", "待确认"].includes(String(info.product))
        && !String(info.product).includes("待确认")
      );
      const preferredTask = preferredCustomerTask(tasks);
      const quoteStageReached = quotes.length > 0 || customerTaskStageRank(preferredTask?.status) >= customerTaskStageRank("已报价");
      const body = document.getElementById("homeContextBody");
      setTextIfExists("homeContextStatus", customerSessionStatus(customer, active));
      if (body) body.innerHTML = `
        <div class="home-context-customer"><strong>${escapeHtml(customer)}</strong><span>${isPendingCustomerName(customer) ? "资料已独立暂存，直接在首页客户名称栏补写真名即可自动合并。" : escapeHtml(profile?.memorySummary || profile?.need || "正在沉淀客户习惯")}</span></div>
        <div class="home-context-progress">
          <div class="home-context-step ${needReady ? "done" : "active"}"><i></i><span><strong>客户需求</strong><span>${needReady ? escapeHtml(info?.product || "已整理") : "等待消息或补齐资料"}</span></span></div>
          <div class="home-context-step ${quoteStageReached ? "done" : needReady ? "active" : ""}"><i></i><span><strong>报价</strong><span>${quotes.length ? `已生成 ${quotes.length} 份` : quoteStageReached ? "已进入报价阶段" : "尚未生成"}</span></span></div>
          <div class="home-context-step ${visuals.length ? "done" : ""}"><i></i><span><strong>效果图/详情</strong><span>${visuals.length ? `${visuals.length} 个版本` : "尚未制作"}</span></span></div>
          <div class="home-context-step ${tasks.length ? "done" : ""}"><i></i><span><strong>跟单</strong><span>${preferredTask?.nextAction ? escapeHtml(preferredTask.nextAction) : "尚未建立提醒"}</span></span></div>
        </div>
        <div class="home-context-actions"><button class="btn light" type="button" onclick="showPage('客户档案')">查看完整档案</button>${isPendingCustomerName(customer) ? `<button class="btn light" type="button" onclick="document.getElementById('homeCustomerName')?.focus()">补充客户姓名</button>` : ""}</div>`;
      renderHomeChatRecordActions();
      renderExistingCustomerSuggestion();
    }
    function initializeCustomerWorkbench() {
      ensureCustomerSessions();
      restoreCustomerSession(activeCustomerSession());
      renderCustomerWorkbench();
      saveState();
    }
    function fillInquiryExample(type) {
      const examples = {
        "亚克力": "客户：亚克力展示架，300x180x120mm，3mm透明亚克力，丝印LOGO，500个，下周要货。问能不能做，多久交货，价格大概多少。",
        "五金": "客户：不锈钢金属标牌，80x30mm，厚度1.2mm，拉丝表面，黑色填漆，背胶，3000个。问能不能做，是否需要图纸，多久能交货。",
        "布袋": "客户：无纺布手提袋，350x400x100mm，80g无纺布，双面单色丝印，10000个，要求承重好，问含税多少钱，运费另计。",
        "包装": "客户：礼品包装盒，200x160x80mm，白卡纸，覆膜，LOGO烫金，2000个，需要打样，问多久能做好。"
      };
      setIfExists("customerText", examples[type] || examples["亚克力"]);
      setTextIfExists("customerOutput", "已切换示例。点击“一键处理询盘”查看 AI 怎么拆解。");
    }
    function inferTemplateName(product) {
      if (hasAny(product, ["无纺布"])) return "无纺布袋";
      if (hasAny(product, ["棉布", "纺织", "布"])) return "棉布纺织";
      if (hasAny(product, ["服装", "内衣", "袜"])) return "服装/内衣/袜子";
      if (hasAny(product, ["塑料袋", "OPP", "opp"])) return "塑料袋/OPP袋";
      if (hasAny(product, ["硅胶", "橡胶"])) return "硅胶橡胶";
      if (hasAny(product, ["五金", "钣金", "冲压"])) return "五金钣金";
      if (hasAny(product, ["标牌", "徽章"])) return "金属标牌/徽章";
      if (hasAny(product, ["亚克力", "展示架"])) return "亚克力展示架";
      if (hasAny(product, ["木制", "竹制"])) return "木制品/竹制品";
      if (hasAny(product, ["礼品", "日用品"])) return "礼品日用品";
      if (hasAny(product, ["电子", "小家电", "数据线"])) return "电子配件/小家电";
      if (hasAny(product, ["名片"])) return "名片印刷";
      if (hasAny(product, ["标签", "贴纸"])) return "标签贴纸";
      if (hasAny(product, ["手提袋"])) return "手提袋";
      if (hasAny(product, ["画册", "说明书"])) return "画册/说明书";
      return "包装彩盒";
    }
    function fillProfileFromInquiry() {
      const { info } = customerContext();
      setIfExists("profileCompany", currentCustomerName());
      setIfExists("profileContact", val("inquiryContact"));
      setIfExists("profileNeed", info.product);
      setIfExists("profilePreference", `${info.material}；${info.finish}`);
      setIfExists("profileDealStatus", "未报价");
      setIfExists("profileNote", `询盘：${val("customerText")}\n风险：${info.risk.join("；")}`);
      ["profileDealTotal", "profileDealQty", "profileOutcomeReason", "profileDealNotes", "profileLastDealAt"].forEach(id => setIfExists(id, ""));
      document.getElementById("profileOutput").textContent = "已从当前询盘带入客户档案，确认后点击保存。";
      showPage("客户档案");
      const editor = document.getElementById("customerEditorDetails");
      if (editor) editor.open = true;
    }
    function saveCustomerProfile() {
      const company = val("profileCompany") || currentCustomerName();
      const existingProfile = (state.customerProfiles || []).find(item => item.company === company) || {};
      const profile = {
        ...existingProfile,
        company,
        contact: val("profileContact") || val("inquiryContact") || "未填写",
        level: val("profileLevel") || "普通客户",
        dealStatus: val("profileDealStatus") || "未报价",
        need: val("profileNeed") || "待沉淀",
        preference: val("profilePreference") || "待沉淀",
        designLearningEnabled: document.getElementById("profileDesignLearning")?.checked !== false,
        note: val("profileNote") || "",
        actualDealTotal: Number(val("profileDealTotal") || 0),
        actualDealQty: Number(val("profileDealQty") || 0),
        outcomeReason: val("profileOutcomeReason") || "",
        dealNotes: val("profileDealNotes") || "",
        lastDealAt: val("profileLastDealAt") || "",
        pricing: {
          level_price: val("profileLevelPrice") === "" ? null : Number(val("profileLevelPrice")),
          history_price: val("profileHistoryPrice") === "" ? null : Number(val("profileHistoryPrice")),
          agreement_price: val("profileAgreementPrice") === "" ? null : Number(val("profileAgreementPrice")),
          manual_price: val("profileManualPrice") === "" ? null : Number(val("profileManualPrice")),
          agreement_valid_from: val("profileAgreementFrom") || "",
          agreement_valid_until: val("profileAgreementUntil") || "",
          note: val("profilePricingNote") || ""
        },
        createdAt: existingProfile.createdAt || new Date().toISOString(),
        updatedAt: new Date().toLocaleString("zh-CN", { hour12: false })
      };
      const rest = (state.customerProfiles || []).filter(item => item.company !== company);
      state.customerProfiles = [profile, ...rest].slice(0, 50);
      saveState();
      window.__saveCustomerProfileCloud?.(profile);
      selectedCustomerAssetName = profile.company;
      loadCustomerAssetFiles(profile.company);
      renderCustomerProfiles();
      document.getElementById("profileOutput").textContent = `已保存客户档案：

客户：${profile.company}
联系人：${profile.contact}
等级：${profile.level}
成交状态：${profile.dealStatus}
常用需求：${profile.need}
偏好：${profile.preference}
实际成交：${profile.actualDealTotal ? `¥${profile.actualDealTotal.toFixed(2)} / ${profile.actualDealQty || "数量待补"}` : "尚未填写"}
结果原因：${profile.outcomeReason || "尚未复盘"}

后续报价、内容素材和跟单记录都可以围绕这个客户沉淀。`;
      const editor = document.getElementById("customerEditorDetails");
      if (editor) editor.open = false;
      notify("客户档案已保存，客户列表已更新。" );
    }
    async function clearCustomerDesignLearning() {
      const company = val("profileCompany") || currentCustomerName();
      if (!company || ["新客户", "未命名客户"].includes(company)) return notify("请先选择已保存的客户。" );
      if (!confirm(`确定清除“${company}”已经学习的颜色、风格、构图和改图偏好吗？\n客户档案和聊天记录不会删除。`)) return;
      if (typeof window.__clearCustomerDesignLearning !== "function") return notify("制图偏好服务尚未加载，请刷新后重试。" );
      try {
        const data = await window.__clearCustomerDesignLearning(company);
        const profile = (state.customerProfiles || []).find(item => item.company === company);
        if (profile) {
          profile.designPreferenceSummary = "";
          profile.designFeedbackCount = 0;
        }
        if (data?.customer) window.__mergeCloudCustomerProfiles?.([data.customer]);
        setTextIfExists("profileOutput", `${company} 的制图偏好已清除。后续只会从新的客户要求和确认结果重新学习。`);
        saveState();
        renderCustomerProfiles();
        notify("客户制图偏好已清除。" );
      } catch (err) {
        notify(`清除失败：${err.message}`);
      }
    }
    function appendCustomerHistory(company, line, defaults = {}) {
      const name = company || currentCustomerName();
      const list = state.customerProfiles || [];
      const existing = list.find(item => item.company === name);
      const stamp = new Date().toLocaleString("zh-CN", { hour12: false });
      if (existing) {
        existing.note = `${existing.note || ""}\n${stamp}：${line}`.trim();
        if (defaults.dealStatus) existing.dealStatus = defaults.dealStatus;
        if (defaults.need) existing.need = defaults.need;
        if (defaults.preference) existing.preference = defaults.preference;
        existing.updatedAt = stamp;
      } else {
        state.customerProfiles = [{
          company: name,
          contact: defaults.contact || "未填写",
          level: defaults.level || "普通客户",
          dealStatus: defaults.dealStatus || "未报价",
          need: defaults.need || "待沉淀",
          preference: defaults.preference || "待沉淀",
          note: `${stamp}：${line}`,
          createdAt: new Date().toISOString(),
          updatedAt: stamp
        }, ...list].slice(0, 50);
      }
    }
    function updateCustomerQuoteMemory(quoteRecord) {
      const company = quoteRecord.customer || currentCustomerName();
      const profile = (state.customerProfiles || []).find(item => item.company === company);
      if (!profile) return;
      const customerQuotes = (state.quoteHistory || []).filter(item => item.customer === company);
      profile.quoteCount = customerQuotes.length;
      profile.lastQuoteTotal = Number(quoteRecord.total || 0);
      profile.lastQuoteUnit = Number(quoteRecord.unit || 0);
      profile.lastQuoteAt = quoteRecord.time || new Date().toLocaleString("zh-CN", { hour12: false });
      profile.lastProduct = quoteRecord.product || val("quoteProduct");
      profile.lastQty = Number(quoteRecord.qty || 0);
      profile.lastMaterial = quoteRecord.material || val("quoteMaterial");
      profile.lastProcess = quoteRecord.process || val("quoteProcess");
    }
    function renderQuoteCustomerMemory() {
      const box = document.getElementById("quoteCustomerMemory");
      if (!box) return;
      const company = currentCustomerName();
      if (!company || company === "新客户") {
        box.innerHTML = "";
        box.classList.add("is-hidden");
        return;
      }
      const profile = (state.customerProfiles || []).find(item => item.company === company);
      const quotes = (state.quoteHistory || []).filter(item => item.customer === company);
      if (!profile && !quotes.length) {
        box.classList.add("is-hidden");
        box.innerHTML = "";
        return;
      }
      const latest = quotes[0];
      const previous = quotes[1];
      const lastPrice = latest ? `最近报价 ¥${Number(latest.total || 0).toFixed(0)}，约 ¥${Number(latest.unit || 0).toFixed(3)}/个` : "尚无历史报价";
      const change = latest && previous && Number(previous.unit || 0) > 0
        ? `；相比上次单价${Number(latest.unit) >= Number(previous.unit) ? "上涨" : "下降"}${Math.abs((Number(latest.unit) / Number(previous.unit) - 1) * 100).toFixed(1)}%`
        : "";
      const suggestion = profile?.dealStatus === "客户压价"
        ? "该客户有压价记录，报价前先看内部最低成交价。"
        : profile?.dealStatus === "已成交"
          ? "该客户已有成交记录，可优先参考上次材质、工艺和付款习惯。"
          : "核对历史价格和客户偏好后再发送报价。";
      box.innerHTML = `<strong>${escapeHtml(company)} · 客户记忆</strong><span>历史报价 ${quotes.length} 次 · ${escapeHtml(lastPrice + change)} · 当前状态：${escapeHtml(profile?.dealStatus || "未报价")}</span><span>${escapeHtml(profile?.preference || "偏好待沉淀")}。${escapeHtml(suggestion)}</span>`;
      box.classList.remove("is-hidden");
    }
    function saveInquiryCustomer() {
      const { info } = customerContext();
      setIfExists("profileCompany", currentCustomerName());
      setIfExists("profileContact", val("inquiryContact"));
      setIfExists("profileNeed", info.product);
      setIfExists("profilePreference", `${info.material}；${info.finish}`);
      setIfExists("profileDealStatus", "未报价");
      setIfExists("profileNote", `询盘：${val("customerText")}\n风险：${info.risk.join("；")}`);
      ["profileDealTotal", "profileDealQty", "profileOutcomeReason", "profileDealNotes", "profileLastDealAt"].forEach(id => setIfExists(id, ""));
      saveCustomerProfile();
      showPage("询盘处理");
      document.getElementById("customerOutput").textContent = `已保存客户档案：${currentCustomerName()}

已记录：
- 产品：${info.product}
- 材质：${info.material}
- 工艺：${info.finish}
- 风险：${info.risk.join("；")}`;
    }
    function renderCustomerProfiles() {
      const body = document.getElementById("profileBody");
      if (!body) return;
      renderCustomerDealMetrics();
      const customerProfit = new Map();
      businessQuotes().forEach(quote => {
        if (quote.outcomeStatus === "已成交" && quote.actualProfit != null) {
          customerProfit.set(quote.customer, (customerProfit.get(quote.customer) || 0) + Number(quote.actualProfit || 0));
        }
      });
      const search = (document.getElementById("customerProfileSearch")?.value || "").trim().toLowerCase();
      const statusFilter = document.getElementById("customerProfileStatusFilter")?.value || "";
      const savedProfiles = businessProfiles();
      const savedNames = new Set(savedProfiles.map(item => item.company));
      const pendingSessions = (state.customerSessions || [])
        .filter(session => !session.archivedAt && isPendingCustomerName(session.customer) && !savedNames.has(session.customer))
        .map(session => {
          const display = customerSessionDisplay(session);
          return {
            company: display.title,
            internalCustomer: session.customer,
            temporarySessionId: session.id,
            contact: session.customer,
            level: "待整理询盘",
            dealStatus: "待整理询盘",
            need: display.action,
            preference: session.lastMessage || "等待继续处理",
            updatedAt: session.updatedAt || ""
          };
        });
      const profiles = [...pendingSessions, ...savedProfiles].map(item => {
        const hasQuote = Number(item.quoteCount || 0) > 0 || Number(item.lastQuoteTotal || 0) > 0 || Number(item.lastQuoteUnit || 0) > 0;
        let displayDealStatus = item.dealStatus || "未报价";
        if (["未报价", "已报价"].includes(displayDealStatus)) displayDealStatus = hasQuote ? "已报价" : "未报价";
        return { ...item, displayDealStatus, hasQuote };
      }).filter(item => {
        const haystack = `${item.company || ""} ${item.contact || ""} ${item.need || ""} ${item.preference || ""} ${item.displayDealStatus}`.toLowerCase();
        const pendingMatch = statusFilter === "待补姓名" ? Boolean(item.temporarySessionId || isPendingCustomerName(item.company)) : true;
        const statusMatch = !statusFilter || statusFilter === "待补姓名" || item.displayDealStatus === statusFilter;
        return (!search || haystack.includes(search)) && pendingMatch && statusMatch;
      }).sort((a, b) => Number(Boolean(b.temporarySessionId) || isPendingCustomerName(b.company)) - Number(Boolean(a.temporarySessionId) || isPendingCustomerName(a.company)));
      setTextIfExists("customerProfileCount", `${profiles.length} 位客户 · 同名资料自动合并`);
      const rows = profiles.map(item => `
        <tr>
          <td class="customer-main"><strong>${escapeHtml(item.company)}${item.temporarySessionId || isPendingCustomerName(item.company) ? ` <span class="home-session-badge" style="display:inline-block">待补姓名</span>` : ""}</strong><span>${escapeHtml(item.contact || "联系人未填")} · ${escapeHtml(item.level || "普通客户")}</span></td>
          <td><span class="status">${escapeHtml(item.displayDealStatus)}</span></td>
          <td class="customer-summary">${item.actualDealTotal
            ? `<strong>成交 ¥${Number(item.actualDealTotal).toFixed(0)}</strong><span>${Number(item.actualDealQty || 0).toLocaleString()}个${customerProfit.has(item.company) ? ` · 实际利润 ¥${Number(customerProfit.get(item.company)).toFixed(0)}` : ""}${item.outcomeReason ? ` · ${escapeHtml(item.outcomeReason)}` : ""}</span>`
            : item.hasQuote
              ? `<strong>报价 ¥${Number(item.lastQuoteTotal || 0).toFixed(0)}</strong><span>¥${Number(item.lastQuoteUnit).toFixed(3)}/个 · 共${item.quoteCount || 1}次</span>`
              : `<strong>尚未报价</strong><span>等待整理需求</span>`}</td>
          <td class="customer-summary"><strong>${escapeHtml(item.need || "需求待沉淀")}</strong><span>${escapeHtml(item.preference || "偏好待沉淀")}${item.designPreferenceSummary ? ` · 制图：${escapeHtml(item.designPreferenceSummary)}` : ""}${item.conversationCount ? ` · ${Number(item.conversationCount)}条沟通` : ""}</span></td>
          <td>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString("zh-CN", { hour12: false }) : "")}</td>
          <td>
            ${item.temporarySessionId
              ? `<button class="btn light" type="button" data-customer-action="continue-session" data-session-id="${escapeHtml(item.temporarySessionId)}">继续处理</button><button class="btn light" type="button" data-customer-action="delete-session" data-session-id="${escapeHtml(item.temporarySessionId)}">删除</button>`
              : `<button class="btn light" type="button" data-customer-action="files" data-customer-name="${escapeHtml(item.company)}">客户资料</button><button class="btn light" type="button" data-customer-action="edit" data-customer-name="${escapeHtml(item.company)}">${isPendingCustomerName(item.company) ? "补姓名" : "详情"}</button><button class="btn light" type="button" data-customer-action="delete" data-customer-name="${escapeHtml(item.company)}">删除</button><select class="customer-status-select" data-customer-name="${escapeHtml(item.company)}">
              <option value="">更新状态</option><option>客户压价</option><option>已成交</option><option>已流失</option>
            </select>`}
          </td>
        </tr>
      `).join("");
      body.innerHTML = rows || `<tr><td colspan="6" class="muted">暂无客户档案。可以从询盘处理里一键保存客户。</td></tr>`;
      body.querySelectorAll("[data-customer-action]").forEach(button => {
        button.addEventListener("click", event => {
          const action = button.dataset.customerAction || "";
          const sessionCode = encodeURIComponent(button.dataset.sessionId || "");
          const companyCode = encodeURIComponent(button.dataset.customerName || "");
          if (action === "continue-session") continuePendingCustomerSession(sessionCode);
          else if (action === "delete-session") deletePendingCustomerSession(sessionCode, event);
          else if (action === "files") openCustomerFiles(companyCode);
          else if (action === "edit") editCustomerProfile(companyCode);
          else if (action === "delete") deleteCustomerProfile(companyCode);
        });
      });
      body.querySelectorAll(".customer-status-select[data-customer-name]").forEach(select => {
        select.addEventListener("change", () => updateCustomerDealFromSelect(
          encodeURIComponent(select.dataset.customerName || ""),
          select
        ));
      });
    }
    function continuePendingCustomerSession(sessionCode) {
      showPage("图片制作");
      window.setTimeout(() => switchCustomerSession(sessionCode), 40);
    }
    function openCustomerConversation(companyCode) {
      const company = decodeURIComponent(companyCode);
      if (!company) return;
      captureActiveCustomerSession();
      let session = (state.customerSessions || [])
        .filter(item => item.customer === company)
        .sort((a, b) => (Date.parse(b.updatedAt || "") || 0) - (Date.parse(a.updatedAt || "") || 0))[0];
      if (!session) {
        const profile = (state.customerProfiles || []).find(item => item.company === company);
        session = {
          id: customerSessionId(), customer: company, draft: "", lastMessage: profile?.need || "历史客户档案",
          updatedAt: new Date().toISOString(), mode: "", resultEmpty: true, hasConversation: false,
          userHidden: true, nextActionsHidden: true
        };
        state.customerSessions.unshift(session);
      }
      delete session.archivedAt;
      session.updatedAt = new Date().toISOString();
      state.activeCustomerSessionId = session.id;
      saveState();
      showPage("图片制作");
      window.setTimeout(() => {
        restoreCustomerSession(session);
        renderCustomerWorkbench();
        notify(`已打开“${company}”的客户会话。` );
      }, 40);
    }
    function openTaskCustomerFiles(taskId) {
      const task = (state.tasks || []).find(item => item.id === taskId);
      if (!task) return notify("没有找到这条跟单任务。" );
      const company = String(task.cloudCustomer || task.customer || task.title || "").split(" · ")[0].trim();
      if (!company) return notify("这条跟单还没有关联客户，请先补充客户名称。" );
      openCustomerFiles(encodeURIComponent(company));
    }
    function updateCustomerDealFromSelect(companyCode, select) {
      const status = select?.value || "";
      if (!status) return;
      updateCustomerDealStatus(companyCode, status);
      if (select) select.value = "";
    }
    async function deleteCustomerProfile(companyCode) {
      const company = decodeURIComponent(companyCode);
      if (!confirm(`确定彻底删除“${company}”吗？\n\n聊天、报价、跟单和关联附件都会一起删除，无法恢复。`)) return;
      if (typeof window.__deleteCustomerCloud !== "function") {
        notify("客户删除服务尚未加载，请刷新页面后重试。" );
        return;
      }
      try {
        await window.__deleteCustomerCloud(company);
        state.customerProfiles = (state.customerProfiles || []).filter(item => item.company !== company);
        const removedSessionIds = new Set((state.customerSessions || []).filter(item => item.customer === company).map(item => item.id));
        state.customerSessions = (state.customerSessions || []).filter(item => item.customer !== company);
        state.tasks = (state.tasks || []).filter(item => {
          const localCustomer = String(item.customer || "");
          const cloudCustomer = String(item.cloudCustomer || "");
          return cloudCustomer !== company && localCustomer !== company && !localCustomer.startsWith(`${company} ·`);
        });
        state.quoteHistory = (state.quoteHistory || []).filter(item => item.customer !== company);
        if (removedSessionIds.has(state.activeCustomerSessionId)) {
          if (!state.customerSessions.length) state.customerSessions.push({ id: customerSessionId(), customer: "新客户", draft: "", lastMessage: "等待客户消息", updatedAt: new Date().toISOString(), mode: "", analysisMode: "standard", lastRequestedAnalysisMode: "standard", resultEmpty: true, hasConversation: false });
          state.activeCustomerSessionId = state.customerSessions[0].id;
          restoreCustomerSession(state.customerSessions[0]);
        }
        if ((document.getElementById("homeCustomerName")?.value || "").trim() === company) {
          setIfExists("homeCustomerName", "新客户");
          startNewCustomerConversation(true);
        }
        saveState();
        renderCustomerProfiles();
        renderTasks();
        renderDashboard();
        renderCustomerWorkbench();
        notify(`已删除 ${company} 的客户资料。` );
      } catch (err) {
        notify(`删除失败：${err.message}`);
      }
    }
    function renderCustomerDealMetrics() {
      const box = document.getElementById("customerDealMetrics");
      if (!box) return;
      const profiles = businessProfiles();
      const won = profiles.filter(item => item.dealStatus === "已成交");
      const lost = profiles.filter(item => item.dealStatus === "已流失");
      const decided = won.length + lost.length;
      const pipelineCustomers = new Set([
        ...businessQuotes().map(item => String(item.customer || "").trim()).filter(Boolean),
        ...won.map(item => String(item.company || "").trim()).filter(Boolean),
        ...lost.map(item => String(item.company || "").trim()).filter(Boolean)
      ]);
      const revenue = won.reduce((sum, item) => sum + Number(item.actualDealTotal || 0), 0);
      const profitQuotes = businessQuotes().filter(item => item.outcomeStatus === "已成交" && item.actualProfit != null && Number.isFinite(Number(item.actualProfit)));
      const actualProfit = profitQuotes.reduce((sum, item) => sum + Number(item.actualProfit || 0), 0);
      const rate = pipelineCustomers.size ? won.length / pipelineCustomers.size * 100 : 0;
      const reviewed = profiles.filter(item => item.outcomeReason || item.dealNotes).length;
      box.innerHTML = `
        <div class="summary-tile"><span>已成交客户</span><strong>${won.length}</strong></div>
        <div class="summary-tile"><span>报价→成交率</span><strong>${rate.toFixed(0)}%</strong><small>${won.length}/${pipelineCustomers.size || 0} 个客户</small></div>
        <div class="summary-tile"><span>实际成交额</span><strong>¥${revenue.toFixed(0)}</strong></div>
        <div class="summary-tile"><span>实际订单利润</span><strong>${profitQuotes.length ? `¥${actualProfit.toFixed(0)}` : "待复盘"}</strong></div>
        <div class="summary-tile"><span>已完成复盘</span><strong>${reviewed}</strong></div>
      `;
    }
    function editCustomerProfile(companyCode) {
      const company = decodeURIComponent(companyCode);
      const profile = (state.customerProfiles || []).find(item => item.company === company);
      if (!profile) return;
      setIfExists("profileCompany", profile.company);
      setIfExists("profileContact", profile.contact);
      setIfExists("profileLevel", profile.level);
      setIfExists("profileDealStatus", profile.dealStatus);
      setIfExists("profileNeed", profile.need);
      setIfExists("profilePreference", profile.preference);
      const designLearning = document.getElementById("profileDesignLearning");
      if (designLearning) designLearning.checked = profile.designLearningEnabled !== false;
      setIfExists("profileNote", profile.note);
      setIfExists("profileDealTotal", profile.actualDealTotal || "");
      setIfExists("profileDealQty", profile.actualDealQty || "");
      setIfExists("profileOutcomeReason", profile.outcomeReason || "");
      setIfExists("profileDealNotes", profile.dealNotes || "");
      setIfExists("profileLastDealAt", profile.lastDealAt || "");
      setIfExists("profileLevelPrice", profile.pricing?.level_price ?? "");
      setIfExists("profileHistoryPrice", profile.pricing?.history_price ?? "");
      setIfExists("profileAgreementPrice", profile.pricing?.agreement_price ?? "");
      setIfExists("profileManualPrice", profile.pricing?.manual_price ?? "");
      setIfExists("profileAgreementFrom", profile.pricing?.agreement_valid_from || "");
      setIfExists("profileAgreementUntil", profile.pricing?.agreement_valid_until || "");
      setIfExists("profilePricingNote", profile.pricing?.note || "");
      setTextIfExists("profileOutput", `${profile.company}：历史报价 ${profile.quoteCount || 0} 次，最近总价 ¥${Number(profile.lastQuoteTotal || 0).toFixed(0)}，最近单价 ¥${Number(profile.lastQuoteUnit || 0).toFixed(3)}/个。\n制图偏好：${profile.designLearningEnabled === false ? "已关闭学习" : (profile.designPreferenceSummary || "等待从改图与确认结果中学习")}。`);
      const editor = document.getElementById("customerEditorDetails");
      if (editor) editor.open = true;
      selectedCustomerAssetName = profile.company;
      loadCustomerAssetFiles(profile.company);
      window.__loadCustomerProfileMemory?.(company);
      editor?.scrollIntoView({ behavior: "smooth", block: "start" });
    }
    let selectedCustomerAssetName = "";
    function customerAssetFileIcon(item = {}) {
      const mime = String(item.mime_type || "").toLowerCase();
      const name = String(item.original_name || "").toLowerCase();
      if (mime.startsWith("image/")) return "图片";
      if (name.endsWith(".pdf")) return "PDF";
      if (/\.(ai|cdr|svg|psd)$/.test(name)) return "设计";
      if (/\.(dwg|dxf)$/.test(name)) return "图纸";
      if (/\.(xls|xlsx|csv)$/.test(name)) return "表格";
      return "文件";
    }
    function renderCustomerAssetFiles(customer, files = [], error = "") {
      const list = document.getElementById("customerAssetList");
      setTextIfExists("customerAssetCustomer", customer
        ? `当前客户：${customer} · 所有文件按客户独立保存。`
        : "请先从客户列表选择客户。图片、PDF、刀版和设计源文件会按客户独立保存。");
      if (!list) return;
      if (error) {
        list.innerHTML = `<div class="customer-assets-empty">${escapeHtml(error)}</div>`;
        return;
      }
      const items = Array.isArray(files) ? files : [];
      list.innerHTML = items.length ? items.map(item => {
        const id = Number(item.id || 0);
        const name = String(item.original_name || item.name || "未命名文件");
        const encodedName = encodeURIComponent(name);
        return `<div class="customer-asset-item">
          <span class="customer-asset-icon">${escapeHtml(customerAssetFileIcon(item))}</span>
          <span class="customer-asset-copy"><strong title="${escapeHtml(name)}">${escapeHtml(name)}</strong><span>${escapeHtml(item.kind || "客户文件")} · ${escapeHtml(fileSizeText(Number(item.size || 0)))} · ${escapeHtml(item.created_at ? new Date(item.created_at).toLocaleString("zh-CN", { hour12:false }) : "")}</span></span>
          <span class="customer-asset-actions"><button class="btn light" type="button" onclick="downloadCustomerAssetFile(${id}, '${encodedName}')">下载</button><button class="btn light" type="button" onclick="removeCustomerAssetFile(${id}, '${encodedName}')">移除</button></span>
        </div>`;
      }).join("") : `<div class="customer-assets-empty">“${escapeHtml(customer)}”还没有保存文件。<br>可以上传客户图片、PDF、刀版、设计源文件、报价单或确认稿。</div>`;
    }
    function updateCustomerAssetSelection() {
      const files = Array.from(document.getElementById("customerAssetFiles")?.files || []);
      setTextIfExists("customerAssetSelection", files.length
        ? `已选择 ${files.length} 个文件 · ${files.map(item => item.name).join("、")}`
        : "可一次选择多个文件，单个文件最大18MB。");
    }
    async function loadCustomerAssetFiles(customer = selectedCustomerAssetName) {
      const name = String(customer || "").trim();
      if (!name || ["新客户", "未命名客户"].includes(name)) return renderCustomerAssetFiles("", []);
      selectedCustomerAssetName = name;
      renderCustomerAssetFiles(name, [], "正在读取客户文件…");
      try {
        if (typeof window.__loadCustomerAssetFilesConnected !== "function") throw new Error("客户文件服务正在连接，请稍后再试。");
        const data = await window.__loadCustomerAssetFilesConnected(name);
        renderCustomerAssetFiles(name, data?.files || []);
      } catch (err) {
        renderCustomerAssetFiles(name, [], `客户文件读取失败：${err.message}`);
      }
    }
    async function uploadCustomerAssetFiles() {
      const customer = String(selectedCustomerAssetName || val("profileCompany") || "").trim();
      const files = Array.from(document.getElementById("customerAssetFiles")?.files || []);
      if (!customer || ["新客户", "未命名客户"].includes(customer)) return notify("请先保存或选择一个正式客户。" );
      if (!files.length) return notify("请先选择需要保存的客户文件。" );
      const oversized = files.find(item => Number(item.size || 0) > 18 * 1024 * 1024);
      if (oversized) return notify(`${oversized.name} 超过18MB，请压缩后再上传。`);
      const button = document.getElementById("customerAssetUploadButton");
      if (button) { button.disabled = true; button.textContent = "正在保存…"; }
      try {
        if (typeof window.__uploadCustomerAssetFilesConnected !== "function") throw new Error("客户文件服务正在连接，请稍后再试。");
        await window.__uploadCustomerAssetFilesConnected(files, customer, val("customerAssetKind") || "客户文件");
        const input = document.getElementById("customerAssetFiles");
        if (input) input.value = "";
        updateCustomerAssetSelection();
        await loadCustomerAssetFiles(customer);
        notify(`已将 ${files.length} 个文件保存到“${customer}”的客户资料。` );
      } catch (err) {
        notify(`保存客户文件失败：${err.message}`);
      } finally {
        if (button) { button.disabled = false; button.textContent = "保存到客户资料"; }
      }
    }
    async function downloadCustomerAssetFile(fileId, nameCode) {
      try {
        if (typeof window.__downloadCustomerAssetFileConnected !== "function") throw new Error("文件服务正在连接，请稍后再试。");
        await window.__downloadCustomerAssetFileConnected(Number(fileId), decodeURIComponent(nameCode));
      } catch (err) { notify(`下载失败：${err.message}`); }
    }
    async function removeCustomerAssetFile(fileId, nameCode) {
      const name = decodeURIComponent(nameCode);
      if (!confirm(`确定从客户资料中移除“${name}”吗？\n\n已经用于报价、制图或制前记录的文件会保留历史，不会破坏业务记录。`)) return;
      try {
        if (typeof window.__removeCustomerAssetFileConnected !== "function") throw new Error("文件服务正在连接，请稍后再试。");
        await window.__removeCustomerAssetFileConnected(Number(fileId));
        await loadCustomerAssetFiles(selectedCustomerAssetName);
        notify(`已移除：${name}`);
      } catch (err) { notify(`无法移除：${err.message}`); }
    }
    function openCustomerFiles(companyCode) {
      const company = decodeURIComponent(companyCode);
      if (!company) return;
      showPage("客户档案");
      window.setTimeout(() => {
        editCustomerProfile(encodeURIComponent(company));
        document.getElementById("customerAssetList")?.scrollIntoView({ behavior:"smooth", block:"center" });
      }, 60);
    }
    function updateCustomerDealStatus(companyCode, status) {
      const company = decodeURIComponent(companyCode);
      const profile = (state.customerProfiles || []).find(item => item.company === company);
      if (!profile) return;
      profile.dealStatus = status;
      if (["已成交", "已流失"].includes(status) && !profile.lastDealAt) {
        profile.lastDealAt = new Date().toISOString().slice(0, 10);
      }
      profile.updatedAt = new Date().toLocaleString("zh-CN", { hour12: false });
      profile.note = `${profile.note || ""}\n${profile.updatedAt}：成交状态更新为${status}`.trim();
      saveState();
      window.__saveCustomerProfileCloud?.(profile);
      renderCustomerProfiles();
      renderDashboard();
    }
    function inquiryQuoteMissingLabels() {
      const missing = [];
      const pending = value => !String(value || "").trim() || /(?:待确认|待补充|未知|未识别)/.test(String(value));
      if (pending(val("quoteProduct")) || !quoteCatalogEntry()) missing.push("报价产品");
      else if (!validateQuoteCatalogSelection(false)) missing.push("产品必要规格");
      if (pending(val("quoteSize"))) missing.push("尺寸");
      if (!(Number(val("quoteQty").replace(/[^\d.]/g, "")) > 0)) missing.push("数量");
      if (pending(val("quoteMaterial"))) missing.push("材质");
      if (pending(val("quoteProcess"))) missing.push("工艺");
      if (!(Number(normalizeQuoteDeliveryDays(val("quoteDelivery"))) > 0)) missing.push("交期");
      if (!["含税", "不含税"].includes(val("quoteTaxMode"))) missing.push("是否含税");
      if (["", "待确认", "需要打样，费用待确认"].includes(val("quoteSampleMode"))) missing.push("打样方式");
      if ((window.__quoteUncertainFields || []).length) missing.push("AI识别内容人工确认");
      return [...new Set(missing)];
    }
    function renderInquiryQuotePreview(info = customerContext().info) {
      const box = document.getElementById("inquiryQuotePreview");
      if (!box) return;
      const record = currentDraftQuoteRecord();
      const missing = inquiryQuoteMissingLabels();
      const fields = [
        ["产品", val("quoteProduct") || info.product],
        ["尺寸", val("quoteSize") || info.size],
        ["数量", val("quoteQty") ? `${Number(val("quoteQty")).toLocaleString()}个` : info.qty],
        ["材质", val("quoteMaterial") || info.material],
        ["工艺", val("quoteProcess") || info.finish]
      ].filter(([, value]) => value && !String(value).includes("待确认"));
      const priceHtml = record ? `
        <div class="inquiry-quote-price">
          <b>¥${Number(record.total || 0).toFixed(0)}</b>
          <em>约 ¥${Number(record.unit || 0).toFixed(3)}/个</em>
        </div>` : "";
      const nextText = record
        ? `已找到相同客户、产品和规格的 V${Number(record.version || 1)} 正式报价，价格已直接显示；进入报价页可复核或另存新版本。`
        : missing.length
          ? `已识别的资料全部带入报价草稿。进入报价后只需补：<strong>${escapeHtml(missing.join("、"))}</strong>。`
          : "业务资料已经齐全；进入报价后核对本厂材料与加工成本，即可生成正式价格。";
      box.innerHTML = `
        <div class="inquiry-quote-preview-head">
          <div><strong>${record ? "同规格价格已找到" : "报价草稿已自动准备"}</strong><span>点击“去报价”不会再要求重填已经识别出的内容。</span></div>
          ${priceHtml}
        </div>
        <div class="inquiry-quote-fields">${fields.map(([label, value]) => `<span>${escapeHtml(label)}：${escapeHtml(String(value))}</span>`).join("")}</div>
        <div class="inquiry-quote-next">${nextText}</div>
      `;
      box.classList.remove("is-hidden");
    }
    function applyInquiryToQuote(options = {}) {
      const inline = options.inline === true;
      const stayOnInquiry = options.stayOnInquiry === true;
      resetFreshQuoteTransientState();
      const { info } = customerContext();
      const detectedProfile = quoteBusinessProfileForProduct(info.product);
      const inferredTemplate = detectedProfile.template;
      const allowedTemplates = currentCategoryTemplates();
      const categoryMismatch = !allowedTemplates.includes(inferredTemplate);
      const templateName = allowedTemplates.includes(inferredTemplate)
        ? inferredTemplate
        : (allowedTemplates.includes(state.settings.factoryType) ? state.settings.factoryType : allowedTemplates[0]);
      const template = productTemplates[templateName] || productTemplates["通用加工"];
      quoteGuideStep = 1;
      quoteGuideHasResult = false;
      quoteGuideDirection = "next";
      hideMissingQuoteCosts();
      state.settings.industryCategory = categoryForTemplate(templateName);
      state.settings.factoryType = templateName;
      state.settings.productType = templateName;
      state.settings.defaultMaterial = template.material;
      saveState();
      renderIndustryCategories();
      renderQuoteTemplateOptions();
      setIfExists("quoteAiSource", val("customerText"));
      setIfExists("quoteCustomerName", val("inquiryCustomerName") || "新客户");
      setIfExists("quoteTemplate", templateName);
      setIfExists("quoteProduct", info.product);
      setIfExists("quoteSize", info.size.includes("待确认") ? "" : info.size.replace(/mm$/i, ""));
      setIfExists("quoteQty", info.qty.includes("待确认") ? "" : info.qty.replace(/[^\d]/g, ""));
      setIfExists("quoteMaterial", info.material.includes("待确认") ? "" : info.material);
      setIfExists("quoteProcess", info.finish.includes("待确认") ? "" : info.finish.replace(/ \+ /g, ", "));
      setQuoteDeliveryValue(info.delivery && !info.delivery.includes("待确认") ? info.delivery : "");
      const paymentHabit = (info.customerHabits || []).find(item => String(item).startsWith("付款习惯："));
      const rememberedPayment = paymentHabit ? String(paymentHabit).replace(/^付款习惯：/, "").trim() : "";
      setIfExists("quotePayment", rememberedPayment ? (/尾款|余款/.test(rememberedPayment) ? rememberedPayment : `${rememberedPayment}，尾款出货前付清`) : state.settings.payment);
      setIfExists("quoteTaxMode", "待确认");
      setIfExists("quoteFreightMode", "不含运");
      setIfExists("quoteRegion", "");
      setIfExists("quoteFreight", "0");
      setIfExists("quoteSample", "0");
      window.__activeQuoteRecordKey = "";
      if (info.taxShipping.includes("不含税")) setIfExists("quoteTaxMode", "不含税");
      else if (info.taxShipping.includes("含税")) setIfExists("quoteTaxMode", "含税");
      if (info.address && !info.address.includes("待确认")) setIfExists("quoteRegion", info.address);
      setIfExists("quoteSampleMode", info.sampleMode || (info.sampleRequired === true
        ? "需要打样，费用待确认"
        : info.sampleRequired === false
          ? "不需要打样"
          : "待确认"));
      if (Number(info.sampleFee || 0) > 0) setIfExists("quoteSample", String(info.sampleFee));
      const catalogMatched = applyQuoteCatalogRecognizedValues({
        product: info.product,
        size: info.size,
        qty: info.qty,
        material: info.material,
        process: info.finish,
        finish: info.finish
      });
      setQuoteAiUncertainty({ confidence: info.confidence || {} });
      const quoteReply = info.customerReply || `您好，这个需求我们可以先按 ${info.product} 方向给您核价。
目前已收到：${info.size}，${info.qty}，${info.material}，${info.finish}。
为了报得更准确，麻烦再确认是否含税、是否需要打样、设计稿/Logo文件是否已定稿。所有报价统一不含运费。确认后我这边给您出正式报价。`;
      setTextIfExists("quoteReplyOutput", quoteReply);
      if (!stayOnInquiry) document.getElementById("customerOutput").textContent = `已把询盘信息带入报价工作台：

客户：${currentCustomerName()}
产品：${info.product}
尺寸：${info.size}
数量：${info.qty}
材质：${info.material}
工艺：${info.finish}

下一步：${inline ? "在当前聊天下方" : "进入报价系统"}核对产品参数、数量、税费、打样费和交期后生成报价；运费统一另计。`;
      if (inline) openHomeInlineQuoteWorkspace();
      else if (!stayOnInquiry) {
        showPage("报价系统");
        if (catalogMatched) setQuoteProductChooser(false);
      }
      updateQuoteSourceNote(info);
      const restoredQuote = restoreExactSavedQuoteResult();
      if (restoredQuote) {
        setTextIfExists("quoteSourceNote", `已找到这位客户相同规格的 V${Number(restoredQuote.version || 1)} 报价，直接显示上次结果；修改参数后再生成会创建新版本。`);
      }
      if (categoryMismatch) {
        const detectedCategory = detectedProfile.category;
        setQuoteIndustryWarning(`AI识别到这是“${detectedCategory}”需求，但当前使用的是“${currentIndustryCategory()}”价格表。`, detectedCategory, inferredTemplate);
      }
      renderInquiryQuotePreview(info);
      if (inline) {
        captureHomeInlineQuoteDraft(activeCustomerSession());
        saveState();
      }
    }
    function setFactoryType() {
      setIndustryCategory(encodeURIComponent(currentIndustryCategory()));
    }
    let inquiryAttachmentFiles = [];
    let inquiryUploadedAttachmentRecords = [];
    function inquiryAttachmentKey(file) {
      return [String(file?.name || ""), Number(file?.size || 0), Number(file?.lastModified || 0)].join("|");
    }
    function supportedInquiryAttachment(file) {
      const name = String(file?.name || "").toLowerCase();
      const type = String(file?.type || "").toLowerCase();
      return (
        ["image/jpeg", "image/png", "image/webp", "application/pdf", "text/plain", "text/csv"].includes(type)
        || /\.(?:jpe?g|png|webp|pdf|docx|xlsx|csv|txt)$/.test(name)
      );
    }
    function syncInquiryAttachmentInput() {
      const input = document.getElementById("inquiryAttachmentFiles");
      if (!input || typeof DataTransfer === "undefined") return;
      const transfer = new DataTransfer();
      inquiryAttachmentFiles.forEach(file => transfer.items.add(file));
      input.files = transfer.files;
    }
    function renderInquiryAttachmentList() {
      const list = document.getElementById("inquiryAttachmentList");
      const status = document.getElementById("inquiryAttachmentStatus");
      if (list) list.innerHTML = inquiryAttachmentFiles.map((file, index) => `
        <div class="inquiry-attachment-item">
          <span><strong>${escapeHtml(file.name || `客户附件${index + 1}`)}</strong><small>${escapeHtml(fileKind(file))} · ${escapeHtml(fileSizeText(file.size))} · 等待AI分析</small></span>
          <button type="button" onclick="removeInquiryAttachment(${index})" aria-label="移除${escapeHtml(file.name || "附件")}">×</button>
        </div>
      `).join("");
      if (status) status.textContent = inquiryAttachmentFiles.length
        ? `已放入 ${inquiryAttachmentFiles.length} 个附件。点击“一键处理询盘”后统一上传并分析，客户资料不会发送给其他客户。`
        : "图片会交给视觉AI识别；文档和表格会先安全提取文字与数据，再和客户消息一起分析。";
    }
    function addInquiryAttachments(files) {
      const existing = new Map(inquiryAttachmentFiles.map(file => [inquiryAttachmentKey(file), file]));
      const rejected = [];
      Array.from(files || []).forEach(file => {
        if (!supportedInquiryAttachment(file)) {
          rejected.push(`${file.name || "未命名文件"}（格式不支持）`);
          return;
        }
        if (Number(file.size || 0) > 18 * 1024 * 1024) {
          rejected.push(`${file.name || "未命名文件"}（超过18MB）`);
          return;
        }
        existing.set(inquiryAttachmentKey(file), file);
      });
      inquiryAttachmentFiles = [...existing.values()].slice(0, 10);
      syncInquiryAttachmentInput();
      renderInquiryAttachmentList();
      if (rejected.length) notify(`以下附件没有加入：${rejected.join("、")}`);
    }
    function appendInquiryAttachmentSelection() {
      addInquiryAttachments(document.getElementById("inquiryAttachmentFiles")?.files || []);
    }
    function removeInquiryAttachment(index) {
      const removed = inquiryAttachmentFiles[index];
      inquiryAttachmentFiles.splice(index, 1);
      if (removed) {
        const key = inquiryAttachmentKey(removed);
        inquiryUploadedAttachmentRecords = inquiryUploadedAttachmentRecords.filter(record => record.key !== key);
      }
      syncInquiryAttachmentInput();
      renderInquiryAttachmentList();
    }
    function handleInquiryAttachmentDrag(event, active) {
      event.preventDefault();
      event.stopPropagation();
      document.getElementById("inquiryAttachmentBox")?.classList.toggle("is-dragging", Boolean(active));
    }
    function handleInquiryAttachmentDrop(event) {
      handleInquiryAttachmentDrag(event, false);
      addInquiryAttachments(event.dataTransfer?.files || []);
    }
    document.addEventListener("paste", event => {
      if (!document.querySelector('.page.active[data-page="询盘处理"]')) return;
      const files = Array.from(event.clipboardData?.files || []);
      if (!files.length) return;
      event.preventDefault();
      addInquiryAttachments(files);
      notify(`已粘贴 ${files.length} 个客户附件，点击“一键处理询盘”即可分析。`);
    });
    let inquiryAnalysisTimer = 0;
    let inquiryAnalysisStartedAt = 0;
    let inquiryAnalysisStage = "upload";
    let inquiryAnalysisDetail = "";
    let inquiryAnalysisRunId = 0;
    const inquiryAnalysisStageOrder = ["upload", "vision", "organize"];
    function renderInquiryAnalysisProgress() {
      const panel = document.getElementById("inquiryAiProgress");
      if (!panel || !inquiryAnalysisStartedAt) return;
      panel.classList.add("is-visible");
      const elapsed = Math.max(0, Math.floor((Date.now() - inquiryAnalysisStartedAt) / 1000));
      const stageIndex = Math.max(0, inquiryAnalysisStageOrder.indexOf(inquiryAnalysisStage));
      const titleByStage = {
        upload: "正在安全上传客户附件",
        vision: inquiryAttachmentFiles.some(file => fileKind(file).startsWith("图片")) ? "视觉 AI 正在识别图片内容" : "AI 正在读取客户资料",
        organize: "图片已识别，正在整理接单结果"
      };
      const slowHint = elapsed >= 45
        ? "识图比平时慢，当前请求仍在等待返回；超时后系统会明确提示并保留原图。"
        : elapsed >= 18 && inquiryAnalysisStage === "vision"
          ? "正在读取图片中的产品、文字、尺寸、材质和工艺，备用线路会在主线路异常时自动接替。"
          : inquiryAnalysisDetail || "请求仍在进行，完成或超时后这里会明确提示。";
      setTextIfExists("inquiryAiProgressTitle", titleByStage[inquiryAnalysisStage] || "AI 正在处理");
      setTextIfExists("inquiryAiProgressDetail", slowHint);
      setTextIfExists("inquiryAiElapsed", `${elapsed}秒`);
      panel.querySelectorAll("[data-inquiry-stage]").forEach((item, index) => {
        item.classList.toggle("is-done", index < stageIndex);
        item.classList.toggle("is-active", index === stageIndex);
      });
    }
    function beginInquiryAnalysisProgress(hasAttachments) {
      window.clearInterval(inquiryAnalysisTimer);
      inquiryAnalysisStartedAt = Date.now();
      inquiryAnalysisStage = hasAttachments ? "upload" : "vision";
      inquiryAnalysisDetail = hasAttachments
        ? "正在准备附件并清理图片元数据。"
        : "正在理解客户文字并核对接单字段。";
      renderInquiryAnalysisProgress();
      inquiryAnalysisTimer = window.setInterval(renderInquiryAnalysisProgress, 1000);
    }
    function updateInquiryAnalysisProgress(update = {}) {
      const nextStage = String(update.stage || "").trim();
      if (inquiryAnalysisStageOrder.includes(nextStage)) inquiryAnalysisStage = nextStage;
      if (update.detail) inquiryAnalysisDetail = String(update.detail);
      renderInquiryAnalysisProgress();
    }
    function endInquiryAnalysisProgress() {
      window.clearInterval(inquiryAnalysisTimer);
      inquiryAnalysisTimer = 0;
      inquiryAnalysisStartedAt = 0;
      document.getElementById("inquiryAiProgress")?.classList.remove("is-visible");
    }
    function renderInquiryAiAnalysis(data) {
      const analysis = data?.analysis || {};
      if (analysis.intent === "service_unavailable") {
        setTextIfExists("customerOutput", analysis.customer_reply || "AI回复服务暂时繁忙，客户文字和附件都已保留，可以原地重试；本次失败不会扣点。");
        window.__latestAiAnalysis = null;
        window.__latestAiSourceText = "";
        return;
      }
      const asList = value => Array.isArray(value) ? value.filter(Boolean) : value ? [value] : [];
      const listText = value => asList(value).join("、") || "待确认";
      const sourceNames = inquiryAttachmentFiles.map(file => file.name);
      const warnings = asList(data?.attachment_warnings);
      const imageUnderstanding = analysis.image_summary || analysis.direct_answer || "";
      const lines = [
        "AI 已结合客户文字和附件完成分析：",
        ...(analysis.analysis_notice ? ["", `处理提醒：${analysis.analysis_notice}`] : []),
        ...(imageUnderstanding ? ["", "图片识别结果", imageUnderstanding] : []),
        "",
        "一、识别到的需求",
        `- 产品：${analysis.product || "待确认"}`,
        `- 尺寸：${analysis.size || "待确认"}`,
        `- 数量：${analysis.qty || "待确认"}`,
        `- 材质：${listText(analysis.material)}`,
        `- 工艺：${listText(analysis.process)}`,
        `- 交期：${analysis.delivery || "待确认"}`,
        `- 税运：${analysis.tax_shipping || "待确认"}`,
        sourceNames.length ? `- 分析附件：${sourceNames.join("、")}` : "- 分析附件：未上传",
        "",
        "二、还需要确认",
        ...(asList(analysis.missing).length ? asList(analysis.missing).map(item => `- ${item}`) : ["- 资料基本齐全"]),
        "",
        "三、报价和生产风险",
        ...(asList(analysis.risk).length ? asList(analysis.risk).map(item => `- ${item}`) : ["- 暂无明显风险，报价前仍需核对原稿和交期"]),
        "",
        "四、可以直接回复客户",
        analysis.customer_reply || analysis.direct_answer || "附件已经收到，我们会按文件内容核对规格后回复。",
        "",
        "五、下一步",
        ...(asList(analysis.next_actions).length ? asList(analysis.next_actions).map((item, index) => `${index + 1}. ${item}`) : ["1. 核对附件识别结果", "2. 进入报价或效果图"]),
        ...(warnings.length ? ["", "附件读取提醒：", ...warnings.map(item => `- ${item}`)] : [])
      ];
      setTextIfExists("customerOutput", lines.join("\n"));
      window.__latestAiAnalysis = analysis;
      window.__latestAiSourceText = val("customerText");
    }
    function inquiryCoreFieldsReady(analysis) {
      const readable = value => Array.isArray(value) ? value.filter(Boolean).join("、") : String(value || "");
      return [analysis?.product, analysis?.size, analysis?.qty, readable(analysis?.material), readable(analysis?.process)]
        .every(value => value && !/(?:待确认|待补充|未知|未识别)/.test(String(value)));
    }
    async function oneClickInquiry() {
      const text = val("customerText").trim();
      if (!text && !inquiryAttachmentFiles.length) {
        notify("请先粘贴客户消息，或添加客户图片/文件。");
        return;
      }
      if (typeof window.__analyzeInquiryAttachments !== "function") {
        setTextIfExists("customerOutput", "AI接单服务尚未连接，请重新打开软件后再试。客户文字和附件都已保留。");
        return;
      }
      const button = document.getElementById("oneClickInquiryButton");
      const status = document.getElementById("inquiryAttachmentStatus");
      const runId = ++inquiryAnalysisRunId;
      if (button) { button.disabled = true; button.textContent = "正在读取图片和文件…"; }
      beginInquiryAnalysisProgress(Boolean(inquiryAttachmentFiles.length));
      if (status) status.textContent = inquiryAttachmentFiles.length
        ? `正在上传并分析 ${inquiryAttachmentFiles.length} 个附件，请勿关闭页面…`
        : "正在理解客户文字并整理报价数据…";
      setTextIfExists("customerOutput", "AI 正在处理，本页上方会持续显示上传、识图和整理阶段以及已等待时间。");
      try {
        const data = await window.__analyzeInquiryAttachments({
          files: inquiryAttachmentFiles,
          existingRecords: inquiryUploadedAttachmentRecords,
          text,
          customer: val("inquiryCustomerName").trim() || "新客户",
          onProgress: updateInquiryAnalysisProgress
        });
        if (runId !== inquiryAnalysisRunId) return;
        inquiryUploadedAttachmentRecords = Array.isArray(data?.uploadedRecords) ? data.uploadedRecords : inquiryUploadedAttachmentRecords;
        renderInquiryAiAnalysis(data);
        window.setTimeout(() => applyInquiryToQuote({ stayOnInquiry: true }), 60);
        if (inquiryCoreFieldsReady(data?.analysis)) {
          if (status) status.textContent = "产品、尺寸、数量、材质和工艺已识别，正在生成报价草稿。";
          notify("核心询盘资料已识别，正在生成报价草稿。");
        } else {
          if (status) status.textContent = inquiryAttachmentFiles.length
            ? `已分析 ${inquiryAttachmentFiles.length} 个附件；原文件已安全归档到当前客户，可继续补资料、报价、做效果图或生成工单。`
            : "客户文字已分析完成，请补齐待确认资料后继续报价。";
          notify("AI接单分析完成，请核对并补充待确认资料。");
        }
      } catch (error) {
        if (runId !== inquiryAnalysisRunId) return;
        setTextIfExists("customerOutput", `本次分析没有完成：${error.message}\n\n客户文字和附件都已保留，可以原地重试。`);
        if (status) status.textContent = "分析未完成，文字和附件仍在当前页面，没有丢失。";
      } finally {
        if (runId !== inquiryAnalysisRunId) return;
        endInquiryAnalysisProgress();
        if (button) { button.disabled = false; button.textContent = "一键处理询盘"; }
      }
    }
    function generateInquiryWorkflow() {
      const { info, platform, goal } = customerContext();
      document.getElementById("customerOutput").textContent = `完整处理方案：

一、客户信息识别
- 产品：${info.product}
- 尺寸：${info.size}
- 数量：${info.qty}
- 材质：${info.material}
- 工艺：${info.finish}
- 风险：${info.risk.join("；")}

二、马上回客户
您好，这个需求我们可以先按 ${info.product} 方向评估。为了给您报准，麻烦确认最终设计稿/样品图/规格要求、是否含税、是否需要打样，以及期望交期。本报价不含运费。

三、内部下一步
1. 把询盘带入报价系统，核对材料、工艺、数量、税运和利润；
2. 生成宝贝详情页草稿和图片方案，用于 ${platform} 的 ${goal}；
3. 生成跟单任务，负责人跟进补资料、报价、打样和生产。

四、建议动作
- 能报价：先给客户预估价，并标注最终以规格、材料、工艺面积、收货地址为准；
- 资料不全：先补尺寸、数量、材质、工艺、税运、交期；
- 有黑底/烫金/急单：建议先打样确认，避免批量争议。

可点击下方“下一步：去报价”，把完整报价工作台直接展开在当前聊天中。`;
    }
    function generateSpecSheet() {
      const { info } = customerContext();
      document.getElementById("customerOutput").textContent = `规格整理表：

客户：${currentCustomerName()}
产品：${info.product}
尺寸：${info.size}
数量：${info.qty}
材质：${info.material}
工艺：${info.finish}
交期：${hasAny(val("customerText"), ["下周", "急", "马上"]) ? "客户较急，需确认排产" : "待确认"}
税运：待确认
图纸/设计稿：待确认
包装要求：待确认

下一步：
1. 补齐税运、收货地、图纸/设计稿；
2. 信息齐全后点击“下一步：去报价”；
3. 有外观要求时点击“下一步：做效果图”。`;
    }
    function generateMissingInfo() {
      const { info } = customerContext();
      const missing = [
        info.size.includes("待确认") ? "尺寸/规格" : "",
        info.qty.includes("待确认") ? "数量" : "",
        info.material.includes("待确认") ? "材质/厚度/克重" : "",
        info.finish.includes("待确认") ? "工艺/Logo位置/颜色" : "",
        "是否含税（运费统一另计）",
        "是否需要打样",
        "设计稿、样品图或参考图",
        "期望交期和验收标准"
      ].filter(Boolean);
      document.getElementById("customerOutput").textContent = `补资料清单：

为了报得准确，需要客户补充：
${missing.map((item, index) => `${index + 1}. ${item}`).join("\n")}

可直接发客户：
您好，这个需求我们可以做初步评估。为了给您报准确价格，麻烦再确认：${missing.slice(0, 6).join("、")}。如果有设计稿、样品图或参考图，也可以一起发我，我们这边会按最终资料核价和排期。`;
    }
    function judgeOrderValue() {
      const { info } = customerContext();
      const risks = info.risk.length ? info.risk : ["资料未完全确认，报价前需补齐规格、数量、材质、工艺和交期"];
      const urgent = hasAny(val("customerText"), ["急", "马上", "下周"]);
      const incomplete = [info.size, info.qty, info.material, info.finish].some(item => item.includes("待确认"));
      const decision = urgent || incomplete || risks.length >= 2
        ? "谨慎接，先补资料/确认风险后再正式报价。"
        : "可以继续跟进，适合进入报价和效果图确认。";
      document.getElementById("customerOutput").textContent = `接单判断：

结论：${decision}

判断依据：
- 产品：${info.product}
- 规格/数量：${info.size}，${info.qty}
- 材质/工艺：${info.material}，${info.finish}
- 风险：${risks.join("；")}

建议：
1. 先补齐会影响价格和交期的信息；
2. 特殊工艺、急单、外观要求高的订单，建议先打样或做效果图确认；
3. 资料齐全后再生成正式报价，避免后期扯皮。`;
    }
    function generateProductionNotes() {
      const { info } = customerContext();
      document.getElementById("customerOutput").textContent = `生产注意事项：

产品：${info.product}
尺寸：${info.size}
数量：${info.qty}
材质：${info.material}
工艺：${info.finish}

车间/制前注意：
1. 核对图纸、尺寸、公差、Logo位置和颜色要求；
2. 确认材料批次、厚度/克重、表面处理和实际工艺可行性；
3. 特殊工艺先做样或首件确认，避免批量返工；
4. 包装方式、运输防护、装箱数量需要提前确认；
5. 交期从资料齐全、定金到账、样品/效果图确认后开始计算。

风险提醒：
${info.risk.length ? info.risk.map((item, index) => `${index + 1}. ${item}`).join("\n") : "1. 暂无明显风险，但仍需确认最终设计稿、规格和验收标准。"}`;
    }
    function generateImagePlan() {
      const { info, platform, goal } = customerContext();
      const visualTone = hasAny(val("customerText"), ["黑", "高端", "化妆"]) ? "高端、干净、黑金质感" : "清爽、真实、工厂直供";
      document.getElementById("customerOutput").textContent = `AI效果图 / 图片制作方案：

适用平台：${platform}
目标：${goal}
产品：${info.product}
核心规格：${info.size}，${info.material}，${info.finish}

建议做 5 张图：
1. 首图：${info.product} 成品 45 度展示，突出“支持定制、打样确认、批量生产”。
2. 工艺图：放大展示 ${info.finish}，用箭头标注关键工艺、材料质感和定制位置。
3. 尺寸图：展示 ${info.size} 的长宽高、结构或使用方式，减少客户反复追问。
4. 场景图：把产品放在实际使用、陈列、发货或安装场景中，体现成品效果。
5. 工厂实力图：展示材料、设备、加工、质检、包装和出货流程，增强信任。

首图文案：
${info.product} 定制
${info.material} · ${info.finish}
支持打样 / 批量生产 / 工艺确认

AI 作图提示词：
一款${info.product}，尺寸 ${info.size}，材质 ${info.material}，工艺 ${info.finish}，${visualTone}，真实产品摄影，电商主图，白色或浅灰背景，产品边缘清晰，表面材质可见，专业灯光，适合 ${platform} 商品图，不要杂乱文字，不要变形。

设计注意：
- 图片里不要写死价格和交期，避免后期修改成本高。
- 工艺效果要和实际可生产能力一致，尤其颜色、表面处理、Logo位置、材质纹理和结构比例。
- 如果要真正出图，正式版应接入图片生成接口，并允许上传客户 logo、图纸、设计稿或参考图。

下一步：也可以进入“AI效果图”页面，把这条需求生成更完整的生图提示词。`;
    }
    function updateVisualFileName() {
      const file = document.getElementById("visualFile").files[0];
      const label = document.getElementById("visualFileName");
      if (label) label.textContent = file ? `已选择：${file.name}` : "未选择文件";
      renderVisualReferencePreview(file);
    }
    function renderVisualReferencePreview(file, label = "") {
      const box = document.getElementById("visualReferencePreview");
      if (!box) return;
      if (window.__visualPreviewUrl) URL.revokeObjectURL(window.__visualPreviewUrl);
      window.__visualPreviewUrl = "";
      if (!file) {
        box.textContent = label || "上传参考图后在这里预览";
        return;
      }
      if (String(file.type || "").startsWith("image/")) {
        window.__visualPreviewUrl = URL.createObjectURL(file);
        box.innerHTML = `<img src="${window.__visualPreviewUrl}" alt="参考图预览">`;
      } else {
        box.textContent = label || file.name || "已带入参考文件";
      }
    }
    function renderVisualGenerationControls() {
      const use = val("visualUse");
      const count = visualCountNumber(val("visualCount"));
      setTextIfExists("visualPlanButton", use === "客户确认效果图" && count === 1 ? "生成1张客户确认图方案" : `生成${count}张${use || "效果图"}方案`);
    }
    function renderVisualCustomerContext() {
      const customer = String(currentCustomerName() || "新客户").trim() || "新客户";
      setTextIfExists("visualCurrentCustomer", customer);
      const box = document.getElementById("visualCustomerContext");
      box?.classList.toggle("is-pending-customer", isPendingCustomerName(customer) || ["新客户", "未命名客户"].includes(customer));
      const hint = box?.querySelector("em");
      if (hint) hint.textContent = box.classList.contains("is-pending-customer")
        ? "建议先在首页填写客户姓名，避免效果图存到临时客户名下"
        : "生成记录和客户偏好都会保存到这个客户名下";
    }
    function imagePromptWithoutCustomerIdentity(value, customerName = "") {
      let text = String(value || "").trim();
      const name = String(customerName || "").trim();
      if (!text || !name) return text;
      const compactName = name.replace(/\s+/g, "");
      text = text.split(/\r?\n/).filter(line => {
        const compactLine = String(line || "").replace(/\s+/g, "").replace(/[，,。；;]+$/g, "");
        return !["客户", "客户名称", "客户姓名", "联系人", "公司", "公司名称"]
          .some(label => compactLine === `${label}：${compactName}` || compactLine === `${label}:${compactName}`);
      }).join("\n");
      if (name.length >= 2) text = text.split(name).join("");
      return text
        .replace(/^(?:客户|客户名称|客户姓名|联系人|公司|公司名称)\s*[:：]\s*$/gm, "")
        .replace(/[ \t]{2,}/g, " ")
        .replace(/\n{3,}/g, "\n\n")
        .trim();
    }
    function imagePromptRelevantInquiryText(value, customerName = "") {
      const cleaned = imagePromptWithoutCustomerIdentity(value, customerName);
      const visualTerms = /(背景|场景|角度|视角|正面|侧面|俯视|特写|居中|构图|光线|阴影|颜色|色号|图案|logo|文字|字体|满版|铺满|留白|白边|边框|位置|摆放|风格|摄影|写实|结构|材质|透明|半透明|磨砂|哑光|亮光|烫金|烫银|丝印|uv|覆膜|电镀|喷涂|纹理|质感|开孔|部件)/i;
      return cleaned.split(/[。！？!?；;\n]+/)
        .map(part => part.trim().replace(/^客户\s*[:：]\s*/, ""))
        .filter(part => part && visualTerms.test(part))
        .slice(0, 6)
        .join("；");
    }
    function inquiryVisualReferenceFiles() {
      return inquiryAttachmentFiles.filter(homeFileCanBeUploaded).slice(0, 6);
    }
    function inquiryVisualReferenceRecords() {
      return inquiryUploadedAttachmentRecords.filter(record => {
        const mimeType = String(record?.mimeType || "").toLowerCase();
        const name = String(record?.name || "").toLowerCase();
        return mimeType.startsWith("image/") || /\.(?:jpe?g|png|webp|ai|cdr|psd|svg)$/.test(name);
      }).slice(0, 6);
    }
    function replaceImageStudioReferencesFromInquiry(files = [], records = []) {
      const input = document.getElementById("homeFiles");
      if (input) {
        input.value = "";
        if (files.length && typeof DataTransfer !== "undefined") {
          const transfer = new DataTransfer();
          files.forEach(file => transfer.items.add(file));
          input.files = transfer.files;
        }
      }
      const session = activeCustomerSession?.();
      if (session) {
        session.pendingFileIds = records.map(record => Number(record?.id || 0)).filter(id => id > 0);
        session.pendingFileRecords = records.map(record => ({ ...record }));
        session.retainedFileIds = [];
        session.retainedFileNames = [];
        session.updatedAt = new Date().toISOString();
        window.__applyHomeSessionUploadState?.(session);
      }
      window.__lastRecognizedHomeFileIds = [];
      window.__homeRetainedFileNames = [];
      updateHomeFileName();
      renderHomeRetainedFileContext();
    }
    function loadVisualFromInquiry(shouldShow = true) {
      const visualText = document.getElementById("visualText");
      if (!visualText) return;
      const raw = val("customerText");
      const customer = val("inquiryCustomerName").trim() || "新客户";
      const sourcePlatform = val("contentPlatform") || "阿里巴巴/1688";
      const inquiryReferenceFiles = inquiryVisualReferenceFiles();
      const inquiryReferenceRecords = inquiryVisualReferenceRecords();
      const hasInquiryReference = inquiryReferenceFiles.length > 0 || inquiryReferenceRecords.length > 0;
      let info = null;
      let use = "阿里巴巴主图";
      if (raw) {
        info = analyze(raw);
        const visualNotes = imagePromptRelevantInquiryText(raw, customer);
        visualText.value = `产品：${info.product}
成品尺寸：${info.size}
材质：${info.material}
工艺：${info.finish}
${visualNotes ? `补充画面要求：${visualNotes}\n` : ""}${hasInquiryReference ? "参考图：已上传，将严格按参考图执行\n" : ""}
想要图片：主图、场景效果图、详情页卖点图`;
        use = hasAny(raw, ["确认效果图", "客户确认", "看效果", "效果图", "打样前确认"])
          ? "客户确认效果图"
          : hasAny(raw, ["详情页", "详情长图", "卖点图"])
            ? "详情页长图"
            : hasAny(raw, ["淘宝", "拼多多"])
              ? "淘宝/拼多多主图"
              : hasAny(raw, ["朋友圈", "海报"])
                ? "朋友圈海报"
                : "阿里巴巴主图";
        setIfExists("visualUse", use);
        setIfExists("visualCount", use === "客户确认效果图" ? "1张主图方案" : use === "详情页长图" ? "5张成套方案" : "3张快速方案");
        if (hasInquiryReference) {
          const names = [...inquiryReferenceFiles.map(file => file.name), ...inquiryReferenceRecords.map(record => record.name)].filter(Boolean);
          setTextIfExists("visualFileName", `已从询盘带入：${[...new Set(names)].join("、")}`);
          renderVisualReferencePreview(inquiryReferenceFiles[0], `已从询盘带入：${names[0] || "客户参考图"}`);
        }
        renderVisualGenerationControls();
      }
      if (!shouldShow) return true;
      const opened = showPage("AI效果图");
      if (opened !== true) return opened;

      selectHomeImageMode(hasInquiryReference ? "mockup" : "text_image");
      syncQuoteCustomerToActiveSession(customer);
      setIfExists("homeCustomerName", customer);
      replaceImageStudioReferencesFromInquiry(inquiryReferenceFiles, inquiryReferenceRecords);
      if (!raw || !info) return opened;

      const purpose = use === "客户确认效果图"
        ? "客户确认图"
        : use === "详情页长图"
          ? "详情页图"
          : use === "朋友圈海报"
            ? "场景展示图"
            : "电商主图";
      const targetPlatform = sourcePlatform === "阿里巴巴/1688"
        ? "1688"
        : sourcePlatform === "淘宝/天猫"
          ? "淘宝 / 天猫"
          : sourcePlatform === "小红书"
            ? "抖音 / 小红书"
            : sourcePlatform === "微信私域"
              ? "智能匹配"
              : sourcePlatform;
      const visualNotes = imagePromptRelevantInquiryText(raw, customer);
      const requirement = `产品主体：${info.product}
成品尺寸与比例：${info.size}
材质与厚度：${info.material}
表面及印刷工艺：${info.finish}
效果图用途：${purpose}
目标平台：${targetPlatform}
${visualNotes ? `补充画面要求：${visualNotes}\n` : ""}${hasInquiryReference ? "参考图执行：已上传设计稿或产品图，必须忠实保持结构、比例、图案、Logo位置和主要颜色。\n" : "参考图执行：尚未上传；本次使用文字生图，不得虚构Logo、品牌文字、尺寸标注或无法从需求确认的部件。\n"}制作要求：先准确表现产品结构、长宽高视觉比例、材质透明度和工艺质感，再安排专业商业摄影的镜头、构图、背景与光线；主体完整、边缘清晰，可直接用于客户确认。
禁止项：不要在提示词或画面中出现客户姓名、公司名、联系人、订单数量、交期、价格或付款信息；不要把归档信息画成Logo、标签、文字或装饰。`;

      setIfExists("aiQuestion", requirement);
      setIfExists("homeMockupPurpose", purpose);
      setIfExists("homeTargetPlatform", targetPlatform);
      autoGrowHomeQuestion(document.getElementById("aiQuestion"));
      syncActiveCustomerSessionDraft();
      notify(hasInquiryReference
        ? "已带入产品资料和参考图，默认使用效果图生成。"
        : "本次没有参考图，已自动切换到文字生图并带入产品资料。");
      return true;
    }
    function visualContext() {
      const text = val("visualText");
      return {
        text,
        info: analyze(text),
        use: val("visualUse") || "阿里巴巴主图",
        style: val("visualStyle") || "真实产品摄影",
        countText: val("visualCount") || "5张成套方案",
        file: document.getElementById("visualFile")?.files[0]?.name || "未上传参考图"
      };
    }
    function visualShotLibrary(info, use, style) {
      const base = `一款${info.product}，${info.size}，${info.material}，工艺为${info.finish}，${style}，用于${use}，产品结构清晰，材质细节真实，边缘干净，光线专业，不要错误文字，不要变形，不要多余Logo`;
      return [
        {
          title: "主图",
          purpose: "给客户第一眼看清产品外观、材质和定制能力。",
          prompt: `${base}，白底或浅灰背景，产品完整居中，45度角展示，突出可定制、批量生产、品质稳定。`
        },
        {
          title: "场景效果图",
          purpose: "让客户看到成品放在真实使用、陈列或安装环境里的效果。",
          prompt: `${base}，真实使用场景，产品放在合适的商业陈列、办公、门店、展会或发货场景中，环境干净专业，突出使用效果。`
        },
        {
          title: "细节工艺图",
          purpose: "展示Logo、材质纹理、边角、表面处理、车缝、切割或组装细节。",
          prompt: `${base}，微距细节特写，突出Logo位置、材料质感、边角处理、表面工艺和加工精度，背景简洁。`
        },
        {
          title: "尺寸卖点图",
          purpose: "用于详情页说明尺寸、结构、承重、防刮、防水或包装卖点。",
          prompt: `${base}，详情页卖点图风格，保留干净留白，展示尺寸比例、结构层次和核心卖点，可预留文字标注区域。`
        },
        {
          title: "工厂实力图",
          purpose: "展示材料、设备、加工、质检、包装和出货流程，增强信任。",
          prompt: `${base}，工厂生产场景，展示材料准备、加工设备、质检、包装出货流程，画面真实可信，不夸大产能。`
        }
      ];
    }
    function visualCountNumber(countText) {
      if (String(countText).includes("1")) return 1;
      if (String(countText).includes("3")) return 3;
      return 5;
    }
    function generateVisualPlan(shouldConsume = true) {
      const { info, use, style, countText, file } = visualContext();
      const shots = visualShotLibrary(info, use, style).slice(0, visualCountNumber(countText));
      lastVisualPrompts = shots.map((shot, index) => `${index + 1}. ${shot.title}\n用途：${shot.purpose}\n提示词：${shot.prompt}`);
      const cards = document.getElementById("visualSetCards");
      if (cards) {
        cards.innerHTML = shots.map((shot, index) => `
          <div class="visual-card">
            <div class="visual-preview">${shot.title}<br>预览位</div>
            <strong>${index + 1}. ${shot.title}</strong>
            <span>${shot.purpose}</span>
            <button class="btn light" onclick="copyVisualShotPrompt(${index})">复制本张提示词</button>
          </div>
        `).join("");
      }
      document.getElementById("visualOutput").textContent = `AI效果图方案：

产品：${info.product}
用途：${use}
画面风格：${style}
生成张数：${shots.length} 张
参考文件：${file}
核心规格：${info.size}，${info.material}，${info.finish}

本次建议生成：
${shots.map((shot, index) => `${index + 1}. ${shot.title}：${shot.purpose}`).join("\n")}

客户还需要补充：
- 是否有 Logo、参考图、样品图或设计稿；
- 图片要用于哪个平台和尺寸比例；
- 是否要白底主图、场景图、详情页长图；
- 哪些工艺效果必须和实物一致。

AI生图提示词：
${lastVisualPrompts.join("\n\n")}

内部提醒：
- 效果图不能夸大实际工艺和材料能力；
- 有 Logo、图纸或样品图时，正式版应优先用参考图生成；
- 发给客户前要标注“效果图仅供确认方向，最终以样品/实物为准”。`;
    }
    function copyVisualShotPrompt(index) {
      const text = lastVisualPrompts[index] || "";
      if (!text) {
        alert("请先生成AI效果图方案。");
        return;
      }
      navigator.clipboard?.writeText(text);
      alert("本张生图提示词已复制。");
    }
    function copyVisualPrompt() {
      const output = document.getElementById("visualOutput")?.textContent || "";
      if (!output || output.includes("填写产品")) {
        alert("请先生成AI效果图方案。");
        return;
      }
      const prompt = lastVisualPrompts.length ? lastVisualPrompts.join("\n\n") : output.split("AI生图提示词：")[1]?.split("内部提醒：")[0]?.trim() || output;
      navigator.clipboard?.writeText(prompt);
      alert("全部AI生图提示词已复制。");
    }
    function visualVersionStatusLabel(status) {
      return ({
        draft: "内部草稿",
        sent: "已发客户",
        revision_requested: "客户要求修改",
        confirmed_locked: "客户已确认·最终稿",
        archived: "历史版本"
      })[status] || "内部草稿";
    }
    function renderVisualVersionBoard() {
      const list = document.getElementById("visualVersionList");
      if (!list) return;
      const versions = (state.visualVersions || []).slice(0, 18);
      if (!versions.length) {
        list.innerHTML = `<div class="vector-empty">生成第一张真实产品图后，这里会自动建立 V1 版本。</div>`;
        return;
      }
      const locked = versions.find(item => item.status === "confirmed_locked");
      const banner = locked ? `<div class="visual-final-banner"><div><strong>生产最终稿已锁定：${escapeHtml(locked.customer)} · ${escapeHtml(locked.product)} · V${Number(locked.version || 1)}</strong>其他效果图只作为沟通历史，生产、打样和交付请统一使用此版本。</div>${generatedImageDownloadButton(locked.url, locked.filename || "客户确认最终稿.webp", "下载最终稿")}</div>` : "";
      list.innerHTML = banner + versions.map(item => {
        const status = item.status || "draft";
        const lockedItem = status === "confirmed_locked";
        const actions = lockedItem ? `
          ${generatedImageDownloadButton(item.url, item.filename || "客户确认最终稿.webp", "下载最终稿")}
        ` : status === "revision_requested" ? `
          <button class="btn dark" type="button" onclick="prepareVisualRevision('${escapeHtml(item.id)}')">按意见生成新版本</button>
        ` : status === "sent" ? `
          <button class="btn light" type="button" onclick="prepareVisualRevision('${escapeHtml(item.id)}')">客户要求修改</button>
          <button class="btn dark" type="button" onclick="lockVisualVersion('${escapeHtml(item.id)}')">客户确认并锁定</button>
        ` : status === "archived" ? "" : `
          <button class="btn light" type="button" onclick="markVisualVersionSent('${escapeHtml(item.id)}')">标记已发客户</button>
          <button class="btn dark" type="button" onclick="lockVisualVersion('${escapeHtml(item.id)}')">客户确认并锁定</button>
        `;
        return `
          <div class="visual-version-item${lockedItem ? " locked" : ""}">
            ${generatedImagePreviewMarkup(item.url, item.filename || `效果图-V${Number(item.version || 1)}.webp`, `${item.customer} ${item.product} V${Number(item.version || 1)}`, item.fileId)}
            <div class="visual-version-body">
              <div class="visual-version-top"><strong>${escapeHtml(item.customer)} · V${Number(item.version || 1)}</strong><span class="visual-version-status ${escapeHtml(status)}">${escapeHtml(visualVersionStatusLabel(status))}</span></div>
              <div class="visual-version-meta">${escapeHtml(item.product)} · ${escapeHtml(new Date(item.createdAt).toLocaleString("zh-CN", { hour12:false }))}</div>
              ${item.appliedDesignPreferenceSummary ? `<div class="visual-version-note">已参考客户习惯：${escapeHtml(item.appliedDesignPreferenceSummary)}</div>` : ""}
              ${item.note ? `<div class="visual-version-note">${escapeHtml(item.note)}</div>` : ""}
              <div class="visual-version-actions">${generatedImageDownloadButton(item.url, item.filename || `效果图-V${Number(item.version || 1)}.webp`, "下载原图", "btn light")}${actions}</div>
            </div>
          </div>
        `;
      }).join("");
    }
    function recordVisualVersion(image = {}, options = {}) {
      if (!image.url) return null;
      const context = options.context || visualContext();
      const customer = String(options.customer || currentCustomerName() || "新客户").trim() || "新客户";
      const product = options.product || context.info.product || "定制产品";
      const related = (state.visualVersions || []).filter(item => item.customer === customer && item.product === product);
      const item = {
        id: `TX-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
        customer,
        product,
        version: related.length + 1,
        status: "draft",
        url: image.url,
        filename: image.filename || `${product}-效果图-V${related.length + 1}.webp`,
        fileId: Number(image.file_id || 0) || null,
        prompt: options.prompt || lastVisualPrompts[0] || "",
        referenceUsed: Boolean(image.reference_used),
        sourceText: options.sourceText || val("visualText") || String(window.__lastHomeVisualUserPrompt || ""),
        designPreferenceSummary: image.design_learning?.summary || "",
        appliedDesignPreferenceSummary: image.design_learning?.applied_summary || "",
        semanticDesignPreferences: image.prompt_enhancement?.learned_preferences || {},
        note: "AI效果示意，发客户前请核对结构、颜色、Logo、材质和工艺。",
        createdAt: new Date().toISOString()
      };
      state.visualVersions = [item, ...(state.visualVersions || [])].slice(0, 60);
      window.__activeVisualVersionId = item.id;
      appendCustomerHistory(customer, `生成效果图 V${item.version}：${product}，等待内部核对`, { need: product });
      saveState();
      renderVisualVersionBoard();
      renderDashboard();
      return item;
    }
    function markVisualVersionSent(id) {
      const item = (state.visualVersions || []).find(version => version.id === id);
      if (!item) return;
      item.status = "sent";
      item.sentAt = new Date().toISOString();
      item.note = "已发客户确认颜色、Logo位置、结构和外观方向。";
      appendCustomerHistory(item.customer, `效果图 V${item.version} 已发客户确认`, { need:item.product, dealStatus:"待确认" });
      saveState(); renderVisualVersionBoard(); renderDashboard();
      notify(`V${item.version} 已标记为发客户确认。`);
    }
    function prepareVisualRevision(id) {
      const item = (state.visualVersions || []).find(version => version.id === id);
      if (!item) return;
      const note = prompt("填写客户修改意见\n例如：Logo缩小20%，颜色改成深蓝，保持结构不变。", item.revisionNote || "");
      if (!note) return;
      item.status = "revision_requested";
      item.revisionNote = note.trim();
      item.note = `客户修改意见：${note.trim()}`;
      const input = document.getElementById("visualText");
      if (input) input.value = `${input.value.trim()}\n客户针对 V${item.version} 的修改意见：${note.trim()}\n必须保持未提及部分与上一版一致。`;
      appendCustomerHistory(item.customer, `效果图 V${item.version} 客户要求修改：${note.trim()}`, { need:item.product, dealStatus:"待确认" });
      saveState(); renderVisualVersionBoard();
      window.__recordCustomerDesignFeedback?.({
        customer: item.customer,
        event_type: "revision",
        feedback: note.trim(),
        source_text: item.sourceText || "",
        prompt: item.prompt || "",
        generated_file_id: item.fileId,
        semantic_preferences: item.semanticDesignPreferences || {},
        version: item.version
      }).then(data => {
        const summary = data?.design_learning?.summary || "";
        if (summary) item.designPreferenceSummary = summary;
        saveState();
      }).catch(err => notify(`修改意见已保存在本机，偏好同步提醒：${err.message}`));
      document.querySelector(".visual-brief-card")?.scrollIntoView({ behavior:"smooth", block:"start" });
      notify("修改意见已带入，点击“AI直接生成”即可建立新版本。" );
    }
    function lockVisualVersion(id) {
      const item = (state.visualVersions || []).find(version => version.id === id);
      if (!item) return;
      if (!confirm(`确认把 ${item.customer} 的 V${item.version} 锁定为生产最终稿吗？\n锁定后其他版本只作为历史记录。`)) return;
      (state.visualVersions || []).forEach(version => {
        if (version.customer === item.customer && version.product === item.product && version.id !== item.id && version.status !== "revision_requested") version.status = "archived";
      });
      item.status = "confirmed_locked";
      item.confirmedAt = new Date().toISOString();
      item.note = "客户已确认并锁定。生产、打样和交付统一使用此版本。";
      let task = (state.tasks || []).find(entry => String(entry.customer || "").includes(item.customer) && `${entry.customer || ""} ${entry.title || ""}`.includes(item.product));
      if (!task) {
        task = { id:newTaskId(), localCompanyId:currentTaskRecycleCompanyKey(), customer:`${item.customer} · ${item.product}`, title:`${item.product} 客户确认稿`, owner:"制前/生产", createdAt:new Date().toISOString(), due:"明天", nextFollow:"明天 09:00", status:"待确认", amount:0, risk:"必须使用已锁定最终稿", logs:[] };
        state.tasks.unshift(task);
      }
      task.status = "打样";
      task.nextAction = `按客户锁定效果图 V${item.version} 核对生产文件并安排打样`;
      task.risk = `只允许使用 V${item.version} 最终稿，禁止使用其他历史效果图`;
      task.logs = [...(task.logs || []), `${new Date().toLocaleString("zh-CN", {hour12:false})} 客户确认并锁定效果图 V${item.version}`].slice(-20);
      appendCustomerHistory(item.customer, `客户确认效果图 V${item.version}，已锁定为生产最终稿`, { need:item.product, dealStatus:"待确认" });
      saveState(); renderVisualVersionBoard(); renderTasks(); renderDashboard();
      window.__recordCustomerDesignFeedback?.({
        customer: item.customer,
        event_type: "confirmed",
        source_text: item.sourceText || "",
        prompt: item.prompt || "",
        generated_file_id: item.fileId,
        semantic_preferences: item.semanticDesignPreferences || {},
        version: item.version
      }).then(data => {
        const summary = data?.design_learning?.summary || "";
        if (summary) item.designPreferenceSummary = summary;
        saveState();
      }).catch(err => notify(`最终稿已锁定，偏好同步提醒：${err.message}`));
      notify(`V${item.version} 已锁定为生产最终稿，并同步到打样任务。`);
    }
    function loadVisualFromQuote() {
      const product = val("quoteProduct") || "定制产品";
      const text = `产品：${product}
尺寸/数量：${val("quoteSize") || "尺寸待确认"}，${val("quoteQty") || "数量待确认"}个
材质/工艺：${val("quoteMaterial") || "材料待确认"}，${val("quoteProcess") || "工艺待确认"}
报价口径：${val("quoteTaxMode") || "含税"}，不含运费（运费另计）
想要图片：主图、场景效果图、详情页卖点图，用于给客户确认效果并促进成交`;
      showPage("AI效果图");
      setIfExists("visualText", text);
    }
    function generateDetailPage() {
      if (typeof window.generateDetailPageConnected === "function") {
        return window.generateDetailPageConnected();
      }
      const { info, platform, goal } = customerContext();
      document.getElementById("customerOutput").textContent = `宝贝详情页草稿：

平台：${platform}
目标：${goal}

商品标题：
${info.product}定制 ${info.material} ${info.finish} 工厂定做 支持打样批量生产

首屏卖点：
1. 支持来图来样定制，尺寸、材质、工艺可按需求调整。
2. ${info.material}，适合按客户使用场景、品牌要求或结构需求定制。
3. 可做 ${info.finish}，提升成品质感和品牌识别度。
4. 批量生产前建议先打样确认颜色、尺寸、公差、工艺位置和成品效果。

产品参数：
- 产品类型：${info.product}
- 参考尺寸：${info.size}
- 参考数量：${info.qty}
- 参考材质：${info.material}
- 加工/表面工艺：${info.finish}
- 适用场景：按客户行业、使用环境、包装方式或销售渠道确认

详情页结构：
1. 成品展示图：展示整体外观和开盒结构。
2. 工艺细节：重点展示印刷、后道、车缝、裁切、冲压、组装或定制工艺细节。
3. 材质说明：说明材料厚度、手感、承重、表面处理或使用环境。
4. 定制流程：咨询规格 -> 确认报价 -> 确认设计/工艺 -> 打样 -> 批量生产 -> 发货。
5. 下单须知：价格受尺寸、数量、材质、工艺、税运、交期影响，详情以客服核价为准。
6. 风险提醒：${info.risk.join("；")}。

客服承接话术：
您好，这款支持按您的尺寸、材质和工艺定制。您可以把尺寸、数量、设计稿和是否含税发我，我这边帮您核算准确报价和交期；运费统一另计。`;
    }
    function generateDetailPackage(options = {}) {
      if (typeof window.generateDetailPackageConnected === "function") {
        return window.generateDetailPackageConnected(options);
      }
      notify("整套详情服务正在连接，请稍后原地重试。");
    }
    function downloadDetailPackage() {
      if (typeof window.downloadDetailPackageConnected === "function") {
        return window.downloadDetailPackageConnected();
      }
      notify("详情长图还没有生成，请先点击一键生成。");
    }
    function copyCustomerOutput() {
      const output = document.getElementById("customerOutput").textContent;
      if (!output || output.includes("粘贴客户询盘")) {
        alert("请先生成内容。");
        return;
      }
      navigator.clipboard?.writeText(output);
      alert("结果已复制。");
    }
    function clearCustomer(options = {}) {
      const emptyOutput = "粘贴客户询盘后，可以生成客户回复、图片制作方案、宝贝详情或内部工单。";
      const customerName = val("inquiryCustomerName").trim();
      const contact = val("inquiryContact").trim();
      const text = val("customerText").trim();
      const output = document.getElementById("customerOutput")?.textContent?.trim() || "";
      const hasCurrentContent = Boolean(
        (customerName && customerName !== "新客户")
        || contact
        || text
        || inquiryAttachmentFiles.length
        || (output && output !== emptyOutput)
      );
      if (hasCurrentContent && options.confirm !== false && !confirm("确定清空本次询盘吗？\n\n将清除当前客户名称、联系人、文字、附件和分析结果；已经保存到客户档案、报价或跟单中的记录不会删除。")) return;
      inquiryAnalysisRunId += 1;
      endInquiryAnalysisProgress();
      setIfExists("inquiryCustomerName", "新客户");
      setIfExists("inquiryContact", "");
      setIfExists("customerText", "");
      setTextIfExists("customerOutput", emptyOutput);
      const quotePreview = document.getElementById("inquiryQuotePreview");
      if (quotePreview) { quotePreview.innerHTML = ""; quotePreview.classList.add("is-hidden"); }
      inquiryAttachmentFiles = [];
      inquiryUploadedAttachmentRecords = [];
      syncInquiryAttachmentInput();
      renderInquiryAttachmentList();
      window.__latestAiAnalysis = null;
      window.__latestAiSourceText = "";
      const button = document.getElementById("oneClickInquiryButton");
      if (button) {
        button.disabled = false;
        button.textContent = "一键处理询盘";
      }
      document.getElementById("customerText")?.focus();
      if (hasCurrentContent && options.silent !== true) notify("本次询盘已清空，已保存的客户、报价和跟单记录不受影响。");
    }
    function createOrder() {
      const info = analyze(val("customerText"));
      const task = {
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: `${currentCustomerName()} · ${info.product}`,
        title: `${info.product} 制前复核`,
        owner: "业务/制前",
        createdAt: new Date().toISOString(),
        createdBy: window.__currentUser?.name || state.member.contact || "当前业务员",
        due: "今天",
        nextFollow: "今天下班前",
        status: info.size.includes("待确认") || info.qty.includes("待确认") ? "待补资料" : "询盘",
        nextAction: "补齐规格、数量、材质、税费和交期要求",
        amount: 0,
        risk: info.risk.join("；"),
        logs: [new Date().toLocaleString("zh-CN", { hour12: false }) + " 从客户需求生成跟单任务"]
      };
      state.tasks.unshift(task);
      appendCustomerHistory(currentCustomerName(), `生成内部工单：${info.product}，状态 ${task.status}`, {
        contact: val("inquiryContact"),
        dealStatus: "未报价",
        need: info.product,
        preference: `${info.material}；${info.finish}`
      });
      saveState();
      document.getElementById("customerOutput").textContent = `已生成内部工单并加入跟单任务：

客户：${currentCustomerName()}
产品：${info.product}
尺寸：${info.size}
材质：${info.material}
工艺：${info.finish}
数量：${info.qty}
风险：${info.risk.join("；")}

下一步：制前核图纸/设计稿 -> 报价确认 -> 打样/生产排单。`;
      renderDashboard();
    }
    function quoteFollowupSchedule(sourceText = "") {
      const source = String(sourceText || "");
      const dueDate = new Date();
      dueDate.setDate(dueDate.getDate() + (/后天/.test(source) ? 2 : 1));
      let hour = /下午|午后/.test(source) ? 15 : /晚上|晚间/.test(source) ? 19 : /中午/.test(source) ? 12 : 9;
      let minute = 0;
      const explicit = source.match(/(上午|下午|晚上|中午)?\s*(\d{1,2})(?:[点时]|[:：])(\d{1,2})?\s*(?:分)?/);
      if (explicit) {
        hour = Math.min(23, Number(explicit[2] || hour));
        minute = Math.min(59, Number(explicit[3] || 0));
        if ((explicit[1] === "下午" || explicit[1] === "晚上") && hour < 12) hour += 12;
        if (explicit[1] === "中午" && hour < 11) hour += 12;
      }
      const dueIso = `${dueDate.getFullYear()}-${String(dueDate.getMonth() + 1).padStart(2, "0")}-${String(dueDate.getDate()).padStart(2, "0")}`;
      return { dueIso, nextFollow: `${dueIso} ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}` };
    }
    function quoteFollowupPendingItems(sourceText = "") {
      const source = String(sourceText || "");
      const pending = [];
      if (hasAny(source, ["效果图", "产品图", "主图", "做图"])) pending.push("效果图方向");
      if (hasAny(source, ["详情页", "宝贝详情", "详情"])) pending.push("宝贝详情内容");
      if (val("quoteSampleMode") === "待确认") pending.push("是否需要打样");
      if (val("quoteTaxMode") === "待确认") pending.push("税费");
      if (!(Number(normalizeQuoteDeliveryDays(val("quoteDelivery"))) > 0)) pending.push("交期");
      return [...new Set(pending)];
    }
    function ensureQuoteFollowupTask(quoteRecord = null, showNotice = false) {
      const product = val("quoteProduct") || "定制产品";
      const qty = val("quoteQty") || "数量待确认";
      const customer = currentCustomerName();
      const customerLabel = `${customer} · ${product}`;
      const sourceText = val("quoteAiSource") || val("customerText") || window.__latestAiSourceText || "";
      const { dueIso, nextFollow } = quoteFollowupSchedule(sourceText);
      const pendingItems = quoteFollowupPendingItems(sourceText);
      const context = quoteBusinessContext();
      const existing = (state.tasks || []).find(task =>
        !["已完成", "已归档"].includes(task.status) &&
        (
          (Number(context.taskServerId || 0) > 0 && Number(task.serverId || 0) === Number(context.taskServerId))
          || (context.taskId && task.id === context.taskId)
          || (context.caseSource && task.source === context.caseSource)
          || ((task.source === "quote_auto" || task.title === `${product} 报价后跟进`) && String(task.customer || "").includes(customer))
        )
      );
      const taskData = {
        customer: customerLabel,
        title: `${product} 报价后跟进`,
        owner: "业务",
        due: dueIso,
        nextFollow,
        status: "已报价",
        nextAction: `把报价发给客户${pendingItems.length ? `，并确认${pendingItems.join("、")}` : "，等待客户反馈"}`,
        amount: Number(quoteRecord?.total || (state.quoteHistory || [])[0]?.total || 0),
        specification: `${val("quoteSize") || "尺寸待确认"}，${qty}个，${val("quoteMaterial") || "材质待确认"}，${val("quoteProcess") || "工艺待确认"}`,
        risk: pendingItems.length ? `待确认：${pendingItems.join("、")}` : "无明显风险",
        source: existing?.source || context.caseSource || "quote_auto",
        quoteId: Number(quoteRecord?.serverId || existing?.quoteId || 0) || null,
        industryCategory: quoteRecord?.industryCategory || currentIndustryCategory(),
        industryStrategyId: quoteRecord?.industryStrategyId || quoteRecord?.formulaSnapshot?.strategyId || "",
        updatedAt: new Date().toISOString()
      };
      const stamp = new Date().toLocaleString("zh-CN", { hour12: false });
      if (existing) {
        const downstream = ["待确认", "打样", "生产", "待质检", "待发货", "已发货", "已签收", "待回款", "已回款", "已完成"].includes(existing.status);
        if (downstream) {
          taskData.status = existing.status;
          taskData.owner = existing.owner;
          taskData.nextAction = existing.nextAction;
          taskData.nextFollow = existing.nextFollow;
          taskData.due = existing.due;
        }
        Object.assign(existing, taskData);
        existing.logs = [...(existing.logs || []), `${stamp} 报价更新，${downstream ? "保留当前业务阶段并刷新金额" : "已刷新跟进金额和下一步"}`].slice(-20);
      } else {
        state.tasks.unshift({
          id: newTaskId(),
          localCompanyId: currentTaskRecycleCompanyKey(),
          createdAt: new Date().toISOString(),
          logs: [`${stamp} 报价生成后自动创建跟进任务`],
          ...taskData
        });
      }
      const savedTask = existing || state.tasks[0];
      if (quoteRecord && savedTask) {
        quoteRecord.followupTaskId = Number(savedTask.serverId || quoteRecord.followupTaskId || 0);
        quoteRecord.caseSource = savedTask.source || quoteRecord.caseSource || context.caseSource || "";
      }
      if (savedTask) {
        context.taskId = savedTask.id || context.taskId || "";
        context.taskServerId = Number(savedTask.serverId || context.taskServerId || 0);
        context.caseSource = savedTask.source || context.caseSource || "";
      }
      if (showNotice) {
        saveState();
        renderTasks();
        renderDashboard();
        notify("报价跟进任务已更新，可以在“跟单”中查看。" );
      }
      return savedTask;
    }
    function createOrderFromQuote() {
      const record = activeQuoteRecord() || (state.quoteHistory || [])[0];
      const context = quoteBusinessContext();
      let task = (state.tasks || []).find(item =>
        (Number(record?.followupTaskId || context.taskServerId || 0) > 0 && Number(item.serverId || 0) === Number(record?.followupTaskId || context.taskServerId))
        || (record?.caseSource && item.source === record.caseSource)
        || (String(item.customer || "").includes(record?.customer || currentCustomerName()) && (item.source === "quote_auto" || String(item.title || "").includes(record?.product || val("quoteProduct"))))
      );
      if (!task) task = ensureQuoteFollowupTask(record, true);
      taskSearch = "";
      const search = document.getElementById("taskSearch");
      if (search) search.value = "";
      taskFilter = taskFilterForStatus(task?.status);
      if (task) openTaskDetail(task.id);
      else {
        showPage("跟单工单");
        renderTasks();
      }
      notify("已定位到这笔报价的当前跟单，不会退回旧阶段。" );
    }
    function returnToQuoteCustomer() {
      const context = quoteBusinessContext();
      const customer = String(context.customer || activeQuoteRecord()?.customer || val("quoteCustomerName") || "").trim();
      const sessions = ensureCustomerSessions();
      const target = sessions.find(item => item.id === context.sessionId)
        || sessions.find(item => Number(item.followupTaskId || 0) > 0 && Number(item.followupTaskId) === Number(context.taskServerId || 0))
        || [...sessions].reverse().find(item => item.customer === customer);
      if (target) state.activeCustomerSessionId = target.id;
      saveState();
      if (quoteWorkspaceIsInline()) closeHomeInlineQuoteWorkspace({ silent: true });
      showPage("图片制作");
      document.getElementById("aiQuestion")?.focus();
    }
    function customerFreightModeLabel(value) {
      const normalized = String(value || "").trim();
      if (!normalized || normalized === "不含运") return "不含运费（运费另计）";
      return normalized;
    }
    function quoteRecordMatchesCurrentDraft(record) {
      if (!record) return false;
      const draft = {
        customer: currentCustomerName(), product: val("quoteProduct"), size: val("quoteSize"),
        qty: val("quoteQty"), material: val("quoteMaterial"), process: val("quoteProcess")
      };
      if ([draft.product, draft.size, draft.qty, draft.material, draft.process].some(value => !String(value || "").trim() || /待确认|当前规格|当前材料|定制产品/.test(String(value)))) return false;
      return normalizeRuleText(record.customer) === normalizeRuleText(draft.customer)
        && quoteProductsEquivalent(record.product, draft.product)
        && quoteSizesEquivalent(record.size, draft.size)
        && Number(record.qty || 0) === Number(draft.qty || 0)
        && normalizeRuleText(record.material) === normalizeRuleText(draft.material)
        && normalizedQuoteValue(record.process) === normalizedQuoteValue(draft.process);
    }
    function currentDraftQuoteRecord() {
      const active = activeQuoteRecord();
      if (quoteRecordMatchesCurrentDraft(active)) return active;
      return (state.quoteHistory || []).find(quoteRecordMatchesCurrentDraft) || null;
    }
    function quoteInfo() {
      const matchingQuote = currentDraftQuoteRecord();
      return {
        customer: currentCustomerName(),
        product: val("quoteProduct") || "定制产品",
        size: val("quoteSize") || "尺寸待确认",
        qty: val("quoteQty") || "数量待确认",
        material: val("quoteMaterial") || "材料待确认",
        process: val("quoteProcess") || "工艺待确认",
        taxMode: val("quoteTaxMode") || "含税",
        freightMode: "不含运",
        sampleMode: val("quoteSampleMode") || "待确认",
        sampleFee: val("quoteSample") || "0",
        delivery: quoteDeliveryText(val("quoteDelivery") || state.settings.delivery),
        payment: val("quotePayment") || state.settings.payment || "付款方式待确认",
        quoteNo: matchingQuote?.quoteNo || "",
        hasFormalQuote: Boolean(matchingQuote),
        total: matchingQuote?.total,
        unit: matchingQuote?.unit
      };
    }
    function generateConfirmationFromQuote(shouldShow = true) {
      const info = quoteInfo();
      const sampleConfirmLine = info.sampleMode === "不需要打样"
        ? "本单已确认不打样；如后续增加打样，需要重新确认费用和交期；"
        : info.sampleMode === "收费打样"
          ? `已确认收费打样，打样费 ¥${info.sampleFee}，请确认样品数量、寄样地址和签样标准；`
          : info.sampleMode === "免费打样"
            ? "已确认免费打样，请确认样品数量、寄样地址和签样标准；"
            : "请确认是否需要先做AI效果图或实物样品；";
      const quoteLine = info.hasFormalQuote
        ? "报价已生成，可随确认单一起发给客户。"
        : "还未生成正式报价，建议先在报价系统计算一次。";
      const totalLine = info.total
        ? `报价参考：总价 ¥${Number(info.total).toFixed(2)}，单价约 ¥${Number(info.unit || 0).toFixed(3)} / 个`
        : "报价参考：以报价系统最终生成为准";
      const text = `客户确认单：

客户：${info.customer}
产品：${info.product}
规格：${info.size}
数量：${info.qty}
材质：${info.material}
工艺：${info.process}
报价口径：${info.taxMode}，${customerFreightModeLabel(info.freightMode)}
打样：${info.sampleMode}${info.sampleMode === "收费打样" ? `，费用 ¥${info.sampleFee}` : ""}
交期：${info.delivery}
付款方式：${info.payment}
${totalLine}

请客户确认：
1. 规格、材质、工艺、数量、交期和报价口径是否正确；
2. ${sampleConfirmLine}
3. Logo、颜色、图纸、位置、包装方式和收货信息是否已最终确认；
4. 确认后再安排打样或大货生产。

风险说明：
- 效果图仅用于确认方向，最终颜色、材质、工艺、结构以样品或实物为准；
- 不同批次材料、印刷颜色、手工/机器加工会存在合理公差；
- 特殊工艺、深色满版、烫金/UV/压凹凸等建议先打样签样；
- 税费、交期、付款以双方确认单为准；运费不包含在报价内。

内部提醒：${quoteLine}`;
      setTextIfExists("confirmOutput", text);
      if (shouldShow) showPage("确认打样");
    }
    function copyConfirmOutput() {
      const output = document.getElementById("confirmOutput")?.textContent || "";
      if (!output || output.includes("从报价系统生成")) {
        alert("请先生成客户确认单。");
        return;
      }
      navigator.clipboard?.writeText(output);
      alert("客户确认单已复制。");
    }
    function fillSampleFromQuote(showNotice = true) {
      const info = quoteInfo();
      setIfExists("sampleProduct", info.product);
      setIfExists("sampleFee", info.sampleFee);
      setIfExists("sampleNote", `样品要求：${info.product}，${info.size}，${info.material}，${info.process}。
确认重点：颜色、尺寸、公差、材质手感、工艺位置、包装方式、寄样地址。
验收标准：客户确认样品后再安排大货生产。`);
      if (showNotice) {
        setTextIfExists("sampleOutput", "已从当前报价带入打样信息，可继续补充寄样地址、样品数量和客户修改意见。");
      }
    }
    function createSampleTaskFromQuote(shouldShow = true) {
      const info = quoteInfo();
      if (["待确认", "需要打样，费用待确认"].includes(info.sampleMode)) {
        notify("请先回到报价页确认是收费打样还是免费打样，再创建打样任务。");
        if (shouldShow) showPage("报价系统");
        return false;
      }
      if (info.sampleMode === "不需要打样") {
        notify("当前报价已选择“不需要打样”，无需创建打样任务。");
        return false;
      }
      fillSampleFromQuote(false);
      const task = {
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: `${info.customer} · ${info.product}`,
        title: `${info.product} 打样确认`,
        owner: "业务/生产",
        createdAt: new Date().toISOString(),
        due: "今天",
        nextFollow: "明天上午",
        status: "打样",
        nextAction: "确认打样费、样品要求、寄样地址和客户签样标准",
        amount: Number(info.sampleFee) || 0,
        risk: "打样前需确认颜色、尺寸、公差、材质、工艺位置和效果图仅供方向",
        logs: [new Date().toLocaleString("zh-CN", { hour12: false }) + " 从报价生成打样任务"]
      };
      state.tasks.unshift(task);
      appendCustomerHistory(info.customer, `生成打样任务：${info.product}，打样费 ¥${info.sampleFee}`, {
        dealStatus: "打样",
        need: info.product,
        preference: `${info.material}；${info.process}`
      });
      saveState();
      renderTasks();
      renderDashboard();
      setTextIfExists("sampleOutput", `已生成打样任务，并同步到跟单工单：

客户：${info.customer}
产品：${info.product}
打样：${info.sampleMode}${info.sampleMode === "收费打样" ? `，费用 ¥${info.sampleFee}` : ""}
样品状态：${val("sampleStatus") || "待收款"}
下一步：确认打样费、寄样地址、样品数量、客户签样标准和预计寄出时间。`);
      if (shouldShow) showPage("确认打样");
    }
    function generateDisputeTipsFromQuote(shouldShow = true) {
      const info = quoteInfo();
      const text = `售后/争议预防提醒：

客户：${info.customer}
产品：${info.product}
规格：${info.size}
材质/工艺：${info.material}，${info.process}

建议提前写清楚：
1. 效果图只作为方向参考，最终以样品或实物为准；
2. 印刷颜色、材料批次、手工位置、尺寸会存在合理公差；
3. 深色满版、烫金、UV、压凹凸、特殊结构建议先打样签样；
4. 报价是否含税、是否含包装以确认单为准；运费统一另计；
5. 交期从资料、定金、样品确认齐全后开始计算；
6. 收货后请在约定时间内验货，运输破损需保留外箱、面单和照片；
7. 客户临时改图、改规格、改数量，需重新核价和排期。

可发客户：
为避免后续误会，我们这边会按确认单安排。效果图仅供确认方向，最终颜色、材质、工艺和结构以样品或实物为准。批量生产前如果您对颜色、工艺位置或细节要求比较高，建议先打样确认。`;
      setTextIfExists("confirmOutput", text);
      if (shouldShow) showPage("确认打样");
    }
    function industryQuoteCostBreakdown(qty) {
      const strategy = currentQuoteIndustryStrategy();
      const hourlyRates = window.JIANGLAI_QUOTE_COST_DEFAULTS?.resolveIndustryRates({ explicitLaborRate:state.settings.laborHourly, explicitMachineRate:state.settings.machineHourly, items:activeCostLibraryItems(), strategyId:strategy.id }) || { labor:{rate:Number(state.settings.laborHourly || 0)}, machine:{rate:Number(state.settings.machineHourly || 0)} };
      const result = quoteIndustryEngine.calculate(strategy.id, {
        quantity: qty,
        values: currentQuoteIndustryValues(),
        laborHourly: hourlyRates.labor?.rate,
        machineHourly: hourlyRates.machine?.rate,
        defaultUtilization: state.settings.utilizationRate
      });
      if (!result.ok) return result;
      return { ...result, category: currentIndustryCategory(), hourlyRates };
    }
    function showMissingQuoteCosts(materialName, processNames = [], options = {}) {
      const panel = document.getElementById("quoteMissingCostsPanel");
      const list = document.getElementById("quoteMissingCostsList");
      if (!panel || !list) return;
      const entries = [];
      if (materialName) entries.push({ type: "material", name: materialName, label: "单件材料成本" });
      processNames.forEach(item => {
        const requirement = item && typeof item === "object" ? item : { name: item };
        const kind = requirement.kind === "print" ? "print" : "process";
        const name = canonicalQuoteOperationLabel(requirement.name || requirement.label || "");
        if (name) entries.push({ type: "process", kind, name, displayName: requirement.label || name, label: kind === "print" ? "单件印刷费" : "单件工艺费" });
      });
      window.__pendingQuoteCosts = entries;
      list.innerHTML = entries.map((entry, index) => `
        <div class="quote-missing-cost-row">
          <label>${escapeHtml(entry.displayName || entry.name)}<span>${escapeHtml(entry.label)}，单位：元/个${entry.type === "material" ? "" : "；没有费用可填 0"}</span></label>
          <input id="quoteMissingCost${index}" inputmode="decimal" placeholder="例如：0.08" aria-label="${escapeHtml(entry.name)}${escapeHtml(entry.label)}">
        </div>
      `).join("");
      panel.classList.remove("is-hidden");
      const generate = document.getElementById("quoteGuideGenerate");
      if (generate) {
        generate.disabled = true;
        generate.textContent = "请先补齐上方成本";
      }
      const missingNames = entries.map(entry => entry.displayName || entry.name).join("、");
      setQuoteCostWarning(`当前报价内容已保留。请在下方补充 ${missingNames} 的成本，保存后系统会自动继续生成报价。`);
      if (options.focus !== false) {
        window.setTimeout(() => {
          panel.scrollIntoView({ behavior: "smooth", block: "center" });
          document.getElementById("quoteMissingCost0")?.focus();
        }, 80);
      }
    }
    function hideMissingQuoteCosts() {
      document.getElementById("quoteMissingCostsPanel")?.classList.add("is-hidden");
      window.__pendingQuoteCosts = [];
      const generate = document.getElementById("quoteGuideGenerate");
      if (generate) {
        generate.disabled = false;
        generate.textContent = "核对完成，生成报价";
      }
    }
    function mergeQuoteRuleCosts(ruleText, additions) {
      const rules = parseRules(ruleText).map(rule => ({ ...rule }));
      additions.forEach(addition => {
        const normalized = normalizeRuleText(addition.name);
        const existing = rules.find(rule => normalizeRuleText(rule.name) === normalized);
        if (existing) existing.price = addition.price;
        else rules.push({ name: addition.name, price: addition.price });
      });
      return rules.map(rule => `${rule.name}=${Number(rule.price).toFixed(3).replace(/0+$/, "").replace(/\.$/, "")}`).join("\n");
    }
    const costLibraryTypeLabels = Object.freeze({
      material: "材料",
      process: "人工/工艺",
      plate: "版费",
      labor: "人工",
      equipment: "设备",
      loss: "损耗",
      other: "其他"
    });
    const costLibraryFieldLimits = Object.freeze({
      id: 80,
      category: 80,
      name: 120,
      spec: 160,
      unit: 30,
      basis: 40,
      effectiveDate: 40,
      supplier: 120,
      note: 500,
      sourceFile: 180
    });
    const costLibrarySafeLegacyUnits = new Set(["个", "件", "只", "套", "张"]);
    function newCostLibraryItemId() {
      if (window.crypto?.randomUUID) return window.crypto.randomUUID();
      return `cost-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
    }
    function newCostLibraryVersionId(now = new Date()) {
      const stamp = now.toISOString().replace(/[-:TZ.]/g, "").slice(0, 17);
      const entropy = window.crypto?.randomUUID
        ? window.crypto.randomUUID().replace(/-/g, "").slice(0, 8)
        : Math.random().toString(36).slice(2, 10);
      return `CB-${stamp}-${entropy}`;
    }
    function localCostDate(value = new Date()) {
      const date = value instanceof Date ? value : new Date(value);
      if (Number.isNaN(date.getTime())) return "";
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, "0");
      const day = String(date.getDate()).padStart(2, "0");
      return `${year}-${month}-${day}`;
    }
    function normalizedCostNumber(value) {
      if (value === null || value === undefined || String(value).trim() === "") return null;
      const parsed = Number(value);
      return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
    }
    function normalizeCostLibraryItem(item, index = 0) {
      const source = item && typeof item === "object" && !Array.isArray(item) ? item : {};
      const type = Object.prototype.hasOwnProperty.call(costLibraryTypeLabels, source.type) ? source.type : "other";
      const price = normalizedCostNumber(source.price);
      const name = String(source.name || "").slice(0, costLibraryFieldLimits.name).trim();
      const unit = String(source.unit || "").slice(0, costLibraryFieldLimits.unit).trim();
      const incomplete = !name || !unit || price === null;
      const rawId = String(source.id || `import-${index + 1}-${newCostLibraryItemId()}`);
      const safeId = rawId.replace(/[^A-Za-z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, costLibraryFieldLimits.id) || newCostLibraryItemId();
      return {
        ...source,
        id: safeId,
        type,
        category: String(source.category || "未分类").slice(0, costLibraryFieldLimits.category).trim() || "未分类",
        name,
        spec: String(source.spec || "").slice(0, costLibraryFieldLimits.spec).trim(),
        unit,
        price,
        maxPrice: normalizedCostNumber(source.maxPrice),
        setupFee: normalizedCostNumber(source.setupFee),
        basis: String(source.basis || "").slice(0, costLibraryFieldLimits.basis).trim(),
        effectiveDate: String(source.effectiveDate || "").slice(0, costLibraryFieldLimits.effectiveDate).trim(),
        supplier: String(source.supplier || "").slice(0, costLibraryFieldLimits.supplier).trim(),
        note: String(source.note || source.description || "").slice(0, costLibraryFieldLimits.note).trim(),
        active: source.active !== false && !incomplete,
        incomplete
      };
    }
    function normalizeCostLibrary(value) {
      const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
      const normalizeItems = items => (Array.isArray(items) ? items : []).slice(0, 500).map(normalizeCostLibraryItem);
      const activeVersionId = String(source.activeVersionId || "").slice(0, 100);
      const normalizedStatus = ["empty", "draft", "active", "scheduled"].includes(source.status) ? source.status : "empty";
      const rawSourceFile = String(source.sourceFile || "").slice(0, costLibraryFieldLimits.sourceFile);
      const draftSourceFile = String(source.draftSourceFile || (normalizedStatus === "draft" ? rawSourceFile : "")).slice(0, costLibraryFieldLimits.sourceFile);
      const activeSourceFile = normalizedStatus === "draft" && !activeVersionId ? "" : rawSourceFile;
      const versions = (Array.isArray(source.versions) ? source.versions : []).slice(0, 5).map(version => ({
        versionId: String(version?.versionId || "").slice(0, 100),
        effectiveAt: String(version?.effectiveAt || "").slice(0, 30),
        createdAt: String(version?.createdAt || "").slice(0, 40),
        sourceFile: String(version?.sourceFile || "").slice(0, costLibraryFieldLimits.sourceFile),
        status: ["active", "scheduled", "archived"].includes(version?.status)
          ? version.status
          : String(version?.versionId || "") === activeVersionId
            ? "active"
            : String(version?.effectiveAt || "").slice(0, 10) > localCostDate()
              ? "scheduled"
              : "archived",
        itemCount: Number(version?.itemCount || 0),
        items: normalizeItems(version?.items)
      })).filter(version => version.versionId);
      return {
        schemaVersion: 1,
        activeVersionId,
        effectiveAt: String(source.effectiveAt || "").slice(0, 30),
        updatedAt: String(source.updatedAt || "").slice(0, 40),
        sourceFile: activeSourceFile,
        draftSourceFile,
        status: normalizedStatus,
        items: normalizeItems(source.items),
        draftItems: normalizeItems(source.draftItems),
        draftLegacyMaterials: Array.isArray(source.draftLegacyMaterials) ? source.draftLegacyMaterials.slice(0, 200) : [],
        draftLegacyProcesses: Array.isArray(source.draftLegacyProcesses) ? source.draftLegacyProcesses.slice(0, 200) : [],
        versions
      };
    }
    function latestScheduledCostVersion(library) {
      return (library.versions || [])
        .filter(version => version.status === "scheduled")
        .sort((left, right) => String(left.effectiveAt || "").localeCompare(String(right.effectiveAt || "")) || String(left.createdAt || "").localeCompare(String(right.createdAt || "")))
        .at(-1) || null;
    }
    function promoteDueCostLibraryVersion(library, today = localCostDate()) {
      const previousItems = (library.items || []).map(item => ({ ...item }));
      const versions = library.versions || [];
      const current = versions.find(version => version.versionId === library.activeVersionId);
      if (current && String(current.effectiveAt || library.effectiveAt || "").slice(0, 10) > today) {
        current.status = "scheduled";
        library.activeVersionId = "";
        library.items = [];
        library.effectiveAt = "";
        library.sourceFile = "";
      }
      const due = versions
        .filter(version => String(version.effectiveAt || "").slice(0, 10) && String(version.effectiveAt).slice(0, 10) <= today)
        .sort((left, right) => String(left.effectiveAt || "").localeCompare(String(right.effectiveAt || "")) || String(left.createdAt || "").localeCompare(String(right.createdAt || "")));
      const selected = due.at(-1) || null;
      const shouldPromote = selected && selected.versionId !== library.activeVersionId && selected.status === "scheduled";
      if (shouldPromote || (!library.activeVersionId && selected)) {
        versions.forEach(version => {
          if (version.versionId === selected.versionId) version.status = "active";
          else if (version.status === "active" || (version.status === "scheduled" && String(version.effectiveAt || "").slice(0, 10) <= today)) version.status = "archived";
        });
        library.activeVersionId = selected.versionId;
        library.items = selected.items.map(item => ({ ...item }));
        library.effectiveAt = selected.effectiveAt;
        library.sourceFile = selected.sourceFile || "";
        library.updatedAt = new Date().toISOString();
      }
      const changed = JSON.stringify(previousItems) !== JSON.stringify(library.items || []) || String(current?.versionId || "") !== String(library.activeVersionId || "");
      if (library.draftItems?.length) library.status = "draft";
      else if (library.activeVersionId) library.status = "active";
      else if (latestScheduledCostVersion(library)) library.status = "scheduled";
      else library.status = "empty";
      return { changed, previousItems, nextItems: (library.items || []).map(item => ({ ...item })) };
    }
    function projectedLegacyCostRules(items, allowedTypes) {
      return (Array.isArray(items) ? items : []).filter(item => (
        allowedTypes.has(item.type)
        && Number(item.price) > 0
        && costLibrarySafeLegacyUnits.has(String(item.unit || "").trim())
      )).map(item => ({
        name: String(item.spec ? `${item.name}（${item.spec}）` : item.name || "").slice(0, 160),
        price: Number(item.price)
      })).filter(item => item.name);
    }
    function replaceProjectedLegacyRules(existingText, previousItems, nextItems, allowedTypes) {
      const previousNames = new Set(projectedLegacyCostRules(previousItems, allowedTypes).map(item => normalizeRuleText(item.name)));
      const retainedText = parseRules(existingText || "")
        .filter(item => !previousNames.has(normalizeRuleText(item.name)))
        .map(item => `${item.name}=${item.price}`)
        .join("\n");
      return mergeQuoteRuleCosts(retainedText, projectedLegacyCostRules(nextItems, allowedTypes));
    }
    function projectCostLibraryLegacySettings(settings, previousItems, nextItems) {
      settings.materialRules = replaceProjectedLegacyRules(settings.materialRules, previousItems, nextItems, new Set(["material"]));
      settings.processRules = replaceProjectedLegacyRules(settings.processRules, previousItems, nextItems, new Set(["process", "plate"]));
      const labor = (Array.isArray(nextItems) ? nextItems : []).find(item => item.type === "labor" && Number(item.price) > 0);
      if (labor) settings.laborHourly = Number(labor.price);
    }
    function ensureCostLibrary() {
      const normalized = normalizeCostLibrary(state.settings.costLibrary);
      const promotion = promoteDueCostLibraryVersion(normalized);
      state.settings.costLibrary = normalized;
      if (promotion.changed) {
        projectCostLibraryLegacySettings(state.settings, promotion.previousItems, promotion.nextItems);
        state.settings.costRulesConfirmedAt = normalized.updatedAt || new Date().toISOString();
        state.settings.quoteRulesUpdatedAt = normalized.updatedAt || new Date().toISOString();
        saveState();
        if (window.location.protocol !== "file:" && window.__formalSettingsReady && canEditCostLibrary()) {
          window.setTimeout(() => window.__saveSettingsCloud?.(state.settings), 0);
        }
      }
      return normalized;
    }
    function canEditCostLibrary() {
      if (window.location.protocol === "file:") return true;
      return Boolean(window.__currentUser && window.__hasPermission?.("price.edit"));
    }
    function requireCostLibraryEditPermission() {
      if (canEditCostLibrary()) return true;
      notify("当前账号只有查看权限，请联系老板分配“修改价格表”权限。");
      return false;
    }
    function activeCostLibraryItems() {
      const library = ensureCostLibrary();
      if (!library.activeVersionId || String(library.effectiveAt || "").slice(0, 10) > localCostDate()) return [];
      return library.items.filter(item => item.active !== false && item.price !== null && Number(item.price) >= 0);
    }
    function editableCostLibraryItems() {
      const library = ensureCostLibrary();
      if (library.status === "draft") return library.draftItems;
      if (!library.activeVersionId && library.status === "scheduled") return latestScheduledCostVersion(library)?.items || [];
      return library.items;
    }
    function beginCostLibraryDraft() {
      const library = ensureCostLibrary();
      if (library.status !== "draft") {
        const baseItems = !library.activeVersionId && library.status === "scheduled"
          ? (latestScheduledCostVersion(library)?.items || [])
          : library.items;
        library.draftItems = baseItems.map(item => ({ ...item }));
        library.draftSourceFile = library.draftSourceFile || (!library.activeVersionId && library.status === "scheduled"
          ? (latestScheduledCostVersion(library)?.sourceFile || "")
          : library.sourceFile);
        library.status = "draft";
      }
      return library;
    }
    function updateCostLibrarySummary() {
      const library = ensureCostLibrary();
      const items = editableCostLibraryItems();
      const incomplete = items.filter(item => item.incomplete || item.price === null).length;
      const referenceCount = items.filter(quoteReferenceItem).length;
      const badges = document.getElementById("costLibraryBadges");
      if (badges) badges.innerHTML = [
        `<span>${items.length} 项成本</span>`,
        referenceCount ? `<span class="warning">${referenceCount} 项仍为行业参考价</span>` : `<span>已全部替换为本厂价</span>`,
        `<span>${library.activeVersionId ? `当前 ${escapeHtml(library.activeVersionId)}` : "尚未发布"}</span>`,
        library.status === "draft" ? `<span class="warning">有未发布修改</span>` : "",
        latestScheduledCostVersion(library) ? `<span class="warning">已排期 ${escapeHtml(latestScheduledCostVersion(library).effectiveAt || "待定")}生效</span>` : "",
        incomplete ? `<span class="warning">${incomplete} 项待补价格</span>` : ""
      ].join("");
      const status = document.getElementById("costLibraryStatus");
      if (status) {
        const visibleSourceFile = library.status === "draft" ? library.draftSourceFile : library.sourceFile;
        const source = visibleSourceFile ? `来源：${visibleSourceFile}。` : "";
        const scheduled = latestScheduledCostVersion(library);
        const stateText = library.status === "draft"
          ? "当前修改仍是草稿，尚未用于新报价。"
          : library.activeVersionId
            ? `当前启用版本 ${library.activeVersionId}，生效日 ${library.effectiveAt || "未填写"}。${scheduled ? ` 已排期版本 ${scheduled.versionId} 将于 ${scheduled.effectiveAt} 生效。` : ""}`
            : scheduled
              ? `版本 ${scheduled.versionId} 已保存，将于 ${scheduled.effectiveAt} 生效；在此之前不会参与报价。`
              : "尚未发布成本版本。";
        status.textContent = `${source}${stateText}${referenceCount ? ` 其中 ${referenceCount} 项为内置行业参考价，可用于首次估算；用到时请按本厂当日价格修改。` : " 当前项目已是本厂价格。"}${incomplete ? ` 另有 ${incomplete} 项价格待补充。` : ""}`;
      }
    }
    function renderCostLibrary() {
      const card = document.getElementById("costLibraryCard");
      if (!card) return;
      const formalAccessReady = window.location.protocol === "file:" || (
        window.__formalSettingsReady === true
        && window.__currentUser
        && window.__hasPermission?.("price.view")
      );
      if (!formalAccessReady) {
        card.classList.add("is-hidden");
        const body = document.getElementById("costLibraryTableBody");
        if (body) body.innerHTML = "";
        return;
      }
      const library = ensureCostLibrary();
      const items = editableCostLibraryItems();
      const canEdit = canEditCostLibrary();
      const importInput = document.getElementById("priceTableFile");
      if (importInput) importInput.disabled = !canEdit;
      const importLabel = document.querySelector('label[for="priceTableFile"]');
      if (importLabel) {
        importLabel.setAttribute("aria-disabled", canEdit ? "false" : "true");
        importLabel.style.opacity = canEdit ? "" : ".55";
        importLabel.style.cursor = canEdit ? "" : "not-allowed";
      }
      const shouldShow = Boolean(items.length || library.items.length || ["import", "manual"].includes(priceSetupMethod));
      card.classList.toggle("is-hidden", !shouldShow);
      if (!shouldShow) return;
      const search = (document.getElementById("costLibrarySearch")?.value || "").trim().toLowerCase();
      const typeSelect = document.getElementById("costLibraryTypeFilter");
      const categorySelect = document.getElementById("costLibraryCategoryFilter");
      const currentType = typeSelect?.value || "";
      const currentCategory = categorySelect?.value || "";
      const availableTypes = [...new Set(items.map(item => item.type))].sort();
      const availableCategories = [...new Set(items.map(item => item.category).filter(Boolean))].sort((a, b) => a.localeCompare(b, "zh-CN"));
      if (typeSelect) {
        typeSelect.innerHTML = `<option value="">全部费用类型</option>${availableTypes.map(type => `<option value="${escapeHtml(type)}">${escapeHtml(costLibraryTypeLabels[type] || type)}</option>`).join("")}`;
        typeSelect.value = availableTypes.includes(currentType) ? currentType : "";
      }
      if (categorySelect) {
        categorySelect.innerHTML = `<option value="">全部分类</option>${availableCategories.map(category => `<option value="${escapeHtml(category)}">${escapeHtml(category)}</option>`).join("")}`;
        categorySelect.value = availableCategories.includes(currentCategory) ? currentCategory : "";
      }
      const filtered = items.map((item, index) => ({ item, index })).filter(({ item }) => {
        if (typeSelect?.value && item.type !== typeSelect.value) return false;
        if (categorySelect?.value && item.category !== categorySelect.value) return false;
        const haystack = `${item.name} ${item.spec} ${item.category} ${item.supplier} ${item.note}`.toLowerCase();
        return !search || haystack.includes(search);
      });
      const disabled = canEdit ? "" : " disabled";
      const body = document.getElementById("costLibraryTableBody");
      if (body) body.innerHTML = filtered.map(({ item, index }) => `
        <tr class="${item.incomplete || item.price === null ? "incomplete" : ""}">
          <td class="col-type"><select data-cost-index="${index}" data-cost-field="type"${disabled}>${Object.entries(costLibraryTypeLabels).map(([value, label]) => `<option value="${value}" ${value === item.type ? "selected" : ""}>${label}</option>`).join("")}</select></td>
          <td class="col-category"><input data-cost-index="${index}" data-cost-field="category" maxlength="${costLibraryFieldLimits.category}" value="${escapeHtml(item.category)}"${disabled}></td>
          <td class="col-name"><input data-cost-index="${index}" data-cost-field="name" maxlength="${costLibraryFieldLimits.name}" value="${escapeHtml(item.name)}" placeholder="必填"${disabled}></td>
          <td class="col-spec"><input data-cost-index="${index}" data-cost-field="spec" maxlength="${costLibraryFieldLimits.spec}" value="${escapeHtml(item.spec)}"${disabled}></td>
          <td class="col-unit"><input data-cost-index="${index}" data-cost-field="unit" maxlength="${costLibraryFieldLimits.unit}" value="${escapeHtml(item.unit)}" placeholder="元/吨、个…"${disabled}></td>
          <td class="col-number"><input data-cost-index="${index}" data-cost-field="price" type="number" min="0" step="0.000001" value="${item.price === null ? "" : escapeHtml(item.price)}" placeholder="待补"${disabled}></td>
          <td class="col-number"><input data-cost-index="${index}" data-cost-field="maxPrice" type="number" min="0" step="0.000001" value="${item.maxPrice === null ? "" : escapeHtml(item.maxPrice)}"${disabled}></td>
          <td class="col-number"><input data-cost-index="${index}" data-cost-field="setupFee" type="number" min="0" step="0.000001" value="${item.setupFee === null ? "" : escapeHtml(item.setupFee)}"${disabled}></td>
          <td class="col-date"><input data-cost-index="${index}" data-cost-field="effectiveDate" type="date" value="${escapeHtml(item.effectiveDate)}"${disabled}></td>
          <td class="col-note"><input data-cost-index="${index}" data-cost-field="supplier" maxlength="${costLibraryFieldLimits.supplier}" value="${escapeHtml(item.supplier)}" placeholder="供应商"${disabled}><input data-cost-index="${index}" data-cost-field="note" maxlength="${costLibraryFieldLimits.note}" value="${escapeHtml(item.note)}" placeholder="说明/备注" style="margin-top:4px"${disabled}></td>
          <td><button class="cost-library-remove" type="button" data-cost-remove="${index}" aria-label="删除${escapeHtml(item.name || "该成本项")}"${disabled}>×</button></td>
        </tr>
      `).join("") || `<tr><td colspan="11" class="muted" style="padding:22px;text-align:center">没有符合筛选条件的成本项。</td></tr>`;
      body?.querySelectorAll("[data-cost-field]").forEach(input => {
        input.addEventListener("input", event => {
          const index = Number(event.currentTarget.dataset.costIndex);
          const field = event.currentTarget.dataset.costField || "";
          const draft = beginCostLibraryDraft();
          const target = draft.draftItems[index];
          if (!target) return;
          const wasReference = quoteReferenceItem(target);
          if (["price", "maxPrice", "setupFee"].includes(field)) target[field] = normalizedCostNumber(event.currentTarget.value);
          else target[field] = String(event.currentTarget.value || "").slice(0, costLibraryFieldLimits[field] || 160);
          if (wasReference && !["supplier", "note"].includes(field)) {
            target.supplier = "本厂已修改";
            target.note = `由内置行业参考价改为本厂价，修改时间：${new Date().toLocaleString("zh-CN", { hour12:false })}`;
            target.effectiveDate = localCostDate();
          }
          target.incomplete = !target.name?.trim() || !target.unit?.trim() || target.price === null;
          target.active = !target.incomplete;
          draft.updatedAt = new Date().toISOString();
          saveState();
          updateCostLibrarySummary();
          event.currentTarget.closest("tr")?.classList.toggle("incomplete", target.incomplete);
        });
      });
      body?.querySelectorAll("[data-cost-remove]").forEach(button => button.addEventListener("click", () => removeCostLibraryItem(Number(button.dataset.costRemove))));
      const effective = document.getElementById("costLibraryEffectiveAt");
      if (effective && !effective.value) effective.value = (latestScheduledCostVersion(library)?.effectiveAt || library.effectiveAt || localCostDate()).slice(0, 10);
      if (effective) effective.disabled = !canEdit;
      document.querySelectorAll(".cost-library-add-button").forEach(button => { button.disabled = !canEdit; });
      ["costLibraryDiscardButton", "costLibraryPublishButton"].forEach(id => {
        const button = document.getElementById(id);
        if (button) button.disabled = !canEdit;
      });
      updateCostLibrarySummary();
    }
    function addCostLibraryItem(requestedType = "material") {
      if (!requireCostLibraryEditPermission()) return;
      const type = ["material", "process", "labor"].includes(requestedType) ? requestedType : "material";
      const defaults = {
        material: { category: "自定义材料", unit: "元/个" },
        process: { category: "自定义工艺", unit: "元/个" },
        labor: { category: "人工工资", unit: "元/小时" }
      };
      const library = beginCostLibraryDraft();
      library.draftItems.push(normalizeCostLibraryItem({ id: newCostLibraryItemId(), type, category: defaults[type].category, name: "", spec: "", unit: defaults[type].unit, price: null }, library.draftItems.length));
      saveState();
      renderCostLibrary();
      document.getElementById("costLibraryTableBody")?.lastElementChild?.querySelector('[data-cost-field="name"]')?.focus();
    }
    function removeCostLibraryItem(index) {
      if (!requireCostLibraryEditPermission()) return;
      const library = beginCostLibraryDraft();
      if (!library.draftItems[index]) return;
      library.draftItems.splice(index, 1);
      library.updatedAt = new Date().toISOString();
      saveState();
      renderCostLibrary();
    }
    function discardCostLibraryDraft() {
      if (!requireCostLibraryEditPermission()) return;
      const library = ensureCostLibrary();
      library.draftItems = [];
      library.draftLegacyMaterials = [];
      library.draftLegacyProcesses = [];
      library.draftSourceFile = "";
      library.status = library.activeVersionId ? "active" : latestScheduledCostVersion(library) ? "scheduled" : "empty";
      saveState();
      renderCostLibrary();
      notify(library.activeVersionId ? "已恢复当前已发布成本版本。" : "已清空未发布的成本草稿。");
    }
    async function saveCostLibraryVersion() {
      if (!requireCostLibraryEditPermission()) return;
      const currentLibrary = ensureCostLibrary();
      const sourceItems = currentLibrary.status === "draft" ? currentLibrary.draftItems : currentLibrary.items;
      if (!sourceItems.length) return notify("请先导入价格表或新增至少一项成本。");
      const normalizedItems = sourceItems.map(normalizeCostLibraryItem);
      const missingNameOrUnit = normalizedItems.find(item => !item.name || !item.unit);
      if (missingNameOrUnit) return notify("发布前请为每一项填写项目名称和计价单位。");
      if (normalizedItems.some(item => !(Number(item.price) > 0))) {
        return notify("发布前请补齐每一项价格；未填写价格的项目不能发布。");
      }
      const availableTypes = new Set(normalizedItems.map(item => item.type));
      const missingTypes = [["material", "材料"], ["process", "工艺"], ["labor", "人工工资"]]
        .filter(([type]) => !availableTypes.has(type)).map(([, label]) => label);
      if (missingTypes.length) return notify(`发布前还要添加并确认：${missingTypes.join("、")}。`);
      const nowIso = new Date().toISOString();
      const effectiveAt = document.getElementById("costLibraryEffectiveAt")?.value || localCostDate();
      const activatesNow = effectiveAt <= localCostDate();
      normalizedItems.forEach(item => {
        item.incomplete = item.price === null;
        item.active = !item.incomplete;
      });
      const versionId = newCostLibraryVersionId(new Date(nowIso));
      const snapshotItems = normalizedItems.map(item => ({ ...item }));
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      const proposedLibrary = normalizeCostLibrary(proposedSettings.costLibrary);
      const previousActiveItems = proposedLibrary.items.map(item => ({ ...item }));
      const versionSourceFile = (proposedLibrary.draftSourceFile || proposedLibrary.sourceFile || "").slice(0, costLibraryFieldLimits.sourceFile);
      const version = {
        versionId,
        effectiveAt,
        createdAt: nowIso,
        sourceFile: versionSourceFile,
        status: activatesNow ? "active" : "scheduled",
        itemCount: snapshotItems.length,
        items: snapshotItems.map(item => ({ ...item }))
      };
      const previousVersions = proposedLibrary.versions.map(item => ({
        ...item,
        items: item.items.map(cost => ({ ...cost })),
        status: activatesNow && item.status === "active" ? "archived" : item.status
      }));
      proposedLibrary.versions = [version, ...previousVersions.filter(item => item.versionId !== versionId)].slice(0, 5);
      proposedLibrary.draftItems = [];
      proposedLibrary.draftLegacyMaterials = [];
      proposedLibrary.draftLegacyProcesses = [];
      proposedLibrary.draftSourceFile = "";
      proposedLibrary.updatedAt = nowIso;
      if (activatesNow) {
        proposedLibrary.items = snapshotItems;
        proposedLibrary.activeVersionId = versionId;
        proposedLibrary.effectiveAt = effectiveAt;
        proposedLibrary.sourceFile = versionSourceFile;
        proposedLibrary.status = "active";
        projectCostLibraryLegacySettings(proposedSettings, previousActiveItems, snapshotItems);
        proposedSettings.configured = true;
        proposedSettings.costRulesConfirmedAt = nowIso;
        proposedSettings.quoteRulesUpdatedAt = nowIso;
      } else {
        proposedLibrary.status = proposedLibrary.activeVersionId ? "active" : "scheduled";
      }
      proposedSettings.costLibrary = proposedLibrary;
      const proposedHistory = [{
        time: new Date(nowIso).toLocaleString("zh-CN", { hour12: false }),
        createdAt: nowIso,
        category: "结构化成本库",
        changes: [`${activatesNow ? "发布" : "排期"} ${versionId}：${snapshotItems.length} 项，${snapshotItems.filter(item => item.incomplete).length} 项待补价格，生效日 ${effectiveAt}`]
      }, ...(state.quoteRuleHistory || [])].slice(0, 20);
      const publishButton = document.getElementById("costLibraryPublishButton");
      const previousButtonText = publishButton?.textContent || "";
      if (publishButton) {
        publishButton.disabled = true;
        publishButton.textContent = "正在安全发布…";
      }
      setPriceImportStatus(`正在同步成本版本 ${versionId}，成功前仍保留当前草稿…`);
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("服务端未确认成本版本");
        }
        state.settings = proposedSettings;
        state.quoteRuleHistory = proposedHistory;
        saveState();
        restoreSettings();
        renderCostLibrary();
        renderQuoteRules();
        const successText = activatesNow
          ? `新版成本 ${versionId} 已发布；新报价开始使用，历史报价不受影响。`
          : `成本版本 ${versionId} 已保存，将于 ${effectiveAt} 自动生效；当前报价暂不受影响。`;
        setPriceImportStatus(successText, "success");
        notify(successText);
        return true;
      } catch (error) {
        setPriceImportStatus(`发布失败：${error.message}。草稿已保留，修正后可直接重试。`, "error");
        notify(`成本版本没有发布：${error.message}。当前草稿未丢失。`);
        renderCostLibrary();
        return false;
      } finally {
        if (publishButton) {
          publishButton.disabled = !canEditCostLibrary();
          publishButton.textContent = previousButtonText || "核对完成，发布新版成本";
        }
      }
    }
    function normalizedCostLookupText(value) {
      return String(value || "").toLowerCase()
        .replace(/白卡纸/g, "白卡")
        .replace(/铜版纸/g, "铜版")
        .replace(/(?:单面|双面)?覆?哑膜/g, "覆膜哑膜")
        .replace(/(?:单面|双面)?覆?(?:光|亮)膜/g, "覆膜亮膜")
        .replace(/[^\p{L}\p{N}]+/gu, "");
    }
    function findActiveCostItem(name, allowedTypes = []) {
      const query = normalizedCostLookupText(name);
      if (!query) return null;
      const gsm = String(name || "").match(/(\d+(?:\.\d+)?)\s*g/i)?.[1] || "";
      const semanticWords = String(name || "")
        .replace(/\d+(?:\.\d+)?\s*(?:g|克)?/gi, " ")
        .match(/[\p{Script=Han}]{2,}|[A-Za-z]{2,}/gu) || [];
      return activeCostLibraryItems().filter(item => !allowedTypes.length || allowedTypes.includes(item.type)).map(item => {
        const itemText = normalizedCostLookupText(`${item.name}${item.spec}`);
        const itemName = normalizedCostLookupText(item.name);
        let score = itemText === query ? 100 : itemText.includes(query) || query.includes(itemText) ? 60 : 0;
        if (!score && itemName && (itemName === query || itemName.includes(query) || query.includes(itemName))) score = 50;
        const semanticMatches = semanticWords.filter(word => itemText.includes(normalizedCostLookupText(word))).length;
        if (!score && semanticMatches) score = 30 + semanticMatches * 5;
        if (score && gsm && String(item.spec || item.name).includes(gsm)) score += 25;
        return { item, score };
      }).filter(match => match.score > 0).sort((a, b) => b.score - a.score)[0]?.item || null;
    }
    function costItemPricePerKg(item) {
      if (!item || item.price === null) return null;
      const unit = String(item.unit || "").replace(/\s/g, "").toLowerCase();
      if (unit.includes("吨") || unit.includes("ton")) return Number(item.price) / 1000;
      if (unit.includes("kg") || unit.includes("千克") || unit.includes("公斤")) return Number(item.price);
      return null;
    }
    function costLibraryOperationsForQuote(processText) {
      const normalizedProcess = normalizedCostLookupText(processText);
      if (!normalizedProcess) return [];
      const supportedBasis = new Set(["piece", "sheet", "area_m2", "local_m2", "meter", "hole", "fold", "page", "book", "run"]);
      return activeCostLibraryItems().filter(item => item.type === "process" && item.price !== null).filter(item => {
        const itemName = normalizedCostLookupText(item.name);
        return itemName && (normalizedProcess.includes(itemName) || itemName.includes(normalizedProcess));
      }).map(item => {
        const basisAliases = {
          piece: "piece", sheet: "sheet", area_m2: "area_m2", length_m: "meter",
          book: "book", run: "run", specification: "run"
        };
        const basis = basisAliases[item.basis] || (String(item.unit).includes("个") ? "piece" : String(item.unit).includes("张") ? "sheet" : String(item.unit).includes("平方") ? "area_m2" : "");
        if (!supportedBasis.has(basis)) return null;
        return {
          id: item.id,
          name: item.name,
          basis,
          unitRate: Number(item.price),
          minimumCharge: Number(item.setupFee || 0),
          setupFee: 0
        };
      }).filter(Boolean);
    }
    function costOperationNamesOverlap(left, right) {
      const leftName = normalizedCostLookupText(left);
      const rightName = normalizedCostLookupText(right);
      return Boolean(leftName && rightName && (leftName === rightName || leftName.includes(rightName) || rightName.includes(leftName)));
    }
    function cloneQuoteSnapshot(value) {
      if (value === null || value === undefined) return null;
      return JSON.parse(JSON.stringify(value));
    }
    function setPriceImportStatus(message, type = "") {
      const box = document.getElementById("priceImportStatus");
      if (!box) return;
      box.textContent = message;
      box.classList.toggle("is-success", type === "success");
      box.classList.toggle("is-error", type === "error");
    }
    function parsePriceCsvFallback(text) {
      const lines = String(text || "").replace(/^\uFEFF/, "").split(/\r?\n/).filter(Boolean);
      if (!lines.length) return { materials: [], processes: [], items: [] };
      const split = line => line.split(line.includes("\t") ? "\t" : ",").map(cell => cell.trim().replace(/^"|"$/g, ""));
      const headers = split(lines[0]).map(cell => cell.replace(/\s/g, ""));
      const typeIndex = headers.findIndex(cell => ["类型", "类别", "分类"].includes(cell));
      const nameIndex = headers.findIndex(cell => /名称|项目|品名|材料|工艺/.test(cell));
      const priceIndex = headers.findIndex(cell => /单个成本|单位成本|单价|成本|价格|加工费/.test(cell));
      const unitIndex = headers.findIndex(cell => /计价单位|单位/.test(cell));
      if (nameIndex < 0 || priceIndex < 0) throw new Error("请使用“类型、名称、单个成本”三列");
      const result = { materials: [], processes: [], items: [] };
      lines.slice(1).forEach((line, rowIndex) => {
        const cells = split(line);
        const name = cells[nameIndex] || "";
        if (!name) return;
        const rawPrice = String(cells[priceIndex] ?? "").trim();
        const blankPrice = !rawPrice || ["-", "--", "—", "待填", "待定", "n/a", "null"].includes(rawPrice.toLowerCase());
        const cleanedPrice = rawPrice.replace(/,/g, "").replace(/[\uffe5¥]/g, "").trim();
        const price = blankPrice ? null : Number(cleanedPrice);
        if (!blankPrice && (!Number.isFinite(price) || price < 0)) throw new Error(`价格表第 ${rowIndex + 2} 行的价格不是有效非负数`);
        const type = cells[typeIndex] || "材料";
        const itemType = type.includes("人工") ? "labor" : type.includes("版") ? "plate" : type.includes("工") || type.includes("加工") ? "process" : "material";
        const unit = (unitIndex >= 0 ? cells[unitIndex] : "") || "个";
        const item = normalizeCostLibraryItem({
          id: `csv-${rowIndex + 1}-${newCostLibraryItemId()}`,
          type: itemType,
          category: type || (itemType === "material" ? "材料" : "工艺"),
          name,
          unit,
          price
        }, rowIndex);
        result.items.push(item);
        if (Number(price) > 0 && costLibrarySafeLegacyUnits.has(unit)) {
          if (itemType === "material") result.materials.push({ name, price });
          else if (["process", "plate"].includes(itemType)) result.processes.push({ name, price });
        }
      });
      return result;
    }
    function applyImportedPriceTable(data, filename = "价格表") {
      const materials = Array.isArray(data?.materials) ? data.materials.filter(item => item.name && Number(item.price) > 0) : [];
      const processes = Array.isArray(data?.processes) ? data.processes.filter(item => item.name && Number(item.price) > 0) : [];
      const importedItems = Array.isArray(data?.costLibrary?.items)
        ? data.costLibrary.items
        : Array.isArray(data?.items) ? data.items : [];
      const importedOperationPrices = normalizeQuoteOperationPrices(data?.quoteOperationPrices);
      if (!importedItems.length && !materials.length && !processes.length) throw new Error("没有识别到有效材料、人工或工艺成本");
      const library = ensureCostLibrary();
      library.draftItems = importedItems.length
        ? importedItems.map(normalizeCostLibraryItem)
        : [
            ...materials.map((item, index) => normalizeCostLibraryItem({ ...item, type: "material", category: "材料", unit: "个" }, index)),
            ...processes.map((item, index) => normalizeCostLibraryItem({ ...item, type: "process", category: "工艺", unit: "个" }, materials.length + index))
          ];
      library.draftLegacyMaterials = materials;
      library.draftLegacyProcesses = processes;
      library.draftSourceFile = String(filename || "价格表").slice(0, costLibraryFieldLimits.sourceFile);
      library.updatedAt = new Date().toISOString();
      library.status = "draft";
      if (importedOperationPrices.length) {
        const existingOperationPrices = normalizeQuoteOperationPrices(state.settings.quoteOperationPrices);
        const existingKeys = new Set(existingOperationPrices.map(item => `${item.kind}:${normalizeRuleText(item.name)}`));
        // Existing factory-edited prices win. Workbook defaults only fill
        // missing operations and never replace a customer's saved pricing.
        state.settings.quoteOperationPrices = normalizeQuoteOperationPrices([
          ...existingOperationPrices,
          ...importedOperationPrices.filter(item => !existingKeys.has(`${item.kind}:${normalizeRuleText(item.name)}`))
        ]);
      }
      const incomplete = Number(data?.incompleteCount ?? library.draftItems.filter(item => item.incomplete).length);
      state.quoteRuleHistory = [{
        time: new Date().toLocaleString("zh-CN", { hour12: false }),
        createdAt: new Date().toISOString(),
        category: "结构化成本库",
        changes: [`导入 ${filename}：${library.draftItems.length} 项，${incomplete} 项待补价格（尚未发布）`]
      }, ...(state.quoteRuleHistory || [])].slice(0, 20);
      saveState();
      choosePriceSetup("import");
      renderCostLibrary();
      const operationText = importedOperationPrices.length ? `，另含 ${importedOperationPrices.length} 组已确认印刷/工艺价格` : "";
      const uvText = Number(data?.pendingUvCount || 0) ? `；${Number(data.pendingUvCount)} 条 UV 待确认项保持停用` : "";
      setPriceImportStatus(`已从“${filename}”识别 ${library.draftItems.length} 项成本${operationText}${incomplete ? `，其中 ${incomplete} 项待厂家补价格` : ""}${uvText}。请在下方核对、修改后发布新版成本。`, "success");
      notify("价格表已导入为草稿，请核对并发布新版成本。" );
    }
    async function importPriceTableFile() {
      const input = document.getElementById("priceTableFile");
      const file = input?.files?.[0];
      if (!file) return;
      if (!requireCostLibraryEditPermission()) {
        input.value = "";
        return;
      }
      const existingLibrary = ensureCostLibrary();
      if (existingLibrary.status === "draft" && existingLibrary.draftItems.length && !confirm("当前还有未发布的成本草稿。继续导入会覆盖这些草稿修改，是否继续？")) {
        input.value = "";
        setPriceImportStatus("已取消导入，当前成本草稿仍已保留。");
        return;
      }
      setPriceImportStatus(`正在识别“${file.name}”…`);
      try {
        let data;
        if (typeof window.__importPriceTableFileConnected === "function" && !window.__isStaticFileMode) {
          data = await window.__importPriceTableFileConnected(file);
        } else if (file.name.toLowerCase().endsWith(".csv")) {
          data = parsePriceCsvFallback(await file.text());
        } else {
          throw new Error("Excel 导入需要从正式版地址打开；本地文件预览可先导入 CSV");
        }
        applyImportedPriceTable(data, file.name);
      } catch (error) {
        setPriceImportStatus(`导入失败：${error.message}`, "error");
        notify(`价格表导入失败：${error.message}`);
      } finally {
        if (input) input.value = "";
      }
    }
    function downloadPriceTableTemplate() {
      const content = "\uFEFF类型,名称,计价单位,单个成本\n材料,常用材料,元/个,\n工艺,常用工艺,元/个,\n人工工资,完全人工成本,元/小时,\n";
      const blob = new Blob([content], { type: "text/csv;charset=utf-8" });
      const link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = "将莱数字员工-价格表导入模板.csv";
      link.click();
      URL.revokeObjectURL(link.href);
    }
    function newQuoteNumber() {
      const date = new Date();
      const digits = date.toISOString().replace(/\D/g, "").slice(0, 14);
      const suffix = String(date.getMilliseconds()).padStart(3, "0");
      return `BJ-${digits.slice(0, 8)}-${digits.slice(8)}${suffix}`;
    }
    function nextQuoteVersion(customer, product) {
      return (state.quoteHistory || []).filter(item => item.customer === customer && item.product === product).length + 1;
    }
    function quoteRecordKey(record) {
      return record?.quoteNo || record?.createdAt || record?.time || "";
    }
    function activeQuoteRecord() {
      const activeKey = window.__activeQuoteRecordKey;
      if (activeKey === "") return null;
      return (state.quoteHistory || []).find(item => quoteRecordKey(item) === activeKey) || (state.quoteHistory || [])[0] || null;
    }
    function latestMatchingQuote(record) {
      if (!record) return null;
      return (state.quoteHistory || []).find(item => item.customer === record.customer && item.product === record.product) || null;
    }
    function renderQuoteApprovalPanel(record = activeQuoteRecord()) {
      const box = document.getElementById("quoteApprovalPanel");
      if (!box) return;
      if (!record) {
        box.innerHTML = "";
        box.classList.remove("is-pending");
        return;
      }
      const history = (state.quoteHistory || []).filter(item => item.customer === record.customer && item.product === record.product);
      const currentIndex = history.findIndex(item => quoteRecordKey(item) === quoteRecordKey(record));
      const previous = currentIndex >= 0 ? history[currentIndex + 1] : null;
      const change = previous && Number(previous.unit || 0) > 0
        ? `${Number(record.unit || 0) >= Number(previous.unit || 0) ? "上涨" : "下降"}${Math.abs((Number(record.unit || 0) / Number(previous.unit || 1) - 1) * 100).toFixed(1)}%`
        : "首次报价";
      const version = Number(record.version || Math.max(1, history.length - Math.max(currentIndex, 0)));
      const pending = record.approvalStatus === "待老板确认";
      const canApprove = window.__hasPermission?.("quote.approve");
      box.classList.toggle("is-pending", pending);
      box.innerHTML = `
        <div><strong>报价版本 V${version} · ${pending ? "待老板确认" : record.approvalStatus || "可发送"}</strong><span>相比上一版本：${escapeHtml(change)}${record.approvalReason ? ` · ${escapeHtml(record.approvalReason)}` : ""}</span></div>
        ${pending ? (canApprove ? `<div class="quote-approval-fields"><input id="quoteApprover" value="${escapeHtml(window.__currentUser?.name || "")}" placeholder="审核人姓名"><input id="quoteApprovalNote" placeholder="审核备注，例如：已核对材料价和税费"><button class="btn" type="button" onclick="approveCurrentQuote()">老板确认并放行</button></div>` : `<span>已自动通知老板；老板确认前不能发送给客户，你填写的资料和报价不会丢失。</span>`) : `<span>${record.approvedAt ? `审核人：${escapeHtml(record.approvedBy || "老板")} · ${escapeHtml(record.approvedAt)}${record.approvalNote ? `<br>备注：${escapeHtml(record.approvalNote)}` : ""}` : "当前版本可复制、打印和导出"}</span>`}
      `;
    }
    function renderQuoteTrustSummary(record, details = {}) {
      const box = document.getElementById("quoteTrustSummary");
      if (!box) return;
      if (!record) {
        box.innerHTML = "";
        return;
      }
      const rulesAge = Number(details.rulesAge || 0);
      const priceWarning = rulesAge > 30;
      const processText = details.processText || "基础加工";
      const marginRate = Number(details.marginRate ?? record.marginRate ?? 0);
      const marginWarning = marginRate < 15;
      const uncertainFields = Array.isArray(details.uncertainFields) ? details.uncertainFields : [];
      const uncertainConfirmed = Boolean(details.uncertainConfirmed || !uncertainFields.length);
      const materialCostConfirmed = details.materialUnitCostConfirmed !== false;
      const priceChangePct = Number(details.priceChangePct || 0);
      const priceChangeWarning = Boolean(details.previousQuoteNo) && Math.abs(priceChangePct) >= 10;
      const materialCostChangePct = Number(details.materialCostChangePct || 0);
      const materialCostChangeWarning = Boolean(details.previousQuoteNo) && Math.abs(materialCostChangePct) >= 10;
      const pendingApproval = record.approvalStatus === "待老板确认";
      const referencePricing = quoteReferencePricePackActive();
      const score = Math.max(0, 100 - (referencePricing ? 15 : 0) - (priceWarning ? 15 : 0) - (!materialCostConfirmed ? 30 : 0) - (!uncertainConfirmed ? 20 : 0) - (priceChangeWarning ? 10 : 0) - (materialCostChangeWarning ? 10 : 0) - (marginWarning ? 25 : 0) - (pendingApproval ? 15 : 0));
      const gateText = pendingApproval ? "等待老板放行" : score >= 85 ? "可以发送客户" : "建议再次复核";
      const items = [
        {
          warning: referencePricing || priceWarning,
          title: referencePricing ? "△ 当前使用行业参考价" : priceWarning ? "△ 价格表建议复核" : "✓ 本厂价格表已确认",
          text: referencePricing ? "可用于估算；对外报价前请复核本单命中的材料和工艺价" : priceWarning ? `距离上次确认已 ${rulesAge} 天` : `最后确认：${state.settings.quoteRulesUpdatedAt ? new Date(state.settings.quoteRulesUpdatedAt).toLocaleDateString("zh-CN") : "本次"}`
        },
        {
          warning: !materialCostConfirmed,
          title: materialCostConfirmed ? "✓ 本单材料成本已核对" : "△ 本单材料成本未核对",
          text: `${details.materialText || record.material || "材料"}${Number(details.materialUnitCost || record.materialUnitCost) > 0 ? ` ¥${Number(details.materialUnitCost || record.materialUnitCost).toFixed(3)}/个` : ""}；${processText}`
        },
        {
          warning: !uncertainConfirmed,
          title: uncertainFields.length ? (uncertainConfirmed ? "✓ AI推测已人工核对" : "△ AI推测尚未确认") : "✓ 客户原文信息明确",
          text: uncertainFields.length ? `涉及：${uncertainFields.join("、")}` : "产品、规格和报价条件已核对"
        },
        details.previousQuoteNo ? {
          warning: priceChangeWarning,
          title: priceChangeWarning ? `△ 同规格单价${priceChangePct > 0 ? "上涨" : "下降"}${Math.abs(priceChangePct).toFixed(1)}%` : "✓ 同规格价格波动正常",
          text: `对比 ${details.previousQuoteNo}：上次 ¥${Number(details.previousUnit || 0).toFixed(3)}/个，本次 ¥${Number(record.unit || 0).toFixed(3)}/个`
        } : null,
        details.previousQuoteNo && Number(details.previousMaterialUnitCost || 0) > 0 ? {
          warning: materialCostChangeWarning,
          title: materialCostChangeWarning ? `△ 同规格材料成本${materialCostChangePct > 0 ? "上涨" : "下降"}${Math.abs(materialCostChangePct).toFixed(1)}%` : "✓ 同规格材料成本稳定",
          text: `上次 ¥${Number(details.previousMaterialUnitCost || 0).toFixed(3)}/个，本次 ¥${Number(details.materialUnitCost || record.materialUnitCost || 0).toFixed(3)}/个`
        } : null,
        {
          warning: marginWarning || pendingApproval,
          title: marginWarning ? `△ 毛利率 ${marginRate.toFixed(1)}%` : `✓ 毛利率 ${marginRate.toFixed(1)}%`,
          text: pendingApproval ? "触发风险规则，老板确认前不能发送" : marginWarning ? "利润偏低，客户压价前先复核" : "利润处于可接单范围"
        },
        {
          warning: false,
          title: `✓ ${details.taxMode || "税费"}、不含运费`,
          text: "客户报价已明确标注运费另计"
        },
        {
          warning: false,
          title: "✓ 报价和跟单已留档",
          text: `${record.quoteNo || "报价编号已生成"} · 后续可继续追踪`
        }
      ].filter(Boolean);
      box.innerHTML = `
        <div class="quote-safety-head">
          <div class="quote-safety-score">${score}</div>
          <div class="quote-safety-copy"><strong>报价安全评分</strong><span>综合价格表时效、成本命中、AI核对、毛利审批和报价口径</span></div>
          <span class="quote-safety-gate${pendingApproval || score < 85 ? " warning" : ""}">${escapeHtml(gateText)}</span>
        </div>
      ` + items.map(item => `
        <div class="quote-trust-item${item.warning ? " warning" : ""}"><strong>${escapeHtml(item.title)}</strong><span>${escapeHtml(item.text)}</span></div>
      `).join("");
    }
    function renderQuoteProfessionalRecord(record = activeQuoteRecord()) {
      const box = document.getElementById("quoteProfessionalRecord");
      if (!box) return;
      if (!record) {
        box.innerHTML = `<div class="quote-record-head"><div><strong>报价凭证将在这里自动生成</strong><span>生成报价后会记录编号、版本、价格表和审批人。</span></div><span class="quote-record-badge">待生成</span></div>`;
        return;
      }
      const created = new Date(record.createdAt || Date.now());
      const validUntil = new Date(created);
      validUntil.setDate(validUntil.getDate() + 7);
      const priceDate = record.pricingUpdatedAt ? new Date(record.pricingUpdatedAt).toLocaleDateString("zh-CN") : "未记录";
      const creator = record.createdBy || window.__currentUser?.name || state.member.contact || "当前业务员";
      const approval = record.approvalStatus === "待老板确认"
        ? "待老板确认"
        : record.approvedBy ? `${record.approvedBy} 已确认` : "规则校验通过";
      box.innerHTML = `
        <div class="quote-record-head"><div><strong>这份报价可以追溯</strong><span>任何改价都会生成新版本，旧版本只留档、不允许误发。</span></div><span class="quote-record-badge">${escapeHtml(record.outcomeStatus || record.status || "已报价")}</span></div>
        <div class="quote-record-grid">
          <div><span>报价编号 / 版本</span><strong>${escapeHtml(record.quoteNo || "待同步")} · V${Number(record.version || 1)}</strong></div>
          <div><span>制单人</span><strong>${escapeHtml(creator)}</strong></div>
          <div><span>价格表版本</span><strong>${escapeHtml(priceDate)} · ${escapeHtml(record.costMode === "printing" ? "精细印刷公式" : record.costMode === "industry" ? "通用行业公式" : record.costMode === "material" ? "原料采购价换算" : "历史按件成本")}</strong></div>
          <div><span>审批状态</span><strong>${escapeHtml(approval)}</strong></div>
          <div><span>有效期</span><strong>至 ${validUntil.toLocaleDateString("zh-CN")}</strong></div>
          <div><span>报价口径</span><strong>${escapeHtml(record.taxMode || "税费待确认")} · 不含运费</strong></div>
          <div><span>对客单价来源</span><strong>${escapeHtml(record.customerPriceSourceLabel || "系统标准报价")}</strong></div>
          <div><span>预计毛利率</span><strong>${Number(record.marginRate || 0).toFixed(1)}%</strong></div>
          <div><span>业务状态</span><strong>${escapeHtml(record.outcomeStatus || "等待客户结果")}</strong></div>
        </div>`;
    }
    async function approveCurrentQuote() {
      const record = activeQuoteRecord();
      if (!record) return;
      if (!window.__hasPermission?.("quote.approve")) {
        notify("当前报价需要老板确认，系统已经提醒老板。" );
        return;
      }
      if (!record.serverId) {
        notify("报价正在保存，请稍后再确认。" );
        return;
      }
      const approver = (document.getElementById("quoteApprover")?.value || window.__currentUser?.name || "").trim();
      const note = (document.getElementById("quoteApprovalNote")?.value || "").trim();
      if (record.approvalStatus === "待老板确认" && approver.length < 2) {
        document.getElementById("quoteApprover")?.focus();
        notify("请填写审核人姓名，系统会保留审批记录。" );
        return;
      }
      record.approvedBy = approver || window.__currentUser?.name || "老板";
      record.approvalNote = note || record.approvalNote || "已核对成本、利润和报价条件";
      try {
        const data = await window.__approveQuoteCloud?.(record);
        record.approvalStatus = "已确认";
        record.approvedAt = data?.approved_at || new Date().toLocaleString("zh-CN", { hour12: false });
        record.approvedBy = data?.approved_by || record.approvedBy;
        saveState(); renderQuoteApprovalPanel(record); renderQuoteTrustSummary(record, record.safetyDetails || {}); renderQuoteProfessionalRecord(record); renderQuoteHistory();
        notify("老板已确认当前报价版本，可以发送给客户。" );
      } catch (err) { notify(err.message || "报价确认失败"); }
    }
    function canSendCurrentQuote() {
      const record = activeQuoteRecord();
      if (!record) return true;
      const latest = latestMatchingQuote(record);
      if (latest && quoteRecordKey(latest) !== quoteRecordKey(record)) {
        notify("这是旧报价版本，不能直接发送。请带入后重新生成最新报价。" );
        return false;
      }
      if (record.approvalStatus === "待老板确认") {
        notify("当前报价存在利润、金额或起订量风险，请先让老板确认。" );
        return false;
      }
      return true;
    }
    function reuseQuoteVersion(recordKeyCode) {
      const key = decodeURIComponent(recordKeyCode);
      const record = (state.quoteHistory || []).find(item => quoteRecordKey(item) === key);
      if (!record) return;
      showPage("报价系统");
      setIfExists("quoteCustomerName", record.customer || "新客户");
      setIfExists("quoteProduct", record.product || "定制产品");
      setIfExists("quoteSize", record.size || "");
      setIfExists("quoteQty", record.qty || "");
      setIfExists("quoteMaterial", record.material || "");
      setIfExists("quoteProcess", record.process || "");
      setQuoteDeliveryValue(record.delivery || state.settings.delivery);
      setIfExists("quotePayment", record.payment || state.settings.payment);
      setIfExists("quoteTaxMode", record.taxMode || "待确认");
      setIfExists("quoteFreightMode", "不含运");
      setIfExists("quoteFreight", "0");
      setIfExists("quoteSampleMode", record.sampleMode || (Number(record.sample || 0) > 0 ? "收费打样" : "待确认"));
      setIfExists("quoteSample", record.sample || 0);
      restoreQuoteCatalogConfiguration(record.productConfiguration);
      quoteGuideHasResult = false;
      window.__activeQuoteRecordKey = "";
      setQuoteOutputs(`已带入历史报价 V${record.version || 1}。请核对当前材料、工艺和价格表后重新生成，旧版本不会被直接发送。`, "历史报价只用于参考，请重新生成内部底价。");
      renderQuoteGuide();
      if (Number(record.materialUnitCost) > 0) setIfExists("quoteMaterialUnitCost", Number(record.materialUnitCost).toFixed(3));
      const materialConfirmation = document.getElementById("quoteMaterialCostConfirmed");
      if (materialConfirmation) materialConfirmation.checked = false;
      notify("历史报价已带入，请重新核价生成新版本。" );
    }
    async function saveMissingQuoteCostsAndContinue() {
      const entries = Array.isArray(window.__pendingQuoteCosts) ? window.__pendingQuoteCosts : [];
      if (!entries.length) return false;
      const additions = entries.map((entry, index) => {
        const input = document.getElementById(`quoteMissingCost${index}`);
        const rawPrice = String(input?.value ?? "").replace(/[,￥¥\s]/g, "");
        const price = rawPrice === "" ? NaN : Number(rawPrice);
        const valid = entry.type === "material" ? price > 0 : price >= 0;
        input?.classList.toggle("quote-field-error", !valid || !Number.isFinite(price));
        const absoluteMaximum = entry.type === "material" ? 100_000_000 : 10_000_000;
        return { ...entry, price, valid: valid && Number.isFinite(price) && price <= absoluteMaximum };
      });
      const invalid = additions.find(item => !item.valid);
      if (invalid) {
        document.getElementById(`quoteMissingCost${additions.indexOf(invalid)}`)?.focus();
        notify(invalid.type === "material"
          ? `请填写“${invalid.displayName || invalid.name}”大于 0 的真实材料成本。`
          : `请填写“${invalid.displayName || invalid.name}”大于或等于 0 的真实费用；没有费用可填 0。`);
        return false;
      }
      const materialAdditions = additions.filter(item => item.type === "material");
      const processAdditions = additions.filter(item => item.type === "process");
      const savedAt = new Date().toISOString();
      const proposedSettings = JSON.parse(JSON.stringify(state.settings));
      const materialPrices = normalizeQuoteMaterialPrices(proposedSettings.quoteMaterialPrices);
      materialAdditions.forEach(item => {
        const next = { name: item.name, price: item.price, unit: "元/个", updatedAt: savedAt };
        const key = item.name.toLowerCase().replace(/\s+/g, "");
        const index = materialPrices.findIndex(price => price.name.toLowerCase().replace(/\s+/g, "") === key);
        if (index >= 0) materialPrices[index] = next; else materialPrices.push(next);
      });
      proposedSettings.quoteMaterialPrices = normalizeQuoteMaterialPrices(materialPrices);
      const operationPrices = normalizeQuoteOperationPrices(proposedSettings.quoteOperationPrices);
      processAdditions.forEach(item => {
        const name = canonicalQuoteOperationLabel(item.name);
        const kind = item.kind === "print" ? "print" : "process";
        const next = {
          name, kind, basis:"piece", chargeMode:"minimum", unitPrice:item.price,
          setupFee:0, minimumCharge:0, includedQuantity:0, excessUnitPrice:item.price,
          usagePerPiece:1, updatedAt:savedAt
        };
        const key = `${kind}:${name.toLowerCase().replace(/\s+/g, "")}`;
        const index = operationPrices.findIndex(price => `${price.kind}:${price.name.toLowerCase().replace(/\s+/g, "")}` === key);
        if (index >= 0) operationPrices[index] = next; else operationPrices.push(next);
      });
      proposedSettings.quoteOperationPrices = normalizeQuoteOperationPrices(operationPrices);
      proposedSettings.quoteRulesUpdatedAt = savedAt;
      try {
        if (window.location.protocol !== "file:") {
          if (typeof window.__saveSettingsCloud !== "function") throw new Error("价格表服务尚未连接，请稍后重试");
          const saved = await window.__saveSettingsCloud(proposedSettings, { throwOnError: true });
          if (!saved?.ok) throw new Error("服务端没有确认这次单项价格修改");
        }
        state.settings = proposedSettings;
        state.quoteRuleHistory = [{
          time: new Date(savedAt).toLocaleString("zh-CN", { hour12: false }),
          createdAt: savedAt,
          category: currentIndustryCategory(),
          changes: [`报价时按需补录：${additions.map(item => `${item.displayName || item.name}=¥${item.price.toFixed(3)}/个`).join("、")}`]
        }, ...(state.quoteRuleHistory || [])].slice(0, 20);
        saveState();
        renderQuoteRules();
        hideMissingQuoteCosts();
        // 只补录印刷/工艺费用时，厂家刚核对的采购价、用量和确认勾选
        // 仍然属于同一张报价。不要强制重置材料核价区，否则会要求重复填写。
        renderQuoteMaterialCostCheck();
        const confirmedMaterial = materialAdditions.find(item => normalizeRuleText(item.name) === normalizeRuleText(val("quoteMaterial")));
        if (confirmedMaterial) {
          setIfExists("quoteMaterialUnitCost", Number(confirmedMaterial.price).toFixed(3));
          const confirmation = document.getElementById("quoteMaterialCostConfirmed");
          if (confirmation) confirmation.checked = true;
        }
        setQuoteCostWarning(`已保存 ${additions.map(item => item.displayName || item.name).join("、")} 的本单成本，正在继续生成报价。`);
        notify("本单缺失成本已补齐，正在继续报价。" );
        return calculateQuote();
      } catch (error) {
        notify(`单项价格没有保存：${error.message}。原价格未改变。`);
        return false;
      }
    }
    function resolveCustomerQuotePrice(standardUnitPrice, customer = currentCustomerName()) { return window.JIANGLAI_QUOTE_COST_DETAILS.resolveCustomerPrice(state.customerProfiles, customer, standardUnitPrice); }
    function renderQuoteCostLineItems(items = [], record = null) { window.JIANGLAI_QUOTE_COST_DETAILS.render(document, items, record); }
    function calculateQuote() {
      setQuoteCostWarning("");
      ["quoteCustomerName", "quoteMaterial", "quoteProcess", "quoteTaxMode", "quoteSampleMode", "quoteSample"].forEach(id => {
        document.getElementById(id)?.classList.remove("quote-field-error");
      });
      // 行业提示可能来自上一次询盘识别或恢复的草稿。同一行业的旧提示立即清除；
      // 真正跨行业的情况不在这里提前退出，稍后按当前产品重新校验并给出明确原因。
      const industryWarning = document.getElementById("quoteIndustryWarning");
      if (industryWarning && !industryWarning.classList.contains("is-hidden")) {
        if (quoteBusinessProfileForProduct(val("quoteProduct")).category === currentIndustryCategory()) setQuoteIndustryWarning("");
      }
      if (!validateQuoteCatalogSelection(true)) return false;
      let s = state.settings;
      let rulesAge = quoteRulesAgeDays();
      if (rulesAge > 90) {
        setQuoteCostWarning(`本单命中的价格已经 ${rulesAge} 天没有更新，仍可继续报价；发送客户前建议复核本单材料、印刷和所选工艺费用。`);
      }
      const requiredFields = [
        { id: "quoteCustomerName", label: "客户名称", valid: value => value.length >= 2 && !["新客户", "未命名客户"].includes(value) },
        { id: "quoteProduct", label: "产品名称", valid: value => value.length >= 2 && !value.includes("待确认") },
        { id: "quoteSize", label: "尺寸", valid: value => /^\s*\d+(?:\.\d+)?\s*[xX×*]\s*\d+(?:\.\d+)?(?:\s*[xX×*]\s*\d+(?:\.\d+)?)?\s*(?:mm|cm|m)?\s*$/i.test(value) },
        { id: "quoteQty", label: "数量", valid: value => Number(String(value).replace(/[^\d.]/g, "")) > 0 },
        { id: "quoteMaterial", label: "材质", valid: value => value.length >= 2 && !value.includes("待确认") },
        { id: "quoteProcess", label: "工艺", valid: value => value.length >= 2 && !value.includes("待确认") },
        { id: "quoteDelivery", label: "交期", valid: value => Number(normalizeQuoteDeliveryDays(value)) > 0 },
        { id: "quotePayment", label: "付款方式", valid: value => value.length >= 2 && !value.includes("待确认") },
        { id: "quoteTaxMode", label: "是否含税", valid: value => ["含税", "不含税"].includes(value) },
        { id: "quoteSampleMode", label: "打样方式", valid: value => !["", "待确认", "需要打样，费用待确认"].includes(value) }
      ];
      requiredFields.forEach(item => {
        document.getElementById(item.id)?.classList.remove("quote-field-error");
        document.getElementById(item.id)?.removeAttribute("aria-invalid");
      });
      const missingFields = requiredFields.filter(item => !item.valid(val(item.id)));
      if (missingFields.length) {
        revealQuoteMissingFields(missingFields);
        notify(`报价前还缺：${missingFields.map(item => item.label).join("、")}`);
        return false;
      }
      if ((window.__quoteUncertainFields || []).length && !document.getElementById("quoteAiConfirmed")?.checked) {
        document.getElementById("quoteAiUncertainConfirm")?.scrollIntoView({ behavior: "smooth", block: "center" });
        notify(`请先人工核对${window.__quoteUncertainSource === "memory" ? "沿用的历史资料" : "AI推测内容"}：${window.__quoteUncertainFields.join("、")}，并勾选确认。`);
        return false;
      }
      const expectedProfile = quoteBusinessProfileForProduct(val("quoteProduct"));
      const expectedTemplate = expectedProfile.template;
      if (expectedProfile.category !== currentIndustryCategory()) {
        setActiveQuoteTemplate(expectedProfile.category, expectedTemplate);
        s = state.settings;
        rulesAge = quoteRulesAgeDays();
        renderIndustryCategories();
        renderQuoteTemplateOptions();
        setQuoteIndustryWarning("");
        setQuoteCostWarning(`已按“${val("quoteProduct")}”自动切换到“${expectedProfile.category} · ${expectedTemplate}”成本公式，客户资料和已选规格均已保留。`);
      }
      if (currentCategoryTemplates().includes(expectedTemplate) && val("quoteTemplate") !== expectedTemplate) {
        setActiveQuoteTemplate(expectedProfile.category, expectedTemplate);
        renderQuoteTemplateOptions();
        renderIndustryQuoteCostFields();
        setQuoteIndustryWarning("");
      }
      const recommendedCostMode = quoteRecommendedCostMode();
      if (recommendedCostMode !== val("quoteCostMode")) { setIfExists("quoteCostMode", recommendedCostMode); renderIndustryQuoteCostFields(); }
      const qty = num("quoteQty", 1000);
      const process = val("quoteProcess");
      const taxMode = val("quoteTaxMode");
      const freightMode = "不含运";
      setIfExists("quoteFreightMode", freightMode);
      setIfExists("quoteFreight", "0");
      const sampleMode = val("quoteSampleMode");
      const sample = num("quoteSample", 0);
      const freight = 0;
      const freightRule = { name: "运费统一另计", price: 0, matched: false };
      const costMode = val("quoteCostMode") || "material";
      const materialConversionMode = costMode === "material";
      const legacyRulesConfirmed = quoteLegacyRulesConfirmed(s);
      const materialRules = legacyRulesConfirmed ? parseRules(s.materialRules) : [];
      const processRules = legacyRulesConfirmed ? parseRules(s.processRules) : [];
      const materialRule = currentQuoteMaterialRule();
      const catalogOperationRequirements = quoteCatalogSelectedOperationRequirements();
      const processNames = processTokens(process);
      const textOperationPrices = normalizeQuoteOperationPrices(s.quoteOperationPrices).filter(item =>
        !(item.kind === "print" && !item.printSheetTierPricing)
        && processNames.some(name => costOperationNamesOverlap(name, item.name))
      ).map(item => ({ ...item, configured: true, source: "本厂快捷价", label: item.name }));
      const catalogOperationPrices = [...catalogOperationRequirements.filter(item => item.configured), ...textOperationPrices].filter((item, index, list) => {
        return list.findIndex(candidate => candidate.kind === item.kind
          && costOperationNamesOverlap(candidate.name, item.name)) === index;
      });
      const sourceOutlierWarnings = catalogOperationPrices
        .map(item => quotePriceOutlierWarning({ kind:item.kind, name:item.name, price:item.unitPrice, setupFee:item.setupFee, minimumCharge:item.minimumCharge, basis:item.basis }))
        .filter(Boolean);
      const catalogOperationNames = catalogOperationPrices.map(item => item.name);
      const costLibraryProcessNames = costMode === "printing" ? costLibraryOperationsForQuote(process).map(item => item.name) : [];
      const unmatchedTextProcesses = unmatchedProcessTokens(processRules, process).filter(token =>
        !costLibraryProcessNames.some(name => costOperationNamesOverlap(token, name))
        && !catalogOperationNames.some(name => costOperationNamesOverlap(token, name))
      );
      const unknownProcesses = [
        ...catalogOperationRequirements.filter(item => !item.configured),
        ...unmatchedTextProcesses.map(name => ({ name, label: name, kind: "process" }))
      ].filter((item, index, list) => list.findIndex(candidate => `${candidate.kind}:${normalizeRuleText(candidate.name)}` === `${item.kind}:${normalizeRuleText(item.name)}`) === index);
      const missingMaterial = costMode === "unit" && (!materialRule || !(Number(materialRule.price) > 0)) ? val("quoteMaterial") : "";
      if (missingMaterial || unknownProcesses.length) {
        document.getElementById("quoteMaterial")?.classList.toggle("quote-field-error", Boolean(missingMaterial));
        document.getElementById("quoteProcess")?.classList.toggle("quote-field-error", Boolean(unknownProcesses.length));
        showMissingQuoteCosts(missingMaterial, unknownProcesses);
        notify("当前内容已保留，请补齐缺失成本后继续报价。" );
        return false;
      }
      const invalidOperationCost = catalogOperationPrices
        // 精细印刷公式会先算拼版和实际生产印张，再选择开数价；这里用成品数量预算会误报。
        .filter(item => !(costMode === "printing" && item.kind === "print" && item.printSheetTierPricing))
        .map(item => ({ item, calculation:(item.kind === "print" && item.printSheetTierPricing)
          ? { ok:false, error:`${item.name}已按上机开数设置价格，请在成本方式中选择“精细印刷公式”，系统才能先拼版再按实际印张计费` }
          : quoteOperationPriceCalculation(item, qty) }))
        .find(entry => !entry.calculation.ok);
      if (invalidOperationCost) {
        const message = invalidOperationCost.calculation.error || `${invalidOperationCost.item.name}的计价用量不完整`;
        setQuoteCostWarning(message);
        if (message.includes("精细印刷公式")) focusQuoteCostMode("printing");
        notify(`${message}。已为你定位到成本方式，当前客户资料已保留。`);
        return false;
      }
      hideMissingQuoteCosts();
      let materialUnitCost = materialRule?.price || 0;
      let materialConversion = null;
      if (materialConversionMode) {
        renderQuoteMaterialCostCheck();
        materialConversion = quoteMaterialConversion({ strict: true });
        const materialCostConfirmed = Boolean(document.getElementById("quoteMaterialCostConfirmed")?.checked);
        if (!materialConversion.ok || !materialCostConfirmed) {
          updateQuoteMaterialConversionPreview();
          document.getElementById("quoteMaterialCostCheck")?.scrollIntoView({ behavior: "smooth", block: "center" });
          notify(materialConversion.ok ? "请核对并确认本厂采购价、材料用量和损耗参数。" : materialConversion.error);
          return false;
        }
        materialUnitCost = materialConversion.unitCost;
      }
      if (costMode === "unit") {
        renderQuoteMaterialCostCheck();
        const savedSpec = quoteSpecMaterialCost();
        materialUnitCost = Number(savedSpec?.price || materialRule?.price || 0);
        const materialCostConfirmed = Boolean(document.getElementById("quoteMaterialCostConfirmed")?.checked);
        if (!(materialUnitCost > 0) || !materialCostConfirmed) {
          document.getElementById("quoteMaterialUnitCost")?.classList.toggle("quote-field-error", !(materialUnitCost > 0));
          document.getElementById("quoteMaterialCostCheck")?.scrollIntoView({ behavior: "smooth", block: "center" });
          notify(materialUnitCost > 0 ? "请确认本单材料成本适用于当前尺寸后再报价。" : "请填写并确认本单单个材料成本。" );
          return false;
        }
      }
      const materialOutlierWarning = materialConversionMode && materialConversion
        ? quotePriceOutlierWarning({ kind:"material", name:val("quoteMaterial"), price:materialConversion.price, unit:materialConversion.unit })
        : costMode === "unit" ? quotePriceOutlierWarning({ kind:"material", name:val("quoteMaterial"), price:materialUnitCost, unit:"元/个" }) : "";
      if (materialOutlierWarning) sourceOutlierWarnings.push(materialOutlierWarning);
      window.__currentQuoteOutlierWarnings = [...new Set(sourceOutlierWarnings)];
      const outlierSignature = window.__currentQuoteOutlierWarnings.join("|");
      if (outlierSignature && window.__quotePriceOutlierConfirmSignature !== outlierSignature) {
        if (!confirmQuotePriceOutlier(window.__currentQuoteOutlierWarnings.join("；"))) {
          setQuoteCostWarning("发现可能填错单位或小数点的价格，已停止报价。请到对应材料、印刷或工艺费用中复核。");
          return false;
        }
        window.__quotePriceOutlierConfirmSignature = outlierSignature;
      } else if (!outlierSignature) {
        window.__quotePriceOutlierConfirmSignature = "";
      }
      if (sampleMode === "收费打样" && sample <= 0) {
        document.getElementById("quoteSample")?.classList.add("quote-field-error");
        setQuoteCostWarning("已选择“收费打样”，请填写打样费；如果本次免费，请明确选择“免费打样”。");
        notify("收费打样必须填写打样费，已停止报价。");
        return false;
      }
      if (sampleMode === "免费打样" && sample > 0) {
        document.getElementById("quoteSample")?.classList.add("quote-field-error");
        notify("已选择免费打样，请把打样费改为0，或改成收费打样。");
        return false;
      }
      if (rulesAge > 30) {
        setQuoteCostWarning(`价格表已 ${rulesAge} 天未更新，建议核对近期材料和加工费是否有变化。`);
      }
      const level = val("quoteLevel");
      const levelRates = {
        "试单客户": s.profitTrial,
        "普通客户": s.profitNormal,
        "长期客户": s.profitLong,
        "VIP客户": s.profitVip
      };
      const profitRate = Number(levelRates[level] ?? s.profit);
      if (!Number.isFinite(profitRate) || profitRate < 0 || profitRate >= 1) {
        setQuoteCostWarning("目标毛利率必须大于或等于0且小于100%，请先在价格表中修正当前客户等级的毛利率。");
        notify("目标毛利率必须小于100%，已停止报价。");
        return false;
      }
      const moq = Number(s.moq || 0);
      const isBelowMoq = moq && qty < moq;
      const urgentRate = Number(s.urgentRate || 0);
      if (costMode === "printing" && Math.max(profitRate + urgentRate, .18) >= 1) {
        setQuoteCostWarning("当前毛利率与加急幅度合计达到或超过100%，无法计算加急报价，请先调整价格表。");
        notify("加急报价的目标毛利率必须小于100%，已停止报价。");
        return false;
      }
      const ladderDiscount = Number(s.ladderDiscount || 0);
      const matchedLegacyProcessRules = matchedProcessRules(processRules, process);
      let formulaBreakdown = null;
      if (costMode === "industry" || costMode === "printing") {
        formulaBreakdown = costMode === "printing" ? printingQuoteCostBreakdown(qty) : industryQuoteCostBreakdown(qty);
        if (!formulaBreakdown.ok) {
          setQuoteCostWarning(formulaBreakdown.error);
          if (formulaBreakdown.focusFieldId) focusQuoteFormulaField(formulaBreakdown.focusFieldId);
          notify(`${formulaBreakdown.error}${formulaBreakdown.focusFieldId ? "。已为你定位到需要填写的位置。" : "。"}`);
          return false;
        }
      }
      const formulaOperationNames = costMode === "printing" ? (formulaBreakdown.libraryOperationNames || []) : [], includedFormulaProcessNames = costMode === "industry" ? (formulaBreakdown.includedProcessNames || []) : [];
      const manualAdjustments = costMode === "printing"
        ? (formulaBreakdown.manualAdjustments || quoteManualAdjustments())
        : { productionSheets:"", plateCount:"", reason:"", active:false };
      const processHitsWithoutFormula = costMode === "printing"
        ? matchedLegacyProcessRules.filter(rule => !formulaOperationNames.some(name => costOperationNamesOverlap(rule.name, name)))
        : matchedLegacyProcessRules.filter(rule => !includedFormulaProcessNames.some(name => costOperationNamesOverlap(rule.name, name)));
      const processHits = processHitsWithoutFormula.filter(rule =>
        !catalogOperationNames.some(name => costOperationNamesOverlap(rule.name, name))
      );
      if (!consumeQuota("报价系统", false)) return false;
      const formulaMode = costMode === "industry" || costMode === "printing";
      const materialCost = materialConversionMode ? materialConversion.totalCost : formulaMode ? formulaBreakdown.materialCost : materialUnitCost * qty;
      const effectiveMaterialUnitCost = formulaMode
        ? Number(formulaBreakdown.materialUnitCost || materialCost / Math.max(qty, 1))
        : materialUnitCost;
      const laborCost = formulaMode ? formulaBreakdown.laborCost : 0;
      const machineCost = formulaMode ? formulaBreakdown.machineCost : 0;
      const extraDirectCost = formulaMode ? formulaBreakdown.extraDirectCost : 0;
      const formulaOperationItems = costMode === "printing" ? (formulaBreakdown.operationItems || []) : [];
      const formulaOperationCost = formulaOperationItems.reduce((sum, item) => sum + Number(item.total || 0), 0);
      const catalogProcessPrices = catalogOperationPrices.filter(item => item.kind === "process"
        && (costMode !== "printing" || !formulaOperationNames.some(name => costOperationNamesOverlap(item.name, name))) && (costMode !== "industry" || !includedFormulaProcessNames.some(name => costOperationNamesOverlap(item.name, name))));
      const catalogPrintPrices = catalogOperationPrices.filter(item => item.kind === "print");
      const catalogPrintCost = catalogPrintPrices.reduce((sum, item) => sum + quoteOperationPriceTotal(item, qty), 0);
      const printing = costMode === "printing"
        ? Number(formulaBreakdown.printingCost || 0)
        : catalogPrintPrices.length ? catalogPrintCost : legacyRulesConfirmed ? (costMode === "industry" ? Number(s.print || 0) : Number(s.print || 0) + qty * (s.printUnit ?? .12)) : 0;
      const processCosts = [
        ...processHits.map(rule => ({ ...rule, unitPrice: Number(rule.price || 0), setupFee: 0, source: "原工艺价格表", total: Number(rule.price || 0) * qty })),
        ...catalogProcessPrices.map(item => {
          const calculation = quoteOperationPriceCalculation(item, qty);
          return { ...item, price:Number(item.unitPrice || 0), source:item.source || "本厂快捷价", total:calculation.total, basisQuantity:calculation.basisQuantity, appliedUnitRate:calculation.unitRate };
        })
      ];
      const processTotal = processCosts.reduce((sum, item) => sum + item.total, 0);
      const directSubtotal = materialCost + printing + laborCost + machineCost + extraDirectCost + formulaOperationCost + processTotal;
      const overheadCost = formulaMode ? directSubtotal * Number(s.overheadRate || 0) : 0;
      const subtotal = directSubtotal + overheadCost;
      const loss = costMode === "printing"
        ? 0
        : materialConversionMode ? Math.max(0, subtotal - materialCost) * s.loss : subtotal * s.loss;
      const rawProfitBase = subtotal + loss;
      const quoteAccuracyResult = calibrateFactoryQuoteCost({ productName:val("quoteProduct"), materialName:val("quoteMaterial"), processName:process, totalCost:rawProfitBase, costItems:[
        { id:"material", kind:"material", name:val("quoteMaterial"), materialName:val("quoteMaterial"), cost:materialCost },
        { id:"process", kind:"process", name:process || "生产工艺", processName:process, cost:printing + laborCost + machineCost + extraDirectCost + formulaOperationCost + processTotal },
        { id:"overhead-loss", kind:"cost", name:"制造间接费与损耗", cost:overheadCost + loss, calibratable:false }
      ]});
      const profitBase = quoteAccuracyResult.autoApplied ? Number(quoteAccuracyResult.suggestion.calibrated.totalCost) : rawProfitBase;
      const accuracyAdjustment = profitBase - rawProfitBase;
      const quoteAccuracyFactor = rawProfitBase > 0 ? profitBase / rawProfitBase : 1;
      // The setting is labelled target gross margin, so every formula route
      // must use selling price = cost / (1 - margin). The old non-print routes
      // used cost * rate (markup), making a configured 24% margin only 19.35%.
      const targetProfit = profitBase / Math.max(1 - profitRate, .01) - profitBase;
      const taxBase = profitBase + targetProfit;
      const standardTax = taxMode === "含税" ? taxBase * s.tax : 0;
      const standardTotal = taxBase + standardTax + sample;
      const standardUnit = standardTotal / Math.max(qty, 1);
      // Customer-specific prices are authoritative. The current quote's manual
      // price wins first, followed by active agreement, history and level price;
      // the newly published factory defaults are only the final fallback.
      const customerPriceResolution = resolveCustomerQuotePrice(standardUnit);
      const unit = customerPriceResolution.price;
      const total = unit * Math.max(qty, 1);
      // Recalculate the tax contained in the actual selected/customer price.
      // Keeping tax from the standard price made customized prices report the
      // wrong profit and could approve an order at a lower real margin.
      const quoteBeforeSample = Math.max(0, total - sample);
      const tax = taxMode === "含税" ? quoteBeforeSample - quoteBeforeSample / (1 + Number(s.tax || 0)) : 0;
      const internalCost = profitBase + tax + sample;
      const profit = total - internalCost;
      const marginBase = Math.max(0, total - tax - sample);
      const marginRate = marginBase ? profit / marginBase * 100 : 0;
      const minProfitRate = Math.max(.08, profitRate * .45);
      const minTaxBase = profitBase / Math.max(1 - minProfitRate, .01);
      const minTax = taxMode === "含税" ? minTaxBase * s.tax : 0;
      const minAcceptTotal = minTaxBase + minTax + sample;
      const minAcceptUnit = minAcceptTotal / Math.max(qty, 1);
      const urgentProfitRate = Math.max(profitRate + urgentRate, .18);
      const urgentTaxBase = profitBase / Math.max(1 - urgentProfitRate, .01);
      const urgentTax = taxMode === "含税" ? urgentTaxBase * s.tax : 0;
      const urgentTotal = urgentTaxBase + urgentTax + sample;
      const urgentUnit = urgentTotal / Math.max(qty, 1);
      const negotiationRoom = Math.max(0, total - minAcceptTotal);
      const decision = marginRate < 12
        ? "利润偏低，建议先复核材料、工艺和税费，不要急着接。"
        : marginRate < 18
          ? "利润一般，可以作为成交价，但不建议再大幅让价。"
          : "利润相对健康，可按客户诚意和订单规模小幅议价。";
      const ladder = [qty, Math.round(qty * 1.5), qty * 2].map(amount => {
        if (customerPriceResolution.customized) {
          const customerTotal = customerPriceResolution.price * amount;
          return `${amount.toLocaleString()}个：¥${customerTotal.toFixed(0)}，约 ¥${customerPriceResolution.price.toFixed(3)}/个（${customerPriceResolution.sourceLabel}）`;
        }
        if (costMode === "printing") {
          const recalculated = printingQuoteCostBreakdown(amount);
          if (!recalculated.ok) return `${amount.toLocaleString()}个：缺少精细印刷公式数据`;
          const ladderProcessTotal = processHits.reduce((sum, item) => sum + item.price * amount, 0)
            + catalogProcessPrices.reduce((sum, item) => sum + quoteOperationPriceTotal(item, amount), 0);
          const ladderDirect = recalculated.materialCost + recalculated.printingCost + recalculated.laborCost + recalculated.machineCost + recalculated.extraDirectCost + Number(recalculated.operationCost || 0) + ladderProcessTotal;
          const ladderSubtotal = ladderDirect * (1 + Number(s.overheadRate || 0));
          const ladderProfitRate = Math.max(.08, profitRate - (amount > qty ? ladderDiscount : 0));
          const ladderCalibratedCost = ladderSubtotal * quoteAccuracyFactor;
          const ladderPreTax = ladderCalibratedCost / Math.max(1 - ladderProfitRate, .01);
          const ladderTax = taxMode === "含税" ? ladderPreTax * s.tax : 0;
          const ladderTotal = ladderPreTax + ladderTax + sample;
          return `${amount.toLocaleString()}个：¥${ladderTotal.toFixed(0)}，约 ¥${(ladderTotal / amount).toFixed(3)}/个（重新拼版核算）`;
        }
        const scale = amount / Math.max(qty, 1);
        const currentCatalogOperationCost = catalogPrintCost + catalogProcessPrices.reduce((sum, item) => sum + quoteOperationPriceTotal(item, qty), 0);
        const ladderCatalogOperationCost = catalogPrintPrices.reduce((sum, item) => sum + quoteOperationPriceTotal(item, amount), 0)
          + catalogProcessPrices.reduce((sum, item) => sum + quoteOperationPriceTotal(item, amount), 0);
        const legacyFixedDirect = !catalogPrintPrices.length && legacyRulesConfirmed ? Number(s.print || 0) : 0;
        const otherVariableDirect = Math.max(0, directSubtotal - currentCatalogOperationCost - legacyFixedDirect);
        const scaledDirect = legacyFixedDirect + otherVariableDirect * scale + ladderCatalogOperationCost;
        const scaledOverhead = formulaMode ? scaledDirect * Number(s.overheadRate || 0) : 0;
        const scaledSubtotal = scaledDirect + scaledOverhead;
        const scaledMaterialCost = materialCost * scale;
        const scaledLoss = materialConversionMode
          ? Math.max(0, scaledSubtotal - scaledMaterialCost) * s.loss
          : scaledSubtotal * s.loss;
        const scaledProfitRate = Math.max(.08, profitRate - (amount > qty ? ladderDiscount : 0));
        const scaledCalibratedCost = (scaledSubtotal + scaledLoss) * quoteAccuracyFactor;
        const scaledPreTax = scaledCalibratedCost / Math.max(1 - scaledProfitRate, .01);
        const scaledTax = taxMode === "含税" ? scaledPreTax * s.tax : 0;
        const scaledTotal = scaledPreTax + scaledTax + sample;
        return `${amount.toLocaleString()}个：¥${scaledTotal.toFixed(0)}，约 ¥${(scaledTotal / amount).toFixed(3)}/个`;
      }).join("\n");
      const warnings = [
        rulesAge > 30 ? `价格表已 ${rulesAge} 天未更新，建议尽快复核成本。` : "",
        isBelowMoq ? `当前数量低于起订量 ${moq.toLocaleString()} 个，建议按打样/小单价处理或收取基础加工费。` : "",
        marginRate < 15 ? "毛利率偏低，客户压价前先确认税费、打样和特殊工艺。" : "",
        hasAny(process, ["黑", "满版"]) ? "黑色或深色满版建议先打样，确认色差、划痕和折痕露白标准。" : "",
        hasAny(process, ["烫", "UV", "击凸", "压凹"]) ? "特殊工艺需确认面积、位置和工艺图，批量前建议客户签样。" : "",
        costMode === "industry" ? "本报价使用通用行业公式，单位耗用、采购价、工时和利用率须由厂家按本单确认。" : "",
        materialConversionMode ? `本报价按材料采购价和实际用量换算；采购单位为${materialConversion.unit}，利用率与生产损耗已由厂家核对。` : "",
        costMode === "printing" ? `本报价已自动使用对应生产公式；${formulaBreakdown.basis}` : "",
        "本报价不含运费，运费另计。",
        taxMode === "不含税" ? "当前报价不含税，如需开票需按税率另计。" : ""
      ].filter(Boolean).join("\n- ");
      const appliedLaborHourly = Number(formulaBreakdown?.formulaInput?.laborHourly ?? formulaBreakdown?.input?.laborHourly ?? s.laborHourly ?? 0), appliedMachineHourly = Number(formulaBreakdown?.formulaInput?.machineHourly ?? formulaBreakdown?.input?.machineHourly ?? s.machineHourly ?? 0);
      const printingWorkHours = costMode === "printing"
        ? Number(formulaBreakdown.formulaResult?.productionHours || 0) + Number(formulaBreakdown.formulaInput?.setupMinutes || 0) / 60
        : 0;
      const printingOperatorCount = costMode === "printing" ? Number(formulaBreakdown.formulaInput?.operatorCount || 1) : 0;
      const operationBasisLabels = { piece: "件", sheet: "生产印张", area_m2: "㎡", local_m2: "局部㎡", meter: "米", kg:"kg", hole: "孔", fold: "折", page: "页", book: "本", run: "次", hour:"小时" };
      const formulaProcessLineItems = formulaOperationItems.map(item => [
        "工艺",
        `${item.name}${item.sheetFormat?.label ? ` · ${item.sheetFormat.automatic ? "自动识别" : "手动指定"}${item.sheetFormat.label}` : ""}${item.laminationSideCount ? ` · ${item.laminationSideCount === 2 ? "双面" : "单面"}` : ""} · ${Number(item.basisQuantity || 0).toFixed(3).replace(/\.0+$/, "")} ${operationBasisLabels[item.basis] || item.basis}`,
        Number(item.total || 0)
      ]);
      const appliedProcessNames = [...new Set([...formulaOperationItems.map(item => item.name), ...processHits.map(item => item.name), ...catalogOperationPrices.map(item => item.name)].filter(Boolean))];
      const appliedProcessTotal = formulaOperationCost + processTotal;
      const appliedProcessRuleText = [
        ...formulaOperationItems.map(item => `${item.name} ¥${Number(item.total || 0).toFixed(2)}（${item.sheetFormat?.label ? `${item.sheetFormat.automatic ? "自动识别" : "手动指定"}${item.sheetFormat.label}，` : ""}${item.laminationSideCount ? `${item.laminationSideCount === 2 ? "双面" : "单面"}，` : ""}按${operationBasisLabels[item.basis] || item.basis}核算）`),
        ...processHits.map(item => `${item.name} ¥${item.price.toFixed(3)}/个`),
        ...catalogProcessPrices.map(item => `${item.name}：${quoteOperationRuleDescription(item, qty)}`),
        ...catalogPrintPrices.map(item => `${item.name}：${quoteOperationRuleDescription(item, qty)}`)
      ].join("；");
      const materialLineLabel = materialConversionMode
        ? `${materialConversion.source?.name || val("quoteMaterial")} · ${materialConversion.price.toFixed(3)}${materialConversion.unit} · ${materialConversion.totalUsage.toFixed(3)}${materialConversion.usageUnit}`
        : formulaMode ? formulaBreakdown.materialLabel : `${materialRule?.name || val("quoteMaterial")} × ${qty.toFixed(0)}个`;
      const catalogPrintPriceText = catalogPrintPrices.map(item => `${item.name}：${quoteOperationRuleDescription(item, qty)}`).join("；");
      const printingBasisText = costMode === "printing"
        ? formulaBreakdown.basis
        : catalogPrintPrices.length ? `本厂所选印刷费用：${catalogPrintPriceText}`
          : legacyRulesConfirmed ? (costMode === "industry" ? "订单准备/开机固定费用" : `基础加工费 + ${qty.toFixed(0)}个单位加工`) : "本单未选择印刷费用";
      const costLineItems = window.JIANGLAI_QUOTE_COST_DETAILS.build({
        costMode, formulaBreakdown, materialConversionMode, materialConversion,
        materialCost, effectiveMaterialUnitCost, qty, materialName:val("quoteMaterial"),
        printing, printRoute:val("quotePrintRoute"), catalogPrintPrices, processCosts,
        operationBasisLabels, printingBasisText,
        operationCalculation:quoteOperationPriceCalculation,
        operationDescription:quoteOperationRuleDescription
      });
      if (manualAdjustments.active) {
        costLineItems.push({
          category:"人工修正",
          name:"本次报价生产参数",
          pricingUnit:"元/次",
          unitPrice:0,
          usage:1,
          usageUnit:"次",
          subtotal:0,
          source:"仅本次报价生效",
          detail:`${manualAdjustments.productionSheets ? `生产印张 ${manualAdjustments.productionSheets}` : ""}${manualAdjustments.productionSheets && manualAdjustments.plateCount ? "；" : ""}${manualAdjustments.plateCount ? `印版/色令 ${manualAdjustments.plateCount}` : ""}；原因：${manualAdjustments.reason}`
        });
      }
      const lineItems = [
        ["材料", materialLineLabel, materialCost],
        ["基础加工", printingBasisText, printing],
        ...(laborCost > 0 ? [["人工", costMode === "printing" ? `${printingWorkHours.toFixed(3)}小时 × ${printingOperatorCount}人 × ¥${appliedLaborHourly.toFixed(2)}/时` : `${num("quoteLaborMinutes", 0).toFixed(2)}分钟/件 × ¥${appliedLaborHourly.toFixed(2)}/时`, laborCost]] : []),
        ...(machineCost > 0 ? [["设备", costMode === "printing" ? `${printingWorkHours.toFixed(3)}机时 × ¥${appliedMachineHourly.toFixed(2)}/时` : `${num("quoteMachineMinutes", 0).toFixed(2)}分钟/件 × ¥${appliedMachineHourly.toFixed(2)}/时`, machineCost]] : []),
        ...(extraDirectCost > 0 ? [[formulaBreakdown.formulaResult?.outsourced ? "外发" : "电子装配/测试", formulaBreakdown.extraLabel, extraDirectCost]] : []),
        ...(manualAdjustments.active ? [["人工修正", `${manualAdjustments.productionSheets ? `生产印张 ${manualAdjustments.productionSheets}` : ""}${manualAdjustments.productionSheets && manualAdjustments.plateCount ? "；" : ""}${manualAdjustments.plateCount ? `印版/色令 ${manualAdjustments.plateCount}` : ""}；原因：${manualAdjustments.reason}`, 0]] : []),
        ...formulaProcessLineItems,
        ...processCosts.map(item => ["工艺", item.basis ? `${item.name} · ${quoteOperationRuleDescription(item, qty)}` : `${item.name} · ¥${Number(item.unitPrice ?? item.price ?? 0).toFixed(4)}/个`, item.total]),
        ...(overheadCost > 0 ? [["制造间接费", `${rateText(s.overheadRate || 0)}%`, overheadCost]] : []),
        costMode === "printing" && formulaBreakdown.formulaResult?.outsourced ? ["外发范围", `${formulaBreakdown.formulaResult.supplier?.name || "供应商"}同规格成品采购价；材料、印刷、工艺已含，不重复计费`, 0] : costMode === "printing"
          ? ["材料损耗", formulaBreakdown.lossLabel || `净印张 ${formulaBreakdown.formulaResult.sheets.netSheets} → 生产印张 ${formulaBreakdown.formulaResult.sheets.productionSheets}，已计入材料成本`, 0]
          : materialConversionMode
            ? ["其他生产损耗", `${Math.round(s.loss * 100)}%（材料损耗已在材料换算中计入）`, loss]
            : ["损耗", `${Math.round(s.loss * 100)}%`, loss],
        ...(Math.abs(accuracyAdjustment) >= .005 ? [["真实订单校准", `高置信本厂实际成本样本 × ${quoteAccuracyFactor.toFixed(4)}`, accuracyAdjustment]] : []),
        ["毛利", `${Math.round(profitRate * 100)}%`, profit],
        ["税费", taxMode, tax],
        ["运费", "统一另计，不计入报价", 0],
        ["打样费", sampleMode === "收费打样" ? "客户承担/可抵扣需另议" : sampleMode, sample]
      ];
      const quoteCustomer = currentCustomerName();
      syncQuoteCustomerToActiveSession(quoteCustomer);
      const quoteProduct = val("quoteProduct");
      const previousComparableQuote = (state.quoteHistory || []).find(item =>
        item.customer === quoteCustomer &&
        normalizedQuoteValue(item.product) === normalizedQuoteValue(quoteProduct) &&
        normalizedQuoteValue(item.size) === normalizedQuoteValue(val("quoteSize")) &&
        normalizedQuoteValue(item.material) === normalizedQuoteValue(val("quoteMaterial")) &&
        Number(item.qty || 0) === Number(qty)
      );
      const priceChangePct = previousComparableQuote && Number(previousComparableQuote.unit || 0) > 0
        ? (unit / Number(previousComparableQuote.unit) - 1) * 100
        : 0;
      const materialCostChangePct = previousComparableQuote && Number(previousComparableQuote.materialUnitCost || 0) > 0
        ? (effectiveMaterialUnitCost / Number(previousComparableQuote.materialUnitCost) - 1) * 100
        : 0;
      const approvalReasons = [
        marginRate < 15 ? `毛利率仅 ${marginRate.toFixed(1)}%` : "",
        total >= 50000 ? `报价金额 ¥${total.toFixed(0)}` : "",
        isBelowMoq ? `数量低于起订量 ${moq.toLocaleString()} 个` : "",
        rulesAge > 30 ? `价格表已 ${rulesAge} 天未确认` : "",
        Math.abs(priceChangePct) >= 20 ? `同规格单价较上次${priceChangePct > 0 ? "上涨" : "下降"}${Math.abs(priceChangePct).toFixed(1)}%` : "",
        Math.abs(materialCostChangePct) >= 20 ? `同规格材料成本较上次${materialCostChangePct > 0 ? "上涨" : "下降"}${Math.abs(materialCostChangePct).toFixed(1)}%` : "",
        manualAdjustments.active ? `人工修正生产参数：${manualAdjustments.reason}` : "",
        ...(window.__currentQuoteOutlierWarnings || []).map(item => `价格异常已人工确认：${item}`)
      ].filter(Boolean);
      const productConfiguration = quoteCatalogConfigurationSnapshot();
      const quoteCreatedAt = new Date().toISOString();
      const formulaSnapshot = costMode === "printing" ? cloneQuoteSnapshot({
        schemaVersion: 1,
        engine: "sheet-quote-v1",
        input: formulaBreakdown.formulaInput,
        result: formulaBreakdown.formulaResult
      }) : costMode === "industry" ? cloneQuoteSnapshot({
        schemaVersion: formulaBreakdown.schemaVersion || 1,
        engine: formulaBreakdown.engine || "industry-quote-v2",
        strategyId: formulaBreakdown.strategyId,
        strategyLabel: formulaBreakdown.strategyLabel,
        input: formulaBreakdown.input,
        result: formulaBreakdown.result
      }) : materialConversionMode ? cloneQuoteSnapshot({
        schemaVersion: 1,
        engine: "material-purchase-conversion-v1",
        input: {
          purchasePrice: materialConversion.price,
          purchaseUnit: materialConversion.unit,
          dimensionsMm: materialConversion.dimensions,
          gsm: materialConversion.gsm,
          utilizationRate: materialConversion.utilization,
          wasteRate: materialConversion.wasteRate,
          quantity: qty
        },
        result: {
          netUsagePerPiece: materialConversion.netUsage,
          grossUsagePerPiece: materialConversion.grossUsage,
          usageUnit: materialConversion.usageUnit,
          totalUsage: materialConversion.totalUsage,
          materialCost,
          materialUnitCost
        }
      }) : null; const marketComparison = window.JIANGLAI_QUOTE_MARKET_BENCHMARKS?.compare({ product:quoteProduct, size:val("quoteSize"), material:val("quoteMaterial"), configurationText:quoteCatalogConfigurationText(productConfiguration), quantity:qty, internalCost }) || null;
      const costSnapshot = costMode === "printing" ? cloneQuoteSnapshot({
        schemaVersion: 1,
        versionId: formulaBreakdown.costVersionId || "",
        effectiveAt: formulaBreakdown.costVersionEffectiveAt || "",
        capturedAt: quoteCreatedAt,
        hourlyRates: {
          labor: appliedLaborHourly,
          machine: appliedMachineHourly,
          overheadRate: Number(s.overheadRate || 0)
        },
        targetGrossMargin: profitRate,
        taxRate: taxMode === "含税" ? Number(s.tax || 0) : 0,
        items: formulaBreakdown.usedCostItems || []
      }) : costMode === "industry" ? cloneQuoteSnapshot({
        schemaVersion: 1,
        versionId: ensureCostLibrary().activeVersionId || "",
        effectiveAt: ensureCostLibrary().effectiveAt || "",
        capturedAt: quoteCreatedAt,
        industryCategory: currentIndustryCategory(),
        strategyId: formulaBreakdown.strategyId,
        hourlyRates: {
          labor: appliedLaborHourly,
          machine: appliedMachineHourly,
          overheadRate: Number(s.overheadRate || 0)
        },
        targetGrossMargin: profitRate,
        taxRate: taxMode === "含税" ? Number(s.tax || 0) : 0,
        calibration: s.industryCalibration || null,
        items: []
      }) : materialConversionMode ? cloneQuoteSnapshot({
        schemaVersion: 1,
        versionId: ensureCostLibrary().activeVersionId || "",
        effectiveAt: ensureCostLibrary().effectiveAt || "",
        capturedAt: quoteCreatedAt,
        hourlyRates: {
          labor: Number(s.laborHourly || 0),
          machine: Number(s.machineHourly || 0),
          overheadRate: Number(s.overheadRate || 0)
        },
        targetGrossMargin: profitRate,
        taxRate: taxMode === "含税" ? Number(s.tax || 0) : 0,
        items: materialConversion.source?.item ? [materialConversion.source.item] : []
      }) : null;
      const costEvidence = window.JIANGLAI_QUOTE_RELIABILITY?.costEvidence(
        { costSnapshot }, quoteCreatedAt
      ) || null;
      if (costEvidence?.requiresApproval) approvalReasons.push(costEvidence.reason);
      const quoteRecord = {
        quoteNo: newQuoteNumber(),
        version: nextQuoteVersion(quoteCustomer, quoteProduct),
        time: new Date().toLocaleString("zh-CN", { hour12: false }),
        createdAt: quoteCreatedAt,
        customer: quoteCustomer,
        product: quoteProduct,
        size: val("quoteSize"),
        material: val("quoteMaterial"),
        process,
        qty,
        total,
        unit,
        minAcceptTotal,
        urgentTotal,
        estimatedCost: internalCost,
        estimatedMaterialCost: materialCost,
        estimatedProcessCost: printing + appliedProcessTotal + laborCost + machineCost,
        estimatedOtherCost: loss + overheadCost + extraDirectCost + tax + sample + accuracyAdjustment,
        estimatedProfit: profit,
        marginRate,
        rawEstimatedCost:rawProfitBase + tax + sample,
        quoteAccuracy:cloneQuoteSnapshot({ autoApplied:quoteAccuracyResult.autoApplied, factor:quoteAccuracyFactor, reason:quoteAccuracyResult.reason,
          diagnostics:quoteAccuracyResult.calibration?.diagnostics || null, suggestion:quoteAccuracyResult.suggestion || null }), marketComparison:cloneQuoteSnapshot(marketComparison),
        pricingUpdatedAt: s.quoteRulesUpdatedAt,
        costMode,
        materialCostDraft: quoteMaterialCostDraft(),
        costVersionId: costSnapshot?.versionId || "",
        costSnapshot,
        costEvidence: cloneQuoteSnapshot(costEvidence),
        formulaSnapshot,
        manualAdjustments: cloneQuoteSnapshot(manualAdjustments),
        industryCategory: currentIndustryCategory(),
        industryStrategyId: formulaSnapshot?.strategyId || costSnapshot?.strategyId || "",
        materialRule: costMode === "printing" ? `精细印刷公式 · ${val("quotePrintRoute") || "offset"}` : costMode === "industry" ? `${currentIndustryCategory()}行业公式` : materialConversionMode ? `${materialConversion.price.toFixed(3)}${materialConversion.unit} · 自动换算` : materialRule?.name || val("quoteMaterial"),
        materialUnitCost: effectiveMaterialUnitCost,
        processRules: appliedProcessNames,
        operationPriceSnapshot: cloneQuoteSnapshot(catalogOperationPrices),
        costLineItems: cloneQuoteSnapshot(costLineItems),
        standardUnit,
        customerPriceSource: customerPriceResolution.source,
        customerPriceSourceLabel: customerPriceResolution.sourceLabel,
        freightRule: "统一不含运费",
        taxMode,
        freightMode,
        freight,
        sampleMode,
        sample,
        delivery: quoteDeliveryText(),
        payment: val("quotePayment"),
        customerLevel: level,
        productConfiguration,
        approvalStatus: approvalReasons.length ? "待老板确认" : "可发送",
        approvalReason: approvalReasons.join("；"),
        safetyDetails: {
          rulesAge,
          materialText: (formulaMode || materialConversionMode) ? val("quoteMaterial") : materialRule?.name || val("quoteMaterial"),
          materialUnitCost: effectiveMaterialUnitCost,
          materialUnitCostConfirmed: true,
          processText: appliedProcessNames.join("、") || "基础加工",
          taxMode,
          freightMode,
          marginRate,
          previousQuoteNo: previousComparableQuote?.quoteNo || "",
          previousUnit: Number(previousComparableQuote?.unit || 0),
          previousMaterialUnitCost: Number(previousComparableQuote?.materialUnitCost || 0),
          priceChangePct,
          materialCostChangePct,
          uncertainFields: [...(window.__quoteUncertainFields || [])],
          uncertainConfirmed: Boolean(document.getElementById("quoteAiConfirmed")?.checked)
        },
        caseSource: quoteBusinessContext().caseSource || "",
        followupTaskId: Number(quoteBusinessContext().taskServerId || 0),
        batchId: String(window.__activeQuoteBatchContext?.batchId || ""),
        batchName: String(window.__activeQuoteBatchContext?.batchName || "").slice(0, 40),
        batchItemId: String(window.__activeQuoteBatchContext?.batchItemId || ""),
        status: "已报价"
      };
      if (costMode === "unit") rememberQuoteSpecMaterialCost(materialUnitCost);
      state.quoteHistory = [quoteRecord, ...(state.quoteHistory || [])].slice(0, 30);
      window.__activeQuoteRecordKey = quoteRecordKey(quoteRecord);
      appendCustomerHistory(currentCustomerName(), `生成报价：${val("quoteProduct")}，${qty.toFixed(0)}个，总价 ¥${total.toFixed(0)}，单价 ¥${unit.toFixed(3)}`, {
        need: val("quoteProduct"),
        dealStatus: "已报价",
        preference: `${val("quoteMaterial")}；${process}`
      });
      updateCustomerQuoteMemory(quoteRecord);
      ensureQuoteFollowupTask(quoteRecord, false);
      saveState();
      const tierCards = document.getElementById("quoteTierCards");
      if (tierCards) {
        tierCards.innerHTML = `
          <div class="quote-tiers">
            <div class="quote-tier"><span>建议客户报价</span><strong>¥${total.toFixed(0)}</strong><em>约 ¥${unit.toFixed(3)}/个 · ${escapeHtml(customerPriceResolution.sourceLabel)}</em></div>
            <div class="quote-tier"><span>最低成交价</span><strong>¥${minAcceptTotal.toFixed(0)}</strong><em>约 ¥${minAcceptUnit.toFixed(3)}/个，业务守底线</em></div>
            <div class="quote-tier"><span>加急（${taxMode}，${customerFreightModeLabel(freightMode)}）</span><strong>¥${urgentTotal.toFixed(0)}</strong><em>约 ¥${urgentUnit.toFixed(3)}/个，急单参考</em></div>
          </div>
        `;
      }
      const decisionSummary = document.getElementById("quoteDecisionSummary");
      if (decisionSummary) {
        const priceUpdatedText = s.quoteRulesUpdatedAt
          ? new Date(s.quoteRulesUpdatedAt).toLocaleString("zh-CN", { hour12: false })
          : "未记录";
        decisionSummary.innerHTML = `
          <div><span>内部成本价</span><strong>¥${internalCost.toFixed(2)}</strong></div>
          <div><span>预计毛利</span><strong>¥${profit.toFixed(2)}</strong></div>
          <div class="${marginRate < 15 ? "is-warning" : ""}"><span>毛利率与判断</span><strong>${marginRate.toFixed(1)}% · ${escapeHtml(marginRate < 15 ? "偏低，先复核" : marginRate < 18 ? "一般，少让价" : "相对健康")}</strong></div>
          <div class="${rulesAge > 30 ? "is-warning" : ""}"><span>价格表最后确认</span><strong>${escapeHtml(priceUpdatedText)}${rulesAge > 30 ? ` · 已${rulesAge}天` : ""}</strong></div>
        `;
      }
      renderQuoteAccuracySummary(quoteAccuracyResult); renderQuoteMarketComparison(marketComparison);
      renderQuoteApprovalPanel(quoteRecord); renderQuoteTrustSummary(quoteRecord, quoteRecord.safetyDetails);
      renderQuoteProfessionalRecord(quoteRecord); renderQuoteReliabilityPanel(quoteRecord); renderQuoteCostLineItems(costLineItems, quoteRecord);
      const costModeDisplay = costMode === "printing"
        ? "精细印刷公式"
        : costMode === "industry" ? `${currentIndustryCategory()}通用公式` : materialConversionMode ? "原料采购价换算" : "历史按件价格表";
      const materialBasisDisplay = costMode === "printing"
        ? `${val("quoteMaterial")}；${formulaBreakdown.materialLabel}`
        : costMode === "industry" ? val("quoteMaterial") : materialConversionMode
          ? `${val("quoteMaterial")}；采购价 ¥${materialConversion.price.toFixed(3)}${materialConversion.unit}；本单用量 ${materialConversion.totalUsage.toFixed(3)}${materialConversion.usageUnit}；折合 ¥${materialUnitCost.toFixed(4)}/个`
          : `${materialRule?.name || val("quoteMaterial")}（历史按件价 ¥${materialUnitCost.toFixed(3)}/个）`;
      const basisSummary = document.getElementById("quoteBasisSummary");
      if (basisSummary) {
        const priceUpdated = s.quoteRulesUpdatedAt
          ? new Date(s.quoteRulesUpdatedAt).toLocaleString("zh-CN", { hour12: false })
          : "尚未记录更新时间";
        basisSummary.innerHTML = `
          <div class="quote-basis-item"><span>材料成本</span><strong>¥${materialCost.toFixed(2)}</strong></div>
          <div class="quote-basis-item"><span>基础加工</span><strong>¥${printing.toFixed(2)}</strong></div>
          <div class="quote-basis-item"><span>工艺费用</span><strong>¥${appliedProcessTotal.toFixed(2)}</strong></div>
          <div class="quote-basis-item"><span>${costMode === "printing" ? "材料损耗（已计入材料）" : "损耗"}</span><strong>¥${loss.toFixed(2)}</strong></div>
          <div class="quote-basis-item"><span>目标毛利</span><strong>¥${profit.toFixed(2)}</strong></div>
          <div class="quote-basis-item"><span>税费</span><strong>¥${tax.toFixed(2)}</strong></div>
          <div class="quote-basis-foot">成本算法：${escapeHtml(costModeDisplay)}；材料：${escapeHtml(materialBasisDisplay)}；工艺：${escapeHtml(appliedProcessNames.join("、") || "基础加工")}。价格表最后确认：${escapeHtml(priceUpdated)}。</div>
        `;
      }
      const freightNotice = "特别提醒：本报价不含运费，运费另计。";
      const sampleCreditText = sampleMode === "不需要打样"
        ? "不涉及"
        : s.sampleCredit === "是" ? "大货成交后可按规则抵扣" : "默认不抵扣，需另行确认";
      const customerQuoteText = `客户可见报价：

客户：${currentCustomerName()}
产品：${val("quoteProduct")}
规格：${val("quoteSize")}
数量：${qty.toFixed(0)} 个
材质：${val("quoteMaterial")}
工艺：${process}
产品参数：${quoteCatalogConfigurationText(productConfiguration) || "按以上规格生产"}
报价口径：${taxMode}，不含运费
打样：${sampleMode}${sampleMode === "收费打样" ? `，费用 ¥${sample.toFixed(2)}` : ""}
打样抵扣：${sampleCreditText}
交期：${quoteDeliveryText()}
付款方式：${val("quotePayment")}
${freightNotice}

建议总价：¥${total.toFixed(2)}
建议单价：¥${unit.toFixed(3)} / 个

可直接发客户：
您好，按目前信息先给您预估：${val("quoteProduct")}，${val("quoteSize")}，${val("quoteMaterial")}，${process}，${qty.toLocaleString()}个，${taxMode}、不含运费，参考总价 ¥${total.toFixed(0)}，折合约 ¥${unit.toFixed(3)}/个。交期约 ${quoteDeliveryText()}。最终价格以确认规格、设计稿和工艺面积后为准。

${freightNotice}`;

      const internalQuoteText = `内部报价明细：

厂家内部决策：
三档报价：
1. 正常报价：¥${total.toFixed(2)}，约 ¥${unit.toFixed(3)}/个
2. 最低成交价：¥${minAcceptTotal.toFixed(2)}，约 ¥${minAcceptUnit.toFixed(3)}/个
3. 加急/${taxMode}/不含运费参考：¥${urgentTotal.toFixed(2)}，约 ¥${urgentUnit.toFixed(3)}/个

议价底线：
- 可议价空间：约 ¥${negotiationRoom.toFixed(2)}
- 当前毛利率：${marginRate.toFixed(1)}%
- 接单建议：${decision}
- 起订量判断：${isBelowMoq ? `低于 ${moq.toLocaleString()} 个起订量，建议按小单处理` : `已达到 ${moq.toLocaleString()} 个起订量`}

产品：${val("quoteProduct")}
尺寸：${val("quoteSize")}
数量：${qty.toFixed(0)} 个
材质：${val("quoteMaterial")}
工艺：${process}
客户等级：${level}
税费：${taxMode}
运费：不包含，运费另计
打样：${sampleMode}${sampleMode === "收费打样" ? `，费用 ¥${sample.toFixed(2)}` : ""}
打样抵扣：${sampleMode === "不需要打样" ? "不涉及" : s.sampleCredit === "是" ? "可抵扣" : "不抵扣"}
交期：${quoteDeliveryText()}
付款方式：${val("quotePayment")}

内部明细：
${lineItems.map(item => `- ${item[0]}：${item[1]} = ¥${Number(item[2]).toFixed(2)}`).join("\n")}

报价依据：
- 成本算法：${costModeDisplay}
- 材料规则：${costMode === "printing" ? `${formulaBreakdown.materialLabel}；${formulaBreakdown.basis}` : costMode === "industry" ? formulaBreakdown.basis : materialConversionMode ? `采购价 ¥${materialConversion.price.toFixed(3)}${materialConversion.unit}，本单实际用量 ${materialConversion.totalUsage.toFixed(3)}${materialConversion.usageUnit}，折合单件 ¥${materialUnitCost.toFixed(4)}` : `历史按件价：${materialRule?.name || val("quoteMaterial")}，¥${materialUnitCost.toFixed(3)}/个`}
- 工艺规则：${appliedProcessRuleText || "未命中具体工艺，按基础加工和材料估算"}
- 基础加工：${costMode === "printing" ? `${printingBasisText}；${formulaBreakdown.hourlyRates?.allInPrintPrice ? "人工与设备已含在本厂印工价，不重复计费" : `人工 ¥${appliedLaborHourly.toFixed(2)}/时，设备 ¥${appliedMachineHourly.toFixed(2)}/时`}，制造间接费 ${rateText(s.overheadRate || 0)}%` : catalogPrintPrices.length ? printingBasisText : legacyRulesConfirmed ? (costMode === "industry" ? `固定加工费 ¥${Number(s.print || 0).toFixed(2)}，人工 ¥${appliedLaborHourly.toFixed(2)}/时，设备 ¥${appliedMachineHourly.toFixed(2)}/时，制造间接费 ${rateText(s.overheadRate || 0)}%` : `固定加工费 ¥${Number(s.print || 0).toFixed(2)}，单位加工费 ¥${Number(s.printUnit || 0).toFixed(3)}`) : "本单未选择印刷费用"}
- 起订/加急/阶梯：起订量 ${Number(s.moq || 0).toLocaleString()} 个，加急费 ${rateText(s.urgentRate)}%，阶梯折扣 ${rateText(s.ladderDiscount)}%
- 运费规则：所有报价统一不含运费，运费不参与任何价格计算
- 损耗/税点/利润：${costMode === "printing" ? (formulaBreakdown.lossLabel || `材料损耗已按净印张 ${formulaBreakdown.formulaResult.sheets.netSheets} → 生产印张 ${formulaBreakdown.formulaResult.sheets.productionSheets}计入材料成本`) : `损耗 ${rateText(s.loss)}%`}，税点 ${rateText(s.tax)}%，当前客户利润 ${rateText(profitRate)}%
- 口径提醒：${taxMode}，不含运费，${sampleMode}${sampleMode === "收费打样" ? ` ¥${sample.toFixed(2)}` : ""}，打样${s.sampleCredit === "是" ? "可抵扣" : "不抵扣"}

内部成本合计：¥${internalCost.toFixed(2)}
毛利金额：¥${profit.toFixed(2)}
毛利率参考：${marginRate.toFixed(1)}%

数量阶梯参考：
${ladder}

风险提醒：
- ${warnings || "报价前仍需确认最终规格、设计稿、工艺要求、包装方式和验收标准。"}`;
      quoteRecord.customerText = customerQuoteText;
      quoteRecord.internalText = internalQuoteText;
      renderQuoteLiveDock(quoteRecord);
      saveState();
      setQuoteOutputs(customerQuoteText, internalQuoteText);
      renderQuoteHistory();
      renderTasks();
      renderCustomerProfiles();
      renderDashboard();
      renderQuoteCustomerMemory();
      quoteGuideStep = 4;
      quoteGuideHasResult = true;
      if (!window.__suppressQuoteResultNavigation) revealQuoteResult();
      return true;
    }
    function openQuoteOutcome(status) {
      const form = document.getElementById("quoteOutcomeForm");
      const record = activeQuoteRecord();
      if (!form || !record) {
        notify("请先生成报价，再记录客户结果。");
        return;
      }
      window.__quoteOutcomeMode = status;
      form.classList.remove("is-hidden");
      document.getElementById("quoteOutcomeTotalField")?.classList.toggle("is-hidden", status !== "已成交");
      document.getElementById("quoteOutcomeQtyField")?.classList.toggle("is-hidden", status !== "已成交");
      document.querySelectorAll(".quote-actual-cost-field").forEach(item => item.classList.toggle("is-hidden", status !== "已成交"));
      setIfExists("quoteOutcomeTotal", status === "已成交" ? String(Number(record.total || 0).toFixed(2)) : "");
      setIfExists("quoteOutcomeQty", status === "已成交" ? String(Number(record.qty || 0).toFixed(0)) : "");
      setIfExists("quoteOutcomeMaterialCost", status === "已成交" ? String(Number(record.actualMaterialCost || 0) || "") : "");
      setIfExists("quoteOutcomeProcessCost", status === "已成交" ? String(Number(record.actualProcessCost || 0) || "") : "");
      setIfExists("quoteOutcomeOtherCost", status === "已成交" ? String(Number(record.actualOtherCost || 0) || "") : "");
      setIfExists("quoteOutcomeReason", "");
      const reason = document.getElementById("quoteOutcomeReason");
      if (reason) reason.placeholder = status === "已成交" ? "例如：交期快、样品满意" : status === "客户压价" ? "例如：客户目标价是多少" : "例如：价格高、交期不合适、客户暂停";
      setTextIfExists("quoteOutcomeStatus", `正在记录：${status}。保存后会更新客户档案、经营数据和跟单阶段。`);
      renderQuoteOutcomeProfitPreview();
      form.scrollIntoView({ behavior: "smooth", block: "nearest" });
    }
    function fillOutcomeEstimatedCosts() {
      const record = activeQuoteRecord();
      if (!record) return notify("请先打开一份成交报价。");
      const material = Number(record.estimatedMaterialCost || 0);
      const process = Number(record.estimatedProcessCost || 0);
      const other = Number(record.estimatedOtherCost || 0);
      if (!(material + process + other > 0)) {
        return notify("这份历史报价没有分项预算成本，请直接填写财务实际数。");
      }
      setIfExists("quoteOutcomeMaterialCost", material.toFixed(2));
      setIfExists("quoteOutcomeProcessCost", process.toFixed(2));
      setIfExists("quoteOutcomeOtherCost", other.toFixed(2));
      renderQuoteOutcomeProfitPreview();
      setTextIfExists("quoteOutcomeStatus", "已带入报价时的预算成本。保存前请按采购、生产、外协和返工的实际发生数修改；预算不等于实际成本。");
      notify("预算成本已带入，请核对财务实际数后保存。");
    }
    function closeQuoteOutcome() {
      document.getElementById("quoteOutcomeForm")?.classList.add("is-hidden");
      window.__quoteOutcomeMode = "";
      setTextIfExists("quoteOutcomeStatus", "报价已留档，后续有结果时在这里点一下即可。");
    }
    function saveQuoteOutcome() {
      const status = window.__quoteOutcomeMode;
      const record = activeQuoteRecord();
      if (!status || !record) return;
      const actualTotal = Number(val("quoteOutcomeTotal") || 0);
      const actualQty = Number(val("quoteOutcomeQty") || 0);
      const actualMaterialCost = Number(val("quoteOutcomeMaterialCost") || 0);
      const actualProcessCost = Number(val("quoteOutcomeProcessCost") || 0);
      const actualOtherCost = Number(val("quoteOutcomeOtherCost") || 0);
      const profitMetrics = quoteActualProfitMetrics(actualTotal, actualMaterialCost, actualProcessCost, actualOtherCost);
      const reason = val("quoteOutcomeReason").trim();
      if (status === "已成交" && !(actualTotal > 0)) {
        document.getElementById("quoteOutcomeTotal")?.focus();
        notify("请填写实际成交额，首页才能准确统计业绩。");
        return;
      }
      if (status === "已流失" && !reason) {
        document.getElementById("quoteOutcomeReason")?.focus();
        notify("请简单填写未成交原因，方便以后复盘。");
        return;
      }
      const customer = record.customer || currentCustomerName();
      appendCustomerHistory(customer, `报价结果更新为${status}${reason ? `：${reason}` : ""}`, {
        dealStatus: status,
        need: record.product || val("quoteProduct"),
        preference: `${record.material || val("quoteMaterial")}；${record.process || val("quoteProcess")}`
      });
      const profile = (state.customerProfiles || []).find(item => item.company === customer);
      const stamp = new Date().toLocaleString("zh-CN", { hour12: false });
      if (profile) {
        profile.dealStatus = status;
        profile.actualDealTotal = status === "已成交" ? actualTotal : 0;
        profile.actualDealQty = status === "已成交" ? actualQty : 0;
        profile.outcomeReason = reason;
        profile.lastDealAt = ["已成交", "已流失"].includes(status) ? new Date().toISOString().slice(0, 10) : "";
        profile.dealNotes = `${profile.dealNotes || ""}\n${stamp}：报价 ${record.quoteNo || ""} 更新为${status}${reason ? `，原因：${reason}` : ""}`.trim();
        profile.updatedAt = stamp;
        window.__saveCustomerProfileCloud?.(profile);
      }
      record.outcomeStatus = status;
      record.actualDealTotal = status === "已成交" ? actualTotal : 0;
      record.actualDealQty = status === "已成交" ? actualQty : 0;
      record.actualMaterialCost = status === "已成交" ? actualMaterialCost : 0;
      record.actualProcessCost = status === "已成交" ? actualProcessCost : 0;
      record.actualOtherCost = status === "已成交" ? actualOtherCost : 0;
      record.actualTotalCost = status === "已成交" ? profitMetrics.actualCost : 0;
      record.actualProfit = status === "已成交" && profitMetrics.hasCost ? profitMetrics.actualProfit : null;
      record.actualMarginRate = status === "已成交" && profitMetrics.hasCost ? profitMetrics.actualMarginRate : null;
      record.outcomeReason = reason;
      window.__saveQuoteOutcomeCloud?.(record, {
        status,
        actual_deal_total: record.actualDealTotal,
        actual_deal_qty: record.actualDealQty,
        actual_material_cost: record.actualMaterialCost,
        actual_process_cost: record.actualProcessCost,
        actual_other_cost: record.actualOtherCost,
        reason
      });
      const task = (state.tasks || []).find(item =>
        String(item.customer || "").includes(customer) &&
        (item.source === "quote_auto" || item.title === `${record.product || val("quoteProduct")} 报价后跟进`)
      );
      let keptClosedTask = false;
      if (task) {
        const tomorrow = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
        if (status === "已成交") {
          keptClosedTask = ["待质检", "待发货", "已发货", "已签收", "待回款", "已回款", "已完成", "已归档"].includes(task.status);
          if (!keptClosedTask) {
            task.status = "生产";
            if (!task.ownerUserId) task.owner = "生产";
            task.nextAction = "确认定金、生产资料和交期，安排生产";
            task.nextFollow = `${tomorrow} 09:00`;
          }
          task.amount = actualTotal;
        } else if (status === "已流失") {
          task.status = "已完成";
          task.nextAction = `订单未成交，已归档${reason ? `：${reason}` : ""}`;
          task.nextFollow = "无需跟进";
        } else {
          task.status = "已报价";
          task.owner = "业务";
          task.nextAction = `客户压价，复核最低成交价并继续跟进${reason ? `：${reason}` : ""}`;
          task.nextFollow = `${tomorrow} 09:00`;
        }
        task.updatedAt = new Date().toISOString();
        task.logs = [...(task.logs || []), `${stamp} 客户结果：${status}${reason ? `（${reason}）` : ""}`].slice(-20);
        window.__setFormalTaskOutcome?.(task);
      }
      saveState();
      renderCustomerProfiles();
      renderTasks();
      renderDashboard();
      renderQuoteCustomerMemory();
      renderQuoteProfessionalRecord(record); renderQuoteReliabilityPanel(record);
      document.getElementById("quoteOutcomeForm")?.classList.add("is-hidden");
      const resultText = status === "已成交"
        ? `已记录成交 ¥${actualTotal.toFixed(2)}${profitMetrics.hasCost ? `，实际利润 ¥${profitMetrics.actualProfit.toFixed(2)}` : ""}，${keptClosedTask ? "原跟单闭环状态保持不变" : "跟单已进入生产阶段"}。`
        : status === "已流失"
          ? `已记录流失原因“${reason}”，该跟单不再占用待办。`
          : "已记录客户压价，跟单将在明天提醒继续推进。";
      setTextIfExists("quoteOutcomeStatus", resultText);
      notify(resultText);
      window.__quoteOutcomeMode = "";
    }
    function copyQuote() {
      const output = quoteCustomerText();
      if (!output || output.includes("点击左侧") || output.includes("参数已修改")) {
        alert("请先生成报价单。");
        return;
      }
      if (!canSendCurrentQuote()) return;
      navigator.clipboard?.writeText(output);
      alert("客户报价话术已复制。");
    }
    function quoteCustomerText() {
      const customer = document.getElementById("quoteCustomerOutput")?.textContent || "";
      if (customer && !customer.includes("点击左侧")) return customer.trim();
      const output = document.getElementById("quoteOutput")?.textContent || "";
      return output.split("内部报价明细：")[0]?.trim() || output;
    }
    function buildFormalQuoteHtml() {
      const output = quoteCustomerText();
      if (!output || output.includes("点击左侧") || output.includes("参数已修改")) {
        alert("请先生成报价单。");
        return "";
      }
      if (!canSendCurrentQuote()) return "";
      const record = activeQuoteRecord() || {};
      const createdAt = new Date(record.createdAt || Date.now());
      const validUntil = new Date(createdAt);
      validUntil.setDate(validUntil.getDate() + 7);
      const stamp = createdAt.toISOString().replace(/\D/g, "").slice(0, 14);
      const quoteNo = record.quoteNo || `BJ-${stamp.slice(0, 8)}-${stamp.slice(8, 14)}`;
      const factory = state.member.companyName || "我的工厂";
      const contact = state.member.contact || "业务负责人";
      const account = state.member.account || "联系方式待补充";
      const total = Number(record.total || 0);
      const unit = Number(record.unit || 0);
      const freightText = "本报价不含运费，运费另计";
      return `<!doctype html><html><head><meta charset="utf-8"><title>正式报价单-${escapeHtml(val("quoteProduct") || "未命名产品")}</title><style>
        *{box-sizing:border-box}body{margin:0;background:#f3f4f6;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,'Microsoft YaHei',Arial,sans-serif}.sheet{width:min(900px,calc(100% - 32px));margin:24px auto;background:#fff;padding:38px 42px;box-shadow:0 18px 55px rgba(15,23,42,.12)}.head{display:flex;justify-content:space-between;gap:28px;padding-bottom:22px;border-bottom:3px solid #111827}.brand{font-size:24px;font-weight:900}.subtitle{margin-top:5px;color:#6b7280;font-size:13px}.title{text-align:right;font-size:28px;font-weight:900}.no{margin-top:6px;color:#6b7280;font-size:12px}.meta{display:grid;grid-template-columns:repeat(2,1fr);gap:12px 28px;margin:24px 0}.meta div{border-bottom:1px solid #e5e7eb;padding:8px 0}.meta span{color:#6b7280;font-size:12px}.meta strong{float:right;font-size:13px}table{width:100%;border-collapse:collapse;margin-top:14px}th,td{border:1px solid #d1d5db;padding:11px 12px;text-align:left;font-size:13px}th{width:18%;background:#f9fafb;color:#4b5563}.pricebox{display:grid;grid-template-columns:1fr 1fr;margin:24px 0 18px;border:2px solid #111827}.pricebox div{padding:18px 20px}.pricebox div+div{border-left:1px solid #111827}.pricebox span{display:block;color:#6b7280;font-size:12px}.pricebox strong{display:block;margin-top:6px;font-size:24px}.terms{border-top:1px solid #d1d5db;padding-top:18px;font-size:12px;line-height:1.8;color:#4b5563}.terms h3{margin:0 0 6px;color:#111827;font-size:14px}.foot{display:flex;justify-content:space-between;gap:20px;margin-top:34px;padding-top:16px;border-top:1px solid #d1d5db;color:#6b7280;font-size:12px}.stamp{width:130px;height:70px;border:1px dashed #c7c9cf;display:grid;place-items:center;color:#9ca3af}@media print{body{background:#fff}.sheet{width:100%;margin:0;padding:24px;box-shadow:none}@page{size:A4;margin:12mm}}@media(max-width:640px){.sheet{padding:24px}.head,.foot{display:block}.title{text-align:left;margin-top:18px}.meta{grid-template-columns:1fr}.pricebox{grid-template-columns:1fr}.pricebox div+div{border-left:0;border-top:1px solid #111827}}
      </style></head><body><article class="sheet"><header class="head"><div><div class="brand">${escapeHtml(factory)}</div><div class="subtitle">工厂经营智能助手 · 正式报价单</div></div><div><div class="title">报价单</div><div class="no">编号：${escapeHtml(quoteNo)}</div></div></header><section class="meta"><div><span>客户名称</span><strong>${escapeHtml(currentCustomerName())}</strong></div><div><span>报价日期</span><strong>${createdAt.toLocaleDateString("zh-CN")}</strong></div><div><span>报价有效期</span><strong>${validUntil.toLocaleDateString("zh-CN")}</strong></div><div><span>报价口径</span><strong>${escapeHtml(val("quoteTaxMode"))} · 不含运费</strong></div></section><table><tr><th>产品</th><td>${escapeHtml(val("quoteProduct"))}</td><th>数量</th><td>${escapeHtml(val("quoteQty"))} 个</td></tr><tr><th>尺寸</th><td>${escapeHtml(val("quoteSize"))}</td><th>材质</th><td>${escapeHtml(val("quoteMaterial"))}</td></tr><tr><th>工艺</th><td colspan="3">${escapeHtml(val("quoteProcess"))}</td></tr>${quoteCatalogConfigurationText() ? `<tr><th>产品参数</th><td colspan="3">${escapeHtml(quoteCatalogConfigurationText())}</td></tr>` : ""}<tr><th>交期</th><td colspan="3">${escapeHtml(quoteDeliveryText())}</td></tr><tr><th>付款方式</th><td colspan="3">${escapeHtml(val("quotePayment"))}</td></tr></table><section class="pricebox"><div><span>参考单价</span><strong>¥${unit.toFixed(3)} / 个</strong></div><div><span>参考总价</span><strong>¥${total.toFixed(2)}</strong></div></section><section class="terms"><h3>报价说明</h3><div>1. ${escapeHtml(freightText)}。</div><div>2. 最终价格以确认规格、设计稿、工艺面积和包装方式后为准。</div><div>3. 批量生产前如涉及颜色、特殊工艺或结构，建议先打样并由双方确认。</div><div>4. 本报价有效期为7天，材料价格发生明显变化时需重新核价。</div></section><footer class="foot"><div>报价方：${escapeHtml(factory)}<br>联系人：${escapeHtml(contact)}<br>联系方式：${escapeHtml(account)}</div><div class="stamp">盖章位置</div></footer></article></body></html>`;
    }
    async function printQuote() {
      if (typeof window.__requireServerFeature === "function") {
        if (!await window.__requireServerFeature("报价导出", "quote_export")) return;
      } else if (!consumeQuota("报价导出", false)) return;
      const html = buildFormalQuoteHtml();
      if (!html) return;
      const win = window.open("", "_blank");
      if (!win) {
        notify("浏览器阻止了打印窗口，请允许弹出窗口后重试。" );
        return;
      }
      win.document.write(html);
      win.document.close();
      window.setTimeout(() => win.print(), 220);
    }
    async function exportQuoteHtml() {
      if (typeof window.__requireServerFeature === "function") {
        if (!await window.__requireServerFeature("报价导出", "quote_export")) return;
      } else if (!consumeQuota("报价导出", false)) return;
      const html = buildFormalQuoteHtml();
      if (!html) return;
      const blob = new Blob([html], { type: "text/html;charset=utf-8" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = `正式报价单-${currentCustomerName()}-${val("quoteProduct") || "未命名产品"}.html`;
      a.click();
      URL.revokeObjectURL(a.href);
      notify("正式报价单已导出，可打开后打印或保存为PDF。" );
    }
    function requireWonQuoteForDocument(documentName) {
      const record = activeQuoteRecord();
      if (!record) {
        notify(`请先生成报价，再制作${documentName}。`);
        return null;
      }
      if (record.outcomeStatus !== "已成交") {
        openQuoteOutcome("已成交");
        notify(`先记录客户已成交，系统会保留成交金额并继续生成${documentName}。`);
        return null;
      }
      return record;
    }
    function businessDocumentRows(record) {
      return `
        <tr><th>客户</th><td>${escapeHtml(record.customer || currentCustomerName())}</td><th>关联报价</th><td>${escapeHtml(record.quoteNo || "未编号")} · V${Number(record.version || 1)}</td></tr>
        <tr><th>产品</th><td>${escapeHtml(record.product || "定制产品")}</td><th>订单数量</th><td>${Number(record.actualDealQty || record.qty || 0).toLocaleString()} 个</td></tr>
        <tr><th>尺寸</th><td>${escapeHtml(record.size || "待确认")}</td><th>材质</th><td>${escapeHtml(record.material || "待确认")}</td></tr>
        <tr><th>工艺</th><td colspan="3">${escapeHtml(record.process || "待确认")}</td></tr>
        <tr><th>交期</th><td>${escapeHtml(record.delivery || "待确认")}</td><th>付款方式</th><td>${escapeHtml(record.payment || "待确认")}</td></tr>`;
    }
    function buildBusinessDocumentHtml(kind, record) {
      const definitions = {
        production: {
          title: "生产工单",
          note: "以下资料由成交报价自动带入；生产前须由业务、设计/工艺、生产三方核对。",
          body: `<section class="block"><h2>生产检查卡</h2><div class="checks">□ 已收定金　□ 客户确认稿已锁定　□ 材料规格已确认　□ 工艺图/刀模图已确认<br>□ 首件已检验　□ 色差/公差标准已确认　□ 包装方式已确认　□ 交期与排产已确认</div></section><section class="sign"><div>业务交接：________</div><div>生产接单：________</div><div>质检确认：________</div></section>`
        },
        delivery: {
          title: "送货单",
          note: "所有报价统一不含运费；实际物流方式和费用由双方另行确认。",
          body: `<section class="block"><h2>出货与签收</h2><div class="checks">本次发货数量：________　累计发货数量：________　欠货数量：________<br>物流/车牌：________　件数：________　收货地址：________________________</div></section><section class="sign"><div>发货人：________</div><div>收货人：________</div><div>签收日期：________</div></section>`
        },
        reconciliation: {
          title: "对账收款单",
          note: "按成交金额生成，实际开票、扣款或返工费用请在双方确认后填写。",
          body: `<section class="money"><div><span>成交金额</span><strong>¥${Number(record.actualDealTotal || record.total || 0).toFixed(2)}</strong></div><div><span>已收金额</span><strong>¥________</strong></div><div><span>待收尾款</span><strong>¥________</strong></div></section><section class="block"><h2>对账确认</h2><div class="checks">开票状态：________　发票类型：________　开票金额：________<br>扣款/补款说明：____________________________________________________</div></section><section class="sign"><div>业务确认：________</div><div>财务确认：________</div><div>客户确认：________</div></section>`
        }
      };
      const def = definitions[kind];
      const factory = state.member.companyName || "我的工厂";
      const docNo = `${kind === "production" ? "SC" : kind === "delivery" ? "SH" : "DZ"}-${String(record.quoteNo || Date.now()).replace(/^BJ-/, "")}`;
      return `<!doctype html><html><head><meta charset="utf-8"><title>${def.title}-${escapeHtml(record.customer || "客户")}</title><style>
      *{box-sizing:border-box}body{margin:0;background:#eef1f5;color:#18202c;font-family:-apple-system,BlinkMacSystemFont,'Microsoft YaHei',Arial,sans-serif}.sheet{width:min(920px,calc(100% - 32px));margin:24px auto;background:#fff;padding:34px 38px;box-shadow:0 16px 48px rgba(24,32,44,.13)}header{display:flex;justify-content:space-between;gap:24px;border-bottom:3px solid #18202c;padding-bottom:18px}.brand{font-size:22px;font-weight:900}.muted{margin-top:5px;color:#6b7280;font-size:12px}.title{text-align:right;font-size:28px;font-weight:900}.docno{text-align:right;margin-top:6px;color:#6b7280;font-size:11px}.notice{margin:18px 0;padding:10px 12px;background:#f5f8fc;border-left:4px solid #1769d2;font-size:12px;line-height:1.6}table{width:100%;border-collapse:collapse}th,td{border:1px solid #cfd5dd;padding:11px 12px;text-align:left;font-size:12px}th{width:16%;background:#f7f8fa;color:#5b6471}.block{margin-top:20px}.block h2{font-size:14px;margin:0 0 9px}.checks{min-height:110px;border:1px solid #cfd5dd;padding:14px;font-size:12px;line-height:2.25}.money{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:20px}.money div{padding:14px;border:1px solid #cfd5dd}.money span,.money strong{display:block}.money span{font-size:11px;color:#6b7280}.money strong{font-size:19px;margin-top:5px}.sign{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:32px;padding-top:18px;border-top:1px solid #cfd5dd;font-size:12px}.foot{margin-top:28px;color:#6b7280;font-size:10px} @media print{body{background:#fff}.sheet{width:100%;margin:0;box-shadow:none}@page{size:A4;margin:11mm}} @media(max-width:640px){.sheet{padding:24px}header{display:block}.title,.docno{text-align:left;margin-top:12px}.money,.sign{grid-template-columns:1fr}}
      </style></head><body><article class="sheet"><header><div><div class="brand">${escapeHtml(factory)}</div><div class="muted">工厂经营智能助手 · 订单资料中心</div></div><div><div class="title">${def.title}</div><div class="docno">编号：${escapeHtml(docNo)}</div></div></header><div class="notice">${escapeHtml(def.note)}</div><table>${businessDocumentRows(record)}</table>${def.body}<div class="foot">生成时间：${new Date().toLocaleString("zh-CN", { hour12:false })}　｜　关联报价：${escapeHtml(record.quoteNo || "未编号")}　｜　本单据修改后请重新确认版本。</div></article></body></html>`;
    }
    function downloadBusinessDocument(kind, label) {
      const record = requireWonQuoteForDocument(label);
      if (!record) return false;
      const html = buildBusinessDocumentHtml(kind, record);
      const blob = new Blob([html], { type:"text/html;charset=utf-8" });
      const link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = `${label}-${record.customer || "客户"}-${record.product || "产品"}.html`;
      link.click();
      URL.revokeObjectURL(link.href);
      notify(`${label}已生成，打开后可打印或保存为PDF。`);
      return true;
    }
    function exportProductionWorkOrder() {
      const record = activeQuoteRecord();
      if (!record || record.outcomeStatus !== "已成交") return downloadBusinessDocument("production", "生产工单");
      const task = (state.tasks || []).find(item => String(item.customer || "").includes(record.customer || "") && String(item.title || "").includes(record.product || ""));
      if (task) {
        task.status = "生产";
        task.owner = task.owner === "业务" ? "生产" : task.owner;
        task.nextAction = "核对定金、确认稿、材料、工艺、首件和交期后安排生产";
        task.logs = [...(task.logs || []), `${new Date().toLocaleString("zh-CN", {hour12:false})} 生成生产工单`].slice(-20);
        window.__setFormalTaskOutcome?.(task);
        saveState(); renderTasks(); renderDashboard();
      }
      return downloadBusinessDocument("production", "生产工单");
    }
    function exportDeliveryDocument() { return downloadBusinessDocument("delivery", "送货单"); }
    function exportReconciliationDocument() { return downloadBusinessDocument("reconciliation", "对账收款单"); }
    function escapeHtml(text) {
      return String(text || "").replace(/[&<>"']/g, ch => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[ch]));
    }
    function normalizedQuoteValue(value) {
      return String(value || "").toLowerCase().replace(/[×x*]/g, "x").replace(/[，、+；;\s]+/g, ",").replace(/^,|,$/g, "");
    }
    function renderSavedQuoteResult(record, options = {}) {
      if (!record) return false;
      window.__activeQuoteRecordKey = quoteRecordKey(record);
      quoteGuideHasResult = true;
      const total = Number(record.total || 0);
      const unit = Number(record.unit || 0);
      const floor = Number(record.minAcceptTotal || total * .9);
      const rush = Number(record.urgentTotal || total * 1.18);
      const componentCost = Number(record.estimatedMaterialCost || 0) + Number(record.estimatedProcessCost || 0) + Number(record.estimatedOtherCost || 0);
      const estimatedCost = Number(record.estimatedCost || componentCost || 0);
      const estimatedCostUnit = Number(record.qty || 0) > 0 ? estimatedCost / Number(record.qty) : 0;
      const estimatedProfit = Number(record.estimatedProfit ?? Math.max(0, total - estimatedCost));
      const margin = Number(record.marginRate || (total ? estimatedProfit / total * 100 : 0));
      const tiers = document.getElementById("quoteTierCards");
      if (tiers) tiers.innerHTML = `<div class="quote-tiers"><div class="quote-tier"><span>保存的建议报价</span><strong>¥${total.toFixed(0)}</strong><em>约 ¥${unit.toFixed(3)}/个 · ${escapeHtml(record.customerPriceSourceLabel || "系统标准报价")} · V${Number(record.version || 1)}</em></div><div class="quote-tier"><span>最低成交价</span><strong>¥${floor.toFixed(0)}</strong><em>历史版本内部底线，仅内部可见</em></div><div class="quote-tier"><span>加急参考</span><strong>¥${rush.toFixed(0)}</strong><em>以当前交期和工艺再次确认为准</em></div></div>`;
      const decision = document.getElementById("quoteDecisionSummary");
      if (decision) decision.innerHTML = `<div><span>内部成本价</span><strong>${estimatedCost ? `¥${estimatedCost.toFixed(2)}` : "见内部明细"}</strong></div><div><span>折合单件成本</span><strong>${estimatedCostUnit ? `¥${estimatedCostUnit.toFixed(4)}/个` : "历史记录"}</strong></div><div><span>预计利润</span><strong>${estimatedProfit ? `¥${estimatedProfit.toFixed(2)}` : "见内部明细"}</strong></div><div class="${margin && margin < 15 ? "is-warning" : ""}"><span>毛利率</span><strong>${margin ? `${margin.toFixed(1)}%` : "历史记录"}</strong></div>`;
      setQuoteOutputs(record.customerText || `客户报价已留档：${record.product || "定制产品"}，${Number(record.qty || 0).toLocaleString()}个，参考总价 ¥${total.toFixed(2)}。本报价不含运费，运费另计。`, record.internalText || "该历史报价已从云端恢复；完整成本明细以生成时留档内容为准。");
      if (record.quoteAccuracy?.suggestion) renderQuoteAccuracySummary({ calibration:{ diagnostics:record.quoteAccuracy.diagnostics || {} },
        suggestion:record.quoteAccuracy.suggestion, autoApplied:Boolean(record.quoteAccuracy.autoApplied),
        reason:record.quoteAccuracy.reason || "这是生成该版本时保存的校准记录" });
      else renderQuoteAccuracySummary(null); renderQuoteMarketComparison(record.marketComparison || null);
      renderQuoteReliabilityPanel(record); renderQuoteApprovalPanel(record);
      renderQuoteTrustSummary(record, record.safetyDetails || { marginRate: margin, materialText: record.material, processText: record.process, taxMode: record.taxMode || "税费以原报价为准", uncertainConfirmed: true });
      renderQuoteProfessionalRecord(record); renderQuoteCostLineItems(record.costLineItems || [], record);
      if (options.openPage !== false && !revealQuoteResult({ returnPage: options.returnPage || "报价系统" })) return false;
      return true;
    }
    function viewSavedQuote(recordKeyCode) {
      const key = decodeURIComponent(recordKeyCode);
      const record = (state.quoteHistory || []).find(item => quoteRecordKey(item) === key);
      if (!record) return false;
      showPage("报价系统");
      setIfExists("quoteCustomerName", record.customer || "新客户");
      setIfExists("quoteProduct", record.product || "定制产品");
      setIfExists("quoteSize", record.size || "");
      setIfExists("quoteQty", record.qty || "");
      setIfExists("quoteMaterial", record.material || "");
      setIfExists("quoteProcess", record.process || "");
      setIfExists("quoteCostMode", record.costMode || record.materialCostDraft?.costMode || "material");
      setQuoteDeliveryValue(record.delivery || state.settings.delivery || "");
      setIfExists("quotePayment", record.payment || state.settings.payment || "待确认");
      setIfExists("quoteTaxMode", record.taxMode || "待确认");
      setIfExists("quoteSampleMode", record.sampleMode || "待确认");
      restoreQuoteCatalogConfiguration(record.productConfiguration);
      const archivedMaterialItem = Array.isArray(record.costSnapshot?.items) ? record.costSnapshot.items[0] : null;
      restoreQuoteMaterialCostDraft(record.materialCostDraft || {
        costMode: record.costMode || "unit",
        purchasePrice: archivedMaterialItem?.price ?? "",
        purchaseUnit: archivedMaterialItem?.unit || "元/吨",
        unitCost: Number(record.materialUnitCost || 0) > 0 ? Number(record.materialUnitCost).toFixed(6) : "",
        confirmed: false
      });
      renderQuoteReviewSummary();
      renderSavedQuoteResult(record, { openPage: true, returnPage: "报价系统" });
      notify(`已打开 ${record.customer || "客户"} 的 V${Number(record.version || 1)} 历史报价，不会重复扣点。`);
      return true;
    }
    function restoreExactSavedQuoteResult() {
      const customer = normalizedQuoteValue(val("quoteCustomerName"));
      const product = normalizedQuoteValue(val("quoteProduct"));
      const size = normalizedQuoteValue(val("quoteSize"));
      const material = normalizedQuoteValue(val("quoteMaterial"));
      const process = normalizedQuoteValue(val("quoteProcess"));
      const qty = Number(val("quoteQty") || 0);
      const record = (state.quoteHistory || []).find(item =>
        normalizedQuoteValue(item.customer) === customer &&
        quoteProductsEquivalent(item.product, product) &&
        quoteSizesEquivalent(item.size, size) &&
        Number(item.qty || 0) === qty &&
        normalizedQuoteValue(item.material) === material &&
        normalizedQuoteValue(item.process) === process
      );
      return record && renderSavedQuoteResult(record, { openPage: false }) ? record : null;
    }
    function renderQuoteHistory() {
      const body = document.getElementById("quoteHistoryBody");
      if (!body) return;
      const history = businessQuotes();
      const hiddenTemporary = (state.quoteHistory || []).length - history.length;
      const rows = history.slice(0, 8).map(item => {
        const sameQuotes = history.filter(record => record.customer === item.customer && record.product === item.product);
        const inferredVersion = Math.max(1, sameQuotes.length - sameQuotes.findIndex(record => quoteRecordKey(record) === quoteRecordKey(item)));
        const version = Number(item.version || inferredVersion);
        return `
        <tr>
          <td><strong>V${version}</strong><br><span class="muted">${escapeHtml(item.quoteNo || "历史记录")}</span></td>
          <td>${escapeHtml(item.time || "")}</td>
          <td>${escapeHtml(item.customer || "新客户")}<br><span class="muted">${escapeHtml(item.product || "未命名产品")}</span></td>
          <td>${Number(item.qty || 0).toLocaleString()} 个</td>
          <td>¥${Number(item.total || 0).toFixed(0)}<br><span class="muted">¥${Number(item.unit || 0).toFixed(3)}/个</span></td>
          <td><span class="status">${escapeHtml(item.approvalStatus || "历史版本")}</span></td>
          <td><div class="actions"><button class="btn light" type="button" onclick="viewSavedQuote('${encodeURIComponent(quoteRecordKey(item))}')">查看结果</button><button class="btn light" type="button" onclick="reuseQuoteVersion('${encodeURIComponent(quoteRecordKey(item))}')">带入重报</button></div></td>
        </tr>
      `;
      }).join("");
      const hiddenNote = hiddenTemporary > 0
        ? `<tr><td colspan="7" class="muted">已隐藏 ${hiddenTemporary} 条未命名或演示报价。正式报价填写客户名称后，会在这里独立留档。</td></tr>`
        : "";
      body.innerHTML = rows
        ? `${rows}${hiddenNote}`
        : `<tr><td colspan="7" class="muted">暂无正式客户报价。填写客户名称并生成报价后，会在这里独立留档。</td></tr>${hiddenNote}`;
    }
    function quoteRuleChangeSummary(before = {}, after = {}) {
      if (!before.configured) return ["首次确认价格表"];
      const fields = [
        ["productType", "主营品类"], ["defaultMaterial", "默认材料"],
        ["capacityRules", "能力范围"], ["materialRules", "材料成本"],
        ["processRules", "加工费"], ["print", "基础加工费"],
        ["printUnit", "单位加工费"], ["sampleDefault", "默认打样费"],
        ["moq", "起订量"], ["urgentRate", "加急费"],
        ["loss", "损耗率"], ["tax", "税费率"],
        ["laborHourly", "完全人工成本"], ["machineHourly", "设备机时费"],
        ["overheadRate", "制造间接费"], ["utilizationRate", "材料利用率"],
        ["profitNormal", "普通客户利润"], ["delivery", "默认交期"],
        ["payment", "付款方式"]
      ];
      const changed = fields.filter(([key]) => JSON.stringify(before[key]) !== JSON.stringify(after[key])).map(([, label]) => label);
      return changed.length ? changed : ["重新确认价格表（参数无变化）"];
    }
    function renderQuoteRuleHistory() {
      const box = document.getElementById("quoteRuleHistoryList");
      if (!box) return;
      const history = state.quoteRuleHistory || [];
      box.innerHTML = history.slice(0, 10).map(item => `
        <div class="priority-item">
          <span class="priority-dot"></span>
          <span><strong>${escapeHtml(item.time || "未知时间")} · ${escapeHtml(item.category || "报价参数")}</strong><em>${escapeHtml((item.changes || []).join("、"))}</em></span>
        </div>
      `).join("") || `<div class="muted">还没有修改记录。首次保存价格表后会自动记录时间和改动项目。</div>`;
    }
    function saveQuoteRules() {
      const ruleRequired = [
        ["ruleProductType", "主营品类"],
        ["ruleDefaultMaterial", "默认材料"],
        ["ruleMaterials", "材料成本"],
        ["ruleProcesses", "加工费"],
        ["ruleDelivery", "默认交期"],
        ["rulePayment", "付款方式"]
      ];
      const missingRule = ruleRequired.find(item => !val(item[0]));
      if (missingRule) {
        document.getElementById(missingRule[0])?.focus();
        notify(`保存前请填写：${missingRule[1]}`);
        return false;
      }
      if (!(Number(normalizeQuoteDeliveryDays(val("ruleDelivery"))) > 0)) {
        document.getElementById("ruleDelivery")?.focus();
        notify("默认交期请输入大于 0 的天数，例如：7。");
        return false;
      }
      const materialRuleList = parseRules(val("ruleMaterials"));
      const processRuleList = parseRules(val("ruleProcesses"));
      if (!materialRuleList.length || !processRuleList.length) {
        const target = !materialRuleList.length ? "ruleMaterials" : "ruleProcesses";
        document.getElementById(target)?.focus();
        notify(`请按“名称=单价”的格式填写${!materialRuleList.length ? "材料" : "工艺"}价格。`);
        return false;
      }
      const invalidMaterialRule = materialRuleList.find(rule => !(Number(rule.price) > 0));
      const invalidProcessRule = processRuleList.find(rule => !(Number(rule.price) > 0));
      if (invalidMaterialRule || invalidProcessRule) {
        const target = invalidMaterialRule ? "ruleMaterials" : "ruleProcesses";
        document.getElementById(target)?.focus();
        notify(`${invalidMaterialRule ? "材料" : "工艺"}单价必须大于 0，请检查“${(invalidMaterialRule || invalidProcessRule).name}”。`);
        return false;
      }
      if (!(num("ruleLaborHourly", 0) > 0)) {
        document.getElementById("ruleLaborHourly")?.focus();
        notify("保存前请填写本厂完全人工成本，金额必须大于 0。");
        return false;
      }
      renderIndustryCalibrationGuide();
      const industryStrategy = currentQuoteIndustryStrategy();
      const industryCalibrationConfirmation = document.getElementById("ruleIndustryCalibrationConfirmed");
      if (!industryCalibrationConfirmation?.checked) {
        ruleGuidePage = 4;
        renderRuleGuide();
        document.getElementById("industryCalibrationGuide")?.scrollIntoView({ behavior: "smooth", block: "center" });
        notify(`保存前请完成“${industryStrategy.label}”首次校准。`);
        return false;
      }
      const previousSettings = { ...state.settings };
      const costFieldsChanged = !previousSettings.costRulesConfirmedAt || [
        ["materialRules", val("ruleMaterials")],
        ["processRules", val("ruleProcesses")],
        ["print", num("rulePrintBase", 180)],
        ["printUnit", num("rulePrintUnit", .12)],
        ["laborHourly", num("ruleLaborHourly", 0)]
      ].some(([key, next]) => String(previousSettings[key] ?? "") !== String(next ?? ""));
      const confirmation = document.getElementById("ruleCostConfirmation");
      if (costFieldsChanged && !confirmation?.checked) {
        ruleGuidePage = 5;
        renderRuleGuide();
        document.getElementById("ruleCostConfirmation")?.scrollIntoView({ behavior: "smooth", block: "center" });
        notify("请先勾选确认：这些是本厂当前真实成本，不是演示价格。");
        return false;
      }
      const savedAt = new Date().toISOString();
      state.settings = {
        ...state.settings,
        configured: true,
        quoteRulesUpdatedAt: savedAt,
        costRulesConfirmedAt: costFieldsChanged ? savedAt : previousSettings.costRulesConfirmedAt,
        industryCalibration: {
          schemaVersion: 1,
          category: currentIndustryCategory(),
          strategyId: industryStrategy.id,
          strategyLabel: industryStrategy.label,
          confirmedAt: savedAt,
          checklist: [...industryStrategy.calibration]
        },
        productType: val("ruleProductType") || "包装彩盒",
        defaultMaterial: val("ruleDefaultMaterial") || "350g白卡",
        capacityRules: val("ruleCapacityRules"),
        materialRules: val("ruleMaterials"),
        processRules: val("ruleProcesses"),
        print: num("rulePrintBase", 180),
        printUnit: num("rulePrintUnit", .12),
        sampleDefault: num("ruleSampleDefault", 0),
        freightDefault: 0,
        moq: num("ruleMoq", 1000),
        urgentRate: rateNum("ruleUrgentRate", .08),
        sampleCredit: val("ruleSampleCredit") || "否",
        ladderDiscount: rateNum("ruleLadderDiscount", .03),
        freightRules: "",
        freightRulesUserConfirmed: false,
        delivery: normalizeQuoteDeliveryDays(val("ruleDelivery")) || "7",
        payment: val("rulePayment") || "30%定金，尾款出货前付清",
        loss: rateNum("ruleLoss", .08),
        tax: rateNum("ruleTax", .06),
        laborHourly: num("ruleLaborHourly", 0),
        machineHourly: num("ruleMachineHourly", 0),
        overheadRate: rateNum("ruleOverheadRate", 0),
        utilizationRate: rateNum("ruleUtilizationRate", .90),
        profitTrial: rateNum("ruleProfitTrial", .3),
        profitNormal: rateNum("ruleProfitNormal", .24),
        profitLong: rateNum("ruleProfitLong", .2),
        profitVip: rateNum("ruleProfitVip", .16)
      };
      const firstMaterial = parseRules(state.settings.materialRules)[0];
      const rules = parseRules(state.settings.processRules);
      if (firstMaterial) state.settings.paper = firstMaterial.price;
      state.settings.film = matchRule(rules, "覆膜", "覆膜", state.settings.film).price;
      state.settings.foil = matchRule(rules, "烫银", "烫银", state.settings.foil).price;
      state.settings.diecut = matchRule(rules, "模切", "模切", state.settings.diecut).price;
      state.settings.glue = matchRule(rules, "糊盒", "糊盒", state.settings.glue).price;
      state.settings.profit = state.settings.profitNormal;
      ensureIndustryQuoteProfiles();
      const currentCategory = currentIndustryCategory();
      state.settings.quoteProfiles[currentCategory] = quoteProfileSnapshot(state.settings);
      const changes = quoteRuleChangeSummary(previousSettings, state.settings);
      state.quoteRuleHistory = [{
        time: new Date(savedAt).toLocaleString("zh-CN", { hour12: false }),
        createdAt: savedAt,
        category: currentCategory,
        changes
      }, ...(state.quoteRuleHistory || [])].slice(0, 20);
      saveState();
      renderQuoteRules();
      if (confirmation) confirmation.checked = false;
      window.__saveSettingsCloud?.(state.settings);
      renderHomeNextActions();
      captureActiveCustomerSession(true);
      notify(`${currentCategory}报价参数已保存，其他行业价格表不会被覆盖。`);
      return true;
    }
    function saveSettings() {
      saveQuoteRules();
    }
    function restoreSettings() {
      const s = state.settings;
      s.costLibrary = normalizeCostLibrary(s.costLibrary);
      renderQuoteReferencePriceNotice();
      const setValue = (id, value) => {
        const el = document.getElementById(id);
        if (el) el.value = value ?? "";
      };
      setValue("ruleProductType", s.productType);
      setValue("ruleDefaultMaterial", s.defaultMaterial);
      setValue("ruleCapacityRules", s.capacityRules);
      setValue("rulePrintBase", s.print);
      setValue("rulePrintUnit", s.printUnit ?? .12);
      setValue("ruleSampleDefault", s.sampleDefault ?? 0);
      setValue("ruleFreightDefault", 0);
      setValue("ruleMoq", s.moq ?? 1000);
      setValue("ruleUrgentRate", rateText(s.urgentRate ?? .08));
      setValue("ruleSampleCredit", s.sampleCredit || "否");
      setValue("ruleLadderDiscount", rateText(s.ladderDiscount ?? .03));
      setValue("ruleFreightRules", "");
      setValue("ruleMaterials", s.materialRules);
      setValue("ruleProcesses", s.processRules);
      setValue("ruleLoss", rateText(s.loss));
      setValue("ruleTax", rateText(s.tax));
      setValue("ruleLaborHourly", s.laborHourly ?? 0);
      setValue("ruleMachineHourly", s.machineHourly ?? 0);
      setValue("ruleOverheadRate", rateText(s.overheadRate ?? 0));
      setValue("ruleUtilizationRate", rateText(s.utilizationRate ?? .90));
      setValue("ruleProfitTrial", rateText(s.profitTrial));
      setValue("ruleProfitNormal", rateText(s.profitNormal));
      setValue("ruleProfitLong", rateText(s.profitLong));
      setValue("ruleProfitVip", rateText(s.profitVip));
      setValue("ruleDelivery", normalizeQuoteDeliveryDays(s.delivery) || "7");
      setValue("rulePayment", s.payment);
      renderQuoteSavedTemplates();
      scheduleQuoteLiveDock();
      renderCostLibrary();
      renderIndustryCalibrationGuide();
    }
    function renderQuoteRules() {
      const output = document.getElementById("ruleOutput");
      const s = state.settings;
      const fieldValue = (id, fallback) => document.getElementById(id)?.value ?? fallback;
      const form = {
        productType: fieldValue("ruleProductType", s.productType),
        defaultMaterial: fieldValue("ruleDefaultMaterial", s.defaultMaterial),
        capacityRules: fieldValue("ruleCapacityRules", s.capacityRules),
        materialRules: fieldValue("ruleMaterials", s.materialRules),
        processRules: fieldValue("ruleProcesses", s.processRules),
        print: fieldValue("rulePrintBase", s.print),
        printUnit: fieldValue("rulePrintUnit", s.printUnit),
        moq: fieldValue("ruleMoq", s.moq),
        urgentRate: fieldValue("ruleUrgentRate", rateText(s.urgentRate)),
        sampleCredit: fieldValue("ruleSampleCredit", s.sampleCredit),
        ladderDiscount: fieldValue("ruleLadderDiscount", rateText(s.ladderDiscount)),
        freightRules: fieldValue("ruleFreightRules", s.freightRules),
        loss: fieldValue("ruleLoss", rateText(s.loss)),
        tax: fieldValue("ruleTax", rateText(s.tax)),
        laborHourly: fieldValue("ruleLaborHourly", s.laborHourly),
        machineHourly: fieldValue("ruleMachineHourly", s.machineHourly),
        overheadRate: fieldValue("ruleOverheadRate", rateText(s.overheadRate || 0)),
        utilizationRate: fieldValue("ruleUtilizationRate", rateText(s.utilizationRate || .90)),
        profitNormal: fieldValue("ruleProfitNormal", rateText(s.profitNormal)),
        delivery: normalizeQuoteDeliveryDays(fieldValue("ruleDelivery", s.delivery)) || "",
        payment: fieldValue("rulePayment", s.payment)
      };
      const materials = parseRules(form.materialRules);
      const processes = parseRules(form.processRules);
      const capacityRows = parseCapacityRows(form.capacityRules);
      renderCapacityTable(form.capacityRules);
      renderIndustryCostModelGuide();
      renderIndustryCalibrationGuide();
      const industryStrategy = currentQuoteIndustryStrategy();
      const industryCalibrated = Boolean(
        s.industryCalibration?.confirmedAt
        && s.industryCalibration.strategyId === industryStrategy.id
        && s.industryCalibration.category === currentIndustryCategory()
      );
      const coreChecks = [
        { label: "主营品类和默认材料", done: !!form.productType && !!form.defaultMaterial },
        { label: `常用材料成本 ${materials.length} 条`, done: materials.length >= 1 },
        { label: `常用加工费 ${processes.length} 条`, done: processes.length >= 1 && !!form.print },
        { label: `完全人工成本 ¥${Number(form.laborHourly || 0).toFixed(2)}/小时`, done: Number(form.laborHourly || 0) > 0 },
        { label: `${industryStrategy.label}首次校准`, done: industryCalibrated }
      ];
      const advancedChecks = [
        { label: `本厂能力范围 ${capacityRows.length} 条（可选）`, done: true },
        { label: "损耗、税费、利润和交付", done: !!form.loss && !!form.tax && !!form.profitNormal && !!form.delivery && !!form.payment },
        { label: `行业公式：设备 ¥${Number(form.machineHourly || 0).toFixed(2)}/时（可选）`, done: true },
        { label: "起订量、加急费；运费统一另计", done: !!form.moq && !!form.urgentRate }
      ];
      const checks = ruleMode === "simple" ? coreChecks : [...coreChecks, ...advancedChecks];
      const rulesConfirmed = quoteLegacyRulesConfirmed(s);
      const checklist = document.getElementById("ruleChecklist");
      if (checklist) {
        checklist.innerHTML = checks.map(item => `
          <div class="check-item ${rulesConfirmed && item.done ? "done" : ""}">
            <span class="check-dot">${rulesConfirmed && item.done ? "✓" : "•"}</span>
            <span>${item.label}${!rulesConfirmed && item.done ? "（待确认）" : ""}</span>
          </div>
        `).join("");
      }
      const updatedText = s.quoteRulesUpdatedAt
        ? new Date(s.quoteRulesUpdatedAt).toLocaleString("zh-CN", { hour12: false })
        : "尚未确认";
      const safetyNotice = document.getElementById("quoteRuleSafetyNotice");
      const rulesAge = quoteRulesAgeDays();
      if (safetyNotice) {
        safetyNotice.textContent = quoteReferencePricePackActive(s)
          ? `已预置 ${Number(s.referencePricePack?.itemCount || s.costLibrary?.items?.length || 0)} 项行业参考价，可直接估算。${quoteReferencePriceNoticeText(s)}`
          : !rulesConfirmed
          ? "当前页面预填的是演示价格，不代表你工厂的真实成本。演示价格不会用于报价；单独保存过的材料、印刷或工艺价格可直接用于对应订单。"
          : rulesAge > 90
            ? `价格表已超过 90 天未确认 · 最后更新：${updatedText}。本单仍可报价，但发送客户前请复核实际命中的价格。`
            : rulesAge > 30
              ? `价格表已 ${rulesAge} 天未更新 · 最后更新：${updatedText}。建议核对近期材料和加工费。`
              : `价格表已确认 · 最后更新：${updatedText}。材料或加工费变化时，请在这里修改并重新保存。`;
      }
      renderQuoteRuleHistory();
      if (!output) return;
      const readyCount = checks.filter(item => item.done).length;
      const referenceReady = quoteReferencePricePackActive(s);
      output.textContent = `配置状态：${referenceReady ? `已预置 ${Number(s.referencePricePack?.itemCount || s.costLibrary?.items?.length || 0)} 项行业参考价` : rulesConfirmed ? `${readyCount}/${checks.length} 已确认` : `成本待确认（已预填 ${readyCount}/${checks.length}）`}
最后更新：${updatedText}
成本时效：${referenceReady ? "行业参考价；对外报价前按本厂当日价格复核" : !rulesConfirmed ? "整表待确认；单项已保存价格可使用" : rulesAge > 90 ? "已过期，报价前强提醒复核" : rulesAge > 30 ? `需复核（${rulesAge} 天未更新）` : "正常"}

建议下一步：
${referenceReady ? "可以直接开始估算；当前订单用到哪项，就把该项改成本厂实际价，不需要一次改完全部价格。" : !rulesConfirmed ? "可以先保存常用价格，也可以直接去报价；系统只会要求补齐本单实际用到的项目。" : readyCount < checks.length ? "先把左侧未完成的步骤补齐，再保存报价参数。" : ruleMode === "simple" ? "三步核心设置已完成，可以开始报价；高级参数以后再补。" : "可以去报价。系统会按这套已确认规则生成报价单和客户话术。"}

当前识别：
- 主营品类：${form.productType || "未填写"}
- 默认材料：${form.defaultMaterial || "未填写"}
- 能力范围：${capacityRows.length} 条
- 材料成本：${materials.length} 条
- 加工费：${processes.length} 条
- 普通客户利润：${form.profitNormal || "未填写"}%
- 行业公式：人工 ¥${Number(form.laborHourly || 0).toFixed(2)}/时，设备 ¥${Number(form.machineHourly || 0).toFixed(2)}/时，利用率 ${form.utilizationRate || "未填写"}%
- 起订量：${form.moq || "未填写"} 个
- 加急费：${form.urgentRate || "未填写"}%
- 阶梯折扣：${form.ladderDiscount || "未填写"}%
- 运费规则：所有报价统一不含运费，运费另计
- 默认交期：${form.delivery ? `${form.delivery}天` : "未填写"}

提示：
不要求第一次报价前填完整张价格表。当前订单用到哪种材料、印刷或工艺，就只检查这一项；未用到的项目可在以后需要时再补。`;
    }
    function fillQuoteRuleDemo() {
      ruleProductType.value = "包装彩盒";
      ruleDefaultMaterial.value = "350g白卡";
      ruleCapacityRules.value = "包装印刷=彩盒/纸袋/标签/画册，起订500-1000，交期7-12天，特殊工艺需打样\n纺织服装=棉布袋/无纺布袋/服装辅料，起订1000，交期10-15天，需确认克重和色牢度\n五金加工=钣金/冲压/金属标牌，按图报价，交期按工艺确认，需提供图纸和公差\n礼品日用=亚克力/木制品/礼品定制，起订按产品确认，需确认Logo位置和包装标准";
      rulePrintBase.value = "180";
      rulePrintUnit.value = "0.12";
      ruleSampleDefault.value = "0";
      ruleFreightDefault.value = "0";
      ruleMoq.value = "1000";
      ruleUrgentRate.value = "8";
      ruleSampleCredit.value = "是";
      ruleLadderDiscount.value = "3";
      ruleFreightRules.value = "";
      ruleMaterials.value = "350g白卡=0.42\n300g铜版纸=0.36\n牛皮纸=0.31\n特种纸=0.58\n700g白卡=0.72";
      ruleProcesses.value = "覆膜=0.08\n哑膜=0.08\n亮膜=0.08\n烫银=0.16\n烫金=0.16\n模切=0.06\n糊盒=0.07\nUV=0.12\n击凸=0.10";
      ruleLoss.value = "8";
      ruleTax.value = "6";
      ruleProfitTrial.value = "30";
      ruleProfitNormal.value = "24";
      ruleProfitLong.value = "20";
      ruleProfitVip.value = "16";
      ruleDelivery.value = "7";
      rulePayment.value = "30%定金，尾款出货前付清";
      state.settings.configured = false;
      state.settings.quoteRulesUpdatedAt = "";
      state.settings.costRulesConfirmedAt = "";
      const confirmation = document.getElementById("ruleCostConfirmation");
      if (confirmation) confirmation.checked = false;
      renderQuoteRules();
      notify("已填入演示价格，请按你工厂的真实成本核对后再保存。");
    }
    function applyRulesToQuote() {
      const resumeInquiryQuote = !!window.__pendingQuoteAfterSetup;
      const resumeInlineQuote = window.__pendingQuoteInlineAfterSetup === true;
      const resumeSessionId = String(window.__pendingQuoteSessionId || "");
      const currentSessionId = String(activeCustomerSession()?.id || "");
      const mayResumeInquiryQuote = resumeInquiryQuote && (!resumeSessionId || resumeSessionId === currentSessionId);
      if (saveQuoteRules() === false) return;
      window.__pendingQuoteAfterSetup = false;
      window.__pendingQuoteInlineAfterSetup = false;
      window.__pendingQuoteSessionId = "";
      if (mayResumeInquiryQuote) {
        applyInquiryToQuote({ inline: resumeInlineQuote });
        return;
      }
      if (resumeInquiryQuote) {
        notify("价格表已保存；当前客户已经切换，没有自动打开上一位客户的报价。");
        return;
      }
      applyTemplateToQuoteFields(state.settings.factoryType || state.settings.productType || "包装彩盒", false);
      setIfExists("quoteCustomerName", val("inquiryCustomerName") || "新客户");
      quoteMaterial.value = state.settings.defaultMaterial;
      quoteSample.value = state.settings.sampleDefault ?? 0;
      quoteFreight.value = 0;
      quoteFreightMode.value = "不含运";
      setQuoteDeliveryValue(state.settings.delivery);
      quotePayment.value = state.settings.payment;
      quoteGuideStep = 1;
      quoteGuideHasResult = false;
      quoteGuideDirection = "next";
      hideMissingQuoteCosts();
      showPage("报价系统");
    }
    function updateDesignFileName() {
      const file = document.getElementById("designFile").files[0];
      const label = document.getElementById("designFileName");
      if (label) label.textContent = file ? `已选择：${file.name}` : "未选择文件";
    }
    function updateVectorFileName() {
      const file = document.getElementById("vectorFile")?.files[0];
      const label = document.getElementById("vectorFileName");
      if (label) label.textContent = file ? `已选择：${file.name}` : "未选择文件";
    }
    function fillVectorFromHome() {
      const text = val("aiQuestion") || val("customerText");
      const customer = (document.getElementById("homeCustomerName")?.value || "新客户").trim() || "新客户";
      const customerInput = document.getElementById("vectorCustomer");
      const noteInput = document.getElementById("vectorNote");
      if (customerInput) customerInput.value = customer;
      if (noteInput && text) noteInput.value = text.slice(0, 240);
      setTextIfExists("vectorOutput", `已从首页需求带入：

${text || "请上传客户图片或Logo文件。"}

下一步：上传图片，确认客户授权后提交人工处理。`);
    }
    function convertVectorDemo() {
      const file = document.getElementById("vectorFile")?.files[0]?.name || "未上传文件";
      const format = val("vectorFormat") || "SVG";
      const use = val("vectorUse") || "丝印/印刷";
      setTextIfExists("vectorOutput", `半人工转矢量任务：

源文件：${file}
输出格式：${format}
生产用途：${use}

正式服务会把图片加入待处理队列，由指定员工人工转换、检查并上传 ${format} 结果；成功交付后才扣30点。`);
    }
    const preflightProfileDefaults = {
      packaging: { dpi:300, bleed:3 },
      label: { dpi:300, bleed:2 },
      print: { dpi:300, bleed:3 },
      large_format: { dpi:150, bleed:3 },
      general: { dpi:300, bleed:3 }
    };
    let currentPreflightReports = [];
    function applyPreflightProfileDefaults() {
      const defaults = preflightProfileDefaults[val("preflightProfile")] || preflightProfileDefaults.general;
      setIfExists("preflightMinimumDpi", String(defaults.dpi));
      setIfExists("preflightBleed", String(defaults.bleed));
    }
    function preflightReviewLabel(status) {
      return status === "approved" ? "已确认可生产" : status === "rejected" ? "已退回修改" : "等待人工复核";
    }
    function preflightCustomerConfirmationLabel(status) {
      return status === "confirmed" ? "客户已确认此版本"
        : status === "revision_requested" ? "客户要求修改"
        : "等待客户确认";
    }
    function preflightMetricItems(result = {}) {
      const metrics = result.metrics || {};
      const size = metrics.width_mm && metrics.height_mm ? `${Number(metrics.width_mm).toFixed(2)} × ${Number(metrics.height_mm).toFixed(2)} mm` : "待人工确认";
      const dpi = metrics.effective_dpi_x && metrics.effective_dpi_y ? `${Math.round(Math.min(metrics.effective_dpi_x, metrics.effective_dpi_y))} DPI` : "待人工确认";
      return [
        ["文件格式", result.file?.format || "—"],
        ["页面/像素", metrics.page_count ? `${metrics.page_count} 页` : metrics.pixel_width ? `${metrics.pixel_width}×${metrics.pixel_height}px` : "—"],
        ["成品尺寸", size],
        ["有效分辨率", dpi],
        ["色彩空间", Array.isArray(metrics.color_spaces) ? metrics.color_spaces.join(" / ") || "待复核" : "待复核"],
        ["最小出血", metrics.bleed_mm != null ? `${metrics.bleed_mm} mm` : "待确认"],
        ["字体", metrics.font_count != null ? `${metrics.font_count} 个 · 未嵌入 ${metrics.unembedded_font_count || 0}` : "待确认"],
        ["PDF/X 与 ICC", metrics.pdfx_version ? `${metrics.pdfx_version}${metrics.has_output_intent ? " · ICC已设置" : " · ICC待补"}` : "待复核"],
        ["总墨量", metrics.maximum_total_ink_pct != null ? `已检测最高约 ${Number(metrics.maximum_total_ink_pct).toFixed(0)}%` : "需专业工具复核"],
        ["自动结论", result.status_label || "待检查"]
      ];
    }
    function renderPreflightReport(report = {}) {
      const output = document.getElementById("designOutput");
      if (!output) return;
      const result = report.result || report;
      const checks = Array.isArray(result.checks) ? result.checks : [];
      const issues = Array.isArray(result.issues) ? result.issues : [];
      const status = ["passed", "warning", "blocked"].includes(result.status) ? result.status : "pending";
      const reviewStatus = report.review_status || "pending";
      const reviewText = preflightReviewLabel(reviewStatus);
      const customerStatus = report.customer_confirmation_status || "pending";
      const versionNo = Math.max(1, Number(report.version_no || 1));
      const createdText = report.created_at ? new Date(report.created_at).toLocaleString("zh-CN", { hour12:false }) : "";
      output.innerHTML = `<div class="preflight-summary">
        <div class="preflight-summary-head"><div><strong>${escapeHtml(report.file?.name || result.file?.name || "生产文件")} <em class="preflight-version-badge">V${versionNo}</em></strong><span>${escapeHtml(report.customer || "新客户")} · 上传人：${escapeHtml(report.created_by_name || "当前员工")} · ${escapeHtml(createdText || "刚刚")}</span><span>${escapeHtml(result.summary || "检查完成")}</span></div><span class="preflight-status ${status}">${escapeHtml(result.status_label || "等待体检")}</span></div>
        <div class="preflight-metrics">${preflightMetricItems(result).map(([label,value]) => `<div class="preflight-metric"><span>${escapeHtml(label)}</span><strong>${escapeHtml(String(value || "—"))}</strong></div>`).join("")}</div>
        <div class="preflight-review-line"><strong>人工复核：${escapeHtml(reviewText)}</strong>${report.reviewed_by_name ? ` · ${escapeHtml(report.reviewed_by_name)} · ${escapeHtml(report.reviewed_at || "")}` : ""}${report.review_note ? `<br>${escapeHtml(report.review_note)}` : ""}</div>
        <div class="preflight-confirmation-line ${escapeHtml(customerStatus)}"><strong>客户确认：${escapeHtml(preflightCustomerConfirmationLabel(customerStatus))}</strong>${report.customer_confirmed_by ? ` · ${escapeHtml(report.customer_confirmed_by)} · ${escapeHtml(report.customer_confirmed_at || "")}` : ""}${report.customer_confirmation_note ? `<br>${escapeHtml(report.customer_confirmation_note)}` : ""}</div>
        ${issues.length ? `<div class="preflight-section-title">需要处理的问题（${issues.length}）</div><div class="preflight-issues">${issues.map(item => `<div class="preflight-issue ${item.severity === "blocked" ? "blocked" : "warning"}"><strong>${item.severity === "blocked" ? "必须修改 · " : "风险提醒 · "}${escapeHtml(item.title || "问题")}</strong><p>${escapeHtml(item.detail || "")}${item.evidence ? `<br>证据：${escapeHtml(item.evidence)}` : ""}</p>${item.suggestion ? `<em>建议：${escapeHtml(item.suggestion)}</em>` : ""}</div>`).join("")}</div>` : `<div class="member-note" style="margin-top:12px">自动规则未发现阻断或风险，但仍需人工核对画面、安全边距和最终版本。</div>`}
        <div class="preflight-section-title">全部检查项</div><div class="preflight-checks">${checks.map(item => `<div class="preflight-check ${escapeHtml(item.status || "manual")}"><i></i><div><strong>${escapeHtml(item.label || "检查项")}</strong><span>${escapeHtml(String(item.value || "—"))}</span>${item.note ? `<small>${escapeHtml(item.note)}</small>` : ""}</div></div>`).join("")}</div>
        ${report.id ? `<div class="preflight-decision-actions">${report.task_id ? `<button class="btn dark" onclick="createPreflightOnlineProof(${Number(report.id)},${Number(report.task_id)},${versionNo})">生成 V${versionNo} 在线校样链接</button>` : ""}${reviewStatus !== "approved" ? `<button class="btn dark" onclick="decidePreflightReport(${Number(report.id)},'approved','${status}')">制前确认可生产</button>` : ""}${reviewStatus !== "rejected" ? `<button class="btn light" onclick="decidePreflightReport(${Number(report.id)},'rejected','${status}')">制前退回修改</button>` : ""}${reviewStatus !== "pending" ? `<button class="btn light" onclick="decidePreflightReport(${Number(report.id)},'pending','${status}')">撤回制前审核</button>` : ""}${customerStatus !== "confirmed" ? `<button class="btn dark" onclick="recordPreflightCustomerConfirmation(${Number(report.id)},'confirmed')">记录客户已确认 V${versionNo}</button>` : ""}${customerStatus !== "revision_requested" ? `<button class="btn light" onclick="recordPreflightCustomerConfirmation(${Number(report.id)},'revision_requested')">记录客户修改意见</button>` : ""}${customerStatus !== "pending" ? `<button class="btn light" onclick="recordPreflightCustomerConfirmation(${Number(report.id)},'pending')">撤回客户确认</button>` : ""}</div>` : ""}
      </div>`;
    }
    function renderPreflightHistory(reports = []) {
      const container = document.getElementById("preflightHistory");
      if (!container) return;
      currentPreflightReports = Array.isArray(reports) ? reports : [];
      container.innerHTML = currentPreflightReports.length ? currentPreflightReports.map(report => {
        const result = report.result || {};
        const status = ["passed", "warning", "blocked"].includes(result.status) ? result.status : "pending";
        const created = report.created_at ? new Date(report.created_at).toLocaleString("zh-CN", { hour12:false }) : "";
        return `<div class="preflight-history-item"><span class="preflight-status ${status}">${escapeHtml(result.status_label || report.status || "待复核")}</span><div><strong><em class="preflight-version-badge">V${Math.max(1, Number(report.version_no || 1))}</em> · ${escapeHtml(report.file?.name || "生产文件")}</strong><span>${escapeHtml(report.customer || "新客户")} · 上传人 ${escapeHtml(report.created_by_name || "当前员工")} · ${escapeHtml(created)}<br>${escapeHtml(preflightReviewLabel(report.review_status))} · ${escapeHtml(preflightCustomerConfirmationLabel(report.customer_confirmation_status))}</span></div><button class="btn light" type="button" onclick="showPreflightReport(${Number(report.id)})">查看报告</button></div>`;
      }).join("") : `<div class="muted">还没有印前体检记录。</div>`;
    }
    function showPreflightReport(reportId) {
      const report = currentPreflightReports.find(item => Number(item.id) === Number(reportId));
      if (report) renderPreflightReport(report);
    }
    async function loadPreflightHistory() {
      return window.__loadPreflightReports?.();
    }
    async function decidePreflightReport(reportId, decision, automaticStatus = "") {
      return window.__decidePreflightReport?.(reportId, decision, automaticStatus);
    }
    async function recordPreflightCustomerConfirmation(reportId, status) {
      return window.__recordPreflightCustomerConfirmation?.(reportId, status);
    }
    async function designCheck() {
      if (typeof window.__requireServerFeature === "function") {
        if (!await window.__requireServerFeature("制前资料", "preflight")) return;
      } else if (!consumeQuota("制前资料", false)) return;
      if (!window.__runPreflightCheck) {
        notify("正式版服务正在连接，请稍后再试。");
        return;
      }
      return window.__runPreflightCheck();
    }
    function fillDesignDemo() {
      setIfExists("designText", "成品210×285mm，四周3mm出血，CMYK印刷，图片不低于300DPI，文字转曲，潘通专色、刀模线和烫金版分别放在独立图层，制版前锁定客户确认稿。");
      setIfExists("preflightExpectedSize", "210×285mm");
      setIfExists("preflightProfile", "packaging");
      setIfExists("preflightMinimumDpi", "300");
      setIfExists("preflightBleed", "3");
    }
    function taskPhase(taskOrStatus) {
      const status = typeof taskOrStatus === "object" ? taskOrStatus?.status : taskOrStatus;
      if (["生产", "生产中"].includes(status)) return "生产中";
      if (["待质检", "待发货", "已发货", "已签收"].includes(status)) return "交付中";
      if (["待回款", "已回款"].includes(status)) return "回款中";
      if (["已完成", "已归档"].includes(status)) return "已完成";
      return "接单前";
    }
    function taskSpecificationText(task = {}) {
      const explicit = String(task.specification || "").trim();
      if (explicit) return explicit;
      const legacy = String(task.risk || "").match(/(?:^|；)\s*本单规格：([^；]+)/);
      return String(legacy?.[1] || "").trim();
    }
    function taskRiskText(task = {}) {
      const cleaned = String(task.risk || "")
        .replace(/(?:^|；)\s*本单规格：[^；]+/g, "")
        .replace(/^\s*[；;，,]+|[；;，,]+\s*$/g, "")
        .trim();
      return cleaned || "无明显风险";
    }
    function productionTextHas(text, words) {
      const source = String(text || "").toLowerCase();
      return words.some(word => {
        const keyword = String(word || "").toLowerCase();
        return keyword && source.includes(keyword) && !productionKeywordNegated(source, keyword);
      });
    }
    function productionKeywordNegated(text, word) {
      const source = String(text || "").toLowerCase();
      const keyword = String(word || "").toLowerCase();
      if (!keyword) return false;
      const directPrefixPattern = /(?:不需要|不要|无需|不用|不做|不含|不带|未选|未选择|没有选择|取消|去掉|删除|免|无|不)\s*(?:再|做|要|需|用|含|带|进行|选择)?\s*$/;
      const directSuffixPattern = /^\s*(?:也|都)?\s*(?:不需要|不要|无需|不用|不做|不含|不带|未选|未选择|没有选择|取消|去掉|删除|免)(?:再?做)?(?=\s*(?:[，,；;。!?！？]|只做|只要|要做|需要|保留|选择|增加|加上|改为|但是要|但要|不过要|而要|而是|但是|但|不过|$))/;
      const groupedSuffixPattern = /^\s*(?:(?:、|和|及|与)\s*[\w\u3400-\u9fff+./-]+){1,8}\s*(?:也|都)?\s*(?:不需要|不要|无需|不用|不做|不含|不带|未选|未选择|没有选择|取消|去掉|删除|免)(?:再?做)?(?=\s*(?:[，,；;。!?！？]|只做|只要|要做|需要|保留|选择|增加|加上|改为|但是要|但要|不过要|而要|而是|但是|但|不过|$))/;
      let cursor = 0;
      let found = false;
      while ((cursor = source.indexOf(keyword, cursor)) >= 0) {
        found = true;
        const prefix = source.slice(Math.max(0, cursor - 16), cursor);
        const suffix = source.slice(cursor + keyword.length, cursor + keyword.length + 48);
        const directPrefix = directPrefixPattern.test(prefix);
        const directSuffix = directSuffixPattern.test(suffix);
        const groupedSuffix = groupedSuffixPattern.test(suffix);
        if (!directPrefix && !directSuffix && !groupedSuffix) {
          const clauseStart = Math.max(...["。", "；", ";", "！", "!", "？", "?", "，", ",", "\n"].map(mark => source.lastIndexOf(mark, cursor))) + 1;
          const scopedPrefix = source.slice(clauseStart, cursor);
          const scopePattern = /(?:^|[，,、/和及与\s])(?:不需要|不要|无需|不用|不做|不含|不带|未选|未选择|没有选择|取消|去掉|删除|免)\s*(?:再|做|要|需|用|含|带|进行|选择)?\s*/g;
          const scopes = [...scopedPrefix.matchAll(scopePattern)];
          if (!scopes.length) return false;
          const positivePattern = /(?:只做|只要|要做|需要|保留|选择|增加|加上|改为|但是要|但要|不过要|而要|而是|但是|但|不过)/g;
          const positives = [...scopedPrefix.matchAll(positivePattern)].filter(positive => !scopes.some(negative => {
            const positiveAt = positive.index || 0;
            const negativeAt = negative.index || 0;
            return negativeAt <= positiveAt && positiveAt < negativeAt + negative[0].length;
          }));
          const lastNegativeAt = scopes[scopes.length - 1].index || 0;
          const lastPositiveAt = positives.length ? (positives[positives.length - 1].index || 0) : -1;
          if (lastPositiveAt > lastNegativeAt) return false;
        }
        cursor += keyword.length;
      }
      return found;
    }
    function localProductionStepId(label, index) {
      let hash = 0;
      const text = `${index}:${label}`;
      for (let i = 0; i < text.length; i++) hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
      return `local-step-${index + 1}-${Math.abs(hash).toString(36)}`;
    }
    function localIndustryProfileKey(task, combined = "") {
      const strategy = String(task?.industryStrategyId || task?.industry_strategy_id || "");
      const byStrategy = {
        packaging_printing:"packaging", acrylic:"acrylic", metal:"metal",
        textile_bag:"textile", electronics_bom:"electronics"
      };
      if (byStrategy[strategy]) return byStrategy[strategy];
      const category = String(task?.industryCategory || task?.industry_category || "");
      const byCategory = {
        "包装印刷":"packaging", "纺织服装":"textile", "塑料制品":"plastic",
        "五金加工":"metal", "电子配件":"electronics"
      };
      if (byCategory[category]) return byCategory[category];
      const text = String(combined || "").toLowerCase();
      if (/亚克力|有机玻璃|pmma/.test(text)) return "acrylic";
      if (/五金|钣金|金属|不锈钢|铝板|冲压/.test(text)) return "metal";
      if (/布袋|无纺布|棉布|服装|纺织|车缝/.test(text)) return "textile";
      if (/塑料袋|opp|pe袋|硅胶|橡胶|制袋/.test(text)) return "plastic";
      if (/电子|小家电|pcba|smt|电路板/.test(text)) return "electronics";
      if (/木制|竹制|木材|竹材|木盒/.test(text)) return "wood";
      if (/包装|彩盒|纸盒|纸袋|标签|贴纸|画册|说明书|名片|瓦楞|印刷/.test(text)) return "packaging";
      return "general";
    }
    function buildLocalProductionSteps(task) {
      const source = taskProductionSource(task);
      const product = source.product;
      const process = source.process;
      const combined = `${product} ${process}`;
      const profileKey = localIndustryProfileKey(task, combined);
      const labels = [];
      const add = label => { if (label && !labels.includes(label)) labels.push(label); };
      const isBox = productionTextHas(combined, ["彩盒", "包装盒", "纸盒", "盒中盒", "瓦楞箱"]);
      const isBag = productionTextHas(combined, ["手提袋", "纸袋", "购物袋"]);
      const isLabel = productionTextHas(combined, ["标签", "贴纸", "不干胶"]);
      const isBook = productionTextHas(combined, ["画册", "说明书", "书刊", "册子"]);
      const isCard = productionTextHas(combined, ["名片", "卡片", "吊牌"]);
      const isPaper = profileKey === "packaging" || isBox || isBag || isLabel || isBook || isCard || productionTextHas(combined, ["印刷", "纸张", "白卡", "铜版纸"]);
      const isAcrylic = profileKey === "acrylic" || productionTextHas(combined, ["亚克力"]);
      const isTextile = profileKey === "textile" || productionTextHas(combined, ["布袋", "无纺布", "棉布", "服装", "纺织", "车缝"]);
      const isPlastic = profileKey === "plastic" || productionTextHas(combined, ["塑料袋", "opp", "pe袋", "硅胶", "橡胶", "制袋"]);
      const isMetal = profileKey === "metal" || productionTextHas(combined, ["五金", "钣金", "金属", "不锈钢", "铝板", "冲压"]);
      const isElectronic = profileKey === "electronics" || productionTextHas(combined, ["电子", "小家电", "pcba", "smt", "电路板"]);
      const isWood = profileKey === "wood" || productionTextHas(combined, ["木制", "竹制", "木材", "竹材"]);
      add("生产资料确认");
      if (isElectronic) {
        ["BOM备料", "SMT贴片", "DIP/焊接", "整机组装", "功能测试"].forEach(add);
        if (productionTextHas(combined, ["老化"])) add("老化测试");
      } else if (isTextile) {
        add("面料检验与备料"); add("裁剪");
        if (productionTextHas(combined, ["印花", "丝印", "刺绣", "热转印"])) add("印花/Logo加工");
        add("车缝");
        if (productionTextHas(combined, ["整烫"])) add("整烫");
      } else if (isPlastic) {
        add("原料配料");
        if (productionTextHas(combined, ["印刷", "丝印", "logo"])) add("印刷/Logo加工");
        add("成型/制袋"); add("封边");
      } else if (isMetal) {
        add("材料备料"); add("下料/切割");
        if (productionTextHas(combined, ["冲压", "折弯"])) add("冲压/折弯");
        if (productionTextHas(combined, ["焊"])) add("焊接");
      } else if (isAcrylic) {
        add("板材备料"); add("开料"); add("激光切割");
        if (productionTextHas(combined, ["丝印", "uv", "logo", "雕刻"])) add("Logo/图案加工");
        if (productionTextHas(combined, ["折弯"])) add("折弯");
        if (productionTextHas(combined, ["抛光", "打磨"])) add("抛光/打磨");
        add("组装");
      } else if (isWood) {
        ["木竹材料备料", "开料", "切割/雕刻", "打磨"].forEach(add);
        if (productionTextHas(combined, ["喷漆", "涂装", "上色"])) add("喷漆/涂装");
        add("组装");
      } else if (isPaper) {
        // 纸品的备料、制版和印刷步骤在下面按生产顺序统一加入。
      } else {
        add("材料备料"); add("核心加工");
      }
      if (isPaper) ["纸张备料/开料", "印前拼版/制版", "印刷"].forEach(add);
      [
        [["覆哑膜", "哑膜"], "覆哑膜"], [["覆亮膜", "亮膜"], "覆亮膜"], [["覆膜"], "覆膜"],
        [["上光油", "光油", "过油"], "上光/过油"], [["烫金"], "烫金"], [["烫银"], "烫银"],
        [["局部uv", "逆向uv", "uv"], "UV工艺"], [["击凸", "凹凸"], "击凸/凹凸"], [["压凹"], "压凹"],
        [["压纹"], "压纹"], [["对裱", "裱卡", "裱瓦楞", "裱纸"], "裱纸/对裱"],
        [["模切", "异形切割"], "模切/异形切割"], [["裁切", "三面切"], "裁切"],
        [["压线", "压痕"], "压线/压痕"], [["折页"], "折页"], [["骑马钉", "胶装", "锁线", "装订"], "装订"],
        [["糊盒"], "糊盒"], [["糊袋"], "糊袋"], [["穿绳", "手提绳"], "穿绳"], [["排废"], "排废"],
        [["打孔"], "打孔"], [["圆角"], "圆角"], [["电镀", "烤漆", "喷涂", "表面处理"], "表面处理"]
      ].forEach(([words, label]) => { if (productionTextHas(combined, words)) add(label); });
      if (isBox) {
        if (!productionKeywordNegated(process, "模切") && !labels.some(label => label.includes("模切"))) add("模切/压线");
        if (!productionKeywordNegated(process, "糊盒")) add("糊盒");
      }
      else if (isBag) {
        if (!productionKeywordNegated(process, "模切") && !labels.some(label => label.includes("模切"))) add("模切/压线");
        if (!productionKeywordNegated(process, "糊袋")) add("糊袋");
        if (!productionKeywordNegated(process, "穿绳")) add("穿绳");
      }
      else if (isLabel) {
        if (!productionKeywordNegated(process, "模切") && !labels.some(label => label.includes("模切"))) add("模切");
        if (!productionKeywordNegated(process, "排废")) add("排废");
      }
      else if (isBook) ["折页", "装订", "成品裁切"].forEach(add);
      else if (isCard) add("成品裁切");
      add("成品质检"); add("包装入库");
      const createdAt = new Date().toISOString();
      return labels.map((label, index) => ({
        id: localProductionStepId(label, index), label, completed: false, custom: false,
        completed_at: "", completed_by: "", owner:"", due:"", note:"", created_at: createdAt
      }));
    }
    function taskProductionSource(task) {
      const quote = relatedQuoteForTask(task);
      const product = String(quote?.product || task?.productionProduct || task?.production_product || task?.title || task?.customer || "本订单产品").trim();
      const process = String(quote?.process || task?.process || task?.productionProcess || task?.production_process || "").trim();
      return { product, process };
    }
    function taskProductionSteps(task) {
      if (!task) return [];
      const source = Array.isArray(task.productionSteps)
        ? task.productionSteps
        : Array.isArray(task.production_steps) ? task.production_steps : [];
      if (source.length) {
        task.productionSteps = source.map((item, index) => ({
          id: String(item?.id || localProductionStepId(item?.label || "步骤", index)),
          label: String(item?.label || "未命名步骤"),
          completed: item?.completed === true,
          custom: item?.custom === true,
          completed_at: String(item?.completed_at || item?.completedAt || ""),
          completed_by: String(item?.completed_by || item?.completedBy || ""),
          owner: String(item?.owner || ""),
          due: String(item?.due || ""),
          note: String(item?.note || ""),
          created_at: String(item?.created_at || item?.createdAt || "")
        }));
        delete task.production_steps;
        return task.productionSteps;
      }
      task.productionSteps = buildLocalProductionSteps(task);
      return task.productionSteps;
    }
    function taskProductionProgress(task) {
      const steps = taskProductionSteps(task);
      const completed = steps.filter(step => step.completed).length;
      return { steps, completed, total: steps.length, percent: steps.length ? Math.round(completed / steps.length * 100) : 0 };
    }
    function normalizeOutsourceProcess(value = "") {
      return String(value || "").toLowerCase().replace(/[\s/／、,，·+\-]/g, "");
    }
    function productionOutsourceOrdersForTask(task) {
      const taskRef = displayTaskId(task);
      return productionOutsourceOrders.filter(order => String(order?.source_task_ref || "") === taskRef);
    }
    function productionOutsourceOrderForStep(task, step) {
      const taskOrders = productionOutsourceOrdersForTask(task);
      const direct = taskOrders.filter(order => String(order?.source_step_ref || "") === String(step?.id || ""));
      const stepProcess = normalizeOutsourceProcess(step?.label);
      const matched = direct.length ? direct : taskOrders.filter(order => normalizeOutsourceProcess(order?.process) === stepProcess);
      return matched.sort((left, right) => Number(right?.id || 0) - Number(left?.id || 0))[0] || null;
    }
    function productionOutsourceState(order) {
      if (!order) return null;
      const status = String(order.status || "");
      return {
        status,
        label:order.status_label || ({
          draft:"草稿", sent:"待供应商接单", accepted:"供应商已接单",
          producing:"外协加工中", delivered:"已交付待验收",
          accepted_delivery:"本厂已验收", rejected:"供应商已拒绝",
          cancelled:"已取消"
        }[status] || status || "状态待确认"),
        done:status === "accepted_delivery",
        risk:["rejected","cancelled"].includes(status)
      };
    }
    async function loadProductionOutsourceOrders(force = false) {
      if (productionOutsourceLoading) return;
      if (!force && productionOutsourceLoadedAt && Date.now() - productionOutsourceLoadedAt < 15000) return;
      if (typeof window.__jianglaiAppApi !== "function") return;
      productionOutsourceLoading = true;
      try {
        const data = await window.__jianglaiAppApi("/api/supplier-collaboration/orders", { method:"GET", timeoutMs:20000 });
        productionOutsourceOrders = Array.isArray(data?.orders) ? data.orders : [];
        productionOutsourceLoadedAt = Date.now();
        productionOutsourceError = "";
      } catch (error) {
        productionOutsourceError = String(error?.message || "外协状态暂时没有读取成功");
      } finally {
        productionOutsourceLoading = false;
        renderProductionCenter();
        if (selectedTaskDetailId && document.querySelector('.page.active[data-page="跟单详情"]')) {
          showTaskDetail(selectedTaskDetailId);
        }
      }
    }
    const productionCenterStages = ["待排产", "制前制版", "印刷", "后道加工", "质检交付"];
    function productionCenterTasks() {
      return businessTasks().filter(task => ["生产中", "交付中"].includes(taskPhase(task)));
    }
    function productionTaskPurchases(task) {
      if (typeof window.purchaseOrdersForTask === "function") return window.purchaseOrdersForTask(task);
      return Array.isArray(task?.purchaseOrders) ? task.purchaseOrders : Array.isArray(task?.purchase_orders) ? task.purchase_orders : [];
    }
    function productionTaskHasPendingMaterial(task) {
      if (Number(task?.inventorySummary?.materials_shortage ?? task?.inventory_summary?.materials_shortage ?? 0) > 0) return true;
      return productionTaskPurchases(task).some(item => {
        const inventory = item?.inventory_state;
        if (inventory && Number(inventory.required || 0) > 0) return Number(inventory.shortage || 0) > 0;
        return item.status !== "已到货";
      });
    }
    function productionTaskHasRisk(task) {
      const risk = taskRiskText(task);
      const operations = factoryOperationsForTask(task);
      const summary = factorySummaryForTask(task);
      const profitReview = task.profitReview && typeof task.profitReview === "object" ? task.profitReview : {};
      const estimated = profitReview.estimated && typeof profitReview.estimated === "object" ? profitReview.estimated : {};
      const actual = profitReview.actual && typeof profitReview.actual === "object" ? profitReview.actual : {};
      const variance = profitReview.variance && typeof profitReview.variance === "object" ? profitReview.variance : {};
      const profitRisk = ["亏损预警","利润预警","成本超支"].includes(profitReview.status);
      const quality = operations.quality?.status;
      const scheduleConflict = task?.scheduleAlerts?.has_conflict || task?.schedule_alerts?.has_conflict;
      const outsourceRisk = productionOutsourceOrdersForTask(task).some(order => {
        const status = String(order?.status || "");
        return ["rejected","cancelled"].includes(status)
          || (order?.due_at && order.due_at < new Date().toISOString().slice(0, 10) && !["delivered","accepted_delivery"].includes(status));
      });
      return taskDueState(task) === "overdue"
        || ["返工","不合格"].includes(quality)
        || !!scheduleConflict
        || outsourceRisk
        || Number(summary.open_after_sales || 0) > 0
        || !["", "无明显风险", "暂无风险备注", "暂无风险"].includes(risk);
    }
    function productionStageForStep(label = "", progress = {}) {
      const text = String(label || "");
      if (!text || (!progress.completed && /生产资料确认/.test(text))) return "待排产";
      if (/质检|包装|入库|测试|验收|交付/.test(text)) return "质检交付";
      if (/印刷|丝印|印花|图案加工/.test(text) && !/印前/.test(text)) return "印刷";
      if (/资料|拼版|制版|备料|开料|下料|BOM|配料|面料检验/.test(text)) return "制前制版";
      return "后道加工";
    }
    function productionTaskMeta(task) {
      const progress = taskProductionProgress(task);
      const next = progress.steps.find(step => !step.completed);
      const stage = next ? productionStageForStep(next.label, progress) : "质检交付";
      return { progress, next, stage };
    }
    function productionCenterDemoTasks() {
      return [
        { id:"production-demo-1", customer:"示例 · 华东食品", title:"节日礼盒 5,000套", owner:"制前", due:"今天", risk:"无明显风险", __demo:true, __stage:"待排产", __percent:0, __next:"核对客户确认稿与生产资料" },
        { id:"production-demo-2", customer:"示例 · 启明日化", title:"350g白卡彩盒 12,000个", owner:"制版", due:"明天", risk:"无明显风险", __demo:true, __stage:"制前制版", __percent:24, __next:"印前拼版/制版" },
        { id:"production-demo-3", customer:"示例 · 南方茶业", title:"茶叶手提袋 3,000个", owner:"印刷", due:"7月23日", risk:"无明显风险", __demo:true, __stage:"印刷", __percent:48, __next:"四色印刷" },
        { id:"production-demo-4", customer:"示例 · 星河文创", title:"文创卡片 8,000张", owner:"后道", due:"7月24日", risk:"烫金版待复核", __demo:true, __stage:"后道加工", __percent:72, __next:"烫金与模切" },
        { id:"production-demo-5", customer:"示例 · 晨光烘焙", title:"蛋糕盒 2,000套", owner:"质检", due:"今天", risk:"首件颜色待签样", __demo:true, __stage:"质检交付", __percent:92, __next:"成品质检与装箱" }
      ];
    }
    function productionCenterTaskMatches(task) {
      const keyword = productionCenterSearch.trim().toLowerCase();
      const meta = task.__demo ? { stage:task.__stage, next:{ label:task.__next } } : productionTaskMeta(task);
      const searchable = [task.id, task.customer, task.cloudCustomer, task.title, task.owner, task.risk, meta.stage, meta.next?.label].filter(Boolean).join(" ").toLowerCase();
      if (keyword && !searchable.includes(keyword)) return false;
      if (productionCenterFilter === "风险" && !productionTaskHasRisk(task)) return false;
      if (productionCenterFilter === "今天" && !task.__demo && !["today", "overdue"].includes(taskDueState(task))) return false;
      if (productionCenterFilter === "外协" && (task.__demo || !productionOutsourceOrdersForTask(task).length)) return false;
      if (productionCenterFilter === "材料" && task.__demo) return false;
      if (productionCenterFilter === "材料" && !task.__demo && !productionTaskHasPendingMaterial(task)) return false;
      if (productionCenterFilter === "质检" && task.__demo) return false;
      if (productionCenterFilter === "质检" && !["待质检","质检中","返工","不合格"].includes(factoryOperationsForTask(task).quality.status)) return false;
      return true;
    }
    function productionWorkCardHtml(task) {
      const demo = task.__demo === true;
      const meta = demo
        ? { stage:task.__stage, progress:{ percent:task.__percent, completed:0, total:0 }, next:{ label:task.__next } }
        : productionTaskMeta(task);
      const customer = String(task.cloudCustomer || task.customer || "未命名客户").split(" · ")[0];
      const product = String(task.productionProduct || task.title || task.customer || "未命名产品").replace(/\s*(?:报价后)?跟进$/, "");
      const dueText = demo ? task.due : formatTaskDue(task);
      const risk = productionTaskHasRisk(task);
      const materialPending = !demo && productionTaskHasPendingMaterial(task);
      const operations = demo ? null : factoryOperationsForTask(task);
      const machine = operations?.schedule?.machine || operations?.schedule?.team || "未排机台";
      const quality = operations?.quality?.status || "待质检";
      const nextOutsource = !demo && meta.next ? productionOutsourceOrderForStep(task, meta.next) : null;
      const nextOutsourceState = productionOutsourceState(nextOutsource);
      const tag = demo ? "流程示例" : displayTaskId(task);
      const click = demo ? "" : ` onclick="openProductionTaskDetail('${escapeHtml(task.id)}')"`;
      return `<button class="production-work-card${demo ? " is-demo" : ""}" type="button"${click}>
        <div class="production-work-top"><span>${escapeHtml(customer)}</span><em>${escapeHtml(tag)}</em></div>
        <h3>${escapeHtml(product)}</h3>
        <p>${escapeHtml(taskProductionSource(task).process || "工艺以订单资料为准")}</p>
        <div class="production-work-current"><span>当前工序</span><strong>${escapeHtml(meta.next?.label || "全部生产步骤已完成")}${nextOutsourceState ? ` · 外协${escapeHtml(nextOutsourceState.label)}` : ""}</strong></div>
        <div class="production-progress-track"><span style="width:${Math.max(0, Math.min(100, Number(meta.progress.percent || 0)))}%"></span></div>
        <div class="production-work-meta"><span>${escapeHtml(nextOutsource ? `${nextOutsource.supplier_name || "外协供应商"} · ${nextOutsource.due_at || dueText}` : `${demo ? task.owner || "待分配" : machine} · ${dueText}`)}</span><span class="${risk ? "is-risk" : ""}">${materialPending ? "材料未齐" : nextOutsourceState ? escapeHtml(nextOutsourceState.label) : risk ? quality : `${Number(meta.progress.percent || 0)}%`}</span></div>
      </button>`;
    }
    function productionSupportEmpty(text) {
      return `<div class="production-support-empty">${escapeHtml(text)}</div>`;
    }
    function goToProductionMetricTarget(target) {
      if (target === "外协") {
        location.href = "supplier_collaboration.html";
        return;
      }
      productionCenterSearch = "";
      productionCenterFilter = "全部";
      const search = document.getElementById("productionCenterSearch");
      const filter = document.getElementById("productionCenterFilter");
      if (search) search.value = "";
      if (filter) filter.value = "全部";
      renderProductionCenter();
      const stageTargets = {
        "待排产": ["待排产"],
        "制前制版": ["制前制版"],
        "印刷后道": ["印刷", "后道加工"],
        "质检交付": ["质检交付"]
      };
      const stages = stageTargets[target] || [];
      const stageSelector = stage => `.production-lane[data-stage="${stage}"]`;
      const targets = target === "风险"
        ? [document.getElementById("productionRiskPanel")].filter(Boolean)
        : stages.map(stage => document.querySelector(stageSelector(stage))).filter(Boolean);
      const primary = targets[0];
      if (!primary) return;
      document.querySelectorAll(".production-lane.is-focus-target,.production-support-card.is-focus-target")
        .forEach(node => node.classList.remove("is-focus-target"));
      targets.forEach(node => node.classList.add("is-focus-target"));
      window.requestAnimationFrame(() => {
        const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
        primary.scrollIntoView({ behavior:reduceMotion ? "auto" : "smooth", block:"center", inline:"center" });
        primary.focus({ preventScroll:true });
      });
      window.clearTimeout(window.__productionMetricFocusTimer);
      window.__productionMetricFocusTimer = window.setTimeout(() => {
        targets.forEach(node => node.classList.remove("is-focus-target"));
      }, 2200);
    }
    function renderProductionCenterMetrics(tasks) {
      const box = document.getElementById("productionCenterMetrics");
      if (!box) return;
      const stageCounts = Object.fromEntries(productionCenterStages.map(stage => [stage, 0]));
      tasks.forEach(task => { stageCounts[productionTaskMeta(task).stage] += 1; });
      const riskCount = tasks.filter(productionTaskHasRisk).length;
      const activeOutsourceCount = productionOutsourceOrders.filter(order =>
        ["draft","sent","accepted","producing","delivered"].includes(String(order?.status || ""))
        && tasks.some(task => displayTaskId(task) === String(order?.source_task_ref || ""))
      ).length;
      const metrics = [
        ["待排产", stageCounts["待排产"], "尚未开始首道工序", "待排产"],
        ["制前 / 制版", stageCounts["制前制版"], "资料、拼版、制版与备料", "制前制版"],
        ["印刷 / 后道", stageCounts["印刷"] + stageCounts["后道加工"], "正在车间加工", "印刷后道"],
        ["待质检交付", stageCounts["质检交付"], "质检、包装与入库", "质检交付"],
        ["外协加工", activeOutsourceCount, activeOutsourceCount ? "接单、加工、交付与验收" : (productionOutsourceError || "当前没有进行中的外协"), "外协"],
        ["风险工单", riskCount, riskCount ? "请优先查看风险与质检待办" : "当前没有已记录风险", "风险"]
      ];
      box.innerHTML = metrics.map(([label, value, note, target], index) => `<button class="production-metric${index === metrics.length - 1 && value ? " is-risk" : ""}" type="button" onclick="goToProductionMetricTarget('${target}')" aria-label="${label} ${value}，进入对应区域"><span>${label}</span><strong>${value}</strong><em>${escapeHtml(note)}</em></button>`).join("");
    }
    function renderProductionSupport(tasks) {
      const loadBox = document.getElementById("productionCenterLoad");
      const materialBox = document.getElementById("productionCenterMaterials");
      const riskBox = document.getElementById("productionCenterRisks");
      const outsourceBox = document.getElementById("productionCenterOutsource");
      const machineLoads = new Map();
      tasks.forEach(task => {
        const operations = factoryOperationsForTask(task);
        const machine = operations.schedule.machine || "未排机台";
        const current = machineLoads.get(machine) || { tasks:0, hours:0, capacity:0, conflicts:0 };
        current.tasks += 1;
        current.hours += Number(operations.schedule.planned_hours || 0);
        current.capacity += Number(operations.schedule.capacity_per_day || 0);
        if (task?.scheduleAlerts?.has_conflict || task?.schedule_alerts?.has_conflict) current.conflicts += 1;
        machineLoads.set(machine, current);
      });
      const machineRows = [...machineLoads.entries()].sort((a,b) => b[1].hours - a[1].hours || b[1].tasks - a[1].tasks);
      const maxHours = Math.max(1, ...machineRows.map(([,item]) => item.hours || item.tasks));
      if (loadBox) loadBox.innerHTML = machineRows.length
        ? machineRows.slice(0, 6).map(([machine,item]) => `<div class="production-support-row"><strong>${escapeHtml(machine)}</strong><span>${item.hours ? `${item.hours.toFixed(1)} 小时` : `${item.tasks} 单`}${item.capacity ? ` · 日产能 ${item.capacity.toLocaleString("zh-CN")}` : ""}${item.conflicts ? ` · ${item.conflicts} 个冲突` : ""}</span><div class="production-load-bar"><i style="width:${Math.round((item.hours || item.tasks) / maxHours * 100)}%"></i></div></div>`).join("")
        : productionSupportEmpty("还没有机台与预计工时记录，请在订单详情的排产区域填写。");
      const pendingMaterials = tasks.flatMap(task => productionTaskPurchases(task)
        .filter(item => item?.inventory_state && Number(item.inventory_state.required || 0) > 0
          ? Number(item.inventory_state.shortage || 0) > 0
          : item.status !== "已到货")
        .map(item => ({ ...item, task })));
      const materialBadge = document.getElementById("productionMaterialBadge");
      if (materialBadge) materialBadge.textContent = pendingMaterials.length ? `${pendingMaterials.length} 项待处理` : "已就绪";
      if (materialBox) materialBox.innerHTML = pendingMaterials.length
        ? pendingMaterials.slice(0, 5).map(item => `<button class="production-support-row" type="button" onclick="openProductionTaskDetail('${escapeHtml(item.task.id)}')"><strong>${escapeHtml(item.material || "未命名材料")}</strong><span>${item?.inventory_state && Number(item.inventory_state.shortage || 0) > 0 ? `缺 ${escapeHtml(item.inventory_state.shortage)}${escapeHtml(item.unit === "待确认" ? "" : item.unit || "")}` : escapeHtml(item.status || "待采购")}</span></button>`).join("")
        : productionSupportEmpty(tasks.length ? "已记录的订单材料都已到货；未录入采购的工单请在任务详情补充。" : "还没有真实生产工单，建立工单后这里会显示待采购和待到货材料。" );
      const risks = tasks.filter(productionTaskHasRisk);
      const riskBadge = document.getElementById("productionRiskBadge");
      if (riskBadge) riskBadge.textContent = risks.length ? `${risks.length} 单待处理` : "无异常";
      if (riskBox) riskBox.innerHTML = risks.length
        ? risks.slice(0, 5).map(task => { const summary = factorySummaryForTask(task); const detail = Number(summary.open_after_sales || 0) > 0 ? `${summary.open_after_sales} 项售后待处理` : (task?.scheduleAlerts?.has_conflict || task?.schedule_alerts?.has_conflict) ? "排产机台冲突" : taskDueState(task) === "overdue" ? "交期已到" : taskRiskText(task); return `<button class="production-support-row" type="button" onclick="openProductionTaskDetail('${escapeHtml(task.id)}')"><strong>${escapeHtml(task.cloudCustomer || task.customer || task.title || "未命名订单")}</strong><span>${escapeHtml(detail)}</span></button>`; }).join("")
        : productionSupportEmpty(tasks.length ? "当前生产工单没有逾期或已记录的质量风险。" : "真实生产工单的逾期、色差、文件和质检风险会集中显示在这里。" );
      const taskRefs = new Set(tasks.map(task => displayTaskId(task)));
      const activeOutsourceOrders = productionOutsourceOrders
        .filter(order => taskRefs.has(String(order?.source_task_ref || "")) && String(order?.status || "") !== "cancelled")
        .sort((left, right) => {
          const leftRisk = String(left?.status || "") === "rejected" ? 1 : 0;
          const rightRisk = String(right?.status || "") === "rejected" ? 1 : 0;
          return rightRisk - leftRisk || String(left?.due_at || "9999-12-31").localeCompare(String(right?.due_at || "9999-12-31"));
        });
      const outsourceBadge = document.getElementById("productionOutsourceBadge");
      if (outsourceBadge) outsourceBadge.textContent = activeOutsourceOrders.length ? `${activeOutsourceOrders.length} 单协作中` : "暂无外协";
      if (outsourceBox) outsourceBox.innerHTML = activeOutsourceOrders.length
        ? activeOutsourceOrders.slice(0, 5).map(order => {
            const state = productionOutsourceState(order);
            return `<button class="production-support-row" type="button" onclick="openTaskOutsourceOrder('${escapeHtml(order.id)}')"><strong>${escapeHtml(order.supplier_name || "待确认供应商")} · ${escapeHtml(order.process || "外协加工")}</strong><span>${escapeHtml(state?.label || "状态待确认")}${order.due_at ? ` · 交期 ${escapeHtml(order.due_at)}` : ""}</span></button>`;
          }).join("")
        : productionSupportEmpty(tasks.length ? "当前生产订单没有进行中的外协；需要时可从具体工序点击“转外协”。" : "建立生产工单后，可把覆膜、模切、糊盒等工序转给外协供应商。");
    }
    function renderProductionCenter() {
      const board = document.getElementById("productionCenterKanban");
      if (!board) return;
      const realTasks = productionCenterTasks();
      const showingDemo = realTasks.length === 0;
      const source = showingDemo ? productionCenterDemoTasks() : realTasks;
      const visible = source.filter(productionCenterTaskMatches);
      renderProductionCenterMetrics(realTasks);
      renderProductionSupport(realTasks);
      const note = document.getElementById("productionCenterExampleNote");
      if (note) note.innerHTML = showingDemo
        ? `<div class="production-example-note"><span><strong>流程示例 · 不计入经营数据</strong>　当前没有真实生产工单，下面只用于预览车间看板的使用方式。</span><button class="btn light" type="button" onclick="openProductionTaskCreator()">建立第一张工单</button></div>`
        : "";
      board.innerHTML = productionCenterStages.map(stage => {
        const cards = visible.filter(task => (task.__demo ? task.__stage : productionTaskMeta(task).stage) === stage);
        return `<section class="production-lane" data-stage="${stage}" tabindex="-1"><div class="production-lane-head"><strong>${stage === "制前制版" ? "制前 / 制版" : stage === "质检交付" ? "质检 / 交付" : stage}</strong><span>${cards.length}</span></div><div class="production-lane-list">${cards.length ? cards.map(productionWorkCardHtml).join("") : `<div class="production-lane-empty">${productionCenterSearch || productionCenterFilter !== "全部" ? "筛选后暂无工单" : "当前没有工单"}</div>`}</div></section>`;
      }).join("");
    }
    function setProductionCenterSearch(value) {
      productionCenterSearch = String(value || "");
      renderProductionCenter();
    }
    function setProductionCenterFilter(value) {
      productionCenterFilter = String(value || "全部");
      renderProductionCenter();
    }
    async function refreshProductionCenter() {
      try {
        await Promise.all([
          Promise.resolve(window.__loadFormalTasks?.()),
          loadProductionOutsourceOrders(true)
        ]);
        renderProductionCenter();
        notify("生产与外协进度已刷新。");
      } catch (error) {
        renderProductionCenter();
        notify("暂时没有读到云端更新，已保留当前生产数据。" );
      }
    }
    function openProductionTaskCreator() {
      window.__taskDetailReturnPage = "生产中心";
      showPage("跟单工单");
      const details = document.getElementById("taskCreateDetails");
      const status = document.getElementById("manualTaskStatus");
      if (details) details.open = true;
      if (status) status.value = "生产";
      window.requestAnimationFrame(() => details?.scrollIntoView({ behavior:"smooth", block:"center" }));
    }
    function openProductionTaskDetail(id) {
      openTaskInProductionCenter(id);
    }
    function taskProgressCompactHtml(task) {
      if (taskPhase(task) !== "生产中") return escapeHtml(task.nextAction || "待安排");
      const progress = taskProductionProgress(task);
      const next = progress.steps.find(step => !step.completed);
      return `<div class="task-progress-compact"><strong>${progress.completed}/${progress.total} 步</strong><div class="task-progress-track"><span style="width:${progress.percent}%"></span></div><span class="task-phase-substatus">${escapeHtml(next ? `当前：${next.label}` : "全部步骤已完成")}</span></div>`;
    }
    function taskMatchesFilter(task, filter) {
      const status = task.status || "待处理";
      if (filter === "全部") return true;
      return taskPhase(status) === filter;
    }
    function taskMatchesSearch(task) {
      const keyword = taskSearch.trim().toLowerCase();
      if (!keyword) return true;
      const text = [
        task.id,
        task.customer,
        task.title,
        task.status,
        task.nextAction,
        task.owner,
        task.due,
        task.nextFollow,
        task.amount,
        task.risk,
        ...taskProductionSteps(task).map(step => step.label),
        ...(task.logs || [])
      ].filter(Boolean).join(" ").toLowerCase();
      return text.includes(keyword);
    }
    function taskFilterForStatus(status) {
      return taskPhase(status);
    }
    function chooseUsefulInitialTaskFilter() {
      if (taskFilterManuallySelected || taskSearch.trim()) return;
      const tasks = businessTasks();
      if (!tasks.length || tasks.some(task => taskMatchesFilter(task, taskFilter))) return;
      const firstPopulated = ["接单前", "生产中", "交付中", "回款中", "已完成"]
        .find(filter => tasks.some(task => taskMatchesFilter(task, filter)));
      if (firstPopulated) taskFilter = firstPopulated;
    }
    function renderTaskFilters() {
      const box = document.getElementById("taskFilters");
      if (!box) return;
      const filters = ["接单前", "生产中", "交付中", "回款中", "已完成"];
      box.innerHTML = filters.map(filter => {
        const count = businessTasks().filter(task => taskMatchesFilter(task, filter)).length;
        return `<button class="${taskFilter === filter ? "active" : ""}" onclick="setTaskFilter('${filter}')">${filter} ${count}</button>`;
      }).join("");
    }
    function setTaskFilter(filter) {
      taskFilterManuallySelected = true;
      taskFilter = filter;
      renderTasks();
    }
    function setTaskSearch(keyword) {
      taskSearch = keyword || "";
      renderTasks();
    }
    function clearTaskSearch() {
      taskSearch = "";
      const input = document.getElementById("taskSearch");
      if (input) input.value = "";
      renderTasks();
    }
    function taskAdvanceMeta(status, task = null) {
      const flow = ["询盘", "待补资料", "已报价", "待确认", "打样", "生产", "待质检", "待发货", "已发货", "已签收", "待回款", "已回款", "已完成"];
      const normalized = ({ "生产中":"生产", "接单前":"询盘", "已归档":"已完成" })[status] || status;
      const current = flow.indexOf(normalized);
      if (current < 0 || current >= flow.length - 1) return { next: "", label: normalized === "已完成" ? "已完成" : "确认下一步" };
      const next = flow[current + 1];
      if (task && (normalized === "已报价" || next === "已报价") && !taskHasRelatedQuote(task)) {
        return { next: "报价", label: normalized === "已报价" ? "补建报价" : "去报价", route: "quote", blocked: false };
      }
      const confirmations = Array.isArray(task?.customerConfirmations) ? task.customerConfirmations : [];
      if (next === "生产" && confirmations.length && confirmations[0]?.status !== "确认通过") {
        return { next, label: "等待客户确认", blocked: true, reason: `客户确认状态为“${confirmations[0]?.status || "待确认"}”，不能进入生产。` };
      }
      if (normalized === "生产") {
        const progress = taskProductionProgress(task);
        const blocked = progress.total > 0 && progress.completed < progress.total;
        return {
          next: "待质检",
          label: blocked ? `生产 ${progress.completed}/${progress.total}` : "提交质检",
          blocked,
        };
      }
      if (normalized === "待质检") {
        const quality = task ? factoryOperationsForTask(task).quality?.status : "待质检";
        return { next:"待发货", label:quality === "合格" ? "质检合格，待发货" : "等待质检合格", blocked:quality !== "合格" };
      }
      if (normalized === "待发货") {
        const shipped = task ? Number(factoryOperationsForTask(task).warehouse?.shipped_quantity || 0) : 0;
        return { next:"已发货", label:shipped > 0 ? "确认已经发货" : "先记录发货数量", blocked:shipped <= 0 };
      }
      if (normalized === "已发货") {
        const warehouse = task ? factoryOperationsForTask(task).warehouse : {};
        const shipped = Number(warehouse?.shipped_quantity || 0);
        const signed = Number(warehouse?.signed_quantity || 0);
        return { next:"已签收", label:shipped > 0 && signed >= shipped ? "确认客户签收" : `待签收 ${Math.max(0, shipped - signed).toLocaleString("zh-CN")} 件`, blocked:shipped <= 0 || signed < shipped };
      }
      if (normalized === "待回款") {
        const summary = task ? factorySummaryForTask(task) : {};
        const hasOrderAmount = Number(summary.order_amount || 0) > 0;
        const settled = hasOrderAmount && Number(summary.receivable_amount || 0) <= 0;
        return { next:"已回款", label:settled ? "确认款项结清" : "仍有应收款", blocked:!settled };
      }
      const labels = {
        "待补资料": "补充资料",
        "已报价": "标记已报价",
        "待确认": "进入待确认",
        "打样": "安排打样",
        "生产": "进入生产中",
        "待质检": "提交质检",
        "待发货": "进入待发货",
        "已发货": "确认已发货",
        "已签收": "确认客户签收",
        "待回款": "进入待回款",
        "已回款": "确认款项结清",
        "已完成": "完成归档"
      };
      return { next, label: labels[next] || `进入${next}`, blocked: false };
    }
    function nextActionForTaskStatus(status, task = null) {
      const actions = {
        "待补资料": "向客户补齐尺寸、数量、材质、工艺、税运和交期",
        "已报价": "把报价发给客户，确认价格口径和下一步",
        "待确认": "确认效果图、规格、工艺和下单条件",
        "打样": "安排打样并记录客户确认意见",
        "生产": "按生产清单逐步完成并打勾",
        "待质检": "核对成品数量、质量标准和不良处理",
        "待发货": "核对收货信息、包装和发货数量",
        "已发货": "跟进物流并等待客户签收",
        "已签收": "确认客户验收结果并核对应收款",
        "待回款": "按账期催收定金、尾款或剩余应收",
        "已回款": "核对开票与回款记录，确认无误后归档",
        "已完成": "事项已闭环"
      };
      if (status === "生产" && task) {
        const next = taskProductionSteps(task).find(step => !step.completed);
        return next ? `当前生产步骤：${next.label}` : "全部生产步骤已完成，可点击“完成订单”";
      }
      if (task?.currentIndustryStage?.action) return task.currentIndustryStage.action;
      return actions[status] || "确认当前阶段资料和下一步动作";
    }
    function taskFollowupMeta(task) {
      if (taskPhase(task) === "已完成") return { score: 0, urgent: false, label: "已完成" };
      const dueState = taskDueState(task);
      const statusScore = {
        "待补资料":34, "已报价":48, "待确认":44, "打样":36, "生产":24,
        "待质检":30, "待发货":38, "已发货":34, "已签收":42, "待回款":58, "已回款":16
      }[task?.status] || 10;
      const dueScore = dueState === "overdue" ? 90 : dueState === "today" ? 70 : dueState === "tomorrow" ? 45 : 0;
      const amountScore = Number(task?.amount || 0) >= 50000 ? 20 : Number(task?.amount || 0) >= 10000 ? 10 : 0;
      const riskScore = isUrgentTask(task) ? 24 : 0;
      const score = dueScore + statusScore + amountScore + riskScore;
      const urgent = dueState === "today" || riskScore > 0;
      const label = dueState === "overdue" ? "待继续跟进" : dueState === "today" ? "今天计划跟进" : dueState === "tomorrow" ? "明天跟进" : Number(task?.amount || 0) >= 50000 ? "大额机会" : ["已报价", "待确认"].includes(task?.status) ? "成交关键期" : "按计划推进";
      return { score, urgent, label };
    }
    function taskFollowupScript(task) {
      const customer = String(task?.customer || "客户").split(" · ")[0] || "客户";
      const product = String(task?.title || task?.customer || "这个产品").replace(/\s*报价后跟进$/, "").replace(/\s*跟进$/, "").split(" · ").pop();
      const amount = Number(task?.amount || 0);
      const priceText = amount > 0 ? `，上次报价总额约 ¥${amount.toFixed(0)}` : "";
      const scripts = {
        "待补资料": `您好，${customer}。您咨询的${product}我这边已经在整理${priceText}。为了把价格和交期核准，还需要补充一下尺寸、数量、材质、工艺、税运或收货地区。您把现有资料直接发我就行，齐了我马上继续核价。`,
        "已报价": `您好，${customer}。之前发您的${product}报价您看到了吗${priceText}？如果预算、数量或工艺需要调整，可以直接告诉我，我可以从材料、工艺和数量阶梯三个方向重新优化，不会简单降低质量。`,
        "待确认": `您好，${customer}。${product}现在就差最终确认了。麻烦您核对一下尺寸、材质、Logo/工艺、税费、交期和付款方式；运费统一另计。您回复“确认”或把要改的地方发我，我这边马上安排下一步。`,
        "打样": `您好，${customer}。${product}打样正在推进，请您再确认颜色、尺寸、公差、Logo位置和验收标准。样品完成后我会第一时间发图或寄样，确认后再安排大货。`,
        "生产": `您好，${customer}。${product}已进入生产阶段，目前按确认稿和计划交期推进。如收货信息或包装要求有变化，请尽快告诉我，避免影响出货。`,
        "待质检": `您好，${customer}。${product}已经完成生产，正在做出货前质检。我们会核对数量、外观、工艺和包装，合格后再安排发货。`,
        "待发货": `您好，${customer}。${product}已质检合格，正在核对收货信息和装箱数量。请确认收货人、电话和地址，确认后安排发货。`,
        "已发货": `您好，${customer}。${product}已经发出，请注意查收物流。收到后麻烦帮我确认数量、外观和包装情况，有问题直接拍照发我，我会马上跟进。`,
        "已签收": `您好，${customer}。物流显示${product}已经签收，请您核对数量、外观和包装。如验收无误，也请回复确认，我们继续处理对账和尾款。`,
        "待回款": `您好，${customer}。${product}已经交付并进入对账阶段。麻烦核对本单定金、尾款、开票和账期，剩余款项请按约定时间安排；如需对账单我可以马上发您。`,
        "已回款": `您好，${customer}。这批${product}的款项已经核对完成，感谢信任。后续补货或规格调整可以直接发消息，我会按本次订单资料快速报价。`
      };
      return scripts[task?.status] || `您好，${customer}。关于${product}这边我来跟进一下。您目前最关心的是价格、交期还是效果确认？告诉我后我马上按您的重点继续处理。`;
    }
    function copyTaskFollowup(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      navigator.clipboard?.writeText(taskFollowupScript(task));
      addTaskLog(task, "已复制本阶段微信跟进话术");
      saveState();
      showTaskDetail(id);
      notify("跟进话术已复制，可以直接发给客户。" );
    }
    function markTaskContacted(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      const nextDate = new Date();
      nextDate.setDate(nextDate.getDate() + 2);
      const nextIso = localDateIso(nextDate);
      task.nextFollow = `${nextIso} 09:00`;
      task.updatedAt = new Date().toISOString();
      addTaskLog(task, `已联系客户，若未回复将在 ${nextIso} 再次提醒`);
      saveState();
      renderTasks();
      renderDashboard();
      showTaskDetail(id);
      notify("已记录联系，2天后会再次提醒。" );
    }
    function canUseTaskRecycleBin() {
      return window.__hasPermission?.("record.delete") === true;
    }
    const TASK_RECYCLE_RETENTION_DAYS = 30;
    const TASK_RECYCLE_RETENTION_MS = TASK_RECYCLE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
    function taskRecycleExpiryTimestamp(task) {
      const explicit = Date.parse(task?.trashExpiresAt || task?.trash_expires_at || "");
      if (Number.isFinite(explicit)) return explicit;
      const deleted = Date.parse(task?.deletedAt || task?.deleted_at || "");
      return Number.isFinite(deleted) ? deleted + TASK_RECYCLE_RETENTION_MS : NaN;
    }
    function purgeExpiredLocalTaskRecycleItems(referenceTime = Date.now()) {
      const items = Array.isArray(state.taskRecycleBin) ? state.taskRecycleBin : [];
      let changed = false;
      state.taskRecycleBin = items.filter(task => {
        if (task?.localRecycle !== true) return true;
        const expiresAt = taskRecycleExpiryTimestamp(task);
        const keep = !Number.isFinite(expiresAt) || expiresAt > referenceTime;
        if (!keep) changed = true;
        return keep;
      });
      if (changed) saveState();
      return changed;
    }
    function currentTaskRecycleCompanyKey() {
      return String(window.__currentUser?.company_id || (window.location.protocol === "file:" ? "local-preview" : ""));
    }
    function refreshCombinedTaskRecycleItems() {
      purgeExpiredLocalTaskRecycleItems();
      const companyKey = currentTaskRecycleCompanyKey();
      const localItems = (state.taskRecycleBin || []).filter(item =>
        item?.localRecycle === true && String(item.recycleCompanyId || "") === companyKey
      );
      taskRecycleItems = [...formalTaskRecycleItems, ...localItems];
    }
    function scheduleTaskRecycleExpiryRefresh() {
      if (taskRecycleExpiryTimer) {
        window.clearTimeout(taskRecycleExpiryTimer);
        taskRecycleExpiryTimer = 0;
      }
      const nowTime = Date.now();
      const nextExpiry = taskRecycleItems
        .map(taskRecycleExpiryTimestamp)
        .filter(expiresAt => Number.isFinite(expiresAt) && expiresAt > nowTime)
        .sort((left, right) => left - right)[0];
      if (!Number.isFinite(nextExpiry)) return;
      const delay = Math.min(Math.max(1000, nextExpiry - nowTime + 250), 2147483000);
      taskRecycleExpiryTimer = window.setTimeout(() => {
        taskRecycleExpiryTimer = 0;
        purgeExpiredLocalTaskRecycleItems();
        const refresh = window.__loadFormalTasks?.();
        if (refresh && typeof refresh.finally === "function") {
          refresh.catch(() => null).finally(renderTaskRecycleBin);
        } else {
          renderTaskRecycleBin();
        }
      }, delay);
    }
    function taskDetailPageIsActive() {
      return document.querySelector('.page.active')?.dataset.page === "跟单详情";
    }
    function resetTaskDetailOutput(message = "请选择一条跟单任务查看详情。") {
      selectedTaskDetailId = "";
      const box = document.getElementById("taskDetailOutput");
      if (box) box.innerHTML = `<div class="task-detail-empty">${escapeHtml(message)}</div>`;
      if (taskDetailPageIsActive()) {
        setTextIfExists("pageTitle", "任务详情");
        setTextIfExists("pageSub", "先看当前进度和下一步，再处理客户跟进、生产步骤和操作记录。");
      }
    }
    function refreshSelectedTaskDetail() {
      if (!selectedTaskDetailId) return;
      const task = businessTasks().find(item => item.id === selectedTaskDetailId);
      if (task) showTaskDetail(task.id);
      else resetTaskDetailOutput();
    }
    function renderTaskRecycleBin() {
      const box = document.getElementById("taskRecycleBin");
      const list = document.getElementById("taskRecycleList");
      const count = document.getElementById("taskRecycleCount");
      if (!box || !list || !count) return;
      const allowed = canUseTaskRecycleBin();
      box.classList.toggle("is-hidden", !allowed);
      refreshCombinedTaskRecycleItems();
      count.textContent = String(taskRecycleItems.length);
      if (!allowed) {
        list.innerHTML = "";
        scheduleTaskRecycleExpiryRefresh();
        return;
      }
      list.innerHTML = taskRecycleItems.map(task => {
        const deletedAt = task.deletedAt ? new Date(task.deletedAt).toLocaleString("zh-CN", { hour12:false }) : "时间未知";
        const expiresAt = taskRecycleExpiryTimestamp(task);
        const remainingDays = Number.isFinite(expiresAt) ? Math.max(0, Math.ceil((expiresAt - Date.now()) / 86400000)) : TASK_RECYCLE_RETENTION_DAYS;
        const expiryDate = Number.isFinite(expiresAt) ? new Date(expiresAt).toLocaleDateString("zh-CN") : "移入后第30天";
        return `<div class="notification-item">
          <div class="notification-main">
            <strong>${escapeHtml(task.customer || task.title || "未命名客户")}</strong>
            <span>${escapeHtml(task.title || "跟单任务")} · ${escapeHtml(task.status || "待处理")}</span>
            <em><span class="task-recycle-expiry">还剩 ${remainingDays} 天</span>预计 ${escapeHtml(expiryDate)} 自动清理 · 移入时间：${escapeHtml(deletedAt)}${task.deletedBy ? ` · 操作人：${escapeHtml(task.deletedBy)}` : ""}</em>
          </div>
          <div class="actions"><button class="btn light" type="button" onclick="restoreTask('${String(task.id || "").replace(/[^A-Za-z0-9_-]/g, "")}')">恢复</button></div>
        </div>`;
      }).join("") || `<div class="notification-empty">回收站是空的</div>`;
      scheduleTaskRecycleExpiryRefresh();
    }
    async function trashTask(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      if (!canUseTaskRecycleBin()) return notify("只有老板或获授权员工可以删除跟单任务。" );
      if (!confirm("确认把这条跟单移入回收站吗？移入回收站后可恢复；客户聊天、报价、图片和文件不会删除。")) return;
      if (task.serverId) return window.__trashFormalTask?.(task);
      const deletedAt = new Date().toISOString();
      const trashExpiresAt = new Date(Date.parse(deletedAt) + TASK_RECYCLE_RETENTION_MS).toISOString();
      const localCopy = {
        ...task,
        localRecycle: true,
        recycleCompanyId: currentTaskRecycleCompanyKey(),
        deletedAt,
        trashExpiresAt,
        trashDaysRemaining: TASK_RECYCLE_RETENTION_DAYS,
        deletedBy: window.__currentUser?.name || "当前用户"
      };
      state.taskRecycleBin = [
        ...(state.taskRecycleBin || []).filter(item => !(
          item?.localRecycle === true
          && item.id === task.id
          && String(item.recycleCompanyId || "") === currentTaskRecycleCompanyKey()
        )),
        localCopy
      ];
      state.tasks = (state.tasks || []).filter(item => item !== task);
      saveState();
      refreshCombinedTaskRecycleItems();
      renderTasks();
      renderDashboard();
      resetTaskDetailOutput("跟单已移入回收站，可在本页底部恢复。客户聊天、报价、图片和文件都没有删除。");
      notify("已移入回收站，需要时可以恢复。" );
    }
    async function restoreTask(taskId) {
      if (!canUseTaskRecycleBin()) return notify("没有权限恢复跟单任务。" );
      const task = taskRecycleItems.find(item => item.id === taskId);
      if (!task) return notify("没有找到这条回收站记录，请刷新后重试。" );
      if (!confirm(`确认恢复“${task.customer || task.title || "这条跟单"}”吗？`)) return;
      if (task.serverId) return window.__restoreFormalTask?.(task);
      const restored = { ...task };
      delete restored.localRecycle;
      delete restored.recycleCompanyId;
      delete restored.deletedAt;
      delete restored.deletedBy;
      const activeCollision = businessTasks().find(item => item.id === restored.id);
      if (activeCollision) {
        const previousId = restored.id;
        restored.id = newTaskId();
        restored.restoredFromId = previousId;
        restored.logs = [
          ...(restored.logs || []),
          `${new Date().toLocaleString("zh-CN", { hour12:false })} 恢复时原编号 ${previousId} 已被占用，安全改为 ${restored.id}`
        ].slice(-20);
      }
      state.taskRecycleBin = (state.taskRecycleBin || []).filter(item => !(
        item?.localRecycle === true
        && item.id === task.id
        && String(item.recycleCompanyId || "") === currentTaskRecycleCompanyKey()
      ));
      state.tasks = [restored, ...(state.tasks || [])];
      saveState();
      refreshCombinedTaskRecycleItems();
      taskFilter = taskFilterForStatus(restored.status);
      renderTasks();
      renderDashboard();
      showTaskDetail(restored.id);
      notify(activeCollision ? `跟单已恢复；原编号被占用，已安全改为 ${restored.id}。` : "跟单已恢复到原来的位置。" );
    }
    function renderTasks() {
      const body = document.getElementById("taskBody");
      body.innerHTML = "";
      chooseUsefulInitialTaskFilter();
      renderTaskFilters();
      renderTaskReminderSummary();
      const visibleTasks = businessTasks()
        .filter(task => (taskSearch.trim() || taskMatchesFilter(task, taskFilter)) && taskMatchesSearch(task))
        .sort((a, b) => taskFollowupMeta(b).score - taskFollowupMeta(a).score || dateValue(b).getTime() - dateValue(a).getTime());
      visibleTasks.forEach(task => {
        const tr = document.createElement("tr");
        const advance = taskAdvanceMeta(task.status, task);
        const followup = taskFollowupMeta(task);
        const phase = taskPhase(task);
        const productionAction = ["生产中", "交付中"].includes(phase)
          ? `<button class="btn light" type="button" onclick="openTaskInProductionCenter('${escapeHtml(task.id)}')">进入生产</button>`
          : `<button class="btn light" type="button" onclick="advanceTask('${escapeHtml(task.id)}')"${advance.next && !advance.blocked ? "" : " disabled"}>${escapeHtml(advance.label)}</button>`;
        const progressOrNextHtml = phase === "生产中"
          ? taskProgressCompactHtml(task)
          : escapeHtml(task.nextAction || "待安排");
        tr.innerHTML = `<td data-label="编号"><span class="task-cell-id">${escapeHtml(displayTaskId(task))}</span></td>
          <td data-label="客户/产品"><span class="task-cell-customer">${escapeHtml(task.customer || task.title || "未命名客户")}</span><br><span class="task-priority-badge${followup.urgent ? " is-urgent" : ""}">${escapeHtml(followup.label)}</span></td>
          <td data-label="当前状态"><span class="status ${statusClass(task.status)}">${escapeHtml(phase)}</span>${phase === "接单前" ? `<span class="task-phase-substatus">${escapeHtml(task.status || "待处理")}</span>` : ""}</td>
          <td data-label="里程碑/下一步"><div class="task-cell-next">${progressOrNextHtml}</div></td>
          <td data-label="负责人"><span class="task-cell-owner">${escapeHtml(task.owner || "业务")}</span></td>
          <td data-label="截止/跟进"><span class="task-cell-due">${escapeHtml(phase === "已完成" ? "无需跟进" : formatTaskDue(task))}</span></td>
          <td data-label="金额"><span class="task-cell-money">${task.amount ? "¥" + Number(task.amount).toFixed(0) : "待报价"}</span></td>
          <td data-label="风险"><span class="task-cell-risk">${escapeHtml(taskRiskText(task))}</span></td>
          <td data-label="操作"><div class="task-row-actions">
            <button class="btn light" type="button" onclick="openTaskInOrderTracking('${escapeHtml(task.id)}')">订单详情</button>
            ${productionAction}
            <button class="btn light" type="button" onclick="rollbackTask('${escapeHtml(task.id)}')">撤回</button>
            ${canUseTaskRecycleBin() ? `<button class="btn light" type="button" onclick="trashTask('${escapeHtml(task.id)}')">删除</button>` : ""}
          </div></td>`;
        body.appendChild(tr);
      });
      if (!visibleTasks.length) {
        const tip = taskSearch.trim()
          ? `没有找到包含“${taskSearch.trim()}”的客户或工单。可以清空搜索后按三种类型查看。`
          : "当前类型下暂无任务。可以切换接单前、生产中或已完成。";
        body.innerHTML = `<tr><td colspan="9" class="muted">${escapeHtml(tip)}</td></tr>`;
      }
      renderTaskRecycleBin();
    }
    function advanceTask(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      const advance = taskAdvanceMeta(task.status, task);
      if (advance.route === "quote") return openQuoteFromTask(id);
      if (!advance.next || advance.blocked) {
        openTaskDetail(id);
        showTaskNotice(advance.reason || (taskPhase(task) === "已完成"
          ? "这条任务已经完成，无需继续推进。"
          : advance.blocked ? "请先逐项完成生产步骤；全部打勾后才能完成订单。" : "当前状态不在标准流程里，请先在详情中确认下一步。"));
        return;
      }
      showTaskConfirm(id, "advance", task.status || "待处理", advance.next);
    }
    function relatedQuoteForTask(task) {
      if (!task) return null;
      const exact = (state.quoteHistory || []).find(record =>
        (Number(task.quoteId || 0) > 0 && Number(record.serverId || 0) === Number(task.quoteId))
        || (task.source && record.caseSource && record.caseSource === task.source)
        || (Number(task.serverId || 0) > 0 && Number(record.followupTaskId || 0) === Number(task.serverId))
      );
      if (exact) return exact;
      const customerText = normalizeRuleText(task.cloudCustomer || task.customer || "");
      const productText = normalizeRuleText(task.title || task.customer || "");
      return (state.quoteHistory || []).find(record => {
        const customer = normalizeRuleText(record.customer || "");
        const product = normalizeRuleText(record.product || "");
        return customer && customerText.includes(customer) && (!product || productText.includes(product) || customerText.includes(product));
      }) || null;
    }
    function taskHasRelatedQuote(task) {
      return Boolean(Number(task?.quoteId || 0) > 0 || relatedQuoteForTask(task));
    }
    function openQuoteFromTask(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      const customer = String(task.cloudCustomer || task.customer || "新客户").split(" · ")[0].trim() || "新客户";
      const sessions = ensureCustomerSessions();
      const target = sessions.find(item => task.source && homeChatTaskSource(item) === task.source)
        || sessions.find(item => Number(item.followupTaskId || 0) > 0 && Number(item.followupTaskId) === Number(task.serverId || 0))
        || [...sessions].reverse().find(item => item.customer === customer);
      if (target) {
        state.activeCustomerSessionId = target.id;
        restoreCustomerSession(target);
      }
      const source = activeHomeBusinessSource(target) || [task.title, task.risk, task.nextAction].filter(Boolean).join("；");
      setIfExists("customerText", source || `${task.title || "定制产品"}需要报价`);
      setIfExists("inquiryCustomerName", customer);
      setQuoteBusinessContext("task", task, source);
      applyInquiryToQuote();
    }
    function syncWonOutcomeFromTask(task, nextStatus) {
      if (!["生产", "待质检", "待发货", "已发货", "已签收", "待回款", "已回款", "已完成"].includes(nextStatus)) return;
      const record = relatedQuoteForTask(task);
      if (!record) return;
      const firstWin = record.outcomeStatus !== "已成交";
      record.outcomeStatus = "已成交";
      record.actualDealTotal = Number(record.actualDealTotal || task.amount || record.total || 0);
      record.actualDealQty = Number(record.actualDealQty || record.qty || 0);
      record.outcomeReason = record.outcomeReason || `跟单已进入${nextStatus}阶段`;
      const profile = (state.customerProfiles || []).find(item => item.company === record.customer);
      if (profile) {
        profile.dealStatus = "已成交";
        profile.actualDealTotal = record.actualDealTotal;
        profile.actualDealQty = record.actualDealQty;
        profile.lastDealAt = profile.lastDealAt || new Date().toISOString().slice(0, 10);
        profile.updatedAt = new Date().toLocaleString("zh-CN", { hour12: false });
        window.__saveCustomerProfileCloud?.(profile);
      }
      if (firstWin) {
        addTaskLog(task, `跟单进入${nextStatus}，已自动记录成交 ¥${record.actualDealTotal.toFixed(2)}`);
        appendCustomerHistory(record.customer, `跟单进入${nextStatus}，自动记录成交 ¥${record.actualDealTotal.toFixed(2)}`, {
          dealStatus: "已成交",
          need: record.product,
          preference: `${record.material || ""}；${record.process || ""}`
        });
        window.__saveQuoteOutcomeCloud?.(record, {
          status: "已成交",
          actual_deal_total: record.actualDealTotal,
          actual_deal_qty: record.actualDealQty,
          actual_material_cost: Number(record.actualMaterialCost || 0),
          actual_process_cost: Number(record.actualProcessCost || 0),
          actual_other_cost: Number(record.actualOtherCost || 0),
          reason: record.outcomeReason
        });
      }
    }
    function openTaskRelatedQuote(id) {
      const task = businessTasks().find(item => item.id === id);
      const record = relatedQuoteForTask(task);
      if (!record) return notify("没有找到这条跟单关联的报价记录。");
      viewSavedQuote(encodeURIComponent(quoteRecordKey(record)));
      if (record.outcomeStatus === "已成交") {
        setTextIfExists("quoteOutcomeStatus", record.actualProfit == null
          ? "已记录成交。订单结束后可补录实际材料、加工和其他成本，首页会计算真实利润。"
          : `已记录实际利润 ¥${Number(record.actualProfit).toFixed(2)}。`);
      }
    }
    function confirmAdvanceTask(id, next) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      const advance = taskAdvanceMeta(task.status, task);
      if (advance.blocked) {
        showTaskDetail(id);
        showTaskNotice(advance.reason || "还有生产步骤未完成，请逐项打勾后再完成订单。");
        return;
      }
      addTaskLog(task, `推进：${task.status || "待处理"} → ${next}`);
      task.status = next;
      if (task.status === "生产") taskProductionSteps(task);
      task.nextAction = nextActionForTaskStatus(task.status, task);
      if (!task.ownerUserId) {
        if (task.status === "生产") task.owner = "生产";
        if (task.status === "待质检") task.owner = "质检";
        if (["待发货", "已发货"].includes(task.status)) task.owner = "仓库";
        if (task.status === "已签收") task.owner = "业务";
        if (["待回款", "已回款", "已完成"].includes(task.status)) task.owner = "财务";
      }
      const nextDate = new Date();
      nextDate.setDate(nextDate.getDate() + 1);
      const nextIso = `${nextDate.getFullYear()}-${String(nextDate.getMonth() + 1).padStart(2, "0")}-${String(nextDate.getDate()).padStart(2, "0")}`;
      task.due = task.status === "已完成" ? task.due : nextIso;
      task.nextFollow = task.status === "已完成" ? "无需跟进" : `${nextIso} 09:00`;
      syncWonOutcomeFromTask(task, next);
      taskFilter = taskFilterForStatus(task.status);
      saveState();
      renderTasks();
      renderDashboard();
      showTaskDetail(id);
      window.__moveFormalTask?.(task, "advance");
    }
    function rollbackTask(id) {
      const flow = ["询盘", "待补资料", "已报价", "待确认", "打样", "生产", "待质检", "待发货", "已发货", "已签收", "待回款", "已回款", "已完成"];
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      const normalized = ({ "生产中":"生产", "已归档":"已完成", "接单前":"询盘" })[task.status] || task.status;
      const current = flow.indexOf(normalized);
      if (current === -1) {
        openTaskDetail(id);
        showTaskNotice("当前状态不在标准流程里，先确认详情后再处理。");
        return;
      }
      if (current <= 0) {
        openTaskDetail(id);
        showTaskNotice("当前已经是最前面的状态，不能再撤回。");
        return;
      }
      const prev = flow[current - 1];
      showTaskConfirm(id, "rollback", task.status, prev);
    }
    function confirmRollbackTask(id, prev) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      addTaskLog(task, `撤回：${task.status} → ${prev}`);
      task.status = prev;
      task.nextAction = "已撤回，请重新确认当前阶段资料和下一步动作";
      const todayIso = localDateIso();
      task.due = todayIso;
      task.nextFollow = `${todayIso} 17:00`;
      taskFilter = taskFilterForStatus(task.status);
      saveState();
      renderTasks();
      renderDashboard();
      showTaskDetail(id);
      window.__moveFormalTask?.(task, "rollback");
    }
    function addTaskLog(task, text) {
      const stamp = new Date().toLocaleString("zh-CN", { hour12: false });
      task.logs = dedupeTaskLogs([`${stamp} ${text}`, ...(task.logs || [])]).slice(0, 10);
    }
    function dedupeTaskLogs(logs = []) {
      const seen = new Set();
      return logs.filter(log => {
        const text = String(log || "").trim();
        if (!text) return false;
        if (/云端跟单已同步/.test(text)) return false;
        let key = text.replace(/^\d{4}[\/\-.]\d{1,2}[\/\-.]\d{1,2}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?\s*/, "").replace(/\s+/g, " ");
        if (/报价生成后自动创建(?:跟进任务|跟单)/.test(key)) key = "报价生成后自动创建跟单";
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
      });
    }
    function showTaskNotice(text) {
      const box = document.getElementById("taskDetailOutput");
      if (!box) return;
      box.querySelector(".task-detail-notice")?.remove();
      box.insertAdjacentHTML("afterbegin", `<div class="task-detail-notice" role="status">${escapeHtml(text)}</div>`);
    }
    function taskAssignmentControls(task) {
      if (!window.__hasPermission?.("task.manage") || !task?.serverId) return "";
      const members = (window.__teamMembers || []).filter(item => item.status === "active");
      if (!members.length) return "";
      const options = members.map(item => `<option value="${Number(item.id)}"${Number(task.ownerUserId || 0) === Number(item.id) ? " selected" : ""}>${escapeHtml(item.name)}（${escapeHtml(item.role_label || item.role || "员工")}）</option>`).join("");
      return `<div style="margin-top:12px"><strong>分配负责人</strong><div class="actions"><select id="taskAssigneeSelect" style="min-width:220px">${options}</select><button class="btn light" type="button" onclick="assignTaskOwner('${escapeHtml(task.id)}')">保存负责人</button></div></div>`;
    }
    function assignTaskOwner(id) {
      const task = businessTasks().find(item => item.id === id);
      const userId = Number(document.getElementById("taskAssigneeSelect")?.value || 0);
      if (!task || !userId) return;
      window.__assignFormalTask?.(task, userId);
    }
    function showTaskConfirm(id, type, from, to) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return;
      const actionText = type === "advance" ? taskAdvanceMeta(from, task).label : "撤回";
      if (!confirm(`确认“${actionText}”吗？\n\n当前状态：${from}\n确认后变为：${to}\n\n确认后会写入操作记录；取消不会改变任务。`)) return;
      if (type === "advance") confirmAdvanceTask(id, to);
      else confirmRollbackTask(id, to);
    }
    function factoryOperationsForTask(task) {
      const source = task?.factoryOperations && typeof task.factoryOperations === "object" ? task.factoryOperations : {};
      const section = (name, defaults) => ({ ...defaults, ...(source[name] && typeof source[name] === "object" ? source[name] : {}) });
      task.factoryOperations = {
        schedule:section("schedule", { priority:"普通", machine:"", team:"", planned_start:"", planned_end:"", planned_hours:0, capacity_per_day:0, rush_order:false, note:"" }),
        report:section("report", { target_quantity:0, good_quantity:0, scrap_quantity:0, current_step:"", note:"" }),
        quality:section("quality", { status:"待质检", inspected_quantity:0, passed_quantity:0, defect_quantity:0, note:"" }),
        warehouse:section("warehouse", { finished_quantity:0, shipped_quantity:0, signed_quantity:0, storage_location:"", tracking_no:"", shipments:[], note:"" }),
        finance:section("finance", { order_amount:Number(task?.amount || 0), deposit_amount:0, final_payment_amount:0, received_amount:0, payment_installments:[], invoiced_amount:0, invoice_status:"未开票", invoice_number:"", invoice_date:"", processing_cost:0, other_cost:0, payment_terms:"", payment_due:"", note:"" }),
        after_sales:section("after_sales", { cases:[], note:"" })
      };
      return task.factoryOperations;
    }
    function factorySummaryForTask(task) {
      const operations = factoryOperationsForTask(task);
      const purchases = productionTaskPurchases(task);
      const materialCost = purchases.reduce((total, item) => total + (Number(item.actual_quantity || item.quantity || 0) * Number(item.unit_price || 0)), 0);
      const processingCost = Number(operations.finance.processing_cost || 0);
      const otherCost = Number(operations.finance.other_cost || 0);
      const orderAmount = Number(operations.finance.order_amount || task.amount || 0);
      const totalCost = materialCost + processingCost + otherCost;
      const profit = orderAmount ? orderAmount - totalCost : 0;
      const installments = Array.isArray(operations.finance.payment_installments) ? operations.finance.payment_installments : [];
      const received = installments.length
        ? installments.filter(item => item?.status !== "已冲正").reduce((total, item) => total + Number(item?.amount || 0), 0)
        : Number(operations.finance.received_amount || 0);
      const paymentDue = String(operations.finance.payment_due || "");
      const dueTime = paymentDue ? Date.parse(`${paymentDue.slice(0,10)}T23:59:59`) : NaN;
      const overdueDays = orderAmount > received && Number.isFinite(dueTime) && dueTime < Date.now()
        ? Math.max(1, Math.floor((Date.now() - dueTime) / 86400000))
        : 0;
      const shipments = Array.isArray(operations.warehouse.shipments) ? operations.warehouse.shipments : [];
      const shippedQuantity = shipments.length
        ? shipments.filter(item => item?.status !== "待发货").reduce((total, item) => total + Number(item?.quantity || 0), 0)
        : Number(operations.warehouse.shipped_quantity || 0);
      const signedQuantity = shipments.length
        ? shipments.filter(item => item?.status === "已签收").reduce((total, item) => total + Number(item?.quantity || 0), 0)
        : Number(operations.warehouse.signed_quantity || 0);
      const afterSalesCases = Array.isArray(operations.after_sales.cases) ? operations.after_sales.cases : [];
      const openAfterSales = afterSalesCases.filter(item => !["已解决","已关闭"].includes(item?.status)).length;
      const localSummary = {
        material_cost:materialCost, processing_cost:processingCost, other_cost:otherCost, total_cost:totalCost,
        order_amount:orderAmount, profit, margin_rate:orderAmount ? profit / orderAmount * 100 : 0,
        received_amount:received, receivable_amount:Math.max(0, orderAmount - received), payment_due:paymentDue,
        overdue_days:overdueDays, invoice_status:operations.finance.invoice_status || "未开票",
        shipment_count:shipments.length, shipment_address_count:new Set(shipments.map(item => String(item?.address || "").trim()).filter(Boolean)).size,
        shipped_quantity:shippedQuantity, signed_quantity:signedQuantity,
        delivery_status:shippedQuantity && signedQuantity >= shippedQuantity ? "已签收" : (shippedQuantity ? "已发货" : "待发货"),
        open_after_sales:openAfterSales, after_sales_total:afterSalesCases.length
      };
      return task.factorySummary && typeof task.factorySummary === "object"
        ? { ...localSummary, ...task.factorySummary }
        : localSummary;
    }
    function factoryMoney(value) {
      return `¥${Number(value || 0).toLocaleString("zh-CN", { maximumFractionDigits:2 })}`;
    }
    function factoryFieldId(task, section, field) {
      return `factory-${section}-${field}-${String(task.id).replace(/[^A-Za-z0-9_-]/g, "")}`;
    }
    function factoryField(task, section, field, label, type = "text", wide = false) {
      const value = factoryOperationsForTask(task)[section][field] ?? "";
      const id = factoryFieldId(task, section, field);
      if (type === "textarea") return `<label class="factory-operation-field is-wide"><span>${escapeHtml(label)}</span><textarea id="${id}" maxlength="300">${escapeHtml(String(value || ""))}</textarea></label>`;
      if (type === "priority") return `<label class="factory-operation-field"><span>${escapeHtml(label)}</span><select id="${id}">${["普通","优先","加急"].map(item => `<option${item === value ? " selected" : ""}>${item}</option>`).join("")}</select></label>`;
      if (type === "quality") return `<label class="factory-operation-field"><span>${escapeHtml(label)}</span><select id="${id}">${["待质检","质检中","合格","返工","不合格"].map(item => `<option${item === value ? " selected" : ""}>${item}</option>`).join("")}</select></label>`;
      if (type === "invoice") return `<label class="factory-operation-field"><span>${escapeHtml(label)}</span><select id="${id}">${["未开票","部分开票","已开票","不开票"].map(item => `<option${item === value ? " selected" : ""}>${item}</option>`).join("")}</select></label>`;
      if (type === "checkbox") return `<label class="factory-operation-field factory-operation-check"><span>${escapeHtml(label)}</span><input id="${id}" type="checkbox"${value ? " checked" : ""}><em>勾选后会提示插单对原交期的影响</em></label>`;
      const normalizedValue = type === "datetime-local" ? String(value || "").slice(0, 16) : value;
      return `<label class="factory-operation-field${wide ? " is-wide" : ""}"><span>${escapeHtml(label)}</span><input id="${id}" type="${type}" value="${escapeHtml(String(normalizedValue || ""))}"${type === "number" ? ' min="0" step="0.01"' : ""}></label>`;
    }
    const TASK_DETAIL_FEATURE_USAGE_KEY = "task_detail_feature_usage_v1";
    function taskDetailFeatureUsage() {
      try {
        const saved = JSON.parse(localStorage.getItem(TASK_DETAIL_FEATURE_USAGE_KEY) || "{}");
        return saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
      } catch (_) {
        return {};
      }
    }
    function recordTaskDetailFeatureUse(feature, opened, toggleEvent = null) {
      if (!opened || toggleEvent?.isTrusted === false) return;
      const key = String(feature || "").trim();
      if (!key) return;
      const usage = taskDetailFeatureUsage();
      usage[key] = Math.min(9999, Math.max(0, Number(usage[key] || 0)) + 1);
      try { localStorage.setItem(TASK_DETAIL_FEATURE_USAGE_KEY, JSON.stringify(usage)); } catch (_) {}
    }
    function taskDetailFeatureOrder(items = [], phase = "") {
      const usage = taskDetailFeatureUsage();
      const recommended = {
        "生产中":["schedule", "report"],
        "交付中":["quality", "warehouse"],
        "回款中":["finance"],
        "已完成":["finance", "after_sales"]
      }[phase] || [];
      return items.map((item, index) => ({ ...item, originalIndex:index })).sort((left, right) => {
        const usageDifference = Number(usage[right.section] || 0) - Number(usage[left.section] || 0);
        if (usageDifference) return usageDifference;
        const leftRecommended = recommended.indexOf(left.section);
        const rightRecommended = recommended.indexOf(right.section);
        if (leftRecommended !== rightRecommended) {
          if (leftRecommended === -1) return 1;
          if (rightRecommended === -1) return -1;
          return leftRecommended - rightRecommended;
        }
        return left.originalIndex - right.originalIndex;
      });
    }
    function factoryOperationEditor(task, section, title, fields, saveLabel = "保存") {
      return `<details class="factory-operation-editor" ontoggle="recordTaskDetailFeatureUse('${section}', this.open, event)"><summary>${escapeHtml(title)}</summary><div class="factory-operation-fields">${fields}<div class="factory-operation-actions"><button class="btn light" type="button" onclick="saveFactoryOperations('${escapeHtml(task.id)}','${section}')">${escapeHtml(saveLabel)}</button></div></div></details>`;
    }
    function factoryOperationsHtml(task) {
      if (taskPhase(task) === "接单前") return "";
      const operations = factoryOperationsForTask(task);
      const summary = factorySummaryForTask(task);
      const scheduleReady = !!(operations.schedule.machine || operations.schedule.team || operations.schedule.planned_start);
      const reportReady = Number(operations.report.good_quantity || 0) > 0;
      const qualityRisk = ["返工","不合格"].includes(operations.quality.status);
      const qualityReady = operations.quality.status === "合格";
      const warehouseReady = Number(operations.warehouse.finished_quantity || 0) > 0;
      const shippedReady = Number(operations.warehouse.shipped_quantity || 0) > 0;
      const signedReady = shippedReady && Number(operations.warehouse.signed_quantity || 0) >= Number(operations.warehouse.shipped_quantity || 0);
      const openAfterSales = Number(summary.open_after_sales || 0);
      const financeReady = Number(summary.receivable_amount || 0) <= 0 && Number(summary.order_amount || 0) > 0;
      const scheduleAlerts = task.scheduleAlerts && typeof task.scheduleAlerts === "object" ? task.scheduleAlerts : {};
      const scheduleAlertHtml = scheduleAlerts.has_conflict
        ? `<div class="factory-operation-alert"><strong>排产冲突</strong><span>${escapeHtml((scheduleAlerts.messages || []).join("；") || "同一机台的计划时间发生重叠，请调整机台或时间。")}</span></div>`
        : "";
      const flow = [
        ["排产", scheduleReady ? (operations.schedule.machine || operations.schedule.team || "已安排") : "待安排", operations.schedule.planned_start ? String(operations.schedule.planned_start).slice(0,10) : operations.schedule.priority, scheduleReady, false],
        ["报工", reportReady ? Number(operations.report.good_quantity).toLocaleString("zh-CN") : "待报工", operations.report.current_step || "记录产量与损耗", reportReady, false],
        ["质检", operations.quality.status, operations.quality.note || "记录抽检与不良", qualityReady, qualityRisk],
        ["成品入库", warehouseReady ? Number(operations.warehouse.finished_quantity).toLocaleString("zh-CN") : "待入库", operations.warehouse.storage_location || "记录库位", warehouseReady, false],
        ["发货", shippedReady ? Number(operations.warehouse.shipped_quantity).toLocaleString("zh-CN") : "待发货", operations.warehouse.tracking_no || "记录物流单号", shippedReady, false],
        ["签收", signedReady ? "已签收" : (shippedReady ? `${Number(operations.warehouse.signed_quantity || 0).toLocaleString("zh-CN")}/${Number(operations.warehouse.shipped_quantity || 0).toLocaleString("zh-CN")}` : "待发货"), signedReady ? "客户已签收" : "签收和发货不合并", signedReady, shippedReady && !signedReady],
        ["回款", financeReady ? "已结清" : factoryMoney(summary.receivable_amount), financeReady ? "订单款已闭环" : "剩余应收", financeReady, false]
      ];
      const flowHtml = flow.map(([label,value,note,done,risk]) => `<div class="factory-flow-item${done ? " is-done" : ""}${risk ? " is-risk" : ""}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(String(value))}</strong><em>${escapeHtml(String(note))}</em></div>`).join("");
      const scheduleFields = factoryField(task,"schedule","priority","优先级","priority") + factoryField(task,"schedule","machine","机台/设备") + factoryField(task,"schedule","team","班组/负责人") + factoryField(task,"schedule","planned_start","计划开工","datetime-local") + factoryField(task,"schedule","planned_end","计划完成","datetime-local") + factoryField(task,"schedule","planned_hours","预计用时（小时）","number") + factoryField(task,"schedule","capacity_per_day","日产能","number") + factoryField(task,"schedule","rush_order","加急插单","checkbox") + factoryField(task,"schedule","note","排产备注","textarea");
      const reportFields = factoryField(task,"report","target_quantity","计划生产数","number") + factoryField(task,"report","good_quantity","合格数","number") + factoryField(task,"report","scrap_quantity","损耗/废品数","number") + factoryField(task,"report","actual_labor_hours","实际人工工时","number") + factoryField(task,"report","labor_hourly_cost","人工小时成本","number") + factoryField(task,"report","actual_machine_hours","实际机时","number") + factoryField(task,"report","machine_hourly_cost","设备小时成本","number") + factoryField(task,"report","outside_processing_cost","实际外协成本","number") + factoryField(task,"report","current_step","当前工序") + factoryField(task,"report","note","报工备注","textarea");
      const qualityFields = factoryField(task,"quality","status","质检结论","quality") + factoryField(task,"quality","inspected_quantity","抽检数","number") + factoryField(task,"quality","passed_quantity","合格数","number") + factoryField(task,"quality","defect_quantity","不良数","number") + factoryField(task,"quality","note","问题、返工与处理意见","textarea");
      const shipments = Array.isArray(operations.warehouse.shipments) ? operations.warehouse.shipments : [];
      const shipmentHistory = `<div class="factory-shipment-history"><strong>分批/合并发货与多地址</strong>${shipments.length ? shipments.map(item => `<div class="factory-record-row"><span>${escapeHtml(item.status || "待发货")} · ${Number(item.quantity || 0).toLocaleString("zh-CN")} 件 · ${escapeHtml(item.address || "未填收货地址")}${item.contact ? ` · ${escapeHtml(item.contact)}` : ""}${item.carrier || item.tracking_no ? ` · ${escapeHtml([item.carrier,item.tracking_no].filter(Boolean).join(" "))}` : ""}</span><span class="factory-record-row-actions">${item.status === "已发货" ? `<button type="button" onclick="setFactoryShipmentStatus('${escapeHtml(task.id)}','${escapeHtml(item.id)}','已签收')">标记签收</button>` : ""}<button class="is-danger" type="button" onclick="removeFactoryShipment('${escapeHtml(task.id)}','${escapeHtml(item.id)}')">删除批次</button></span></div>`).join("") : `<span>还没有发货批次。同一订单可分批发往多个地址，也可把多个成品合并登记在一个批次中。</span>`}</div>`;
      const shipmentEntry = `<div class="factory-shipment-entry"><label>本次状态<select id="${factoryFieldId(task,"warehouse","new_shipment_status")}">${["待发货","已发货","已签收","异常"].map(value => `<option>${value}</option>`).join("")}</select></label><label>本次数量<input id="${factoryFieldId(task,"warehouse","new_shipment_quantity")}" type="number" min="0" step="1" placeholder="例如：500"></label><label>收货地址<input id="${factoryFieldId(task,"warehouse","new_shipment_address")}" placeholder="例如：上海分仓"></label><label>联系人/电话<input id="${factoryFieldId(task,"warehouse","new_shipment_contact")}" placeholder="收货人与联系方式"></label><label>物流公司<input id="${factoryFieldId(task,"warehouse","new_shipment_carrier")}" placeholder="例如：顺丰"></label><label>物流/送货单号<input id="${factoryFieldId(task,"warehouse","new_shipment_tracking")}" placeholder="可稍后补录"></label><label>发货日期<input id="${factoryFieldId(task,"warehouse","new_shipment_date")}" type="date"></label><label>签收日期<input id="${factoryFieldId(task,"warehouse","new_signed_date")}" type="date"></label><label>本批备注<input id="${factoryFieldId(task,"warehouse","new_shipment_note")}" placeholder="例如：第一批急件"></label></div>`;
      const warehouseFields = factoryField(task,"warehouse","finished_quantity","成品入库数","number") + factoryField(task,"warehouse","shipped_quantity","累计发货数（有批次时自动汇总）","number") + factoryField(task,"warehouse","signed_quantity","累计签收数（有批次时自动汇总）","number") + factoryField(task,"warehouse","storage_location","库位") + factoryField(task,"warehouse","tracking_no","旧单据物流号") + shipmentHistory + shipmentEntry + factoryField(task,"warehouse","note","入库与发货备注","textarea");
      const installments = Array.isArray(operations.finance.payment_installments) ? operations.finance.payment_installments : [];
      const paymentHistory = installments.length
        ? `<div class="factory-payment-history"><strong>客户对账记录</strong>${installments.map(item => `<div class="factory-record-row"><span${item.status === "已冲正" ? ' style="text-decoration:line-through;opacity:.65"' : ""}>${escapeHtml(item.received_at || "未填日期")} · ${escapeHtml(item.type || "收款")} ${factoryMoney(item.amount)}${item.note ? ` · ${escapeHtml(item.note)}` : ""}${item.status === "已冲正" ? ` · 已冲正（${escapeHtml(item.reversal_reason || "未填写原因")}）` : ""}</span>${item.status === "已冲正" ? "" : `<span class="factory-record-row-actions"><button class="is-danger" type="button" onclick="reverseFactoryPayment('${escapeHtml(task.id)}','${escapeHtml(item.id)}')">冲正</button></span>`}</div>`).join("")}</div>`
        : `<div class="factory-payment-history"><strong>客户对账记录</strong><span>还没有逐笔回款记录；可在下面登记本次收款。</span></div>`;
      const paymentEntry = `<div class="factory-payment-entry"><label>本次收款类型<select id="${factoryFieldId(task,"finance","new_payment_type")}">${["定金","进度款","尾款","其他"].map(value => `<option>${value}</option>`).join("")}</select></label><label>本次到账金额<input id="${factoryFieldId(task,"finance","new_payment_amount")}" type="number" min="0" step="0.01" placeholder="不登记可留空"></label><label>到账日期<input id="${factoryFieldId(task,"finance","new_payment_date")}" type="date"></label><label>对账备注<input id="${factoryFieldId(task,"finance","new_payment_note")}" placeholder="例如：尾款已到账"></label></div>`;
      const financeFields = factoryField(task,"finance","order_amount","订单金额","number") + factoryField(task,"finance","deposit_amount","约定定金","number") + factoryField(task,"finance","final_payment_amount","约定尾款","number") + factoryField(task,"finance","received_amount","累计回款（无逐笔记录时填写）","number") + factoryField(task,"finance","payment_terms","账期/付款约定") + factoryField(task,"finance","payment_due","约定回款日","date") + factoryField(task,"finance","invoice_status","开票状态","invoice") + factoryField(task,"finance","invoiced_amount","累计开票","number") + factoryField(task,"finance","invoice_number","发票号码") + factoryField(task,"finance","invoice_date","开票日期","date") + factoryField(task,"finance","processing_cost","实际加工成本","number") + factoryField(task,"finance","other_cost","其他实际成本","number") + paymentHistory + paymentEntry + factoryField(task,"finance","note","财务备注","textarea");
      const afterSalesCases = Array.isArray(operations.after_sales.cases) ? operations.after_sales.cases : [];
      const afterSalesHistory = `<div class="factory-after-sales-history"><strong>售后与质量追溯${openAfterSales ? ` · ${openAfterSales} 项待处理` : ""}</strong>${afterSalesCases.length ? afterSalesCases.map(item => `<div class="factory-record-row"><span>${escapeHtml(item.status || "待处理")} · ${escapeHtml(item.type || "客诉")} · ${Number(item.quantity || 0).toLocaleString("zh-CN")} 件 · ${escapeHtml(item.reason || "未填原因")}${item.raw_material_batch ? ` · 材料批次 ${escapeHtml(item.raw_material_batch)}` : ""}${item.machine ? ` · ${escapeHtml(item.machine)}` : ""}${item.operator ? ` · ${escapeHtml(item.operator)}` : ""}</span><span class="factory-record-row-actions">${!["已解决","已关闭"].includes(item.status) ? `<button type="button" onclick="setFactoryAfterSalesStatus('${escapeHtml(task.id)}','${escapeHtml(item.id)}','已解决')">标记解决</button>` : ""}<button class="is-danger" type="button" onclick="removeFactoryAfterSalesCase('${escapeHtml(task.id)}','${escapeHtml(item.id)}')">删除记录</button></span></div>`).join("") : `<span>暂无售后记录。出现客诉、补货或返工时，记录材料批次、机台和操作员，方便追查根因。</span>`}</div>`;
      const afterSalesEntry = `<div class="factory-after-sales-entry"><label>类型<select id="${factoryFieldId(task,"after_sales","new_case_type")}">${["客诉","补货","返工","退款","其他"].map(value => `<option>${value}</option>`).join("")}</select></label><label>状态<select id="${factoryFieldId(task,"after_sales","new_case_status")}">${["待处理","处理中","已解决","已关闭"].map(value => `<option>${value}</option>`).join("")}</select></label><label>涉及数量<input id="${factoryFieldId(task,"after_sales","new_case_quantity")}" type="number" min="0" step="1"></label><label>问题原因<input id="${factoryFieldId(task,"after_sales","new_case_reason")}" placeholder="必填：客户反馈或质量问题"></label><label>处理方案<input id="${factoryFieldId(task,"after_sales","new_case_resolution")}" placeholder="例如：补货 20 件"></label><label>负责人<input id="${factoryFieldId(task,"after_sales","new_case_owner")}"></label><label>材料批次<input id="${factoryFieldId(task,"after_sales","new_case_batch")}" placeholder="例如：PAPER-0721"></label><label>生产机台<input id="${factoryFieldId(task,"after_sales","new_case_machine")}"></label><label>操作员<input id="${factoryFieldId(task,"after_sales","new_case_operator")}"></label></div>`;
      const afterSalesFields = afterSalesHistory + afterSalesEntry + factoryField(task,"after_sales","note","售后总结与预防措施","textarea");
      const overdueText = Number(summary.overdue_days || 0) > 0 ? `已超过回款日 ${Number(summary.overdue_days)} 天` : (summary.payment_due ? `应收日 ${String(summary.payment_due).slice(0,10)}` : "未设置回款日");
      const editorHtml = taskDetailFeatureOrder([
        { section:"schedule", title:"机台与工时排产", fields:scheduleFields },
        { section:"report", title:"车间报工", fields:reportFields },
        { section:"quality", title:"质检记录", fields:qualityFields },
        { section:"warehouse", title:"分批发货与签收", fields:warehouseFields },
        { section:"finance", title:"应收、开票与对账", fields:financeFields },
        { section:"after_sales", title:"售后与质量追溯", fields:afterSalesFields }
      ], taskPhase(task)).map(item => factoryOperationEditor(task, item.section, item.title, item.fields)).join("");
      const actualCostNote = summary.actual_cost_complete ? "净领料与报工已核准" : Number(summary.total_cost || 0) > 0 ? "暂估：请补领退料或采购单价" : "采购与生产自动汇总";
      const hasAttention = Boolean(scheduleAlerts.has_conflict || qualityRisk || openAfterSales);
      const attentionText = scheduleAlerts.has_conflict ? "存在排产冲突" : qualityRisk ? `质检${operations.quality.status}` : openAfterSales ? `${openAfterSales}项售后待处理` : "常用功能会按本厂使用频率自动前移";
      return `<details class="factory-operations-card task-detail-secondary"${hasAttention || profitRisk ? " open" : ""} ontoggle="recordTaskDetailFeatureUse('factory_operations', this.open, event)"><summary class="factory-operations-summary"><div><strong>${hasAttention || profitRisk ? "生产经营有事项需要处理" : "更多生产经营管理"}</strong><span>${escapeHtml(profitRisk ? `${profitReview.status}：${(profitReview.reasons || []).join("；")}` : attentionText)}</span></div><em class="${hasAttention || profitRisk ? "is-risk" : ""}">${hasAttention || profitRisk ? "需处理" : "按需展开"}</em></summary><div class="factory-operations-body"><div class="factory-operations-head"><div><strong>订单执行闭环</strong><span>生产、质检、发货、签收、回款分开记录；利润单独复盘。</span></div><em>${escapeHtml(profitReview.status || "成本待复盘")}</em></div><div class="factory-flow">${flowHtml}</div>${scheduleAlertHtml}<div class="factory-profit-strip"><div><span>订单金额</span><strong>${factoryMoney(summary.order_amount)}</strong></div><div><span>预计成本</span><strong>${estimated.recorded ? factoryMoney(estimated.total) : "待完善"}</strong><em>报价时锁定</em></div><div class="${summary.actual_cost_complete ? "" : "is-risk"}"><span>实际成本</span><strong>${actual.recorded ? factoryMoney(actual.total) : factoryMoney(summary.total_cost)}</strong><em>${escapeHtml(actualCostNote)}</em></div><div class="${Number(variance.total || 0) > 0 ? "is-risk" : ""}"><span>成本偏差</span><strong>${actual.recorded && estimated.recorded ? `${Number(variance.total) >= 0 ? "+" : ""}${factoryMoney(variance.total)}` : "待完整数据"}</strong><em>${actual.recorded && estimated.recorded ? `${Number(variance.percent || 0).toFixed(1)}%` : "—"}</em></div><div><span>预计利润</span><strong>${estimated.recorded ? factoryMoney(estimated.profit) : "待完善"}</strong></div><div class="${profitRisk ? "is-risk" : ""}"><span>实际利润</span><strong>${actual.recorded ? factoryMoney(actual.profit) : "待录成本"}</strong><em>${actual.recorded ? `毛利率 ${Number(actual.margin_rate || 0).toFixed(1)}%` : escapeHtml(profitReview.status || "")}</em></div><div><span>累计回款</span><strong>${factoryMoney(summary.received_amount)}</strong></div><div class="${Number(summary.receivable_amount || 0) > 0 ? "is-risk" : ""}"><span>剩余应收</span><strong>${factoryMoney(summary.receivable_amount)}</strong><em>${escapeHtml(overdueText)}</em></div><div class="${openAfterSales ? "is-risk" : ""}"><span>待处理售后</span><strong>${openAfterSales} 项</strong><em>可追溯材料、机台和操作员</em></div></div><div class="factory-operations-editors">${editorHtml}</div></div></details>`;
    }
    function factoryOperationPayload(task, section) {
      const fields = {
        schedule:["priority","machine","team","planned_start","planned_end","planned_hours","capacity_per_day","rush_order","note"],
        report:["target_quantity","good_quantity","scrap_quantity","actual_labor_hours","labor_hourly_cost","actual_machine_hours","machine_hourly_cost","outside_processing_cost","current_step","note"],
        quality:["status","inspected_quantity","passed_quantity","defect_quantity","note"],
        warehouse:["finished_quantity","shipped_quantity","signed_quantity","storage_location","tracking_no","note"],
        finance:["order_amount","deposit_amount","final_payment_amount","received_amount","invoiced_amount","invoice_status","invoice_number","invoice_date","processing_cost","other_cost","payment_terms","payment_due","note"],
        after_sales:["note"]
      }[section] || [];
      const numeric = new Set(["target_quantity","good_quantity","scrap_quantity","actual_labor_hours","labor_hourly_cost","actual_machine_hours","machine_hourly_cost","outside_processing_cost","inspected_quantity","passed_quantity","defect_quantity","finished_quantity","shipped_quantity","signed_quantity","planned_hours","capacity_per_day","order_amount","deposit_amount","final_payment_amount","received_amount","invoiced_amount","processing_cost","other_cost"]);
      const payload = Object.fromEntries(fields.map(field => {
        const element = document.getElementById(factoryFieldId(task, section, field));
        const value = element?.type === "checkbox" ? element.checked : element?.value ?? "";
        return [field, numeric.has(field) ? Number(value || 0) : value];
      }));
      if (section === "finance") {
        const amount = Number(document.getElementById(factoryFieldId(task,"finance","new_payment_amount"))?.value || 0);
        if (amount > 0) payload.payment_installments = [...(factoryOperationsForTask(task).finance.payment_installments || []), {
          id:`payment-${Date.now()}`,
          type:document.getElementById(factoryFieldId(task,"finance","new_payment_type"))?.value || "其他",
          amount,
          received_at:document.getElementById(factoryFieldId(task,"finance","new_payment_date"))?.value || new Date().toISOString().slice(0,10),
          note:document.getElementById(factoryFieldId(task,"finance","new_payment_note"))?.value || ""
        }];
      }
      if (section === "warehouse") {
        const quantity = Number(document.getElementById(factoryFieldId(task,"warehouse","new_shipment_quantity"))?.value || 0);
        const address = document.getElementById(factoryFieldId(task,"warehouse","new_shipment_address"))?.value?.trim() || "";
        if (quantity > 0 || address) payload.shipments = [...(factoryOperationsForTask(task).warehouse.shipments || []), {
          id:`shipment-${Date.now()}`,
          status:document.getElementById(factoryFieldId(task,"warehouse","new_shipment_status"))?.value || "待发货",
          quantity,
          address,
          contact:document.getElementById(factoryFieldId(task,"warehouse","new_shipment_contact"))?.value || "",
          carrier:document.getElementById(factoryFieldId(task,"warehouse","new_shipment_carrier"))?.value || "",
          tracking_no:document.getElementById(factoryFieldId(task,"warehouse","new_shipment_tracking"))?.value || "",
          shipped_at:document.getElementById(factoryFieldId(task,"warehouse","new_shipment_date"))?.value || "",
          signed_at:document.getElementById(factoryFieldId(task,"warehouse","new_signed_date"))?.value || "",
          note:document.getElementById(factoryFieldId(task,"warehouse","new_shipment_note"))?.value || ""
        }];
      }
      if (section === "after_sales") {
        const reason = document.getElementById(factoryFieldId(task,"after_sales","new_case_reason"))?.value?.trim() || "";
        const quantity = Number(document.getElementById(factoryFieldId(task,"after_sales","new_case_quantity"))?.value || 0);
        if (reason || quantity > 0) payload.cases = [...(factoryOperationsForTask(task).after_sales.cases || []), {
          id:`after-sales-${Date.now()}`,
          type:document.getElementById(factoryFieldId(task,"after_sales","new_case_type"))?.value || "客诉",
          status:document.getElementById(factoryFieldId(task,"after_sales","new_case_status"))?.value || "待处理",
          quantity,
          reason,
          resolution:document.getElementById(factoryFieldId(task,"after_sales","new_case_resolution"))?.value || "",
          owner:document.getElementById(factoryFieldId(task,"after_sales","new_case_owner"))?.value || "",
          raw_material_batch:document.getElementById(factoryFieldId(task,"after_sales","new_case_batch"))?.value || "",
          machine:document.getElementById(factoryFieldId(task,"after_sales","new_case_machine"))?.value || "",
          operator:document.getElementById(factoryFieldId(task,"after_sales","new_case_operator"))?.value || "",
          created_at:new Date().toISOString()
        }];
      }
      return payload;
    }
    async function saveFactoryOperations(id, section) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return notify("没有找到这张订单，请刷新后重试。");
      if (section === "finance" && window.__currentUser && !window.__hasPermission?.("finance.manage")) return notify("当前账号没有财务记录权限。");
      const payload = factoryOperationPayload(task, section);
      if (section === "report" && payload.target_quantity && payload.good_quantity + payload.scrap_quantity > payload.target_quantity) return notify("合格数与损耗数不能超过计划生产数。");
      if (section === "quality" && payload.inspected_quantity && payload.passed_quantity + payload.defect_quantity > payload.inspected_quantity) return notify("合格数与不良数不能超过抽检数。");
      if (section === "warehouse" && payload.finished_quantity && payload.shipped_quantity > payload.finished_quantity) return notify("发货数不能超过成品入库数。");
      if (section === "warehouse" && payload.shipped_quantity && payload.signed_quantity > payload.shipped_quantity) return notify("签收数不能超过累计发货数。");
      if (section === "warehouse" && payload.shipments?.length > (factoryOperationsForTask(task).warehouse.shipments || []).length) {
        const latest = payload.shipments[payload.shipments.length - 1];
        if (!latest.quantity || !latest.address) return notify("新增发货批次请同时填写数量和收货地址。");
      }
      if (section === "after_sales" && payload.cases?.length > (factoryOperationsForTask(task).after_sales.cases || []).length && !payload.cases[payload.cases.length - 1].reason) return notify("新增售后记录请先填写问题原因。");
      if (task.serverId && window.__updateFormalTaskOperations) {
        const result = await window.__updateFormalTaskOperations(task, section, payload);
        if (result?.ok) { showTaskDetail(id); renderProductionCenter(); notify("订单执行记录已保存。"); }
        return;
      }
      factoryOperationsForTask(task)[section] = { ...factoryOperationsForTask(task)[section], ...payload, updated_at:new Date().toISOString(), updated_by:window.__currentUser?.name || "当前用户" };
      task.factorySummary = null;
      addTaskLog(task, `更新${({schedule:"排产",report:"报工",quality:"质检",warehouse:"入库发货",finance:"财务",after_sales:"售后追溯"})[section]}记录`);
      saveState();
      showTaskDetail(id);
      renderProductionCenter();
      notify("订单执行记录已保存。");
    }
    async function persistFactoryRecordList(task, section, payload, message) {
      if (task.serverId && window.__updateFormalTaskOperations) {
        const result = await window.__updateFormalTaskOperations(task, section, payload);
        if (!result?.ok) return;
      } else {
        factoryOperationsForTask(task)[section] = { ...factoryOperationsForTask(task)[section], ...payload, updated_at:new Date().toISOString(), updated_by:window.__currentUser?.name || "当前用户" };
        task.factorySummary = null;
        addTaskLog(task, message);
        saveState();
      }
      showTaskDetail(task.id);
      renderProductionCenter();
      renderDashboard();
      notify(message);
    }
    async function setFactoryShipmentStatus(taskId, shipmentId, status) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task) return notify("没有找到这张订单。");
      const shipments = (factoryOperationsForTask(task).warehouse.shipments || []).map(item => item.id === shipmentId ? { ...item, status, signed_at:status === "已签收" ? (item.signed_at || new Date().toISOString().slice(0,10)) : item.signed_at } : item);
      await persistFactoryRecordList(task, "warehouse", { shipments }, status === "已签收" ? "发货批次已标记签收。" : "发货批次状态已更新。");
    }
    async function removeFactoryShipment(taskId, shipmentId) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task || !confirm("确认删除这个发货批次吗？")) return;
      const shipments = (factoryOperationsForTask(task).warehouse.shipments || []).filter(item => item.id !== shipmentId);
      await persistFactoryRecordList(task, "warehouse", { shipments }, "发货批次已删除。");
    }
    async function setFactoryAfterSalesStatus(taskId, caseId, status) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task) return notify("没有找到这张订单。");
      const cases = (factoryOperationsForTask(task).after_sales.cases || []).map(item => item.id === caseId ? { ...item, status, resolved_at:["已解决","已关闭"].includes(status) ? new Date().toISOString() : "" } : item);
      await persistFactoryRecordList(task, "after_sales", { cases }, "售后记录状态已更新。");
    }
    async function removeFactoryAfterSalesCase(taskId, caseId) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task || !confirm("确认删除这条售后追溯记录吗？")) return;
      const cases = (factoryOperationsForTask(task).after_sales.cases || []).filter(item => item.id !== caseId);
      await persistFactoryRecordList(task, "after_sales", { cases }, "售后追溯记录已删除。");
    }
    async function reverseFactoryPayment(taskId, paymentId) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task) return notify("没有找到这张订单。");
      if (!task.serverId || !window.__reverseFormalTaskPayment) return notify("收款冲正需要先把订单保存到服务端。");
      const reason = prompt("请输入冲正原因（必填，例如：金额录错、重复登记、款项退回）");
      if (reason == null) return;
      if (!reason.trim()) return notify("冲正原因不能为空。");
      if (!confirm("确认冲正这笔收款吗？原记录会保留并标记为已冲正，不能直接删除。")) return;
      const result = await window.__reverseFormalTaskPayment(task, paymentId, reason.trim());
      if (result?.ok) {
        showTaskDetail(taskId);
        renderProductionCenter();
        notify("收款已冲正，原记录和原因已保留。");
      }
    }
    function productionChecklistHtml(task) {
      const progress = taskProductionProgress(task);
      const phase = taskPhase(task);
      const source = taskProductionSource(task);
      const firstPending = progress.steps.findIndex(step => !step.completed);
      const lastCompleted = progress.steps.reduce((last, step, index) => step.completed ? index : last, -1);
      const canManage = !task.serverId || typeof window.__hasPermission !== "function" || window.__hasPermission("task.manage");
      const list = progress.steps.map((step, index) => {
        const checkLocked = phase !== "生产中" || (!step.completed && index !== firstPending);
        const visuallyLocked = phase === "生产中" && !step.completed && index !== firstPending;
        const meta = step.completed
          ? `${step.completed_by ? `${step.completed_by} · ` : ""}${step.completed_at ? new Date(step.completed_at).toLocaleString("zh-CN", { hour12:false }) : "已完成"}`
          : step.custom ? "厂家为本订单添加的步骤"
            : phase === "接单前" ? "预计步骤 · 可在开工前调整"
              : index === firstPending && phase === "生产中" ? "当前应完成步骤" : "待前序步骤完成";
        const workOrderMeta = [step.owner ? `负责人 ${step.owner}` : "", step.due ? `期限 ${step.due}` : "", step.note || ""].filter(Boolean).join(" · ");
        const outsourceOrder = productionOutsourceOrderForStep(task, step);
        const outsourceState = productionOutsourceState(outsourceOrder);
        const outsourceStatus = outsourceOrder
          ? `<span class="production-step-outsource-status${outsourceState.done ? " is-done" : ""}${outsourceState.risk ? " is-risk" : ""}">外协 · ${escapeHtml(outsourceOrder.supplier_name || "待定供应商")} · ${escapeHtml(outsourceState.label)}${outsourceOrder.due_at ? ` · ${escapeHtml(outsourceOrder.due_at)}` : ""}</span>`
          : "";
        const undo = canManage && phase === "生产中" && step.completed && index === lastCompleted
          ? `<button class="production-step-undo" type="button" onclick="undoProductionStep('${escapeHtml(task.id)}','${escapeHtml(step.id)}')">撤回</button>`
          : "";
        const remove = canManage && phase !== "已完成" && !step.completed
          ? `<button class="production-step-remove" type="button" onclick="removeProductionStep('${escapeHtml(task.id)}','${escapeHtml(step.id)}')">移除</button>`
          : "";
        const canMoveUp = index > 0 && !progress.steps[index - 1].completed;
        const canMoveDown = index < progress.steps.length - 1 && !progress.steps[index + 1].completed;
        const move = canManage && phase !== "已完成" && !step.completed
          ? `<span class="production-step-order" aria-label="调整${escapeHtml(step.label)}的顺序"><button class="production-step-move" type="button" title="向上移动一步" onclick="moveProductionStep('${escapeHtml(task.id)}','${escapeHtml(step.id)}','up')"${canMoveUp ? "" : " disabled"}>↑ 上移</button><button class="production-step-move" type="button" title="向下移动一步" onclick="moveProductionStep('${escapeHtml(task.id)}','${escapeHtml(step.id)}','down')"${canMoveDown ? "" : " disabled"}>↓ 下移</button></span>`
          : "";
        const edit = canManage && phase !== "已完成"
          ? `<button class="production-step-move" type="button" onclick="editProductionStepWorkOrder('${escapeHtml(task.id)}','${escapeHtml(step.id)}')">责任/期限</button>`
          : "";
        const canOutsource = canManage && phase !== "已完成" && !step.completed && !/生产资料确认|成品质检|包装入库|发货|回款/.test(step.label);
        const outsource = outsourceOrder
          ? `${outsourceState.done && canOutsource ? `<button class="production-step-move" type="button" onclick="syncOutsourceProductionStep('${escapeHtml(task.id)}','${escapeHtml(step.id)}')"${index === firstPending ? "" : " disabled"}>同步完成</button>` : ""}<button class="production-step-move" type="button" onclick="openTaskOutsourceOrder('${escapeHtml(outsourceOrder.id)}')">${outsourceState.status === "delivered" ? "去验收" : "查看外协"}</button>${outsourceState.risk && canOutsource ? `<button class="production-step-move" type="button" onclick="openTaskStepOutsourceCenter('${escapeHtml(task.id)}','${escapeHtml(step.id)}')">重新外协</button>` : ""}`
          : canOutsource
            ? `<button class="production-step-move" type="button" onclick="openTaskStepOutsourceCenter('${escapeHtml(task.id)}','${escapeHtml(step.id)}')">${/覆膜|烫金|UV|模切|印刷|电镀|喷涂|丝印|雕刻|热转印|刺绣|SMT|表面处理/.test(step.label) ? "建议外协" : "转外协"}</button>`
            : "";
        return `<div class="production-step${step.completed ? " is-completed" : ""}${visuallyLocked ? " is-locked" : ""}">
          <button class="production-step-check${step.completed ? " is-completed" : ""}" type="button" aria-label="${escapeHtml(step.completed ? `已完成${step.label}` : `完成${step.label}`)}" onclick="completeProductionStep('${escapeHtml(task.id)}','${escapeHtml(step.id)}')"${step.completed || checkLocked || !canManage ? " disabled" : ""}>✓</button>
          <div class="production-step-label">${escapeHtml(`${index + 1}. ${step.label}`)}<div class="production-step-meta">${escapeHtml(meta)}${workOrderMeta ? `<br>${escapeHtml(workOrderMeta)}` : ""}${outsourceStatus ? `<br>${outsourceStatus}` : ""}</div></div>
          <div class="production-step-actions">${outsource}${edit}${move}${undo}${remove}</div>
        </div>`;
      }).join("");
      const phaseNote = phase === "接单前"
        ? "开工前可以自由增加或移除步骤；进入生产中后，再按最终清单逐项完成。"
        : phase === "已完成" ? "订单已经完成。如需更正，请先撤回到生产中，再撤回对应步骤。"
        : "未完成步骤可用“上移 / 下移”调整顺序；每次勾选前会再次确认，误点后可撤回。";
      const add = canManage && phase !== "已完成"
        ? `<div class="production-step-add"><input id="customProductionStep-${escapeHtml(task.id)}" placeholder="增加本订单自己的步骤，例如：贴窗、装磁铁" maxlength="80" onkeydown="if(event.key==='Enter'){event.preventDefault();addCustomProductionStep('${escapeHtml(task.id)}')}"><button class="btn light" type="button" onclick="addCustomProductionStep('${escapeHtml(task.id)}')">＋ 添加本订单步骤</button></div>`
        : "";
      return `<div class="production-progress-card">
        <div class="production-progress-head"><div><strong>${phase === "接单前" ? "开工前确认生产步骤" : "生产进度"}</strong><div class="production-step-meta">依据：${escapeHtml(source.product)}${source.process ? ` · 已确认工艺：${escapeHtml(source.process)}` : " · 尚未记录明确工艺"}</div></div><div class="production-progress-head-actions"><span class="production-progress-count">${progress.completed}/${progress.total} 步 · ${progress.percent}%</span>${canManage && phase !== "已完成" ? `<button class="production-reset" type="button" onclick="resetProductionStepsFromProcess('${escapeHtml(task.id)}')">按当前工艺重新生成</button>` : ""}</div></div>
        <div class="production-progress-track"><span style="width:${progress.percent}%"></span></div>
        <div class="production-step-list">${list || `<div class="muted">尚未生成生产步骤。</div>`}</div>
        ${add}
        <div class="production-step-note">${escapeHtml(phaseNote)}</div>
      </div>`;
    }
    function taskExecutionBridgeHtml(task) {
      const phase = taskPhase(task);
      const progress = taskProductionProgress(task);
      const next = progress.steps.find(step => !step.completed);
      const operations = factoryOperationsForTask(task);
      const outsource = next ? productionOutsourceOrderForStep(task, next) : null;
      const outsourceState = productionOutsourceState(outsource);
      if (phase === "接单前") {
        return `<section class="task-execution-bridge" aria-label="生产执行说明">
          <div class="task-execution-bridge-copy"><span>生产执行</span><strong>客户确认并进入生产后再执行工序</strong><em>订单跟进页只确认价格、规格、交期和客户意见，不在这里提前勾选生产步骤。</em></div>
        </section>`;
      }
      const finished = ["回款中", "已完成"].includes(phase);
      const delivery = phase === "交付中";
      const title = finished
        ? "生产与交付执行已结束"
        : delivery ? `当前交付状态：${task.status || "待处理"}`
          : `生产 ${progress.completed}/${progress.total} 步 · ${progress.percent}%`;
      const detail = finished
        ? `质检 ${operations.quality?.status || "待确认"} · 已发货 ${Number(operations.warehouse?.shipped_quantity || 0).toLocaleString("zh-CN")} · 已签收 ${Number(operations.warehouse?.signed_quantity || 0).toLocaleString("zh-CN")}`
        : delivery
          ? `质检、入库、发货和签收统一在生产执行中心处理。`
          : `${next ? `当前工序：${next.label}` : "全部生产步骤已完成"}${outsourceState ? ` · 外协${outsourceState.label}` : ""}`;
      return `<section class="task-execution-bridge" aria-label="生产执行摘要">
        <div class="task-execution-bridge-copy"><span>生产摘要</span><strong>${escapeHtml(title)}</strong><em>${escapeHtml(detail)}</em>${!finished ? `<div class="task-execution-bridge-progress"><div class="production-progress-track"><span style="width:${progress.percent}%"></span></div><small>这里只显示摘要，工序修改、外协和质检请进入生产执行。</small></div>` : ""}</div>
        ${finished ? "" : `<button class="btn" type="button" onclick="openTaskInProductionCenter('${escapeHtml(task.id)}')">进入生产执行</button>`}
      </section>`;
    }
    function persistLocalProductionStep(task, logText) {
      task.nextAction = nextActionForTaskStatus(task.status, task);
      task.updatedAt = new Date().toISOString();
      addTaskLog(task, logText);
      saveState();
      renderTasks();
      renderDashboard();
      showTaskDetail(task.id);
    }
    async function completeProductionStep(id, stepId) {
      const task = businessTasks().find(item => item.id === id);
      if (!task || taskPhase(task) !== "生产中") return notify("进入生产中后才能勾选生产步骤。");
      const steps = taskProductionSteps(task);
      const index = steps.findIndex(step => step.id === stepId);
      if (index < 0 || steps[index].completed) return;
      const previous = steps.slice(0, index).find(step => !step.completed);
      if (previous) return notify(`请先完成前一步：${previous.label}`);
      if (!confirm(`确认“${steps[index].label}”已经完成吗？`)) return;
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "complete", { step_id: stepId, confirm: true });
        if (result?.ok) showTaskDetail(id);
        return;
      }
      steps[index].completed = true;
      steps[index].completed_at = new Date().toISOString();
      steps[index].completed_by = window.__currentUser?.name || "当前用户";
      persistLocalProductionStep(task, `完成生产步骤：${steps[index].label}`);
    }
    let productionStepEditorContext = null;
    function editProductionStepWorkOrder(id, stepId) {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return notify("没有找到这个订单。");
      const step = taskProductionSteps(task).find(item => item.id === stepId);
      if (!step) return notify("没有找到这个工序。");
      productionStepEditorContext = { taskId:id, stepId };
      setTextIfExists("productionStepEditorSubtitle", `${step.label} · 直接从日历选择计划完成日期，无需手输格式。`);
      setIfExists("productionStepOwner", step.owner || "");
      setIfExists("productionStepDue", step.due || "");
      setIfExists("productionStepNote", step.note || "");
      const editor = document.getElementById("productionStepEditor");
      if (!editor) return;
      editor.classList.add("show");
      editor.setAttribute("aria-hidden", "false");
      window.setTimeout(() => document.getElementById("productionStepOwner")?.focus(), 50);
    }
    function closeProductionStepEditor() {
      const editor = document.getElementById("productionStepEditor");
      editor?.classList.remove("show");
      editor?.setAttribute("aria-hidden", "true");
      productionStepEditorContext = null;
    }
    function clearProductionStepDue() {
      setIfExists("productionStepDue", "");
      document.getElementById("productionStepDue")?.focus();
    }
    async function saveProductionStepWorkOrder() {
      const context = productionStepEditorContext;
      const task = businessTasks().find(item => item.id === context?.taskId);
      if (!task) {
        closeProductionStepEditor();
        return notify("没有找到这个订单，请刷新后重试。");
      }
      const step = taskProductionSteps(task).find(item => item.id === context?.stepId);
      if (!step) {
        closeProductionStepEditor();
        return notify("没有找到这个工序，请刷新后重试。");
      }
      const payload = {
        step_id:context.stepId,
        owner:val("productionStepOwner").trim(),
        due:val("productionStepDue").trim(),
        note:val("productionStepNote").trim()
      };
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "update", payload);
        if (result?.ok) {
          closeProductionStepEditor();
          showTaskDetail(context.taskId);
          notify("工序责任与期限已保存。");
        }
        return;
      }
      Object.assign(step, payload);
      delete step.step_id;
      closeProductionStepEditor();
      persistLocalProductionStep(task, `更新工序责任：${step.label} · ${step.owner || "未分配"}`);
    }
    async function undoProductionStep(id, stepId) {
      const task = businessTasks().find(item => item.id === id);
      if (!task || taskPhase(task) !== "生产中") return notify("请先把订单撤回到生产中。");
      const steps = taskProductionSteps(task);
      const index = steps.findIndex(step => step.id === stepId);
      if (index < 0 || !steps[index].completed) return;
      const later = steps.slice(index + 1).filter(step => step.completed);
      if (later.length) return notify(`请先撤回后面的步骤：${later[later.length - 1].label}`);
      if (!confirm(`确认撤回“${steps[index].label}”的完成状态吗？`)) return;
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "undo", { step_id: stepId, confirm: true });
        if (result?.ok) showTaskDetail(id);
        return;
      }
      steps[index].completed = false;
      steps[index].completed_at = "";
      steps[index].completed_by = "";
      persistLocalProductionStep(task, `撤回生产步骤：${steps[index].label}`);
    }
    async function addCustomProductionStep(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task || taskPhase(task) === "已完成") return;
      const input = document.getElementById(`customProductionStep-${id}`);
      const label = String(input?.value || "").trim().replace(/\s+/g, " ").slice(0, 80);
      if (!label) return notify("请输入要增加的生产步骤。");
      const steps = taskProductionSteps(task);
      if (steps.some(step => step.label === label)) return notify("这个生产步骤已经存在。");
      if (steps.length >= 80) return notify("每个订单最多保留80个生产步骤。");
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "add", { label });
        if (result?.ok) showTaskDetail(id);
        return;
      }
      steps.push({ id:`custom-${Date.now()}-${Math.random().toString(36).slice(2,8)}`, label, completed:false, custom:true, completed_at:"", completed_by:"", owner:"", due:"", note:"", created_at:new Date().toISOString() });
      if (input) input.value = "";
      persistLocalProductionStep(task, `新增自定义生产步骤：${label}`);
    }
    async function removeProductionStep(id, stepId) {
      const task = businessTasks().find(item => item.id === id);
      if (!task || taskPhase(task) === "已完成") return notify("已完成订单请先撤回到生产中，再移除步骤。");
      const steps = taskProductionSteps(task);
      const index = steps.findIndex(step => step.id === stepId);
      if (index < 0) return notify("没有找到这个生产步骤，请刷新后重试。");
      if (steps[index].completed) return notify(`请先撤回“${steps[index].label}”的完成状态，再移除这一步。`);
      if (steps.length <= 1) return notify("至少要保留一个生产步骤。");
      if (!confirm(`确认从本订单移除“${steps[index].label}”吗？\n只影响本订单，不会修改工艺模板和价格表。`)) return;
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "remove", { step_id: stepId, confirm: true });
        if (result?.ok) showTaskDetail(id);
        return;
      }
      const [removed] = steps.splice(index, 1);
      persistLocalProductionStep(task, `移除生产步骤：${removed.label}`);
    }
    async function moveProductionStep(id, stepId, direction) {
      const task = businessTasks().find(item => item.id === id);
      if (!task || taskPhase(task) === "已完成") return notify("已完成订单请先撤回到生产中，再调整步骤顺序。");
      if (!["up", "down"].includes(direction)) return notify("步骤移动方向不正确，请刷新后重试。");
      const steps = taskProductionSteps(task);
      const index = steps.findIndex(step => step.id === stepId);
      if (index < 0) return notify("没有找到这个生产步骤，请刷新后重试。");
      if (steps[index].completed) return notify("已完成步骤的顺序不能调整。");
      const targetIndex = direction === "up" ? index - 1 : index + 1;
      if (targetIndex < 0 || targetIndex >= steps.length || steps[targetIndex].completed) {
        return notify(direction === "up" ? "这一步已经不能再上移。" : "这一步已经不能再下移。");
      }
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "move", { step_id: stepId, direction });
        if (result?.ok) showTaskDetail(id);
        return;
      }
      [steps[index], steps[targetIndex]] = [steps[targetIndex], steps[index]];
      persistLocalProductionStep(task, `${direction === "up" ? "上移" : "下移"}生产步骤：${steps[targetIndex].label}`);
    }
    async function resetProductionStepsFromProcess(id) {
      const task = businessTasks().find(item => item.id === id);
      if (!task || taskPhase(task) === "已完成") return notify("已完成订单请先撤回到生产中，再重新生成步骤。");
      const steps = taskProductionSteps(task);
      const completed = steps.filter(step => step.completed);
      if (completed.length) return notify(`请先撤回已完成步骤：${completed[completed.length - 1].label}`);
      const source = taskProductionSource(task);
      const processText = source.process || "尚未记录明确工艺，将只按产品生成基础流程";
      if (!confirm(`按当前产品和工艺重新生成系统步骤吗？\n\n产品：${source.product}\n工艺：${processText}\n\n厂家自己添加的步骤会保留。`)) return;
      if (task.serverId && window.__updateFormalTaskStep) {
        const result = await window.__updateFormalTaskStep(task, "reset", {
          confirm: true,
          product: source.product,
          process: source.process
        });
        if (result?.ok) showTaskDetail(id);
        return;
      }
      const custom = steps.filter(step => step.custom);
      const generated = buildLocalProductionSteps({ ...task, productionSteps: [], production_steps: [] });
      const labels = new Set(generated.map(step => step.label));
      task.productionSteps = [...generated, ...custom.filter(step => !labels.has(step.label))];
      persistLocalProductionStep(task, `按当前产品工艺重新生成生产步骤，保留 ${custom.length} 个自定义步骤`);
    }
    function taskJourneyMeta(task) {
      const phase = taskPhase(task);
      const phases = ["接单前", "生产中", "交付中", "回款中", "已完成"];
      const phaseIndex = { "接单前":0, "生产中":1, "交付中":2, "回款中":3, "已完成":4 }[phase] ?? 0;
      const normalizedStatus = ({ "生产中":"生产", "已归档":"已完成", "接单前":"询盘" })[task?.status] || task?.status || "询盘";
      const production = phase === "生产中" ? taskProductionProgress(task) : { completed:0, total:0, percent:0 };
      const currentPhaseProgress = phase === "已完成" ? 1
        : phase === "回款中" ? ({ "待回款":.3, "已回款":.85 }[normalizedStatus] || .3)
          : phase === "交付中" ? ({ "待质检":.2, "待发货":.4, "已发货":.65, "已签收":.9 }[normalizedStatus] || .2)
            : phase === "生产中" ? Math.max(0, Math.min(1, production.percent / 100))
              : ({ "询盘":.1, "待补资料":.25, "已报价":.5, "待确认":.75, "打样":.9 }[normalizedStatus] || .1);
      const percent = phase === "已完成"
        ? 100
        : Math.max(1, Math.min(99, Math.round((phaseIndex + currentPhaseProgress) / phases.length * 100)));
      return {
        phase,
        phaseIndex,
        status: normalizedStatus,
        production,
        percent,
        phases: ["接单前", "生产中", "交付中", "回款中", "已完成"]
      };
    }
    function taskDetailJourneyHtml(task, followup, mode = "tracking") {
      const journey = taskJourneyMeta(task);
      const phaseRail = journey.phases.map((phase, index) => {
        const stateClass = index < journey.phaseIndex ? "is-done" : index === journey.phaseIndex ? "is-current" : "is-pending";
        const detail = index < journey.phaseIndex
          ? "已完成"
          : index > journey.phaseIndex ? "待进入"
            : phase === "接单前" ? journey.status
              : phase === "生产中" ? `${journey.production.completed}/${journey.production.total} 个步骤`
                : phase === "交付中" ? journey.status
                  : phase === "回款中" ? journey.status : "订单已闭环";
        return `<div class="task-detail-phase ${stateClass}" data-step="${index + 1}"><strong>${phase}</strong><span>${escapeHtml(detail)}</span></div>`;
      }).join("");
      const automaticNextAction = nextActionForTaskStatus(task.status, task);
      const nextAction = ["生产中", "交付中", "回款中", "已完成"].includes(journey.phase)
        ? automaticNextAction
        : task.nextAction || automaticNextAction;
      const nextFollow = journey.phase === "已完成" ? "无需继续跟进" : `计划跟进：${task.nextFollow || task.due || "未设置"}`;
      return `<section class="task-detail-overview" aria-label="任务进度概览">
        <div class="task-detail-overview-head">
          <div class="task-detail-current"><span>当前状态</span><strong>${escapeHtml(journey.status)}</strong><em>${escapeHtml(journey.phase)}</em></div>
          <div class="task-detail-progress"><div class="task-detail-progress-copy"><span class="task-detail-progress-label">订单总进度</span><strong class="task-detail-progress-value">${journey.percent}%</strong></div><div class="task-detail-progress-track"><span style="width:${journey.percent}%"></span></div></div>
          <span class="task-detail-urgency${followup.urgent ? " is-urgent" : ""}">${escapeHtml(followup.label)}</span>
        </div>
        <div class="task-detail-phase-rail">${phaseRail}</div>
        <div class="task-detail-next"><span>下一步</span><div><strong>${escapeHtml(nextAction || "待安排")}</strong><em>${escapeHtml(nextFollow)}</em></div></div>
        ${mode === "production" ? taskProductionPrimaryActionsHtml(task) : taskDetailPrimaryActionsHtml(task)}
      </section>`;
    }
    function taskIndustryWorkflowHtml(task) {
      const plan = task?.industryFollowupPlan;
      const stages = Array.isArray(plan?.stages) ? plan.stages : [];
      if (!stages.length) return "";
      const normalizedStatus = ({ "生产中":"生产", "已归档":"已完成", "接单前":"询盘" })[task?.status] || task?.status || "询盘";
      const currentIndex = Math.max(0, stages.findIndex(item => item.status === normalizedStatus));
      const current = stages[currentIndex] || task.currentIndustryStage || {};
      const stageRail = stages.map((item, index) => {
        const stateClass = index < currentIndex ? "is-done" : index === currentIndex ? "is-current" : "";
        return `<span class="industry-workflow-stage ${stateClass}" title="${escapeHtml(item.label || item.status)}">${escapeHtml(item.status)}</span>`;
      }).join("");
      const checklist = Array.isArray(current.checklist) ? current.checklist : [];
      const quality = Array.isArray(current.quality) ? current.quality : [];
      const tags = [
        ...checklist.map(item => `<em>${escapeHtml(item)}</em>`),
        ...quality.map(item => `<em class="is-quality">质检：${escapeHtml(item)}</em>`)
      ].join("");
      return `<section class="industry-workflow-card" aria-label="${escapeHtml(plan.label || "行业")}专属工作流">
        <div class="industry-workflow-head"><div><strong>${escapeHtml(plan.label || "行业")}专属跟单与生产流程</strong><span>本流程来自报价行业和成本公式，不再仅凭产品名称猜测；本订单仍可增删、排序生产步骤。</span></div><em class="industry-workflow-badge">${escapeHtml(current.label || normalizedStatus)}</em></div>
        <div class="industry-workflow-stages">${stageRail}</div>
        <div class="industry-workflow-current">
          <div class="industry-workflow-action"><span>当前阶段该做什么</span><strong>${escapeHtml(current.action || task.nextAction || "确认当前资料和下一步")}</strong></div>
          <div class="industry-workflow-check"><span>${quality.length ? "资料与质检重点" : "当前要确认的资料"}</span><div class="industry-workflow-tags">${tags || "<em>按订单实际情况补充</em>"}</div></div>
        </div>
      </section>`;
    }
    function taskDetailPrimaryActionsHtml(task) {
      const id = escapeHtml(task.id);
      const advance = taskAdvanceMeta(task.status, task);
      const completed = taskPhase(task) === "已完成";
      let primary = "";
      if (advance.route === "quote") {
        primary = `<button class="btn" type="button" onclick="openQuoteFromTask('${id}')">去报价</button>`;
      } else if (!completed) {
        primary = `<button class="btn" type="button" onclick="advanceTask('${id}')"${advance.next && !advance.blocked ? "" : " disabled"}>${escapeHtml(advance.label)}</button>`;
      }
      const quote = advance.route === "quote" ? "" : taskHasRelatedQuote(task)
        ? `<button class="btn light" type="button" onclick="openTaskRelatedQuote('${id}')">查看关联报价</button>`
        : `<button class="btn light" type="button" onclick="openQuoteFromTask('${id}')">去报价</button>`;
      return `<div class="task-detail-primary-actions">${primary}<button class="btn light" type="button" onclick="openTaskCustomerFiles('${id}')">客户资料</button>${quote}${completed ? "" : `<button class="btn light" type="button" onclick="openTaskOutsourceCenter('${id}')">外协下单</button><button class="btn light" type="button" onclick="rollbackTask('${id}')">撤回上一步</button>`}</div>`;
    }
    function taskProductionPrimaryActionsHtml(task) {
      const id = escapeHtml(task.id);
      const phase = taskPhase(task);
      const advance = taskAdvanceMeta(task.status, task);
      const completed = phase === "已完成";
      const canAdvance = ["生产中", "交付中"].includes(phase) && advance.next;
      const primary = canAdvance
        ? `<button class="btn" type="button" onclick="advanceTask('${id}')"${advance.blocked ? " disabled" : ""}>${escapeHtml(advance.label)}</button>`
        : "";
      return `<div class="task-detail-primary-actions">${primary}<button class="btn light" type="button" onclick="openTaskInOrderTracking('${id}')">查看订单跟进</button><button class="btn light" type="button" onclick="openTaskCustomerFiles('${id}')">客户资料</button>${completed ? "" : `<button class="btn light" type="button" onclick="openTaskOutsourceCenter('${id}')">外协下单</button>`}</div>`;
    }
    function openTaskStepOutsourceCenter(id, stepId) {
      const task = businessTasks().find(item => item.id === id);
      const step = taskProductionSteps(task).find(item => item.id === stepId);
      if (!task || !step) return notify("没有找到这道生产工序。");
      openTaskOutsourceCenter(id, step.label, step.id);
    }
    function openTaskOutsourceOrder(orderId) {
      location.href = `supplier_collaboration.html?order_id=${encodeURIComponent(String(orderId || ""))}`;
    }
    async function syncOutsourceProductionStep(id, stepId) {
      const task = businessTasks().find(item => item.id === id);
      const step = taskProductionSteps(task).find(item => item.id === stepId);
      if (!task || !step) return notify("没有找到这道生产工序。");
      const order = productionOutsourceOrderForStep(task, step);
      if (!order || String(order.status || "") !== "accepted_delivery") {
        return notify("外协单尚未完成本厂验收，不能同步完成生产工序。");
      }
      await completeProductionStep(id, stepId);
    }
    function openTaskOutsourceCenter(id, selectedProcess = "", sourceStepRef = "") {
      const task = businessTasks().find(item => item.id === id);
      if (!task) return notify("没有找到这张订单。");
      const source = taskProductionSource(task);
      const quote = relatedQuoteForTask(task);
      const process = selectedProcess || source.process || "";
      const query = new URLSearchParams({ create:"order", source_task_ref:displayTaskId(task), source_step_ref:sourceStepRef, title:`${source.product || task.title || "订单"} · ${process || "外协加工"}`, product:source.product || task.title || "", process, quantity:String(quote?.qty || task.qty || ""), unit:quote?.unit || task.unit || "件", due_at:task.due || "" });
      location.href = `supplier_collaboration.html?${query}`;
    }
    function taskDetailSupportHtml(task, followup, completed, amountText, riskText, specificationText) {
      const id = escapeHtml(task.id);
      const shouldOpen = taskPhase(task) === "接单前" || riskText !== "无明显风险";
      const summaryLabel = riskText !== "无明显风险" ? "有事项需留意" : taskPhase(task) === "接单前" ? "当前阶段常用" : "按需展开";
      const followupHtml = completed
        ? `<section class="task-followup-card"><div class="task-detail-panel-head"><strong>这笔跟单已经闭环</strong><span>无需继续提醒</span></div><div class="task-followup-script">系统已同步成交数据。订单结束后补录实际材料、加工和其他成本，老板首页会显示真实利润。</div>${taskHasRelatedQuote(task) ? `<div class="actions"><button class="btn" type="button" onclick="openTaskRelatedQuote('${id}')">查看成交与补录成本</button></div>` : ""}</section>`
        : `<section class="task-followup-card"><div class="task-detail-panel-head"><strong>建议跟客户这样说</strong><span>按当前阶段生成</span></div><div class="task-followup-script">${escapeHtml(taskFollowupScript(task))}</div><div class="actions"><button class="btn" type="button" onclick="copyTaskFollowup('${id}')">复制微信话术</button><button class="btn light" type="button" onclick="markTaskContacted('${id}')">已联系，2天后提醒</button></div></section>`;
      return `<details class="task-detail-support task-detail-secondary"${shouldOpen ? " open" : ""} ontoggle="recordTaskDetailFeatureUse('customer_support', this.open, event)"><summary><div><strong>客户跟进与任务资料</strong><span>话术、规格、负责人、金额和风险集中在这里</span></div><em class="${riskText !== "无明显风险" ? "is-risk" : ""}">${escapeHtml(summaryLabel)}</em></summary><div class="task-detail-content-grid">${followupHtml}<aside class="task-detail-panel"><div class="task-detail-panel-head"><strong>任务信息</strong><span>关键数据</span></div><div class="task-detail-facts">
        <div class="task-detail-fact"><span>负责人</span><strong>${escapeHtml(task.owner || "业务")}</strong></div>
        <div class="task-detail-fact"><span>下次跟进</span><strong>${escapeHtml(completed ? "无需跟进" : task.nextFollow || task.due || "未设置")}</strong></div>
        <div class="task-detail-fact"><span>订单金额</span><strong>${escapeHtml(amountText)}</strong></div>
        <div class="task-detail-fact"><span>订单规格</span><strong>${escapeHtml(specificationText)}</strong></div>
        <div class="task-detail-fact${followup.urgent || riskText !== "无明显风险" ? " is-risk" : ""}"><span>风险</span><strong>${escapeHtml(riskText)}</strong></div>
      </div>${taskAssignmentControls(task)}</aside></div></details>`;
    }
    function customerConfirmationHtml(task) {
      if (!task?.serverId) return "";
      const confirmations = Array.isArray(task.customerConfirmations) ? task.customerConfirmations : [];
      const latest = confirmations[0];
      const status = latest?.status || "尚未发起";
      const share = task.latestConfirmationUrl || "";
      return `<section class="task-followup-card task-detail-secondary">
        <div class="task-detail-panel-head"><strong>客户在线确认</strong><span>${escapeHtml(status)}</span></div>
        <div class="task-followup-script">生成链接后发给客户。客户可核对产品、工艺、金额和交期，并选择“确认通过”或“需要修改”；结果会写入订单日志。</div>
        ${latest ? `<div class="muted">最近一次：${escapeHtml(latest.created_at || "")}${latest.decided_by ? ` · ${escapeHtml(latest.decided_by)}` : ""}${latest.decision_note ? ` · ${escapeHtml(latest.decision_note)}` : ""}</div>` : ""}
        <div class="actions">
          <button class="btn" type="button" onclick="createTaskCustomerConfirmation('${escapeHtml(task.id)}')">生成7天确认链接</button>
          ${share ? `<button class="btn light" type="button" onclick="copyTaskCustomerConfirmation('${escapeHtml(task.id)}')">复制确认链接</button>` : ""}
        </div>
        ${share ? `<div class="muted" style="overflow-wrap:anywhere">${escapeHtml(share)}（出于安全考虑，刷新后不再显示原令牌，需要时可重新生成）</div>` : ""}
      </section>`;
    }
    async function createTaskCustomerConfirmation(taskId) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task?.serverId || !window.__createTaskConfirmation) return notify("请先把跟单保存到服务端。");
      const result = await window.__createTaskConfirmation(task, 7);
      if (!result?.ok) return;
      task.latestConfirmationUrl = result.share_url || "";
      task.customerConfirmations = [result.confirmation, ...(task.customerConfirmations || [])];
      showTaskDetail(taskId);
      if (task.latestConfirmationUrl) {
        await navigator.clipboard?.writeText(task.latestConfirmationUrl).catch(() => null);
        notify("确认链接已生成并复制。部署到客户可访问的域名后即可直接发送。");
      }
    }
    async function copyTaskCustomerConfirmation(taskId) {
      const task = businessTasks().find(item => item.id === taskId);
      if (!task?.latestConfirmationUrl) return notify("请重新生成确认链接。");
      await navigator.clipboard?.writeText(task.latestConfirmationUrl).catch(() => null);
      notify("确认链接已复制。");
    }
    function showTaskDetail(id) {
      const task = businessTasks().find(item => item.id === id);
      const box = document.getElementById("taskDetailOutput");
      if (!task || !box) return resetTaskDetailOutput();
      selectedTaskDetailId = id;
      const detailMode = window.__taskDetailReturnPage === "生产中心" ? "production" : "tracking";
      const productionMode = detailMode === "production";
      if (taskDetailPageIsActive()) {
        setTextIfExists("pageTitle", `${productionMode ? "生产工单" : "订单"} ${displayTaskId(task)} · ${task.customer || task.title || "未命名客户"}`);
        setTextIfExists("pageSub", productionMode
          ? `${taskPhase(task)} · 当前工序 ${taskProductionProgress(task).steps.find(step => !step.completed)?.label || task.status || "待安排"} · 负责人 ${task.owner || "生产"}`
          : `${taskPhase(task)} · ${task.status || "待处理"} · 负责人 ${task.owner || "业务"}`);
      }
      const followup = taskFollowupMeta(task);
      const logs = dedupeTaskLogs(task.logs || []).slice(0, 8);
      const completed = taskPhase(task) === "已完成";
      const amountText = task.amount ? `¥${Number(task.amount).toFixed(0)}` : "待报价";
      const riskText = taskRiskText(task);
      const specificationText = taskSpecificationText(task) || "待从报价或订单资料补充";
      box.innerHTML = `
        <div class="task-detail-context"><span>${productionMode ? "生产执行" : "订单跟进"} / ${escapeHtml(task.customer || "未命名客户")} / ${escapeHtml(task.title || "未命名产品")}</span><strong>${escapeHtml(taskPhase(task))}</strong></div>
        ${taskDetailJourneyHtml(task, followup, detailMode)}
        ${taskIndustryWorkflowHtml(task)}
        ${productionMode ? productionChecklistHtml(task) : taskExecutionBridgeHtml(task)}
        ${productionMode && typeof window.purchaseCardHtml === "function" ? window.purchaseCardHtml(task) : ""}
        ${productionMode ? factoryOperationsHtml(task) : ""}
        ${productionMode ? "" : customerConfirmationHtml(task)}
        ${productionMode ? "" : taskDetailSupportHtml(task, followup, completed, amountText, riskText, specificationText)}
        <details class="task-detail-history"><summary>操作记录 <span>${logs.length} 条</span></summary><div class="task-detail-history-list">${(logs.length ? logs : ["暂无操作记录"]).map(item => `<div>${escapeHtml(item)}</div>`).join("")}</div></details>
      `;
      window.__jianglaiUsability?.enhanceTaskDetail?.(task, detailMode);
      if (task.serverId && !Array.isArray(task.customerConfirmations) && window.__loadTaskConfirmations) {
        window.__loadTaskConfirmations(task).then(items => {
          task.customerConfirmations = Array.isArray(items) ? items : [];
          if (selectedTaskDetailId === task.id) showTaskDetail(task.id);
        }).catch(() => { task.customerConfirmations = []; });
      }
    }
    function captureTaskListReturnState() {
      const main = document.querySelector("main");
      taskListReturnState = {
        filter: taskFilter,
        search: taskSearch,
        mainScrollTop: Number(main?.scrollTop || 0),
        windowScrollY: Number(window.scrollY || 0)
      };
    }
    function restoreTaskListReturnState() {
      taskFilter = taskListReturnState.filter || taskFilter;
      taskSearch = taskListReturnState.search || "";
      const search = document.getElementById("taskSearch");
      if (search) search.value = taskSearch;
      showPage("跟单工单");
      window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
        const main = document.querySelector("main");
        if (main) main.scrollTop = Number(taskListReturnState.mainScrollTop || 0);
        window.scrollTo(0, Number(taskListReturnState.windowScrollY || 0));
      }));
    }
    function openTaskDetail(id) {
      const sourcePage = document.querySelector('.page.active')?.dataset.page || "";
      if (sourcePage === "跟单工单") {
        window.__taskDetailReturnPage = "跟单工单";
        captureTaskListReturnState();
      } else if (sourcePage === "生产中心") {
        window.__taskDetailReturnPage = "生产中心";
      }
      selectedTaskDetailId = id;
      showPage("跟单详情");
      showTaskDetail(id);
      window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
        const main = document.querySelector("main");
        if (main) main.scrollTop = 0;
        window.scrollTo(0, 0);
      }));
    }
    function openTaskInProductionCenter(id) {
      window.__taskDetailReturnPage = "生产中心";
      selectedTaskDetailId = id;
      showPage("跟单详情");
      showTaskDetail(id);
      window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
        const main = document.querySelector("main");
        if (main) main.scrollTop = 0;
        window.scrollTo(0, 0);
      }));
    }
    function openTaskInOrderTracking(id) {
      const sourcePage = document.querySelector('.page.active')?.dataset.page || "";
      if (sourcePage === "跟单工单") captureTaskListReturnState();
      window.__taskDetailReturnPage = "跟单工单";
      selectedTaskDetailId = id;
      showPage("跟单详情");
      showTaskDetail(id);
      window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
        const main = document.querySelector("main");
        if (main) main.scrollTop = 0;
        window.scrollTo(0, 0);
      }));
    }
    function backToTaskList() {
      if (window.__taskDetailReturnPage === "生产中心") {
        showPage("生产中心");
        return;
      }
      restoreTaskListReturnState();
    }
    function addManualTask() {
      const customer = val("manualTaskCustomer");
      const note = val("manualTaskNote");
      const process = val("manualTaskProcess");
      const task = {
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: customer || "未命名客户 · 手动任务",
        title: customer || "手动跟单任务",
        owner: val("manualTaskOwner") || "业务",
        createdAt: new Date().toISOString(),
        due: val("manualTaskFollow") || "待排",
        nextFollow: val("manualTaskFollow") || "待设置",
        status: val("manualTaskStatus") || "待补资料",
        process,
        nextAction: note || "补齐资料并跟进客户确认",
        amount: 0,
        risk: [process ? `工艺：${process}` : "", note].filter(Boolean).join("；") || "暂无风险备注",
        logs: [new Date().toLocaleString("zh-CN", { hour12: false }) + " 创建手动跟单任务"]
      };
      task.productionSteps = buildLocalProductionSteps(task);
      if (taskPhase(task) === "已完成") {
        task.productionSteps.forEach(step => {
          step.completed = true;
          step.completed_at = new Date().toISOString();
          step.completed_by = window.__currentUser?.name || "当前用户";
        });
      }
      task.nextAction = nextActionForTaskStatus(task.status, task);
      state.tasks.unshift(task);
      saveState();
      ["manualTaskCustomer", "manualTaskFollow", "manualTaskNote", "manualTaskProcess"].forEach(id => setIfExists(id, ""));
      setIfExists("manualTaskOwner", "业务");
      taskFilter = taskPhase(task);
      const creator = document.getElementById("taskCreateDetails");
      if (creator) creator.open = false;
      renderTasks();
      renderDashboard();
      window.__saveFormalTask?.(task);
    }
    function addDemoTask() {
      state.tasks.unshift({
        id: newTaskId(),
        localCompanyId: currentTaskRecycleCompanyKey(),
        customer: "星河包装 · 黑色彩盒",
        title: "黑色彩盒制前资料复核",
        owner: "制前",
        createdAt: new Date().toISOString(),
        due: "明天",
        nextFollow: "明天上午",
        status: "待确认",
        nextAction: "确认图纸/设计稿、烫银位置和是否含税；运费另计",
        amount: 12800,
        risk: "黑底满版、烫银、覆膜，需先打样确认",
        logs: [new Date().toLocaleString("zh-CN", { hour12: false }) + " 创建示例跟单任务"]
      });
      saveState();
      renderTasks();
      renderDashboard();
    }
    async function refreshTaskList() {
      try {
        await window.__loadFormalTasks?.();
        notify("跟单任务和回收站已刷新。" );
      } catch (err) {
        notify("刷新失败：" + err.message);
      }
    }
    if (window.location.protocol !== "file:") {
      window.__legacyCachedSettingsForMigration = JSON.parse(JSON.stringify(state.settings || defaults.settings));
      state.settings = JSON.parse(JSON.stringify(defaults.settings));
      window.__formalSettingsReady = false;
    } else {
      window.__formalSettingsReady = true;
    }
    initNav();
    initHomeExperience();
    restoreSettings();
    initializeQuoteCatalog();
    renderQuoteRules();
    initializeQuoteExpressWorkspace();
    renderCostLibrary();
    setRuleMode("simple");
    ["ruleCapacityRules", "ruleMaterials", "ruleProcesses", "ruleFreightRules"].forEach(id => {
      const el = document.getElementById(id);
      if (el) el.addEventListener("input", renderQuoteRules);
    });
    ["quoteCustomerName", "quoteProduct", "quoteMaterial", "quoteProcess", "quoteRegion", "quoteFreight", "quoteSize", "quoteQty", "quoteDelivery", "quotePayment", "quoteSample"].forEach(id => {
      const el = document.getElementById(id);
      if (el) el.addEventListener("input", () => {
        if (id === "quoteProduct") {
          const selectedCatalogProduct = quoteCatalogEntry();
          if (selectedCatalogProduct && normalizeRuleText(el.value) !== normalizeRuleText(selectedCatalogProduct.name)) {
            quoteCatalogProductId = "";
            renderQuoteProductPicker();
            renderQuoteProductConfigurator();
          }
          renderQuoteTemplateOptions();
        }
        el.classList.remove("quote-field-error");
        if (id === "quoteMaterial" || id === "quoteProcess") hideMissingQuoteCosts();
        if (id === "quoteMaterial" || id === "quoteSize") renderQuoteMaterialCostCheck(true);
        markQuoteDirty();
        renderQuoteReviewSummary({ renderMissing: false });
        syncManualQuoteCertainty(id, { renderMissing: false });
        if (id === "quoteQty") { renderQuoteQuantityPresets(); refreshQuoteBookPagePrices(); }
      });
      if (el) el.addEventListener("blur", () => {
        renderQuoteMissingFirst();
        if (id === "quoteCustomerName") renderQuoteCustomerMemory();
      });
    });
    ["quoteTemplate", "quoteLevel", "quoteTaxMode", "quoteFreightMode", "quoteSampleMode"].forEach(id => {
      const el = document.getElementById(id);
      if (el) el.addEventListener("change", () => {
        el.classList.remove("quote-field-error");
        markQuoteDirty();
        renderQuoteReviewSummary();
        syncManualQuoteCertainty(id);
      });
    });
    document.getElementById("quoteAiConfirmed")?.addEventListener("change", renderQuoteMissingFirst);
    ["quoteMaterialPurchasePrice", "quoteMaterialFlatSize", "quoteMaterialGsm", "quoteMaterialUsage", "quoteMaterialUtilization", "quoteMaterialWasteRate"].forEach(id => {
      document.getElementById(id)?.addEventListener("input", () => {
        const checkbox = document.getElementById("quoteMaterialCostConfirmed");
        if (checkbox) checkbox.checked = false;
        updateQuoteMaterialConversionPreview();
        markQuoteDirty();
      });
    });
    document.getElementById("quoteMaterialPurchaseUnit")?.addEventListener("change", () => {
      const checkbox = document.getElementById("quoteMaterialCostConfirmed");
      if (checkbox) checkbox.checked = false;
      const kind = quoteMaterialPurchaseKind(val("quoteMaterialPurchaseUnit"));
      if (["piece", "sheet"].includes(kind)) {
        setIfExists("quoteMaterialUtilization", "100%");
        setIfExists("quoteMaterialWasteRate", "0%");
      }
      renderQuoteMaterialCostCheck();
      markQuoteDirty();
    });
    renderTasks();
    renderCustomerProfiles();
    renderQuoteHistory();
    renderQuoteProfessionalRecord((state.quoteHistory || [])[0] || null);
    initializeCustomerWorkbench();
    renderDashboard();
    renderMemberStatus();
