{"id":107,"date":"2025-11-20T17:24:31","date_gmt":"2025-11-20T09:24:31","guid":{"rendered":"https:\/\/awakencraft.com\/?p=107"},"modified":"2025-11-24T16:48:53","modified_gmt":"2025-11-24T08:48:53","slug":"107","status":"publish","type":"post","link":"https:\/\/awakencraft.com\/index.php\/2025\/11\/20\/107\/","title":{"rendered":""},"content":{"rendered":"\n<meta charset=\"UTF-8\">\n\n\n\n<title>\u901a\u7528\u52a0\u5de5\u62a5\u4ef7\u8ba1\u7b97\u5668<\/title>\n\n\n\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n\n\n<style>\n    body {\n      font-family: -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",\"PingFang SC\",\"Microsoft YaHei\",sans-serif;\n      background: #f5f5f7;\n      margin: 0;\n      padding: 20px;\n    }\n    .container {\n      max-width: 980px;\n      margin: 0 auto;\n      background: #fff;\n      border-radius: 16px;\n      padding: 24px 20px 28px;\n      box-shadow: 0 12px 30px rgba(0,0,0,.06);\n      position: relative;\n    }\n    h1 {\n      font-size: 22px;\n      margin: 0 0 4px;\n      text-align: center;\n    }\n    .subtitle {\n      text-align: center;\n      font-size: 13px;\n      color: #777;\n      margin-bottom: 18px;\n    }\n    .admin-bar {\n      position: absolute;\n      top: 16px;\n      right: 18px;\n      font-size: 12px;\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      color: #666;\n    }\n    .auth-bar {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      gap: 10px;\n      background: #0f172a;\n      color: #fff;\n      padding: 10px 12px;\n      border-radius: 12px;\n      margin-bottom: 12px;\n    }\n    .auth-meta {\n      font-size: 13px;\n      color: #e5e7eb;\n    }\n    .auth-actions {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n    }\n    .admin-name {\n      font-size: 12px;\n      color: #555;\n    }\n    .login-panel {\n      background: linear-gradient(135deg, #0b1220, #111827);\n      color: #e5e7eb;\n      padding: 18px;\n      border-radius: 14px;\n      margin-bottom: 16px;\n      box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 18px 30px rgba(0,0,0,.12);\n    }\n    .login-panel h3 {\n      margin: 0 0 8px;\n      color: #fff;\n    }\n    .login-panel label { color: #cbd5e1; }\n    .login-row {\n      display: grid;\n      grid-template-columns: 1fr 1fr auto;\n      gap: 10px;\n      align-items: end;\n    }\n    @media (max-width: 640px) {\n      .login-row { grid-template-columns: 1fr; }\n    }\n    .grid {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 16px 20px;\n    }\n    @media (max-width: 768px) {\n      .grid {\n        grid-template-columns: 1fr;\n      }\n      .admin-bar {\n        position: static;\n        justify-content: flex-end;\n        margin-bottom: 6px;\n      }\n    }\n    label {\n      display: block;\n      font-size: 14px;\n      margin-bottom: 4px;\n      color: #333;\n    }\n    input, select {\n      width: 100%;\n      box-sizing: border-box;\n      padding: 8px 10px;\n      font-size: 14px;\n      border-radius: 8px;\n      border: 1px solid #d0d0d5;\n      outline: none;\n      transition: border-color 0.15s, box-shadow 0.15s;\n      background: #fafafa;\n    }\n    input:focus, select:focus {\n      border-color: #0070f3;\n      box-shadow: 0 0 0 1px rgba(0,112,243,.2);\n      background: #fff;\n    }\n    .hint {\n      font-size: 12px;\n      color: #999;\n      margin-top: 2px;\n    }\n    .inline-time {\n      display: grid;\n      grid-template-columns: 1fr 1fr 1fr;\n      gap: 6px;\n    }\n    @media (max-width: 640px) {\n      .inline-time {\n        grid-template-columns: 1fr;\n      }\n    }\n    .time-field {\n      display: flex;\n      align-items: center;\n      gap: 4px;\n    }\n    .time-field span {\n      font-size: 13px;\n      color: #555;\n      white-space: nowrap;\n    }\n    .time-field input {\n      flex: 1;\n    }\n    .actions {\n      margin-top: 20px;\n      display: flex;\n      justify-content: center;\n      gap: 12px;\n      flex-wrap: wrap;\n    }\n    button {\n      border: none;\n      border-radius: 999px;\n      padding: 9px 20px;\n      font-size: 14px;\n      cursor: pointer;\n      background: #0070f3;\n      color: #fff;\n      font-weight: 500;\n      box-shadow: 0 6px 14px rgba(0,112,243,.25);\n      transition: transform 0.1s, box-shadow 0.1s, background 0.1s;\n    }\n    button:hover {\n      transform: translateY(-1px);\n      box-shadow: 0 10px 20px rgba(0,112,243,.28);\n      background: #005ad1;\n    }\n    button.secondary {\n      background: #e5e7eb;\n      color: #333;\n      box-shadow: none;\n    }\n    button.secondary:hover {\n      background: #d1d5db;\n      box-shadow: none;\n      transform: none;\n    }\n    .mini-btn {\n      border-radius: 999px;\n      border: none;\n      padding: 4px 10px;\n      font-size: 12px;\n      cursor: pointer;\n      background: #e5e7eb;\n      color: #333;\n    }\n    .mini-btn:hover {\n      background: #d1d5db;\n    }\n\n    .material-price-wrap {\n      display: flex;\n      flex-direction: column;\n      gap: 4px;\n      min-width: 0;\n    }\n    .material-price-wrap .mat-price-volume,\n    .material-price-wrap .mat-price-weight {\n      width: 100%;\n      min-width: 0;\n    }\n    .material-price-wrap .mat-price-volume[style*=\"display:none\"] {\n      height: 0;\n      padding: 0;\n      margin: 0;\n      border: 0;\n    }\n    .material-price-hint {\n      margin-top: 0;\n      color: #888;\n      min-height: 18px;\n      font-size: 12px;\n      line-height: 18px;\n    }\n    .material-unit {\n      font-size: 14px;\n      line-height: 36px;\n      white-space: nowrap;\n      color: #333;\n    }\n    .result {\n      margin-top: 22px;\n      padding-top: 18px;\n      border-top: 1px solid #eee;\n    }\n    .result h2 {\n      font-size: 18px;\n      margin: 0 0 10px;\n    }\n    .result-main {\n      font-size: 22px;\n      font-weight: 600;\n      margin-bottom: 8px;\n    }\n    .result-main span {\n      font-size: 16px;\n      font-weight: 400;\n      color: #555;\n      margin-left: 8px;\n    }\n    .result-detail {\n      font-size: 13px;\n      color: #555;\n      line-height: 1.6;\n      background: #fafafa;\n      padding: 10px 12px;\n      border-radius: 10px;\n      white-space: pre-line;\n    }\n    .badge {\n      display: inline-block;\n      font-size: 11px;\n      padding: 2px 7px;\n      border-radius: 999px;\n      background: #eff6ff;\n      color: #1d4ed8;\n      margin-left: 6px;\n    }\n    .settings {\n      margin-top: 28px;\n      padding-top: 18px;\n      border-top: 1px dashed #e5e7eb;\n    }\n    .settings h3 {\n      font-size: 16px;\n      margin: 0 0 8px;\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      gap: 8px;\n      flex-wrap: wrap;\n    }\n    .settings small {\n      font-size: 12px;\n      color: #888;\n    }\n    table {\n      width: 100%;\n      border-collapse: collapse;\n      font-size: 13px;\n      margin-bottom: 12px;\n    }\n    th, td {\n      border-bottom: 1px solid #f0f0f0;\n      padding: 6px 4px;\n      text-align: left;\n      vertical-align: middle;\n    }\n    th {\n      color: #666;\n      font-weight: 500;\n    }\n    td input {\n      width: 100%;\n      padding: 7px 9px;\n      font-size: 13px;\n      border-radius: 6px;\n      border: 1px solid #d0d0d5;\n      background: #fafafa;\n      box-sizing: border-box;\n    }\n    .inline-config {\n      display: grid;\n      grid-template-columns: repeat(3, 1fr);\n      gap: 10px;\n      margin: 10px 0 6px;\n    }\n    @media (max-width: 768px) {\n      .inline-config {\n        grid-template-columns: 1fr;\n      }\n    }\n    .vendor-filter {\n      margin: 4px 0 6px;\n      display: flex;\n      flex-wrap: wrap;\n      align-items: center;\n      gap: 6px;\n      font-size: 13px;\n    }\n    .vendor-filter label {\n      margin: 0;\n      font-size: 13px;\n    }\n    .vendor-filter select,\n    .vendor-filter input {\n      width: auto;\n      min-width: 140px;\n    }\n    .table-wrapper {\n      width: 100%;\n      overflow-x: auto;\n    }\n    .page-tabs {\n      display: flex;\n      gap: 10px;\n      margin-bottom: 12px;\n      flex-wrap: wrap;\n    }\n    .page-tab {\n      padding: 8px 14px;\n      border-radius: 999px;\n      border: 1px solid #111827;\n      background: #111827;\n      color: #f8fafc;\n      cursor: pointer;\n      font-size: 13px;\n      transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;\n    }\n    .page-tab.active {\n      background: #0070f3;\n      color: #fff;\n      border-color: #0070f3;\n    }\n    .page-tab:hover { background: #1f2937; }\n    .main-section { display: none; }\n    .main-section.active { display: block; }\n    .sub-tabs { margin-bottom: 6px; }\n    .section { display: none; }\n    .section.active { display: block; }\n    .pager {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      font-size: 13px;\n      margin: 6px 0 10px;\n      flex-wrap: wrap;\n    }\n    .pager button { padding: 5px 10px; border-radius: 8px; }\n    .record-card {\n      background: #fafafa;\n      border: 1px solid #eee;\n      border-radius: 12px;\n      padding: 12px;\n      margin-bottom: 10px;\n      font-size: 13px;\n    }\n    .modal-overlay {\n      position: fixed;\n      inset: 0;\n      background: rgba(0,0,0,0.45);\n      display: none;\n      align-items: center;\n      justify-content: center;\n      z-index: 999;\n    }\n    .modal {\n      background: #fff;\n      border-radius: 12px;\n      padding: 18px;\n      width: min(640px, 92%);\n      box-shadow: 0 20px 40px rgba(0,0,0,0.15);\n    }\n    .modal-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      margin-bottom: 10px;\n    }\n    .modal-actions {\n      display: flex;\n      gap: 8px;\n      justify-content: flex-end;\n      margin-top: 12px;\n    }\n    .badge.gray { background: #e5e7eb; color: #111827; }\n  <\/style>\n\n\n\n<div class=\"container\">\n    <h1>\u901a\u7528\u52a0\u5de5\u62a5\u4ef7\u8ba1\u7b97\u5668<\/h1>\n    <div class=\"subtitle\">\u6839\u636e\u5de5\u827a\u9009\u62e9\u5bf9\u5e94\u6750\u6599\u4e0e\u8bbe\u5907\uff0c\u5feb\u901f\u4f30\u7b97\u6210\u672c\u4e0e\u5229\u6da6<\/div>\n\n    <div class=\"auth-bar\" id=\"authBar\">\n      <div class=\"auth-meta\" id=\"userStatus\">\u8bf7\u5148\u767b\u5f55\u540e\u4f7f\u7528\u62a5\u4ef7\u529f\u80fd\u3002<\/div>\n      <div class=\"auth-actions\">\n        <button type=\"button\" class=\"mini-btn\" id=\"userLogoutBtn\" style=\"display:none;\">\u9000\u51fa\u767b\u5f55<\/button>\n    <\/div>\n  <\/div>\n\n  <div class=\"modal-overlay\" id=\"projectModal\">\n    <div class=\"modal\">\n      <div class=\"modal-header\">\n        <h3 id=\"projectModalTitle\">\u65b0\u5efa\u9879\u76ee<\/h3>\n        <button type=\"button\" class=\"mini-btn\" id=\"projectModalClose\">\u5173\u95ed<\/button>\n      <\/div>\n      <div class=\"grid\" style=\"grid-template-columns: repeat(2, 1fr);\">\n        <div>\n          <label for=\"projectModalName\">\u9879\u76ee\u540d\u79f0<\/label>\n          <input id=\"projectModalName\" type=\"text\" placeholder=\"\u5fc5\u586b\">\n        <\/div>\n        <div>\n          <label for=\"projectModalClient\">\u5ba2\u6237\u540d\u79f0<\/label>\n          <input id=\"projectModalClient\" type=\"text\" placeholder=\"\u53ef\u9009\">\n        <\/div>\n        <div>\n          <label for=\"projectModalCode\">\u9879\u76ee\u7f16\u53f7 \/ \u5185\u90e8\u7f16\u7801<\/label>\n          <input id=\"projectModalCode\" type=\"text\" placeholder=\"\u53ef\u9009\">\n        <\/div>\n        <div>\n          <label for=\"projectModalStatus\">\u9879\u76ee\u72b6\u6001<\/label>\n          <select id=\"projectModalStatus\">\n            <option value=\"\">\u672a\u6307\u5b9a<\/option>\n            <option value=\"ongoing\">\u8fdb\u884c\u4e2d<\/option>\n            <option value=\"completed\">\u5df2\u5b8c\u6210<\/option>\n            <option value=\"canceled\">\u5df2\u53d6\u6d88<\/option>\n          <\/select>\n        <\/div>\n        <div style=\"grid-column: 1 \/ span 2;\">\n          <label for=\"projectModalRemark\">\u5907\u6ce8\u8bf4\u660e<\/label>\n          <input id=\"projectModalRemark\" type=\"text\" placeholder=\"\u53ef\u9009\">\n        <\/div>\n      <\/div>\n      <div class=\"modal-actions\">\n        <button type=\"button\" class=\"secondary\" id=\"projectModalCancel\">\u53d6\u6d88<\/button>\n        <button type=\"button\" id=\"projectModalSubmit\">\u4fdd\u5b58<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n    <div class=\"login-panel\" id=\"loginPanel\">\n      <h3>\u767b\u5f55\u540e\u5f00\u59cb\u4f7f\u7528<\/h3>\n      <div class=\"login-row\">\n        <div>\n          <label for=\"userLoginName\">\u7528\u6237\u540d<\/label>\n          <input id=\"userLoginName\" type=\"text\" placeholder=\"\u8bf7\u8f93\u5165\u7528\u6237\u540d\">\n        <\/div>\n        <div>\n          <label for=\"userLoginPassword\">\u5bc6\u7801<\/label>\n          <input id=\"userLoginPassword\" type=\"password\" placeholder=\"\u8bf7\u8f93\u5165\u5bc6\u7801\">\n        <\/div>\n        <button type=\"button\" id=\"userLoginBtn\">\u767b\u5f55<\/button>\n      <\/div>\n      <div class=\"hint\">\u4e0d\u540c\u8d26\u6237\u5bf9\u5e94\u4e0d\u540c\u6743\u9650\uff1a\u7ba1\u7406\u5458\u767b\u5f55\u540e\u53ef\u7ba1\u7406\u914d\u7f6e\u3001\u62a5\u4ef7\u8bb0\u5f55\u4e0e\u7528\u6237\uff1b\u666e\u901a\u8d26\u53f7\u53ef\u8fdb\u884c\u62a5\u4ef7\u8ba1\u7b97\u3002<\/div>\n    <\/div>\n\n    <div id=\"appContent\" style=\"display:none;\">\n\n    <div class=\"page-tabs main-tabs\">\n      <button class=\"page-tab active\" data-main=\"quoteMain\">\u62a5\u4ef7\u5de5\u4f5c\u53f0<\/button>\n      <button class=\"page-tab\" data-main=\"recordsMain\" style=\"display:none;\">\u62a5\u4ef7\u8bb0\u5f55<\/button>\n      <button class=\"page-tab\" data-main=\"accountMain\" style=\"display:none;\">\u8d26\u6237\u8bbe\u7f6e<\/button>\n      <button class=\"page-tab\" data-main=\"adminMain\" data-require-admin=\"true\">\u7ba1\u7406\u4e2d\u5fc3<\/button>\n    <\/div>\n\n    <div class=\"main-section active\" id=\"quoteMain\">\n    <div class=\"section active\" id=\"quoteSection\">\n\n    <!-- \u62a5\u4ef7\u8868\u5355 -->\n    <form id=\"quote-form\">\n      <div class=\"grid\">\n        <div>\n          <label for=\"processType\">\u52a0\u5de5\u7c7b\u578b<\/label>\n          <select id=\"processType\"><\/select>\n          <div class=\"hint\">\u9009\u62e9\u5de5\u827a\u540e\uff0c\u4e0b\u65b9\u6750\u6599\u548c\u8bbe\u5907\u5217\u8868\u5c06\u6309\u5de5\u827a\u81ea\u52a8\u7b5b\u9009\u3002<\/div>\n        <\/div>\n        <div>\n          <label for=\"projectSelect\">\u6240\u5c5e\u9879\u76ee<\/label>\n          <select id=\"projectSelect\"><\/select>\n          <div class=\"vendor-filter\" style=\"padding:0;\">\n            <input id=\"projectSearch\" type=\"text\" placeholder=\"\u8f93\u5165\u540d\u79f0\/\u5ba2\u6237\u641c\u7d22\">\n            <button type=\"button\" class=\"mini-btn\" id=\"projectSearchBtn\">\u641c\u7d22<\/button>\n            <button type=\"button\" class=\"mini-btn\" id=\"projectNewBtn\">\u65b0\u5efa\u9879\u76ee<\/button>\n          <\/div>\n          <div class=\"hint\">\u5efa\u8bae\u5728\u62a5\u4ef7\u524d\u5173\u8054\u9879\u76ee\uff0c\u4fbf\u4e8e\u540e\u7eed\u5f52\u6863\u4e0e\u7edf\u8ba1\uff1b\u53ef\u4e3a\u7a7a\u8868\u793a\u672a\u5173\u8054\u3002<\/div>\n        <\/div>\n        <!-- \u6750\u6599\u9009\u62e9\uff1a\u5382\u5546 + \u578b\u53f7 -->\n        <div>\n          <label>\u6750\u6599\u5382\u5546<\/label>\n          <select id=\"materialVendor\"><\/select>\n\n          <label style=\"margin-top:6px; display:block;\">\u6750\u6599\u578b\u53f7 \/ \u540d\u79f0<\/label>\n          <select id=\"material\" required=\"\"><\/select>\n\n          <div class=\"hint\">\u5148\u9009\u5382\u5546\uff0c\u518d\u9009\u6750\u6599\u578b\u53f7\u3002\u6750\u6599\u5217\u8868\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458\u7ef4\u62a4\u3002<\/div>\n        <\/div>\n\n        <div id=\"weightField\">\n          <label for=\"weight\">\u5355\u4ef6\u8017\u6750\u91cd\u91cf\uff08g\uff09<\/label>\n          <input id=\"weight\" type=\"number\" step=\"0.01\" min=\"0\" required=\"\">\n          <div class=\"hint\">\u5f53\u6750\u6599\u6309\u91cd\u91cf\u8ba1\u4ef7\u65f6\u586b\u5199\uff0c\u652f\u6301\u5230 0.01 g\uff08\u4ece\u5207\u7247\u8f6f\u4ef6\u4e2d\u67e5\u770b\u8017\u6750\u7528\u91cf\uff09\u3002<\/div>\n        <\/div>\n\n        <div id=\"volumeField\">\n          <label for=\"volume\">\u5355\u4ef6\u6750\u6599\u4f53\u79ef\uff08cm\u00b3\uff09<\/label>\n          <input id=\"volume\" type=\"number\" step=\"0.01\" min=\"0\">\n          <div class=\"hint\">\u5f53\u6750\u6599\u6309\u4f53\u79ef\u8ba1\u4ef7\uff08\u5982\u6728\u6750\uff09\u65f6\u586b\u5199\uff1b\u6309\u91cd\u91cf\u8ba1\u4ef7\u65f6\u81ea\u52a8\u9690\u85cf\u3002<\/div>\n        <\/div>\n\n        <!-- \u6253\u5370\u65f6\u95f4\u4e0e\u8bbe\u5907 -->\n        <div>\n          <label>\u5355\u4ef6\u6253\u5370\u65f6\u95f4<\/label>\n          <div class=\"inline-time\">\n            <div class=\"time-field\">\n              <input id=\"printDays\" type=\"number\" step=\"1\" min=\"0\">\n              <span>\u5929<\/span>\n            <\/div>\n            <div class=\"time-field\">\n              <input id=\"printHours\" type=\"number\" step=\"1\" min=\"0\">\n              <span>\u5c0f\u65f6<\/span>\n            <\/div>\n            <div class=\"time-field\">\n              <input id=\"printMinutes\" type=\"number\" step=\"1\" min=\"0\">\n              <span>\u5206\u949f<\/span>\n            <\/div>\n          <\/div>\n          <div class=\"hint\">\u53ef\u53ea\u586b\u5c0f\u65f6\u6216\u5206\u949f\uff0c\u4e5f\u53ef\u4ee5\u4e09\u9879\u4e00\u8d77\u586b\uff0c\u5c06\u81ea\u52a8\u6298\u7b97\u4e3a\u603b\u5c0f\u65f6\u3002<\/div>\n        <\/div>\n\n        <div>\n          <label>\u8bbe\u5907\u5382\u5546<\/label>\n          <select id=\"machineVendor\"><\/select>\n\n          <label style=\"margin-top:6px; display:block;\">\u8bbe\u5907\u578b\u53f7 \/ \u540d\u79f0<\/label>\n          <select id=\"machine\" required=\"\"><\/select>\n\n          <div class=\"hint\">\u8bbe\u5907\u5217\u8868\u5728\u4e0b\u65b9\u201c\u8bbe\u5907\u8bbe\u7f6e\u201d\u4e2d\u7ef4\u62a4\u3002<\/div>\n        <\/div>\n\n        <!-- \u540e\u5904\u7406\u4e0e\u6570\u91cf -->\n        <div>\n          <label for=\"postProcess\">\u540e\u5904\u7406\u7b49\u7ea7<\/label>\n          <select id=\"postProcess\" required=\"\"><\/select>\n          <div class=\"hint\">\u540e\u5904\u7406\u8017\u65f6\u4e0e\u6750\u6599\u6d88\u8017\u53ef\u5728\u4e0b\u65b9\u201c\u540e\u5904\u7406\u8bbe\u7f6e\u201d\u4e2d\u914d\u7f6e\u3002<\/div>\n        <\/div>\n\n        <div>\n          <label for=\"quantity\">\u6570\u91cf\uff08\u4ef6\uff09<\/label>\n          <input id=\"quantity\" type=\"number\" step=\"1\" min=\"1\" value=\"1\" required=\"\">\n        <\/div>\n\n        <!-- \u4e34\u65f6\u53c2\u6570\uff08\u53ea\u5f71\u54cd\u672c\u6b21\u8ba1\u7b97\uff09 -->\n        <div>\n          <label for=\"customMargin\">\u5229\u6da6\u7387\uff08\u672c\u6b21\uff0c%\uff09<\/label>\n          <input id=\"customMargin\" type=\"number\" step=\"1\" min=\"0\" placeholder=\"\u7559\u7a7a\u5219\u7528\u9ed8\u8ba4\u914d\u7f6e\">\n          <div class=\"hint\">\u9ed8\u8ba43% \uff0c\u4f8b\u5982 3 \u8868\u793a\u52a0 3% \u5229\u6da6\u3002<\/div>\n        <\/div>\n\n        <div>\n          <label for=\"customMin\">\u5355\u4ef6\u6700\u4f4e\u4ef7\uff08\u672c\u6b21\uff0c\u5143\uff09<\/label>\n          <input id=\"customMin\" type=\"number\" step=\"0.1\" min=\"0\" placeholder=\"\u7559\u7a7a\u5219\u7528\u9ed8\u8ba4\u914d\u7f6e\">\n          <div class=\"hint\">\u7528\u4e8e\u9650\u5236\u6700\u5c0f\u5355\u4ef7\uff0c\u9632\u6b62\u5c0f\u96f6\u4ef6\u8fc7\u4f4e\u3002<\/div>\n        <\/div>\n\n        <div>\n          <label for=\"recordVisibilitySelect\">\u62a5\u4ef7\u8bb0\u5f55\u53ef\u89c1\u8303\u56f4<\/label>\n          <select id=\"recordVisibilitySelect\">\n            <option value=\"admin_only\">\u4ec5\u7ba1\u7406\u5458<\/option>\n            <option value=\"all_users\">\u5168\u90e8\u767b\u5f55\u7528\u6237<\/option>\n            <option value=\"owner_only\">\u4ec5\u521b\u5efa\u4eba<\/option>\n          <\/select>\n          <div class=\"hint\">\u5f71\u54cd\u4fdd\u5b58\u5230\u8bb0\u5f55\u4e2d\u5fc3\u7684\u53ef\u89c1\u6027\uff0c\u7ba1\u7406\u5458\u53ef\u540e\u7eed\u8c03\u6574\u3002<\/div>\n        <\/div>\n\n      <\/div>\n\n      <div class=\"actions\">\n        <button type=\"submit\">\u8ba1\u7b97\u62a5\u4ef7<\/button>\n        <button type=\"button\" class=\"secondary\" id=\"resetBtn\">\u6e05\u7a7a<\/button>\n        <button type=\"button\" class=\"secondary\" id=\"exportBtn\">\u5bfc\u51fa\u62a5\u4ef7\u5355<\/button>\n      <\/div>\n    <\/form>\n\n    <!-- \u62a5\u4ef7\u7ed3\u679c -->\n    <div class=\"result\" id=\"result\" style=\"display:none;\">\n      <h2>\u62a5\u4ef7\u7ed3\u679c <span class=\"badge\">\u5185\u90e8\u8bd5\u7b97<\/span><\/h2>\n      <div class=\"result-main\" id=\"result-total\"><\/div>\n      <div class=\"result-detail\" id=\"result-detail\"><\/div>\n    <\/div>\n\n    <\/div> <!-- \/quoteSection -->\n    <\/div> <!-- \/quoteMain -->\n\n    <div class=\"main-section\" id=\"accountMain\">\n      <div class=\"section active\" id=\"accountSection\">\n        <div class=\"login-panel\" style=\"margin:0 0 10px;\">\n          <h3>\u8d26\u6237\u8bbe\u7f6e<\/h3>\n          <div class=\"login-row\">\n            <div>\n              <label for=\"selfNewUsername\">\u65b0\u7528\u6237\u540d<\/label>\n              <input id=\"selfNewUsername\" type=\"text\" placeholder=\"\u8f93\u5165\u65b0\u7684\u7528\u6237\u540d\">\n            <\/div>\n            <div>\n              <label for=\"selfUsernamePassword\">\u5f53\u524d\u5bc6\u7801<\/label>\n              <input id=\"selfUsernamePassword\" type=\"password\" placeholder=\"\u9a8c\u8bc1\u5f53\u524d\u5bc6\u7801\">\n            <\/div>\n            <button type=\"button\" id=\"selfChangeUsernameBtn\">\u4fee\u6539\u7528\u6237\u540d<\/button>\n          <\/div>\n          <div class=\"login-row\" style=\"margin-top:10px;\">\n            <div>\n              <label for=\"selfOldPassword\">\u5f53\u524d\u5bc6\u7801<\/label>\n              <input id=\"selfOldPassword\" type=\"password\" placeholder=\"\u8bf7\u8f93\u5165\u5f53\u524d\u5bc6\u7801\">\n            <\/div>\n            <div>\n              <label for=\"selfNewPassword\">\u65b0\u5bc6\u7801<\/label>\n              <input id=\"selfNewPassword\" type=\"password\" placeholder=\"\u8bf7\u8f93\u5165\u65b0\u5bc6\u7801\">\n            <\/div>\n            <button type=\"button\" id=\"selfChangePasswordBtn\">\u4fee\u6539\u5bc6\u7801<\/button>\n          <\/div>\n          <div class=\"hint\">\u4ec5\u5f53\u524d\u767b\u5f55\u8d26\u6237\u53ef\u89c1\uff0c\u4fee\u6539\u540e\u4f1a\u81ea\u52a8\u9000\u51fa\u767b\u5f55\u4ee5\u91cd\u65b0\u9a8c\u8bc1\u3002<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"main-section\" id=\"recordsMain\">\n      <div class=\"section active\" id=\"recordsSection\">\n        <h3>\u62a5\u4ef7\u8bb0\u5f55<\/h3>\n        <small>\u767b\u5f55\u540e\u53ef\u6309\u6743\u9650\u67e5\u770b\uff1b\u7ba1\u7406\u5458\u53ef\u4ee5\u7b5b\u9009\u3001\u4fee\u6539\u53ef\u89c1\u8303\u56f4\u6216\u6807\u8bb0\u91c7\u7528\u3002<\/small>\n        <div class=\"inline-config\">\n          <div>\n            <label for=\"recordsMonth\">\u6309\u6708\u4efd\u7b5b\u9009<\/label>\n            <input id=\"recordsMonth\" type=\"month\">\n          <\/div>\n          <div>\n            <label for=\"recordsCreatorFilter\">\u521b\u5efa\u4eba<\/label>\n            <select id=\"recordsCreatorFilter\"><\/select>\n          <\/div>\n          <div>\n            <label for=\"recordsProjectFilter\">\u9879\u76ee<\/label>\n            <select id=\"recordsProjectFilter\"><\/select>\n          <\/div>\n          <div>\n            <label for=\"recordsAdoptedFilter\">\u662f\u5426\u91c7\u7528<\/label>\n            <select id=\"recordsAdoptedFilter\">\n              <option value=\"all\">\u5168\u90e8<\/option>\n              <option value=\"true\">\u5df2\u91c7\u7528<\/option>\n              <option value=\"false\">\u672a\u91c7\u7528<\/option>\n            <\/select>\n          <\/div>\n          <div id=\"recordsVisibilityFilterWrap\">\n            <label for=\"recordsVisibilityFilter\">\u53ef\u89c1\u8303\u56f4<\/label>\n            <select id=\"recordsVisibilityFilter\">\n              <option value=\"all\">\u5168\u90e8<\/option>\n              <option value=\"admin_only\">\u4ec5\u7ba1\u7406\u5458<\/option>\n              <option value=\"all_users\">\u5168\u90e8\u767b\u5f55\u7528\u6237<\/option>\n              <option value=\"owner_only\">\u4ec5\u521b\u5efa\u4eba<\/option>\n            <\/select>\n          <\/div>\n          <div>\n            <label for=\"recordsPageSize\">\u6bcf\u9875\u663e\u793a<\/label>\n            <select id=\"recordsPageSize\">\n              <option value=\"5\">5<\/option>\n              <option value=\"10\">10<\/option>\n              <option value=\"20\">20<\/option>\n            <\/select>\n          <\/div>\n        <\/div>\n        <div class=\"pager\">\n          <button type=\"button\" class=\"mini-btn\" id=\"recordsPrev\">\u4e0a\u4e00\u9875<\/button>\n          <span id=\"recordsPageInfo\"><\/span>\n          <button type=\"button\" class=\"mini-btn\" id=\"recordsNext\">\u4e0b\u4e00\u9875<\/button>\n        <\/div>\n        <div id=\"recordsList\"><\/div>\n        <h4>\u6708\u5ea6\u6c47\u603b<\/h4>\n        <div id=\"recordsSummary\"><\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"main-section\" id=\"adminMain\" style=\"display:none;\">\n      <div class=\"page-tabs sub-tabs\" id=\"adminTabs\">\n        <button class=\"page-tab active\" data-section=\"paramsSection\">\u53c2\u6570\u914d\u7f6e<\/button>\n        <button class=\"page-tab\" data-section=\"materialsSection\">\u6750\u6599\u8bbe\u7f6e<\/button>\n        <button class=\"page-tab\" data-section=\"machinesSection\">\u8bbe\u5907\u8bbe\u7f6e<\/button>\n        <button class=\"page-tab\" data-section=\"projectSection\">\u9879\u76ee\u7ba1\u7406<\/button>\n        <button class=\"page-tab\" data-section=\"userManagementSection\">\u7528\u6237\u7ba1\u7406<\/button>\n      <\/div>\n\n    <!-- \u7ba1\u7406\u5458\u8bbe\u7f6e\u533a\u57df\uff08\u767b\u5f55\u540e\u53ef\u89c1\uff09 -->\n    <div class=\"settings section admin-sub-section active\" id=\"paramsSection\">\n      <h3>\n        \u5168\u5c40\u53c2\u6570\n        <div style=\"display:flex;flex-wrap:wrap;gap:6px;align-items:center;\">\n          <button type=\"button\" class=\"mini-btn\" id=\"exportSettingsBtn\">\u5bfc\u51fa\u5f53\u524d\u914d\u7f6e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"importSettingsBtn\">\u5bfc\u5165\u5907\u4efd\/\u914d\u7f6e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"backupSettingsBtn\">\u5907\u4efd\u5f53\u524d\u914d\u7f6e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"restoreBackupBtn\">\u6062\u590d\u5907\u4efd\u914d\u7f6e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"backupFullBtn\">\u5907\u4efd\u5168\u90e8\u6570\u636e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"restoreFullBtn\">\u6062\u590d\u5168\u90e8\u6570\u636e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"restoreFactoryBtn\">\u6062\u590d\u51fa\u5382\u8bbe\u7f6e<\/button>\n          <button type=\"button\" class=\"mini-btn\" id=\"saveSettingsBtn\">\u4fdd\u5b58\u8bbe\u7f6e\u5230\u670d\u52a1\u5668<\/button>\n        <\/div>\n      <\/h3>\n      <input type=\"file\" accept=\"application\/json\" id=\"importSettingsFile\" style=\"display:none\">\n      <small>\u8fd9\u91cc\u7684\u53c2\u6570\u4f5c\u4e3a\u9ed8\u8ba4\u503c\uff0c\u5f71\u54cd\u6240\u6709\u62a5\u4ef7\uff1b\u4fdd\u5b58\u540e\u5bf9\u6240\u6709\u7528\u6237\u751f\u6548\u3002<\/small>\n      <div class=\"inline-config\">\n        <div>\n          <label for=\"configProfitMargin\">\u9ed8\u8ba4\u5229\u6da6\u7387\uff08%\uff09<\/label>\n          <input id=\"configProfitMargin\" type=\"number\" step=\"1\" min=\"0\">\n        <\/div>\n        <div>\n          <label for=\"configMinPrice\">\u9ed8\u8ba4\u5355\u4ef6\u6700\u4f4e\u4ef7\uff08\u5143\uff09<\/label>\n          <input id=\"configMinPrice\" type=\"number\" step=\"0.1\" min=\"0\">\n        <\/div>\n        <div>\n          <label for=\"configSetupFee\">\u5f00\u673a\/\u8c03\u673a\u8d39\uff08\u5143\/\u8ba2\u5355\uff09<\/label>\n          <input id=\"configSetupFee\" type=\"number\" step=\"0.1\" min=\"0\">\n        <\/div>\n        <div>\n          <label for=\"configElectricityPrice\">\u7535\u4ef7\uff08\u5143 \/ kWh\uff09<\/label>\n          <input id=\"configElectricityPrice\" type=\"number\" step=\"0.01\" min=\"0\">\n        <\/div>\n      <\/div>\n\n      <h4>\u6309\u5de5\u827a\u7684\u6210\u672c\u53c2\u6570<\/h4>\n      <div class=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr>\n              <th style=\"width:24%;\">\u5de5\u827a<\/th>\n              <th style=\"width:24%;\">\u4eba\u5de5\u6210\u672c\uff08\u5143\/\u5c0f\u65f6\uff09<\/th>\n              <th style=\"width:24%;\">\u6bcf\u4eba\u8d1f\u8d23\u8bbe\u5907\u6570<\/th>\n              <th style=\"width:28%;\">\u6742\u9879\u6210\u672c\uff08\u5143\/\u53f0\u00b7\u5c0f\u65f6\uff09<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody id=\"processCostTableBody\"><\/tbody>\n        <\/table>\n      <\/div>\n\n      <!-- \u540e\u5904\u7406\u8bbe\u7f6e -->\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        \u540e\u5904\u7406\u8bbe\u7f6e\n      <\/h3>\n      <small>\u5b57\u6bb5\u8bf4\u660e\uff1a\u6bcf\u4ef6\u57fa\u7840\u65f6\u95f4 + \uff08\u91cd\u91cf \u00d7 \u6bcf\u514b\u65f6\u95f4\uff09\u7528\u4e8e\u4f30\u7b97\u4eba\u5de5\u5de5\u65f6\uff1b\u6bcf\u514b\u6750\u6599\u6210\u672c\u7528\u4e8e\u8017\u6750\/\u6d82\u6599\u7b49\u3002key \u548c\u540d\u79f0\u5fc5\u586b\uff0c\u65f6\u95f4\/\u6210\u672c\u4e0d\u53ef\u4e3a\u8d1f\u3002<\/small>\n      <table>\n        <thead>\n          <tr>\n            <th style=\"width:14%;\">Key<\/th>\n            <th style=\"width:20%;\">\u663e\u793a\u540d\u79f0<\/th>\n            <th style=\"width:14%;\">\u5de5\u827a<\/th>\n            <th style=\"width:16%;\">\u57fa\u7840\u65f6\u95f4\uff08\u5206\u949f\uff09<\/th>\n            <th style=\"width:18%;\">\u6bcf\u514b\u65f6\u95f4\uff08\u5206\u949f\/\u514b\uff09<\/th>\n            <th style=\"width:12%;\">\u6750\u6599\u6210\u672c\uff08\u5143\/\u514b\uff09<\/th>\n            <th style=\"width:10%;\">\u6210\u672c\u7cfb\u6570<\/th>\n            <th style=\"width:6%;\">\u64cd\u4f5c<\/th>\n          <\/tr>\n        <\/thead>\n        <tbody id=\"postProcessTableBody\"><\/tbody>\n      <\/table>\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        &#8211;\n        <button id=\"addPostProcessBtn\" class=\"btn-sub\">\u65b0\u589e\u540e\u5904\u7406\u7b49\u7ea7<\/button>\n      <\/h3>\n    <\/div>\n\n    <div class=\"settings section admin-sub-section\" id=\"materialsSection\" data-require-admin=\"true\">\n      <!-- \u6750\u6599\u8bbe\u7f6e -->\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        \u6750\u6599\u8bbe\u7f6e\n        <button type=\"button\" class=\"mini-btn\" id=\"saveSettingsBtnMaterials\">\u4fdd\u5b58\u8bbe\u7f6e\u5230\u670d\u52a1\u5668<\/button>\n      <\/h3>\n      <div class=\"vendor-filter\">\n        <label>\u6309\u5de5\u827a\u7b5b\u9009\uff1a<\/label>\n        <select id=\"materialsProcessFilter\"><\/select>\n        <label>\u5f53\u524d\u6750\u6599\u5382\u5546\uff1a<\/label>\n        <select id=\"materialsVendorFilter\"><\/select>\n        <input id=\"newMaterialVendorInput\" type=\"text\" placeholder=\"\u65b0\u5382\u5546\u540d\u79f0\">\n        <button type=\"button\" class=\"mini-btn\" id=\"addMaterialVendorBtn\">\u6dfb\u52a0\u5382\u5546<\/button>\n      <\/div>\n      <small>\u6309\u8ba1\u4ef7\u6a21\u5f0f\u586b\u5199\u5355\u4ef7\uff1a\u91cd\u91cf\uff08\u5143\/kg\uff09\u6216\u4f53\u79ef\uff08\u5143\/m\u00b3\uff09\uff0c\u5185\u5bb9\u4f1a\u540c\u6b65\u5230\u4e0a\u65b9\u4e0b\u62c9\u6846\u3002<\/small>\n      <div class=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr class=\"material-header\">\n              <th style=\"width:14%; min-width:140px;\">\u5382\u5546<\/th>\n              <th style=\"width:14%; min-width:140px;\">\u5de5\u827a<\/th>\n              <th style=\"width:20%; min-width:180px;\">\u6750\u6599\u540d\u79f0<\/th>\n              <th style=\"width:14%; min-width:150px;\">\u8ba1\u4ef7\u6a21\u5f0f<\/th>\n              <th style=\"width:16%; min-width:170px;\">\u5355\u4ef7<\/th>\n              <th style=\"width:10%; min-width:90px;\">\u5355\u4f4d<\/th>\n              <th style=\"width:6%; min-width:80px;\">\u64cd\u4f5c<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody id=\"materialsTableBody\"><\/tbody>\n        <\/table>\n      <\/div>\n      <div class=\"pager\">\n        <label>\u6bcf\u9875\u663e\u793a<\/label>\n        <select id=\"materialsPageSize\">\n          <option value=\"5\">5<\/option>\n          <option value=\"10\">10<\/option>\n          <option value=\"20\">20<\/option>\n        <\/select>\n        <button type=\"button\" class=\"mini-btn\" id=\"materialsPrev\">\u4e0a\u4e00\u9875<\/button>\n        <span id=\"materialsPageInfo\"><\/span>\n        <button type=\"button\" class=\"mini-btn\" id=\"materialsNext\">\u4e0b\u4e00\u9875<\/button>\n      <\/div>\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        &#8211;\n        <button id=\"addMaterialBtn\" class=\"btn-sub\">\u65b0\u589e\u6750\u6599<\/button>\n      <\/h3>\n    <\/div>\n\n    <div class=\"settings section admin-sub-section\" id=\"machinesSection\" data-require-admin=\"true\">\n      <!-- \u8bbe\u5907\u8bbe\u7f6e -->\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        \u8bbe\u5907\u8bbe\u7f6e\n        <button type=\"button\" class=\"mini-btn\" id=\"saveSettingsBtnMachines\">\u4fdd\u5b58\u8bbe\u7f6e\u5230\u670d\u52a1\u5668<\/button>\n      <\/h3>\n\n      <div class=\"vendor-filter\">\n        <label>\u6309\u5de5\u827a\u7b5b\u9009\uff1a<\/label>\n        <select id=\"machinesProcessFilter\"><\/select>\n        <label>\u5f53\u524d\u8bbe\u5907\u5382\u5546\uff1a<\/label>\n        <select id=\"machinesVendorFilter\"><\/select>\n        <input id=\"newMachineVendorInput\" type=\"text\" placeholder=\"\u65b0\u5382\u5546\u540d\u79f0\">\n        <button type=\"button\" class=\"mini-btn\" id=\"addMachineVendorBtn\">\u6dfb\u52a0\u5382\u5546<\/button>\n      <\/div>\n      <small>\n        \u5355\u4f4d\uff1a\u5143 \/ \u5c0f\u65f6\u3002\u8bf7\u586b\u5199\u8bbe\u5907\u6298\u65e7\/\u79df\u8d41\u6210\u672c\uff0c\u4eba\u5de5\u3001\u7535\u8d39\u3001\u6742\u9879\u4f1a\u6839\u636e\u5de5\u827a\u6210\u672c\u53c2\u6570\u548c\u7535\u4ef7\u81ea\u52a8\u7d2f\u52a0\uff0c\u907f\u514d\u91cd\u590d\u8ba1\u7b97\u3002<br>\n        \u586b\u5199\u8bbe\u5907\u4ef7\u683c\u3001\u6298\u65e7\u5e74\u9650\u548c\u6bcf\u6708\u5de5\u65f6\u540e\uff0c\u70b9\u51fb\u201c\u7b97\u201d\u81ea\u52a8\u8ba1\u7b97\u6298\u65e7\u5c0f\u65f6\u6210\u672c\u3002\n      <\/small>\n      <div class=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr>\n              <th style=\"width:14%; min-width:140px;\">\u5382\u5546<\/th>\n              <th style=\"width:14%; min-width:140px;\">\u5de5\u827a<\/th>\n              <th style=\"width:20%; min-width:180px;\">\u8bbe\u5907\u540d\u79f0<\/th>\n              <th style=\"width:11%; min-width:130px;\">\u8bbe\u5907\u4ef7\uff08\u5143\uff09<\/th>\n              <th style=\"width:11%; min-width:120px;\">\u6298\u65e7\u5e74\u9650\uff08\u5e74\uff09<\/th>\n              <th style=\"width:11%; min-width:120px;\">\u6708\u5de5\u65f6\uff08\u5c0f\u65f6\uff09<\/th>\n              <th style=\"width:8%; min-width:100px;\">\u529f\u7387\uff08W\uff09<\/th>\n              <th style=\"width:10%; min-width:150px;\">\u5c0f\u65f6\u6210\u672c\uff08\u5143\uff09<\/th>\n              <th style=\"width:8%; min-width:120px;\">\u5305\u542b\u8fd0\u8425\u6210\u672c<\/th>\n              <th style=\"width:6%; min-width:90px;\">\u64cd\u4f5c<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody id=\"machinesTableBody\"><\/tbody>\n        <\/table>\n      <\/div>\n      <div class=\"pager\">\n        <label>\u6bcf\u9875\u663e\u793a<\/label>\n        <select id=\"machinesPageSize\">\n          <option value=\"5\">5<\/option>\n          <option value=\"10\">10<\/option>\n          <option value=\"20\">20<\/option>\n        <\/select>\n        <button type=\"button\" class=\"mini-btn\" id=\"machinesPrev\">\u4e0a\u4e00\u9875<\/button>\n        <span id=\"machinesPageInfo\"><\/span>\n        <button type=\"button\" class=\"mini-btn\" id=\"machinesNext\">\u4e0b\u4e00\u9875<\/button>\n      <\/div>\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        &#8211;\n        <button id=\"addMachineBtn\" class=\"btn-sub\">\u65b0\u589e\u8bbe\u5907<\/button>\n      <\/h3>\n    <\/div>\n\n    <div class=\"settings section admin-sub-section\" id=\"projectSection\" data-require-admin=\"true\">\n      <h3 style=\"display:flex;justify-content:space-between;align-items:center;\">\n        \u9879\u76ee\u7ba1\u7406\n        <button type=\"button\" class=\"mini-btn\" id=\"projectCreateBtn\">\u65b0\u5efa\u9879\u76ee<\/button>\n      <\/h3>\n      <div class=\"vendor-filter\">\n        <input id=\"projectAdminSearch\" type=\"text\" placeholder=\"\u6309\u540d\u79f0\/\u5ba2\u6237\/\u7f16\u53f7\u641c\u7d22\">\n        <button type=\"button\" class=\"mini-btn\" id=\"projectAdminSearchBtn\">\u641c\u7d22<\/button>\n        <button type=\"button\" class=\"mini-btn\" id=\"projectAdminResetBtn\">\u91cd\u7f6e<\/button>\n      <\/div>\n      <div class=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr>\n              <th style=\"width:20%;\">\u540d\u79f0<\/th>\n              <th style=\"width:16%;\">\u5ba2\u6237<\/th>\n              <th style=\"width:16%;\">\u7f16\u53f7<\/th>\n              <th style=\"width:12%;\">\u72b6\u6001<\/th>\n              <th style=\"width:22%;\">\u5907\u6ce8<\/th>\n              <th style=\"width:14%;\">\u64cd\u4f5c<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody id=\"projectTableBody\"><\/tbody>\n        <\/table>\n      <\/div>\n    <\/div>\n\n    <div class=\"section admin-sub-section\" id=\"userManagementSection\">\n      <h3>\u7528\u6237\u7ba1\u7406<\/h3>\n      <small>\u4ec5\u7ba1\u7406\u5458\u53ef\u89c1\uff1a\u65b0\u589e\u666e\u901a\u7528\u6237\u6216\u7ba1\u7406\u5458\uff0c\u91cd\u7f6e\u5bc6\u7801\u3001\u542f\u7528\/\u7981\u7528\u8d26\u53f7\uff0c\u5e76\u914d\u7f6e\u62a5\u4ef7\u8bb0\u5f55\u6743\u9650\u3002<\/small>\n      <div style=\"margin:8px 0;\">\n        <button type=\"button\" class=\"mini-btn\" id=\"changePasswordBtn\">\u4fee\u6539\u7ba1\u7406\u5458\u5bc6\u7801<\/button>\n      <\/div>\n      <div class=\"inline-config\">\n        <div>\n          <label for=\"newUserName\">\u7528\u6237\u540d<\/label>\n          <input id=\"newUserName\" type=\"text\" placeholder=\"\u8bf7\u8f93\u5165\u65b0\u7528\u6237\u540d\">\n        <\/div>\n        <div>\n          <label for=\"newUserPassword\">\u521d\u59cb\u5bc6\u7801<\/label>\n          <input id=\"newUserPassword\" type=\"password\" placeholder=\"\u8bbe\u7f6e\u521d\u59cb\u5bc6\u7801\">\n        <\/div>\n        <div>\n          <label for=\"newUserRole\">\u89d2\u8272<\/label>\n          <select id=\"newUserRole\">\n            <option value=\"user\">\u666e\u901a\u7528\u6237<\/option>\n            <option value=\"admin\">\u7ba1\u7406\u5458<\/option>\n          <\/select>\n        <\/div>\n        <div>\n          <label for=\"newUserRecordEnabled\">\u662f\u5426\u8bb0\u5f55\u62a5\u4ef7<\/label>\n          <select id=\"newUserRecordEnabled\">\n            <option value=\"true\">\u8bb0\u5f55\uff08\u9ed8\u8ba4\uff09<\/option>\n            <option value=\"false\">\u4e0d\u8bb0\u5f55<\/option>\n          <\/select>\n        <\/div>\n        <div>\n          <label for=\"newUserCanViewRecords\">\u662f\u5426\u53ef\u67e5\u770b\u8bb0\u5f55<\/label>\n          <select id=\"newUserCanViewRecords\">\n            <option value=\"false\">\u4e0d\u53ef\u67e5\u770b\uff08\u9ed8\u8ba4\uff09<\/option>\n            <option value=\"true\">\u5141\u8bb8\u67e5\u770b<\/option>\n          <\/select>\n        <\/div>\n        <div style=\"display:flex;align-items:flex-end;\">\n          <button type=\"button\" class=\"mini-btn\" id=\"createUserBtn\">\u65b0\u589e\u7528\u6237<\/button>\n        <\/div>\n      <\/div>\n      <div class=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr>\n              <th>\u7528\u6237\u540d<\/th>\n              <th>\u89d2\u8272<\/th>\n              <th>\u72b6\u6001<\/th>\n              <th>\u8bb0\u5f55\u5f00\u5173<\/th>\n              <th>\u67e5\u770b\u6743\u9650<\/th>\n              <th>\u64cd\u4f5c<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody id=\"userTableBody\"><\/tbody>\n        <\/table>\n      <\/div>\n    <\/div>\n    <\/div> <!-- \/adminMain -->\n  <\/div>\n\n  <script>\n    \/\/ ===== API \u5730\u5740\uff1a\u8fd9\u91cc\u6539\u6210\u4f60\u7684\u540e\u7aef\u5730\u5740 =====\n    const API_BASE = \"http:\/\/10.1.1.21:8000\/api\";\n\n    const PROCESS_TYPES = [\n      { value: \"FDM_3D_PRINT\", label: \"FDM 3D \u6253\u5370\" },\n      { value: \"RESIN_3D_PRINT\", label: \"\u5149\u56fa\u5316 3D \u6253\u5370\" },\n      { value: \"CNC_MILLING\", label: \"CNC \u96d5\u523b \/ \u94e3\u524a\" },\n      { value: \"CO2_LASER_ENGRAVE_CUT\", label: \"CO\u2082 \u6fc0\u5149\u96d5\u523b \/ \u5207\u5272\" }\n    ];\n\n    const UNIVERSAL_PROCESS_VALUE = \"ALL\"; \/\/ \u7a7a\u6216 ALL \u89c6\u4e3a\u5168\u90e8\u5de5\u827a\u901a\u7528\n    const PROCESS_FILTER_ALL = \"__ALL__\";\n    const VENDOR_FILTER_ALL = \"__ALL_VENDOR__\";\n\n    const CLIENT_DEFAULTS = {\n      defaultProfitMargin: 0.4,\n      defaultMinPricePerPart: 15,\n      setupFee: 10,\n      electricityPrice: 0.8,          \/\/ \u7535\u4ef7 \u5143\/kWh\n      laborHourlyCost: 0.5,           \/\/ \u64cd\u4f5c\u4eba\u5de5\u6210\u672c \u5143\/\u5c0f\u65f6\n      machinesPerOperator: 1,         \/\/ \u6bcf\u4eba\u770b\u51e0\u53f0\u8bbe\u5907\n      overheadHourlyPerMachine: 0,    \/\/ \u6742\u9879\u6210\u672c \u5143\/\u53f0\u00b7\u5c0f\u65f6\n      processCosts: {\n        FDM_3D_PRINT: { laborHourlyCost: 0.5, machinesPerOperator: 1, overheadHourlyPerMachine: 0 },\n        RESIN_3D_PRINT: { laborHourlyCost: 0.6, machinesPerOperator: 1, overheadHourlyPerMachine: 0 },\n        CNC_MILLING: { laborHourlyCost: 1, machinesPerOperator: 1, overheadHourlyPerMachine: 0 },\n        CO2_LASER_ENGRAVE_CUT: { laborHourlyCost: 0.6, machinesPerOperator: 1, overheadHourlyPerMachine: 0 }\n      },\n\n      materials: [\n        { vendor: \"\u901a\u7528\", name: \"PLA\", pricePerKg: 120, processType: \"FDM_3D_PRINT\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\", name: \"PETG\", pricePerKg: 150, processType: \"FDM_3D_PRINT\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\", name: \"ABS\", pricePerKg: 160, processType: \"FDM_3D_PRINT\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\", name: \"\u6c34\u6d17\u6811\u8102\", pricePerKg: 220, processType: \"RESIN_3D_PRINT\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\", name: \"\u9ad8\u97e7\u6811\u8102\", pricePerKg: 260, processType: \"RESIN_3D_PRINT\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\u6728\u6599\", name: \"\u6866\u6728\u677f\", pricingMode: \"VOLUME\", pricePerCubicMeter: 3800, processType: \"CNC_MILLING\" },\n        { vendor: \"\u901a\u7528\u6728\u6599\", name: \"\u6a31\u6843\u6728\u677f\", pricingMode: \"VOLUME\", pricePerCubicMeter: 5200, processType: \"CNC_MILLING\" },\n        { vendor: \"\u901a\u7528\u77f3\u6750\", name: \"\u4eba\u9020\u77f3\u677f\", pricePerKg: 55, processType: \"CNC_MILLING\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\u77f3\u6750\", name: \"\u82b1\u5c97\u5ca9\u6bdb\u576f\", pricePerKg: 95, processType: \"CNC_MILLING\", pricingMode: \"WEIGHT\" },\n        { vendor: \"\u901a\u7528\u677f\u6750\", name: \"\u4e9a\u514b\u529b\u677f\", pricePerKg: 45, processType: \"CO2_LASER_ENGRAVE_CUT\", pricingMode: \"WEIGHT\" }\n      ],\n      machines: [\n        { vendor: \"Bambu Lab\", name: \"A1\/\u684c\u9762\u673a\", hourlyRate: 10, price: null, expectedLifeYears: null, expectedMonthlyHours: null, processType: \"FDM_3D_PRINT\" },\n        { vendor: \"Elegoo\", name: \"Saturn \u6811\u8102\u673a\", hourlyRate: 28, price: null, expectedLifeYears: null, expectedMonthlyHours: null, processType: \"RESIN_3D_PRINT\" },\n        { vendor: \"\u901a\u7528\u6728\u5de5\", name: \"\u4e09\u8f74\u96d5\u523b\u673a\", hourlyRate: 80, price: null, expectedLifeYears: null, expectedMonthlyHours: null, processType: \"CNC_MILLING\" },\n        { vendor: \"\u901a\u7528\u6fc0\u5149\", name: \"CO\u2082 \u6fc0\u5149\u96d5\u523b\u673a\", hourlyRate: 120, price: null, expectedLifeYears: null, expectedMonthlyHours: null, processType: \"CO2_LASER_ENGRAVE_CUT\" }\n      ],\n      postProcessRules: [\n        { key: \"NONE\",  name: \"\u65e0\u540e\u5904\u7406\", baseMinutes: 0, minutesPerGram: 0,    extraMaterialCostPerGram: 0,    costMultiplier: 1 },\n        { key: \"BASIC\", name: \"\u57fa\u7840\u53bb\u652f\u6491+\u7b80\u5355\u6253\u78e8\", baseMinutes: 5, minutesPerGram: 0.02, extraMaterialCostPerGram: 0.02, costMultiplier: 1 },\n        { key: \"FINE\",  name: \"\u7cbe\u7ec6\u6253\u78e8+\u5e95\u6f06\", baseMinutes: 10, minutesPerGram: 0.05, extraMaterialCostPerGram: 0.05, costMultiplier: 1.1 },\n        { key: \"PAINT\", name: \"\u7cbe\u7ec6\u6253\u78e8+\u55b7\u6d82\u4e0a\u8272\", baseMinutes: 20, minutesPerGram: 0.08, extraMaterialCostPerGram: 0.10, costMultiplier: 1.2 }\n      ]\n    };\n\n    let runtimeConfig = JSON.parse(JSON.stringify(CLIENT_DEFAULTS));\n    let lastQuote = null;\n\n    const SESSION_KEY = \"quote_session_token\";\n    const MATERIALS_PAGE_SIZE_KEY = \"quote_materials_page_size\";\n    const MACHINES_PAGE_SIZE_KEY = \"quote_machines_page_size\";\n    const RECORDS_PAGE_SIZE_KEY = \"quote_records_page_size\";\n    let cachedUsers = [];\n    let sessionToken = localStorage.getItem(SESSION_KEY) || null;\n    let sessionRole = null;\n    let sessionUsername = \"\";\n    let isUser = false;\n    let isAdmin = false;\n    let canViewRecords = false;\n    let recordSavingEnabled = true;\n    let settingsLoaded = false;\n\n    let currentMaterialsVendor = VENDOR_FILTER_ALL;\n    let currentMachinesVendor = VENDOR_FILTER_ALL;\n    let currentProcessType = PROCESS_TYPES[0].value;\n    let currentMaterialsProcessFilter = PROCESS_FILTER_ALL;\n    let currentMachinesProcessFilter = PROCESS_FILTER_ALL;\n    let materialsPage = 1;\n    let machinesPage = 1;\n    let materialsPageSize = parseInt(localStorage.getItem(MATERIALS_PAGE_SIZE_KEY) || \"10\", 10);\n    let machinesPageSize = parseInt(localStorage.getItem(MACHINES_PAGE_SIZE_KEY) || \"10\", 10);\n    let recordsPage = 1;\n    let recordsPageSize = parseInt(localStorage.getItem(RECORDS_PAGE_SIZE_KEY) || \"10\", 10);\n    let recordsMaxPage = 1;\n    let recordsCreatorFilter = \"\";\n    let recordsProjectFilter = \"\";\n    let recordsAdoptedFilter = \"all\";\n    let recordsVisibilityFilter = \"all\";\n    let recordVisibilitySelection = \"owner_only\";\n    let cachedProjects = [];\n    let projectSearchKeyword = \"\";\n    let editingProjectId = null;\n\n    function formatMoney(v) {\n      return \"\u00a5\" + v.toFixed(2);\n    }\n\n    function authHeaders() {\n      const headers = {};\n      if (sessionToken) {\n        headers[\"X-Session\"] = sessionToken;\n        if (isAdmin) headers[\"X-Admin-Session\"] = sessionToken;\n        else headers[\"X-User-Session\"] = sessionToken;\n      }\n      return headers;\n    }\n\n    function isAuthenticated() {\n      return !!sessionToken;\n    }\n\n    function isProcessMatch(itemProcess, current) {\n      if (!itemProcess || itemProcess === UNIVERSAL_PROCESS_VALUE) return true;\n      return itemProcess === current;\n    }\n\n    function matchesProcessFilter(rowProcessValue, filterValue) {\n      if (filterValue === PROCESS_FILTER_ALL) return true;\n      const normalized = rowProcessValue === UNIVERSAL_PROCESS_VALUE ? null : rowProcessValue;\n      return isProcessMatch(normalized, filterValue);\n    }\n\n    function getPostRuleByKey(key) {\n      const tableRules = getPostProcessRulesFromTable();\n      const sourceRules = tableRules && tableRules.length ? tableRules : runtimeConfig.postProcessRules || [];\n      const currentProcess = getCurrentProcessType();\n      const matched = sourceRules.find(r => r.key === key && isProcessMatch(r.processType, currentProcess));\n      const fallback = sourceRules.find(r => isProcessMatch(r.processType, currentProcess));\n      return matched || fallback || {\n        key: \"NONE\",\n        name: \"\u65e0\u540e\u5904\u7406\",\n        baseMinutes: 0,\n        minutesPerGram: 0,\n        extraMaterialCostPerGram: 0,\n        costMultiplier: 1,\n      };\n    }\n\n    function renderProcessOptions(selectEl, value, { includeUniversal = false } = {}) {\n      if (!selectEl) return;\n      selectEl.innerHTML = \"\";\n      if (includeUniversal) {\n        const optAll = document.createElement(\"option\");\n        optAll.value = UNIVERSAL_PROCESS_VALUE;\n        optAll.textContent = \"\u901a\u7528\uff08\u5168\u90e8\u5de5\u827a\uff09\";\n        selectEl.appendChild(optAll);\n      }\n      PROCESS_TYPES.forEach(pt => {\n        const opt = document.createElement(\"option\");\n        opt.value = pt.value;\n        opt.textContent = pt.label;\n        selectEl.appendChild(opt);\n      });\n      const allowedValues = PROCESS_TYPES.map(pt => pt.value);\n      const fallback = includeUniversal ? UNIVERSAL_PROCESS_VALUE : PROCESS_TYPES[0]?.value;\n      selectEl.value = (includeUniversal && value === UNIVERSAL_PROCESS_VALUE) || allowedValues.includes(value)\n        ? value\n        : fallback;\n    }\n\n    function renderProcessFilterOptions(selectEl, value = PROCESS_FILTER_ALL) {\n      if (!selectEl) return;\n      selectEl.innerHTML = \"\";\n      const allOpt = document.createElement(\"option\");\n      allOpt.value = PROCESS_FILTER_ALL;\n      allOpt.textContent = \"\u5168\u90e8\u5de5\u827a\";\n      selectEl.appendChild(allOpt);\n      PROCESS_TYPES.forEach(pt => {\n        const opt = document.createElement(\"option\");\n        opt.value = pt.value;\n        opt.textContent = pt.label;\n        selectEl.appendChild(opt);\n      });\n      const allowed = [PROCESS_FILTER_ALL, ...PROCESS_TYPES.map(pt => pt.value)];\n      selectEl.value = allowed.includes(value) ? value : PROCESS_FILTER_ALL;\n    }\n\n    async function fetchProjects(keyword = \"\") {\n      if (!isAuthenticated()) throw new Error(\"\u8bf7\u5148\u767b\u5f55\u540e\u518d\u83b7\u53d6\u9879\u76ee\");\n      const params = new URLSearchParams();\n      if (keyword) params.append(\"keyword\", keyword);\n      const resp = await fetch(`${API_BASE}\/projects?${params.toString()}`, { headers: authHeaders() });\n      if (!resp.ok) throw new Error(\"\u65e0\u6cd5\u83b7\u53d6\u9879\u76ee\u5217\u8868\");\n      return resp.json();\n    }\n\n    function populateProjectOptions(selectEl, includeEmpty = true, selected = \"\", emptyLabel = \"\u672a\u5173\u8054\u9879\u76ee\") {\n      if (!selectEl) return;\n      selectEl.innerHTML = \"\";\n      if (includeEmpty) {\n        const opt = document.createElement(\"option\");\n        opt.value = \"\";\n        opt.textContent = emptyLabel;\n        selectEl.appendChild(opt);\n      }\n      cachedProjects.forEach(p => {\n        const opt = document.createElement(\"option\");\n        opt.value = p.id;\n        opt.textContent = p.name;\n        selectEl.appendChild(opt);\n      });\n      const allowed = Array.from(selectEl.options).map(o => o.value);\n      selectEl.value = allowed.includes(selected) ? selected : \"\";\n    }\n\n    async function refreshProjects(keyword = projectSearchKeyword) {\n      if (!isAuthenticated()) {\n        cachedProjects = [];\n        populateProjectOptions(document.getElementById(\"projectSelect\"));\n        populateProjectOptions(document.getElementById(\"recordsProjectFilter\"), true, \"\", \"\u5168\u90e8\u9879\u76ee\");\n        return [];\n      }\n      projectSearchKeyword = keyword || \"\";\n      cachedProjects = await fetchProjects(projectSearchKeyword);\n      populateProjectOptions(document.getElementById(\"projectSelect\"), true, document.getElementById(\"projectSelect\")?.value || \"\");\n      populateProjectOptions(document.getElementById(\"recordsProjectFilter\"), true, recordsProjectFilter, \"\u5168\u90e8\u9879\u76ee\");\n      renderProjectTable();\n      return cachedProjects;\n    }\n\n    function openProjectModal(project = null) {\n      const overlay = document.getElementById(\"projectModal\");\n      if (!overlay) return;\n      editingProjectId = project?.id || null;\n      document.getElementById(\"projectModalTitle\").textContent = project ? \"\u7f16\u8f91\u9879\u76ee\" : \"\u65b0\u5efa\u9879\u76ee\";\n      document.getElementById(\"projectModalName\").value = project?.name || \"\";\n      document.getElementById(\"projectModalClient\").value = project?.clientName || \"\";\n      document.getElementById(\"projectModalCode\").value = project?.code || \"\";\n      document.getElementById(\"projectModalStatus\").value = project?.status || \"\";\n      document.getElementById(\"projectModalRemark\").value = project?.remark || \"\";\n      overlay.style.display = \"flex\";\n    }\n\n    function closeProjectModal() {\n      const overlay = document.getElementById(\"projectModal\");\n      if (overlay) overlay.style.display = \"none\";\n      editingProjectId = null;\n    }\n\n    async function submitProjectModal() {\n      const name = document.getElementById(\"projectModalName\").value.trim();\n      const clientName = document.getElementById(\"projectModalClient\").value.trim();\n      const code = document.getElementById(\"projectModalCode\").value.trim();\n      const status = document.getElementById(\"projectModalStatus\").value;\n      const remark = document.getElementById(\"projectModalRemark\").value.trim();\n      if (!name) {\n        alert(\"\u9879\u76ee\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a\");\n        return;\n      }\n      const payload = { name, clientName, code, status, remark };\n      try {\n        let resp;\n        if (editingProjectId) {\n          resp = await fetch(`${API_BASE}\/projects\/${editingProjectId}`, {\n            method: \"PUT\",\n            headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n            body: JSON.stringify(payload)\n          });\n        } else {\n          resp = await fetch(`${API_BASE}\/projects`, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n            body: JSON.stringify(payload)\n          });\n        }\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u4fdd\u5b58\u5931\u8d25\");\n        await refreshProjects();\n        if (!editingProjectId) {\n          const select = document.getElementById(\"projectSelect\");\n          if (select) select.value = data.id;\n        }\n        closeProjectModal();\n      } catch (e) {\n        alert(e.message || \"\u4fdd\u5b58\u5931\u8d25\");\n      }\n    }\n\n    async function deleteProject(id) {\n      if (!id) return;\n      if (!window.confirm(\"\u786e\u5b9a\u5220\u9664\u8be5\u9879\u76ee\u5417\uff1f\u5220\u9664\u540e\u4e0d\u53ef\u6062\u590d\u3002\")) return;\n      try {\n        const resp = await fetch(`${API_BASE}\/projects\/${id}`, { method: \"DELETE\", headers: authHeaders() });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u5220\u9664\u5931\u8d25\");\n        await refreshProjects();\n      } catch (e) {\n        alert(e.message || \"\u5220\u9664\u5931\u8d25\");\n      }\n    }\n\n    function renderProjectTable() {\n      const tbody = document.getElementById(\"projectTableBody\");\n      if (!tbody) return;\n      if (!isAdmin) {\n        tbody.innerHTML = \"<tr><td colspan='6'>\u4ec5\u7ba1\u7406\u5458\u53ef\u7ba1\u7406\u9879\u76ee<\/td><\/tr>\";\n        return;\n      }\n      if (!cachedProjects.length) {\n        tbody.innerHTML = \"<tr><td colspan='6'>\u6682\u65e0\u9879\u76ee<\/td><\/tr>\";\n        return;\n      }\n      const statusMap = { ongoing: \"\u8fdb\u884c\u4e2d\", completed: \"\u5df2\u5b8c\u6210\", canceled: \"\u5df2\u53d6\u6d88\" };\n      tbody.innerHTML = \"\";\n      cachedProjects.forEach(p => {\n        const tr = document.createElement(\"tr\");\n        tr.innerHTML = `\n          <td>${p.name}<\/td>\n          <td>${p.clientName || \"-\"}<\/td>\n          <td>${p.code || \"-\"}<\/td>\n          <td>${statusMap[p.status] || \"-\"}<\/td>\n          <td>${p.remark || \"-\"}<\/td>\n          <td>\n            <button type=\\\"button\\\" class=\\\"mini-btn\\\" data-action=\\\"edit\\\" data-id=\\\"${p.id}\\\">\u7f16\u8f91<\/button>\n            <button type=\\\"button\\\" class=\\\"mini-btn\\\" data-action=\\\"delete\\\" data-id=\\\"${p.id}\\\">\u5220\u9664<\/button>\n          <\/td>\n        `;\n        tbody.appendChild(tr);\n      });\n    }\n\n    function getProcessCostConfig(processType) {\n      const laborInput = document.querySelector(`.process-cost-labor[data-process=\"${processType}\"]`);\n      const mpoInput = document.querySelector(`.process-cost-mpo[data-process=\"${processType}\"]`);\n      const ohInput = document.querySelector(`.process-cost-oh[data-process=\"${processType}\"]`);\n      if (laborInput && mpoInput && ohInput) {\n        const labor = parseFloat(laborInput.value);\n        const mpo = parseFloat(mpoInput.value);\n        const oh = parseFloat(ohInput.value);\n        return {\n          laborHourlyCost: isNaN(labor) ? 0 : labor,\n          machinesPerOperator: isNaN(mpo) || mpo <= 0 ? 1 : mpo,\n          overheadHourlyPerMachine: isNaN(oh) ? 0 : oh,\n        };\n      }\n\n      const costs = runtimeConfig.processCosts || {};\n      const fallback = costs[processType] || CLIENT_DEFAULTS.processCosts?.[processType] || {};\n      return {\n        laborHourlyCost: fallback.laborHourlyCost ?? 0,\n        machinesPerOperator: fallback.machinesPerOperator ?? 1,\n        overheadHourlyPerMachine: fallback.overheadHourlyPerMachine ?? 0,\n      };\n    }\n\n    function renderProcessCostRows() {\n      const tbody = document.getElementById(\"processCostTableBody\");\n      if (!tbody) return;\n      tbody.innerHTML = \"\";\n      PROCESS_TYPES.forEach(pt => {\n        const row = document.createElement(\"tr\");\n        const conf = getProcessCostConfig(pt.value);\n        row.innerHTML = `\n          <td>${pt.label}<\/td>\n          <td><input type=\"number\" class=\"process-cost-labor\" data-process=\"${pt.value}\" step=\"0.1\" min=\"0\" value=\"${conf.laborHourlyCost ?? \"\"}\" \/><\/td>\n          <td><input type=\"number\" class=\"process-cost-mpo\" data-process=\"${pt.value}\" step=\"0.1\" min=\"0\" value=\"${conf.machinesPerOperator ?? \"\"}\" \/><\/td>\n          <td><input type=\"number\" class=\"process-cost-oh\" data-process=\"${pt.value}\" step=\"0.1\" min=\"0\" value=\"${conf.overheadHourlyPerMachine ?? \"\"}\" \/><\/td>\n        `;\n        tbody.appendChild(row);\n      });\n    }\n\n    function getProcessCostsFromTable() {\n      const rows = document.querySelectorAll(\"#processCostTableBody tr\");\n      const result = {};\n      for (const row of rows) {\n        const process = row.querySelector(\".process-cost-labor\")?.getAttribute(\"data-process\");\n        if (!process) continue;\n        const labor = parseFloat(row.querySelector(\".process-cost-labor\")?.value);\n        const mpo = parseFloat(row.querySelector(\".process-cost-mpo\")?.value);\n        const oh = parseFloat(row.querySelector(\".process-cost-oh\")?.value);\n        if ([labor, mpo, oh].some(v => isNaN(v) || v < 0) || mpo === 0) {\n          alert(\"\u8bf7\u4e3a\u6bcf\u4e2a\u5de5\u827a\u586b\u5199\u6709\u6548\u7684\u4eba\u5de5\u6210\u672c\u3001\u8bbe\u5907\u6570\u91cf\u548c\u6742\u9879\u6210\u672c\uff08>=0\uff0c\u8bbe\u5907\u6570\u91cf>0\uff09\");\n          return null;\n        }\n        result[process] = {\n          laborHourlyCost: labor,\n          machinesPerOperator: mpo,\n          overheadHourlyPerMachine: oh,\n        };\n      }\n      return result;\n    }\n\n    function getCurrentProcessType() {\n      const select = document.getElementById(\"processType\");\n      return select?.value || currentProcessType || PROCESS_TYPES[0].value;\n    }\n\n    function initProcessTypeSelector() {\n      const select = document.getElementById(\"processType\");\n      renderProcessOptions(select, currentProcessType);\n      currentProcessType = getCurrentProcessType();\n      select?.addEventListener(\"change\", () => {\n        currentProcessType = getCurrentProcessType();\n        rebuildMaterialOptions();\n        rebuildMachineOptions();\n        rebuildPostProcessOptions();\n        updateMaterialUsageFields();\n      });\n    }\n\n    \/\/ ===== \u6750\u6599 \/ \u8bbe\u5907\uff1a\u8868\u683c\u6570\u636e\u8bfb\u53d6 =====\n    function getMaterialsFromTable() {\n      const rows = document.querySelectorAll(\"#materialsTableBody .material-row\");\n      const arr = [];\n      rows.forEach(row => {\n        const vendorInput = row.querySelector(\".mat-vendor\");\n        const nameInput = row.querySelector(\".mat-name\");\n        const priceWeightInput = row.querySelector(\".mat-price-weight\");\n        const priceVolumeInput = row.querySelector(\".mat-price-volume\");\n        const pricingSelect = row.querySelector(\".mat-pricing\");\n        const processSelect = row.querySelector(\".mat-process\");\n        const vendor = (vendorInput.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n        const name = (nameInput.value || \"\").trim();\n        const priceWeight = parseFloat(priceWeightInput.value);\n        const priceVolume = parseFloat(priceVolumeInput.value);\n        const pricingMode = pricingSelect?.value === \"VOLUME\" ? \"VOLUME\" : \"WEIGHT\";\n        const processType = processSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const normalizedProcess = processType === UNIVERSAL_PROCESS_VALUE ? null : processType;\n        const validPrice = pricingMode === \"VOLUME\" ? priceVolume : priceWeight;\n        if (name && !isNaN(validPrice) && validPrice >= 0) {\n          arr.push({\n            vendor,\n            name,\n            pricingMode,\n            pricePerKg: pricingMode === \"WEIGHT\" ? validPrice : null,\n            pricePerCubicMeter: pricingMode === \"VOLUME\" ? validPrice : null,\n            processType: normalizedProcess\n          });\n        }\n      });\n      return arr;\n    }\n\n    function getMachinesFromTable() {\n      const rows = document.querySelectorAll(\"#machinesTableBody tr\");\n      const arr = [];\n      rows.forEach(row => {\n        const vendorInput = row.querySelector(\".mac-vendor\");\n        const nameInput   = row.querySelector(\".mac-name\");\n        const priceInput  = row.querySelector(\".mac-price\");\n        const lifeInput   = row.querySelector(\".mac-life\");\n        const monthInput  = row.querySelector(\".mac-monthly\");\n        const powerInput  = row.querySelector(\".mac-power\");\n        const rateInput   = row.querySelector(\".mac-rate\");\n        const includesCheckbox = row.querySelector(\".mac-rate-includes\");\n        const processSelect = row.querySelector(\".mac-process\");\n\n        const vendor = (vendorInput.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n        const name   = (nameInput.value || \"\").trim();\n        const processType = processSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const normalizedProcess = processType === UNIVERSAL_PROCESS_VALUE ? null : processType;\n\n        const hourlyRate = parseFloat(rateInput.value);\n        const price      = parseFloat(priceInput.value);\n        const life       = parseFloat(lifeInput.value);\n        const monthH     = parseFloat(monthInput.value);\n        const power      = parseFloat(powerInput.value);\n\n        \/\/ \u6ca1\u586b\u540d\u5b57\u5c31\u8df3\u8fc7\u8fd9\u4e00\u884c\n        if (!name) return;\n\n        const hasDepreciationInputs = !(\n          isNaN(price) || price <= 0 ||\n          isNaN(life) || life <= 0 ||\n          isNaN(monthH) || monthH <= 0\n        );\n\n        arr.push({\n          vendor,\n          name,\n          \/\/ \u5982\u679c\u6ca1\u586b\u6216\u5c0f\u4e8e 0\uff0c\u5c31\u6309 0 \u5904\u7406\n          hourlyRate: isNaN(hourlyRate) || hourlyRate < 0 ? 0 : hourlyRate,\n          \/\/ \u8fd9\u4e9b\u5b57\u6bb5\u7ed9\u540e\u7aef\uff0c\u5982\u679c\u6ca1\u586b\u6216 <=0\uff0c\u5c31\u7528 null\n          price: isNaN(price) || price <= 0 ? null : price,\n          expectedLifeYears: isNaN(life) || life <= 0 ? null : life,\n          expectedMonthlyHours: isNaN(monthH) || monthH <= 0 ? null : monthH,\n          powerW: isNaN(power) || power <= 0 ? null : power,\n          processType: normalizedProcess,\n          hourlyRateIncludesOperational: hasDepreciationInputs ? false : !!includesCheckbox?.checked\n        });\n      });\n      return arr;\n    }\n\n    function getPostProcessRulesFromTable() {\n      const rows = document.querySelectorAll(\"#postProcessTableBody tr\");\n      const rules = [];\n      let hasError = false;\n      rows.forEach(row => {\n        const keyInput = row.querySelector(\".pp-key\");\n        const nameInput = row.querySelector(\".pp-name\");\n        const baseInput = row.querySelector(\".pp-base\");\n        const perGramInput = row.querySelector(\".pp-pergram\");\n        const matInput = row.querySelector(\".pp-matcost\");\n        const processSelect = row.querySelector(\".pp-process\");\n        const multInput = row.querySelector(\".pp-mult\");\n\n        const key = (keyInput.value || \"\").trim();\n        const name = (nameInput.value || \"\").trim();\n        const baseMinutes = parseFloat(baseInput.value);\n        const minutesPerGram = parseFloat(perGramInput.value);\n        const extraMaterialCostPerGram = parseFloat(matInput.value);\n        const processType = processSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const normalizedProcess = processType === UNIVERSAL_PROCESS_VALUE ? null : processType;\n        const costMultiplier = parseFloat(multInput.value);\n\n        if (!key || !name) {\n          hasError = true;\n          return;\n        }\n        if (\n          isNaN(baseMinutes) || baseMinutes < 0 ||\n          isNaN(minutesPerGram) || minutesPerGram < 0 ||\n          isNaN(extraMaterialCostPerGram) || extraMaterialCostPerGram < 0 ||\n          isNaN(costMultiplier) || costMultiplier < 0\n        ) {\n          hasError = true;\n          return;\n        }\n\n        rules.push({ key, name, baseMinutes, minutesPerGram, extraMaterialCostPerGram, processType: normalizedProcess, costMultiplier });\n      });\n      if (hasError) return null;\n      return rules;\n    }\n\n    function computeMachineDepreciationHourly(machine) {\n      if (!machine) return 0;\n      const price = parseFloat(machine.price);\n      const lifeYears = parseFloat(machine.expectedLifeYears);\n      const monthlyHours = parseFloat(machine.expectedMonthlyHours);\n      if (\n        isNaN(price) || price <= 0 ||\n        isNaN(lifeYears) || lifeYears <= 0 ||\n        isNaN(monthlyHours) || monthlyHours <= 0\n      ) {\n        return 0;\n      }\n      const totalHours = lifeYears * 12 * monthlyHours;\n      return totalHours > 0 ? price \/ totalHours : 0;\n    }\n\n\n    \/\/ ===== \u6750\u6599 \/ \u8bbe\u5907\uff1a\u8868\u683c\u6784\u9020 =====\n    function appendMaterialRow(\n      vendor = \"\",\n      name = \"\",\n      pricePerKg = \"\",\n      processType = \"FDM_3D_PRINT\",\n      pricingMode = \"WEIGHT\",\n      pricePerCubicMeter = \"\"\n    ) {\n      const tbody = document.getElementById(\"materialsTableBody\");\n      const row = document.createElement(\"tr\");\n      row.className = \"material-row\";\n      row.innerHTML = `\n        <td><input type=\"text\" class=\"mat-vendor\" value=\"${vendor}\"><\/td>\n        <td><select class=\"mat-process\"><\/select><\/td>\n        <td><input type=\"text\" class=\"mat-name\" value=\"${name}\"><\/td>\n        <td>\n          <select class=\"mat-pricing\">\n            <option value=\"WEIGHT\">\u6309\u91cd\u91cf\uff08\u5143\/kg\uff09<\/option>\n            <option value=\"VOLUME\">\u6309\u4f53\u79ef\uff08\u5143\/m\u00b3\uff09<\/option>\n          <\/select>\n        <\/td>\n        <td>\n          <div class=\"material-price-wrap\">\n            <input type=\"number\" class=\"mat-price-weight\" step=\"0.1\" min=\"0\" value=\"${pricePerKg}\">\n            <input type=\"number\" class=\"mat-price-volume\" step=\"0.1\" min=\"0\" value=\"${pricePerCubicMeter}\" style=\"display:none;\" title=\"\u5143\/\u7acb\u65b9\u7c73\">\n            <div class=\"hint mat-price-hint material-price-hint sub-label\">\u6309\u91cd\u91cf\uff1a\u5143\/kg<\/div>\n          <\/div>\n        <\/td>\n        <td><div class=\"material-unit unit-text mat-unit\">\u5143\/kg<\/div><\/td>\n        <td><button type=\"button\" class=\"mini-btn mat-delete\">\u5220<\/button><\/td>\n      `;\n      tbody.appendChild(row);\n\n      renderProcessOptions(row.querySelector(\".mat-process\"), processType || UNIVERSAL_PROCESS_VALUE, { includeUniversal: true });\n\n      const pricingSelect = row.querySelector(\".mat-pricing\");\n      pricingSelect.value = pricingMode === \"VOLUME\" ? \"VOLUME\" : \"WEIGHT\";\n      function syncMaterialPriceInputs() {\n        const mode = pricingSelect.value;\n        const weightInput = row.querySelector(\".mat-price-weight\");\n        const volumeInput = row.querySelector(\".mat-price-volume\");\n        const hint = row.querySelector(\".mat-price-hint\");\n        const unitLabel = row.querySelector(\".mat-unit\");\n        if (mode === \"VOLUME\") {\n          weightInput.style.display = \"none\";\n          volumeInput.style.display = \"inline-block\";\n          hint.textContent = \"\u6309\u4f53\u79ef\uff1a\u5143\/m\u00b3\";\n          unitLabel.textContent = \"\u5143\/m\u00b3\";\n        } else {\n          weightInput.style.display = \"inline-block\";\n          volumeInput.style.display = \"none\";\n          hint.textContent = \"\u6309\u91cd\u91cf\uff1a\u5143\/kg\";\n          unitLabel.textContent = \"\u5143\/kg\";\n        }\n      }\n      syncMaterialPriceInputs();\n      pricingSelect.addEventListener(\"change\", () => syncMaterialPriceInputs());\n\n      row.querySelector(\".mat-delete\").addEventListener(\"click\", () => {\n        row.remove();\n        rebuildMaterialsVendorFilter();\n        rebuildMaterialOptions();\n        applyMaterialsPagination();\n      });\n      row.querySelectorAll(\"input, select\").forEach(input => {\n        input.addEventListener(\"input\", () => {\n          rebuildMaterialsVendorFilter();\n          rebuildMaterialOptions();\n          applyMaterialsPagination();\n        });\n      });\n    }\n\n    function recalcMachineHourlyFromRow(tr, silent = false) {\n      const priceInput  = tr.querySelector(\".mac-price\");\n      const lifeInput   = tr.querySelector(\".mac-life\");\n      const monthInput  = tr.querySelector(\".mac-monthly\");\n      const powerInput  = tr.querySelector(\".mac-power\");\n      const rateInput   = tr.querySelector(\".mac-rate\");\n      const processValue = tr.querySelector(\".mac-process\")?.value;\n\n      const price  = parseFloat(priceInput.value);\n      const life   = parseFloat(lifeInput.value);\n      const monthH = parseFloat(monthInput.value);\n      const power  = parseFloat(powerInput.value);\n\n      const elecPrice = parseFloat(document.getElementById(\"configElectricityPrice\").value) || 0;\n\n      if (\n        isNaN(price) || price <= 0 ||\n        isNaN(life)  || life  <= 0 ||\n        isNaN(monthH)|| monthH<= 0\n      ) {\n        if (!silent) {\n          alert(\"\u8bf7\u5148\u586b\u5199\u6709\u6548\u7684\u8bbe\u5907\u4ef7\u3001\u6298\u65e7\u5e74\u9650\u548c\u6bcf\u6708\u5de5\u65f6\u3002\");\n        }\n        return;\n      }\n\n      const totalHours = life * 12 * monthH;\n      const depreciation = price \/ totalHours;                 \/\/ \u6298\u65e7\n      const electricity = (!isNaN(power) &#038;&#038; power > 0) ? (power \/ 1000) * elecPrice : 0;\n\n      const rate  = depreciation;    \/\/ \u8bbe\u5907\u6298\u65e7\/\u79df\u8d41\u5c0f\u65f6\u6210\u672c\uff0c\u4eba\u5de5\/\u7535\u8d39\/\u6742\u9879\u6309\u5de5\u827a\u914d\u7f6e\u5355\u72ec\u8ba1\u5165\n\n      const processHint = processValue ? getProcessCostConfig(processValue) : null;\n      if (!silent && processHint) {\n        const labor = (processHint.laborHourlyCost || 0) \/ (processHint.machinesPerOperator || 1);\n        const msg = `\u5df2\u6309\u6298\u65e7\u8ba1\u7b97\u5c0f\u65f6\u6210\u672c\uff1a\u00a5${rate.toFixed(1)}\uff0c\u8fd0\u884c\u7535\u8d39\u53e6\u52a0\u7ea6 \u00a5${electricity.toFixed(2)} \/\u5c0f\u65f6\uff0c\u4eba\u5de5\/\u6742\u9879\u6309\u5de5\u827a\u8bbe\u7f6e\u5355\u72ec\u8ba1\u5165\u3002`;\n        console.info(msg, labor >= 0 ? `\uff08\u4eba\u5de5: \u00a5${labor.toFixed(2)}\/\u53f0\u00b7\u65f6\uff09` : \"\");\n      }\n\n      rateInput.value = rate.toFixed(1);\n    }\n\n\n    function appendMachineRow(\n      vendor = \"\",\n      name = \"\",\n      hourlyRate = \"\",\n      price = \"\",\n      life = \"\",\n      monthH = \"\",\n      powerW = \"\",\n      processType = \"FDM_3D_PRINT\",\n      hourlyRateIncludesOperational = true\n    ) {\n      const tbody = document.getElementById(\"machinesTableBody\");\n      const tr = document.createElement(\"tr\");\n      tr.innerHTML = `\n        <td><input type=\"text\"   class=\"mac-vendor\"  value=\"${vendor}\"><\/td>\n        <td>\n          <select class=\"mac-process\"><\/select>\n        <\/td>\n        <td><input type=\"text\"   class=\"mac-name\"    value=\"${name}\"><\/td>\n        <td><input type=\"number\" class=\"mac-price\"   step=\"0.1\" min=\"0\" value=\"${price}\"><\/td>\n        <td><input type=\"number\" class=\"mac-life\"    step=\"0.1\" min=\"0\" value=\"${life}\"><\/td>\n        <td><input type=\"number\" class=\"mac-monthly\" step=\"1\"   min=\"0\" value=\"${monthH}\"><\/td>\n        <td><input type=\"number\" class=\"mac-power\"   step=\"1\"   min=\"0\" value=\"${powerW}\"><\/td>\n        <td>\n          <div style=\"display:flex;align-items:center;gap:4px;\">\n            <input type=\"number\" class=\"mac-rate\" step=\"0.1\" min=\"0\" value=\"${hourlyRate}\">\n            <button type=\"button\" class=\"mini-btn mac-calc\">\u7b97<\/button>\n          <\/div>\n        <\/td>\n        <td>\n          <label style=\"display:flex;align-items:center;gap:6px;font-size:12px;color:#4b5563;\">\n            <input type=\"checkbox\" class=\"mac-rate-includes\" ${hourlyRateIncludesOperational ? \"checked\" : \"\"}>\n            <span>\u8d39\u7387\u5df2\u542b<\/span>\n          <\/label>\n        <\/td>\n        <td><button type=\"button\" class=\"mini-btn mac-delete\">\u5220<\/button><\/td>\n      `;\n      tbody.appendChild(tr);\n\n      renderProcessOptions(tr.querySelector(\".mac-process\"), processType || UNIVERSAL_PROCESS_VALUE, { includeUniversal: true });\n\n      \/\/ \u5220\u9664\u6309\u94ae\n      tr.querySelector(\".mac-delete\").addEventListener(\"click\", () => {\n        tr.remove();\n        rebuildMachinesVendorFilter();\n        rebuildMachineOptions();\n        applyMachinesPagination();\n      });\n\n      \/\/ \u201c\u7b97\u201d\u6309\u94ae\uff08\u8c03\u7528\u6211\u4eec\u524d\u9762\u5199\u7684\u8ba1\u7b97\u5c0f\u65f6\u6210\u672c\u51fd\u6570\uff09\n      tr.querySelector(\".mac-calc\").addEventListener(\"click\", () => {\n        recalcMachineHourlyFromRow(tr, false);\n      });\n\n      \/\/ \u4efb\u610f\u8f93\u5165\u53d8\u5316\u65f6\uff0c\u5237\u65b0\u5382\u5546\u4e0b\u62c9 + \u524d\u53f0\u4e0b\u62c9\n      tr.querySelectorAll(\"input, select\").forEach(input => {\n        input.addEventListener(\"input\", () => {\n          rebuildMachinesVendorFilter();\n          rebuildMachineOptions();\n          \/\/ \u6ce8\u610f\uff1a\u8fd9\u91cc\u53ea\u5237\u65b0 UI\uff0c\u4e0d\u81ea\u52a8\u8986\u76d6\u5c0f\u65f6\u6210\u672c\uff0c\u907f\u514d\u4f60\u624b\u52a8\u6539\u88ab\u8986\u76d6\n          applyMachinesPagination();\n        });\n      });\n    }\n\n    function appendPostProcessRow(key = \"\", name = \"\", baseMinutes = \"\", minutesPerGram = \"\", extraMaterialCostPerGram = \"\", processType = UNIVERSAL_PROCESS_VALUE, costMultiplier = \"1\") {\n      const tbody = document.getElementById(\"postProcessTableBody\");\n      const tr = document.createElement(\"tr\");\n      tr.innerHTML = `\n        <td><input type=\"text\" class=\"pp-key\" value=\"${key}\"><\/td>\n        <td><input type=\"text\" class=\"pp-name\" value=\"${name}\"><\/td>\n        <td><select class=\"pp-process\"><\/select><\/td>\n        <td><input type=\"number\" min=\"0\" step=\"0.1\" class=\"pp-base\" value=\"${baseMinutes}\"><\/td>\n        <td><input type=\"number\" min=\"0\" step=\"0.001\" class=\"pp-pergram\" value=\"${minutesPerGram}\"><\/td>\n        <td><input type=\"number\" min=\"0\" step=\"0.001\" class=\"pp-matcost\" value=\"${extraMaterialCostPerGram}\"><\/td>\n        <td><input type=\"number\" min=\"0\" step=\"0.01\" class=\"pp-mult\" value=\"${costMultiplier}\"><\/td>\n        <td><button type=\"button\" class=\"mini-btn pp-delete\">\u5220<\/button><\/td>\n      `;\n      tbody.appendChild(tr);\n\n      renderProcessOptions(tr.querySelector(\".pp-process\"), processType || UNIVERSAL_PROCESS_VALUE, { includeUniversal: true });\n\n      tr.querySelector(\".pp-delete\").addEventListener(\"click\", () => {\n        tr.remove();\n        rebuildPostProcessOptions();\n      });\n\n      tr.querySelectorAll(\"input, select\").forEach(input => {\n        input.addEventListener(\"input\", () => {\n          rebuildPostProcessOptions();\n        });\n      });\n    }\n\n\n    \/\/ ===== \u8bbe\u7f6e\u533a\uff1a\u5382\u5546\u8fc7\u6ee4 & \u65b0\u589e\u5382\u5546 =====\n    function rebuildMaterialsVendorFilter() {\n      const select = document.getElementById(\"materialsVendorFilter\");\n      const prev = currentMaterialsVendor || select.value || VENDOR_FILTER_ALL;\n      const processFilter = document.getElementById(\"materialsProcessFilter\")?.value || PROCESS_FILTER_ALL;\n      currentMaterialsProcessFilter = processFilter;\n\n      \/\/ \u8bfb\u53d6\u8868\u683c\u4e2d\u7684\u6240\u6709\u5382\u5546\uff08\u5373\u4fbf\u5f53\u524d\u884c\u8fd8\u6ca1\u586b\u5199\u6750\u6599\u540d\u79f0\uff0c\u4e5f\u8981\u4fdd\u7559\u5382\u5546\uff09\n      const rows = document.querySelectorAll(\"#materialsTableBody .material-row\");\n      let vendors = [...new Set(Array.from(rows).map(row => {\n        const vInput = row.querySelector(\".mat-vendor\");\n        const pSelect = row.querySelector(\".mat-process\");\n        const rowProcess = pSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        if (!matchesProcessFilter(rowProcess, processFilter)) return null;\n        return (vInput?.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n      }).filter(Boolean))];\n      if (vendors.length === 0) vendors = [\"\u672a\u5206\u7c7b\"];\n\n      select.innerHTML = \"\";\n      const allOpt = document.createElement(\"option\");\n      allOpt.value = VENDOR_FILTER_ALL;\n      allOpt.textContent = \"\u5168\u90e8\u5382\u5546\";\n      select.appendChild(allOpt);\n      vendors.forEach(v => {\n        const opt = document.createElement(\"option\");\n        opt.value = v;\n        opt.textContent = v;\n        select.appendChild(opt);\n      });\n\n      const allowed = [VENDOR_FILTER_ALL, ...vendors];\n      currentMaterialsVendor = allowed.includes(prev) ? prev : VENDOR_FILTER_ALL;\n      select.value = currentMaterialsVendor;\n    }\n\n    function rebuildMachinesVendorFilter() {\n      const select = document.getElementById(\"machinesVendorFilter\");\n      const prev = currentMachinesVendor || select.value || VENDOR_FILTER_ALL;\n      const processFilter = document.getElementById(\"machinesProcessFilter\")?.value || PROCESS_FILTER_ALL;\n      currentMachinesProcessFilter = processFilter;\n\n      \/\/ \u8bfb\u53d6\u8868\u683c\u4e2d\u6240\u6709\u8bbe\u5907\u884c\u7684\u5382\u5546\uff08\u5373\u4fbf\u672a\u586b\u5199\u540d\u79f0\u4e5f\u8981\u4fdd\u7559\uff09\uff0c\u4fdd\u8bc1\u65b0\u589e\u5382\u5546\u7acb\u5373\u53ef\u9009\n      const rows = document.querySelectorAll(\"#machinesTableBody tr\");\n      let vendors = [...new Set(Array.from(rows).map(row => {\n        const vInput = row.querySelector(\".mac-vendor\");\n        const pSelect = row.querySelector(\".mac-process\");\n        const rowProcess = pSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        if (!matchesProcessFilter(rowProcess, processFilter)) return null;\n        return (vInput?.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n      }).filter(Boolean))];\n      if (vendors.length === 0) vendors = [\"\u672a\u5206\u7c7b\"];\n\n      select.innerHTML = \"\";\n      const allOpt = document.createElement(\"option\");\n      allOpt.value = VENDOR_FILTER_ALL;\n      allOpt.textContent = \"\u5168\u90e8\u5382\u5546\";\n      select.appendChild(allOpt);\n      vendors.forEach(v => {\n        const opt = document.createElement(\"option\");\n        opt.value = v;\n        opt.textContent = v;\n        select.appendChild(opt);\n      });\n\n      const allowed = [VENDOR_FILTER_ALL, ...vendors];\n      currentMachinesVendor = allowed.includes(prev) ? prev : VENDOR_FILTER_ALL;\n      select.value = currentMachinesVendor;\n    }\n\n    function applyMaterialsPagination() {\n      const rows = Array.from(document.querySelectorAll(\"#materialsTableBody .material-row\"));\n      const processFilter = document.getElementById(\"materialsProcessFilter\")?.value || PROCESS_FILTER_ALL;\n      const totalMatched = rows.filter(row => {\n        const vInput = row.querySelector(\".mat-vendor\");\n        const pSelect = row.querySelector(\".mat-process\");\n        const rowProcess = pSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const v = (vInput?.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n        const vendorHit = currentMaterialsVendor === VENDOR_FILTER_ALL || v === currentMaterialsVendor;\n        return vendorHit && matchesProcessFilter(rowProcess, processFilter);\n      }).length;\n      const totalPages = Math.max(1, Math.ceil(totalMatched \/ materialsPageSize));\n      if (materialsPage > totalPages) materialsPage = totalPages;\n      let currentIndex = 0;\n      rows.forEach(row => {\n        const vInput = row.querySelector(\".mat-vendor\");\n        const pSelect = row.querySelector(\".mat-process\");\n        const rowProcess = pSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const v = (vInput?.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n        const vendorHit = currentMaterialsVendor === VENDOR_FILTER_ALL || v === currentMaterialsVendor;\n        const match = vendorHit && matchesProcessFilter(rowProcess, processFilter);\n        if (!match) {\n          row.style.display = \"none\";\n          return;\n        }\n        const pageStart = (materialsPage - 1) * materialsPageSize;\n        const pageEnd = pageStart + materialsPageSize;\n        row.style.display = currentIndex >= pageStart && currentIndex < pageEnd ? \"\" : \"none\";\n        currentIndex += 1;\n      });\n      const info = document.getElementById(\"materialsPageInfo\");\n      if (info) info.textContent = `${materialsPage}\/${totalPages}\uff08\u5171 ${totalMatched} \u6761\uff09`;\n      const pageSizeSelect = document.getElementById(\"materialsPageSize\");\n      if (pageSizeSelect) pageSizeSelect.value = materialsPageSize.toString();\n    }\n\n    function applyMachinesPagination() {\n      const rows = Array.from(document.querySelectorAll(\"#machinesTableBody tr\"));\n      const processFilter = document.getElementById(\"machinesProcessFilter\")?.value || PROCESS_FILTER_ALL;\n      const totalMatched = rows.filter(row => {\n        const vInput = row.querySelector(\".mac-vendor\");\n        const pSelect = row.querySelector(\".mac-process\");\n        const rowProcess = pSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const v = (vInput?.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n        const vendorHit = currentMachinesVendor === VENDOR_FILTER_ALL || v === currentMachinesVendor;\n        return vendorHit && matchesProcessFilter(rowProcess, processFilter);\n      }).length;\n      const totalPages = Math.max(1, Math.ceil(totalMatched \/ machinesPageSize));\n      if (machinesPage > totalPages) machinesPage = totalPages;\n      let currentIndex = 0;\n      rows.forEach(row => {\n        const vInput = row.querySelector(\".mac-vendor\");\n        const pSelect = row.querySelector(\".mac-process\");\n        const rowProcess = pSelect?.value || UNIVERSAL_PROCESS_VALUE;\n        const v = (vInput?.value || \"\").trim() || \"\u672a\u5206\u7c7b\";\n        const vendorHit = currentMachinesVendor === VENDOR_FILTER_ALL || v === currentMachinesVendor;\n        const match = vendorHit && matchesProcessFilter(rowProcess, processFilter);\n        if (!match) {\n          row.style.display = \"none\";\n          return;\n        }\n        const pageStart = (machinesPage - 1) * machinesPageSize;\n        const pageEnd = pageStart + machinesPageSize;\n        row.style.display = currentIndex >= pageStart && currentIndex < pageEnd ? \"\" : \"none\";\n        currentIndex += 1;\n      });\n      const info = document.getElementById(\"machinesPageInfo\");\n      if (info) info.textContent = `${machinesPage}\/${totalPages}\uff08\u5171 ${totalMatched} \u6761\uff09`;\n      const pageSizeSelect = document.getElementById(\"machinesPageSize\");\n      if (pageSizeSelect) pageSizeSelect.value = machinesPageSize.toString();\n    }\n\n    document.getElementById(\"materialsVendorFilter\").addEventListener(\"change\", () => {\n      currentMaterialsVendor = document.getElementById(\"materialsVendorFilter\").value;\n      rebuildMaterialsVendorFilter();\n      rebuildMaterialOptions();\n      applyMaterialsPagination();\n    });\n    document.getElementById(\"materialsProcessFilter\").addEventListener(\"change\", () => {\n      currentMaterialsProcessFilter = document.getElementById(\"materialsProcessFilter\").value;\n      rebuildMaterialsVendorFilter();\n      rebuildMaterialOptions();\n      applyMaterialsPagination();\n    });\n    document.getElementById(\"machinesVendorFilter\").addEventListener(\"change\", () => {\n      currentMachinesVendor = document.getElementById(\"machinesVendorFilter\").value;\n      rebuildMachinesVendorFilter();\n      rebuildMachineOptions();\n      applyMachinesPagination();\n    });\n    document.getElementById(\"machinesProcessFilter\").addEventListener(\"change\", () => {\n      currentMachinesProcessFilter = document.getElementById(\"machinesProcessFilter\").value;\n      rebuildMachinesVendorFilter();\n      rebuildMachineOptions();\n      applyMachinesPagination();\n    });\n\n    document.getElementById(\"addMaterialVendorBtn\").addEventListener(\"click\", () => {\n      const input = document.getElementById(\"newMaterialVendorInput\");\n      const name = (input.value || \"\").trim();\n      if (!name) return;\n\n      currentMaterialsVendor = name;\n      input.value = \"\";\n\n      \/\/ \u2b50 \u65b0\u589e\uff1a\u81ea\u52a8\u6dfb\u52a0\u4e00\u884c\u6750\u6599\uff0c\u907f\u514d\u7a7a vendor \u65e0\u6570\u636e\n      appendMaterialRow(name, \"\", \"\", getCurrentProcessType(), \"WEIGHT\", \"\");\n\n      rebuildMaterialsVendorFilter();\n      rebuildMaterialOptions();\n    });\n\n\n    document.getElementById(\"addMachineVendorBtn\").addEventListener(\"click\", () => {\n      const input = document.getElementById(\"newMachineVendorInput\");\n      const name = (input.value || \"\").trim();\n      if (!name) return;\n\n      currentMachinesVendor = name;\n      input.value = \"\";\n\n      \/\/ \u2b50 \u65b0\u589e\uff1a\u81ea\u52a8\u6dfb\u52a0\u4e00\u884c\u8bbe\u5907\uff0c\u907f\u514d\u5382\u5546\u6ca1\u6709\u8bbe\u5907\u884c\n      appendMachineRow(name, \"\", \"\", \"\", \"\", \"\", \"\", getCurrentProcessType());\n\n      rebuildMachinesVendorFilter();\n      rebuildMachineOptions();\n    });\n\n    \/\/ \u5206\u9875\u63a7\u5236\n    const materialsPageSizeSelect = document.getElementById(\"materialsPageSize\");\n    materialsPageSizeSelect.value = materialsPageSize.toString();\n    materialsPageSizeSelect.addEventListener(\"change\", () => {\n      materialsPageSize = parseInt(materialsPageSizeSelect.value, 10) || 10;\n      localStorage.setItem(MATERIALS_PAGE_SIZE_KEY, materialsPageSize.toString());\n      materialsPage = 1;\n      applyMaterialsPagination();\n    });\n    document.getElementById(\"materialsPrev\").addEventListener(\"click\", () => {\n      if (materialsPage > 1) {\n        materialsPage -= 1;\n        applyMaterialsPagination();\n      }\n    });\n    document.getElementById(\"materialsNext\").addEventListener(\"click\", () => {\n      materialsPage += 1;\n      applyMaterialsPagination();\n    });\n\n    const machinesPageSizeSelect = document.getElementById(\"machinesPageSize\");\n    machinesPageSizeSelect.value = machinesPageSize.toString();\n    machinesPageSizeSelect.addEventListener(\"change\", () => {\n      machinesPageSize = parseInt(machinesPageSizeSelect.value, 10) || 10;\n      localStorage.setItem(MACHINES_PAGE_SIZE_KEY, machinesPageSize.toString());\n      machinesPage = 1;\n      applyMachinesPagination();\n    });\n    document.getElementById(\"machinesPrev\").addEventListener(\"click\", () => {\n      if (machinesPage > 1) {\n        machinesPage -= 1;\n        applyMachinesPagination();\n      }\n    });\n    document.getElementById(\"machinesNext\").addEventListener(\"click\", () => {\n      machinesPage += 1;\n      applyMachinesPagination();\n    });\n\n    const recordsPageSizeSelect = document.getElementById(\"recordsPageSize\");\n    if (recordsPageSizeSelect) {\n      recordsPageSizeSelect.value = recordsPageSize.toString();\n      recordsPageSizeSelect.addEventListener(\"change\", () => {\n        recordsPageSize = parseInt(recordsPageSizeSelect.value, 10) || 10;\n        localStorage.setItem(RECORDS_PAGE_SIZE_KEY, recordsPageSize.toString());\n        recordsPage = 1;\n        renderQuoteRecords();\n      });\n    }\n    const recordsCreatorSelect = document.getElementById(\"recordsCreatorFilter\");\n    if (recordsCreatorSelect) {\n      recordsCreatorSelect.addEventListener(\"change\", () => {\n        recordsCreatorFilter = recordsCreatorSelect.value;\n        recordsPage = 1;\n        renderQuoteRecords();\n      });\n    }\n    const recordsAdoptedSelect = document.getElementById(\"recordsAdoptedFilter\");\n    if (recordsAdoptedSelect) {\n      recordsAdoptedSelect.value = recordsAdoptedFilter;\n      recordsAdoptedSelect.addEventListener(\"change\", () => {\n        recordsAdoptedFilter = recordsAdoptedSelect.value;\n        recordsPage = 1;\n        renderQuoteRecords();\n      });\n    }\n    const recordsVisibilitySelect = document.getElementById(\"recordsVisibilityFilter\");\n    if (recordsVisibilitySelect) {\n      recordsVisibilitySelect.value = recordsVisibilityFilter;\n      recordsVisibilitySelect.addEventListener(\"change\", () => {\n        recordsVisibilityFilter = recordsVisibilitySelect.value;\n        recordsPage = 1;\n        renderQuoteRecords();\n      });\n    }\n    const recordVisibilitySelect = document.getElementById(\"recordVisibilitySelect\");\n    if (recordVisibilitySelect) {\n      recordVisibilitySelect.value = recordVisibilitySelection;\n      recordVisibilitySelect.addEventListener(\"change\", () => {\n        recordVisibilitySelection = recordVisibilitySelect.value;\n      });\n    }\n    document.getElementById(\"recordsPrev\")?.addEventListener(\"click\", () => {\n      if (recordsPage > 1) {\n        recordsPage -= 1;\n        renderQuoteRecords();\n      }\n    });\n    document.getElementById(\"recordsNext\")?.addEventListener(\"click\", () => {\n      if (recordsPage < recordsMaxPage) {\n        recordsPage += 1;\n        renderQuoteRecords();\n      }\n    });\n    document.getElementById(\"recordsMonth\")?.addEventListener(\"change\", () => {\n      recordsPage = 1;\n      renderQuoteRecords();\n      renderQuoteSummary();\n    });\n    const recordsProjectSelect = document.getElementById(\"recordsProjectFilter\");\n    if (recordsProjectSelect) {\n      recordsProjectSelect.addEventListener(\"change\", () => {\n        recordsProjectFilter = recordsProjectSelect.value;\n        recordsPage = 1;\n        renderQuoteRecords();\n      });\n    }\n\n\n    \/\/ ===== \u62a5\u4ef7\u533a\u57df\u4e0b\u62c9\uff1a\u6839\u636e\u5f53\u524d\u8868\u683c\u91cd\u5efa options =====\n    function rebuildMaterialOptions() {\n      const materials = getMaterialsFromTable();\n      const currentProcess = getCurrentProcessType();\n      const filteredMaterials = materials\n        .map((m, idx) => ({ ...m, _idx: idx }))\n        .filter(m => isProcessMatch(m.processType, currentProcess));\n      const vendorSelect = document.getElementById(\"materialVendor\");\n      const matSelect = document.getElementById(\"material\");\n\n      const vendors = [...new Set(filteredMaterials.map(m => m.vendor))];\n      const prevVendor = vendorSelect.value || VENDOR_FILTER_ALL;\n      vendorSelect.innerHTML = \"\";\n      const allOpt = document.createElement(\"option\");\n      allOpt.value = VENDOR_FILTER_ALL;\n      allOpt.textContent = \"\u5168\u90e8\u5382\u5546\";\n      vendorSelect.appendChild(allOpt);\n      vendors.forEach(v => {\n        const opt = document.createElement(\"option\");\n        opt.value = v;\n        opt.textContent = v;\n        vendorSelect.appendChild(opt);\n      });\n      const allowed = [VENDOR_FILTER_ALL, ...vendors];\n      vendorSelect.value = allowed.includes(prevVendor) ? prevVendor : VENDOR_FILTER_ALL;\n\n      const currentVendor = vendorSelect.value;\n      matSelect.innerHTML = \"\";\n      filteredMaterials.forEach((m) => {\n        if (currentVendor === VENDOR_FILTER_ALL || m.vendor === currentVendor) {\n          const opt = document.createElement(\"option\");\n          opt.value = String(m._idx);\n          opt.textContent = m.name;\n          matSelect.appendChild(opt);\n        }\n      });\n\n      if (matSelect.options.length && !matSelect.value) {\n        matSelect.value = matSelect.options[0].value;\n      }\n      updateMaterialUsageFields();\n    }\n\n    function getSelectedMaterial() {\n      const materials = getMaterialsFromTable();\n      const matSelect = document.getElementById(\"material\");\n      const idx = parseInt(matSelect?.value || \"\", 10);\n      if (isNaN(idx) || idx < 0 || idx >= materials.length) return null;\n      return materials[idx];\n    }\n\n    function updateMaterialUsageFields() {\n      const material = getSelectedMaterial();\n      const pricingMode = material?.pricingMode === \"VOLUME\" ? \"VOLUME\" : \"WEIGHT\";\n      const weightWrap = document.getElementById(\"weightField\");\n      const volumeWrap = document.getElementById(\"volumeField\");\n      const weightInput = document.getElementById(\"weight\");\n      const volumeInput = document.getElementById(\"volume\");\n      if (!weightInput || !volumeInput) return;\n\n      if (pricingMode === \"VOLUME\") {\n        if (weightWrap) weightWrap.style.display = \"none\";\n        weightInput.required = false;\n        weightInput.value = \"\";\n        if (volumeWrap) volumeWrap.style.display = \"\";\n        volumeInput.required = true;\n      } else {\n        if (weightWrap) weightWrap.style.display = \"\";\n        weightInput.required = true;\n        if (volumeWrap) volumeWrap.style.display = \"none\";\n        volumeInput.required = false;\n        volumeInput.value = \"\";\n      }\n    }\n\n    function rebuildMachineOptions() {\n      const machines = getMachinesFromTable();\n      const currentProcess = getCurrentProcessType();\n      const filteredMachines = machines\n        .map((m, idx) => ({ ...m, _idx: idx }))\n        .filter(m => isProcessMatch(m.processType, currentProcess));\n      const vendorSelect = document.getElementById(\"machineVendor\");\n      const macSelect = document.getElementById(\"machine\");\n\n      const vendors = [...new Set(filteredMachines.map(m => m.vendor))];\n      const prevVendor = vendorSelect.value || VENDOR_FILTER_ALL;\n      vendorSelect.innerHTML = \"\";\n      const allOpt = document.createElement(\"option\");\n      allOpt.value = VENDOR_FILTER_ALL;\n      allOpt.textContent = \"\u5168\u90e8\u5382\u5546\";\n      vendorSelect.appendChild(allOpt);\n      vendors.forEach(v => {\n        const opt = document.createElement(\"option\");\n        opt.value = v;\n        opt.textContent = v;\n        vendorSelect.appendChild(opt);\n      });\n      const allowed = [VENDOR_FILTER_ALL, ...vendors];\n      vendorSelect.value = allowed.includes(prevVendor) ? prevVendor : VENDOR_FILTER_ALL;\n\n      const currentVendor = vendorSelect.value;\n      macSelect.innerHTML = \"\";\n      filteredMachines.forEach((m) => {\n        if (currentVendor === VENDOR_FILTER_ALL || m.vendor === currentVendor) {\n          const opt = document.createElement(\"option\");\n          opt.value = String(m._idx);\n          opt.textContent = m.name;\n          macSelect.appendChild(opt);\n        }\n      });\n    }\n\n    function rebuildPostProcessOptions() {\n      const select = document.getElementById(\"postProcess\");\n      if (!select) return;\n      const rules = getPostProcessRulesFromTable();\n      select.innerHTML = \"\";\n      const currentProcess = getCurrentProcessType();\n      const validRules = rules && rules.length ? rules : runtimeConfig.postProcessRules || [];\n      validRules\n        .filter(rule => isProcessMatch(rule.processType, currentProcess))\n        .forEach(rule => {\n          const opt = document.createElement(\"option\");\n          opt.value = rule.key;\n          opt.textContent = rule.name;\n          select.appendChild(opt);\n        });\n      if (select.options.length === 0) {\n        const opt = document.createElement(\"option\");\n        opt.value = \"NONE\";\n        opt.textContent = \"\u65e0\u540e\u5904\u7406\";\n        select.appendChild(opt);\n      }\n    }\n\n    document.getElementById(\"materialVendor\").addEventListener(\"change\", rebuildMaterialOptions);\n    document.getElementById(\"machineVendor\").addEventListener(\"change\", rebuildMachineOptions);\n    document.getElementById(\"material\").addEventListener(\"change\", updateMaterialUsageFields);\n\n    function initSettingsFromRuntime() {\n      document.getElementById(\"materialsTableBody\").innerHTML = \"\";\n      document.getElementById(\"machinesTableBody\").innerHTML = \"\";\n      document.getElementById(\"postProcessTableBody\").innerHTML = \"\";\n\n      renderProcessFilterOptions(document.getElementById(\"materialsProcessFilter\"), currentMaterialsProcessFilter);\n      renderProcessFilterOptions(document.getElementById(\"machinesProcessFilter\"), currentMachinesProcessFilter);\n\n      runtimeConfig.materials.forEach(m => appendMaterialRow(\n        m.vendor,\n        m.name,\n        m.pricePerKg ?? \"\",\n        m.processType || UNIVERSAL_PROCESS_VALUE,\n        m.pricingMode || \"WEIGHT\",\n        m.pricePerCubicMeter ?? \"\"\n      ));\n      runtimeConfig.machines.forEach(m => {\n        appendMachineRow(\n          m.vendor,\n          m.name,\n          m.hourlyRate,\n          m.price ?? \"\",\n          m.expectedLifeYears ?? \"\",\n          m.expectedMonthlyHours ?? \"\",\n          m.powerW ?? \"\",\n          m.processType || UNIVERSAL_PROCESS_VALUE,\n          m.hourlyRateIncludesOperational !== false\n        );\n      });\n\n      (runtimeConfig.postProcessRules || []).forEach(rule => {\n        appendPostProcessRow(\n          rule.key,\n          rule.name,\n          rule.baseMinutes,\n          rule.minutesPerGram,\n          rule.extraMaterialCostPerGram,\n          rule.processType || UNIVERSAL_PROCESS_VALUE,\n          rule.costMultiplier ?? 1\n        );\n      });\n\n\n      rebuildMaterialsVendorFilter();\n      rebuildMachinesVendorFilter();\n      rebuildMaterialOptions();\n      rebuildMachineOptions();\n      rebuildPostProcessOptions();\n      applyMaterialsPagination();\n      applyMachinesPagination();\n\n      document.getElementById(\"configProfitMargin\").value = (runtimeConfig.defaultProfitMargin * 100).toFixed(0);\n      document.getElementById(\"configMinPrice\").value = runtimeConfig.defaultMinPricePerPart.toString();\n      document.getElementById(\"configSetupFee\").value = runtimeConfig.setupFee.toString();\n      document.getElementById(\"configElectricityPrice\").value = (runtimeConfig.electricityPrice ?? CLIENT_DEFAULTS.electricityPrice).toString();\n      renderProcessCostRows();\n\n      const customMarginInput = document.getElementById(\"customMargin\");\n      if (customMarginInput && customMarginInput.value === \"\") {\n        customMarginInput.value = (runtimeConfig.defaultProfitMargin * 100).toFixed(0);\n      }\n    }\n\n    function applySettingsToRuntime(data) {\n      if (!data) return;\n      runtimeConfig.defaultProfitMargin = data.defaultProfitMargin;\n      runtimeConfig.defaultMinPricePerPart = data.defaultMinPricePerPart;\n      runtimeConfig.setupFee = data.setupFee;\n      runtimeConfig.electricityPrice = data.electricityPrice ?? CLIENT_DEFAULTS.electricityPrice;\n      runtimeConfig.materials = (data.materials || []).map(m => ({\n        ...m,\n        processType: m.processType ?? null,\n      }));\n      runtimeConfig.machines = (data.machines || []).map(m => ({\n        ...m,\n        processType: m.processType ?? null,\n        hourlyRateIncludesOperational: m.hourlyRateIncludesOperational\n          ?? (m.hourlyRate > 0 ? true : null),\n        }));\n      runtimeConfig.processCosts = data.processCosts && Object.keys(data.processCosts).length\n        ? data.processCosts\n        : CLIENT_DEFAULTS.processCosts;\n      runtimeConfig.postProcessRules = Array.isArray(data.postProcessRules) && data.postProcessRules.length\n        ? data.postProcessRules.map(r => ({\n            ...r,\n            processType: r.processType ?? null,\n            costMultiplier: r.costMultiplier ?? 1,\n          }))\n        : CLIENT_DEFAULTS.postProcessRules;\n    }\n\n    async function loadSettingsFromServer() {\n      try {\n        if (!isAuthenticated()) throw new Error(\"\u672a\u767b\u5f55\");\n        const resp = await fetch(`${API_BASE}\/settings`, { headers: authHeaders() });\n        if (!resp.ok) throw new Error(\"HTTP \" + resp.status);\n        const data = await resp.json();\n        applySettingsToRuntime(data);\n\n      } catch (e) {\n        console.warn(\"\u52a0\u8f7d\u670d\u52a1\u5668\u8bbe\u7f6e\u5931\u8d25\uff0c\u4f7f\u7528\u524d\u7aef\u9ed8\u8ba4\u503c:\", e);\n      }\n      settingsLoaded = true;\n      initSettingsFromRuntime();\n    }\n\n    \/\/ ===== \u767b\u5f55 \/ \u6743\u9650\u63a7\u5236 =====\n    function setMainTab(btn) {\n      if (!btn) return;\n      document.querySelectorAll(\".main-tabs .page-tab\").forEach(b => b.classList.remove(\"active\"));\n      document.querySelectorAll(\".main-section\").forEach(sec => sec.classList.remove(\"active\"));\n      btn.classList.add(\"active\");\n      const id = btn.getAttribute(\"data-main\");\n      const target = document.getElementById(id);\n      if (target) target.classList.add(\"active\");\n      if (id === \"adminMain\") {\n        const firstAdminTab = document.querySelector('#adminTabs .page-tab');\n        setAdminSubTab(firstAdminTab);\n      }\n      if (id === \"recordsMain\") {\n        renderQuoteRecords();\n        if (isAdmin) renderQuoteSummary();\n      }\n    }\n\n    function setAdminSubTab(btn) {\n      if (!btn) return;\n      document.querySelectorAll(\"#adminTabs .page-tab\").forEach(b => b.classList.remove(\"active\"));\n      document.querySelectorAll(\".admin-sub-section\").forEach(sec => sec.classList.remove(\"active\"));\n      btn.classList.add(\"active\");\n      const id = btn.getAttribute(\"data-section\");\n      const target = document.getElementById(id);\n      if (target) target.classList.add(\"active\");\n    }\n\n    function updateAuthUI() {\n      const loginPanel = document.getElementById(\"loginPanel\");\n      const appContent = document.getElementById(\"appContent\");\n      const statusEl = document.getElementById(\"userStatus\");\n      const logoutBtn = document.getElementById(\"userLogoutBtn\");\n      const adminTab = document.querySelector('.main-tabs .page-tab[data-main=\"adminMain\"]');\n      const adminMain = document.getElementById(\"adminMain\");\n      const recordsTab = document.querySelector('.main-tabs .page-tab[data-main=\"recordsMain\"]');\n      const accountTab = document.querySelector('.main-tabs .page-tab[data-main=\"accountMain\"]');\n      const accountMain = document.getElementById(\"accountMain\");\n      const recordsMain = document.getElementById(\"recordsMain\");\n      const recordVisibilitySelect = document.getElementById(\"recordVisibilitySelect\");\n      const recordVisibilityWrap = recordVisibilitySelect?.closest(\"div\");\n      const recordsVisibilityFilterWrap = document.getElementById(\"recordsVisibilityFilterWrap\");\n      const recordsVisibilitySelect = document.getElementById(\"recordsVisibilityFilter\");\n\n      const authed = isAuthenticated();\n      if (loginPanel) loginPanel.style.display = authed ? \"none\" : \"\";\n      if (appContent) appContent.style.display = authed ? \"\" : \"none\";\n      if (logoutBtn) logoutBtn.style.display = authed ? \"\" : \"none\";\n\n      if (statusEl) {\n        if (isAdmin) statusEl.textContent = `\u7ba1\u7406\u5458\u5df2\u767b\u5f55\uff1a${sessionUsername || \"\"}`;\n        else if (isUser) statusEl.textContent = `\u5df2\u767b\u5f55\u7528\u6237\uff1a${sessionUsername || \"\"}`;\n        else statusEl.textContent = \"\u8bf7\u5148\u767b\u5f55\u540e\u4f7f\u7528\u62a5\u4ef7\u529f\u80fd\u3002\";\n      }\n\n      if (adminTab) adminTab.style.display = isAdmin ? \"\" : \"none\";\n      if (accountTab) accountTab.style.display = authed ? \"\" : \"none\";\n      const canSeeRecordsTab = authed && (isAdmin || canViewRecords);\n      if (recordsTab) recordsTab.style.display = canSeeRecordsTab ? \"\" : \"none\";\n      if (!isAdmin && adminTab?.classList.contains(\"active\")) {\n        setMainTab(document.querySelector('.main-tabs .page-tab[data-main=\"quoteMain\"]'));\n      }\n      if ((!authed || !canSeeRecordsTab) && recordsTab?.classList.contains(\"active\")) {\n        setMainTab(document.querySelector('.main-tabs .page-tab[data-main=\"quoteMain\"]'));\n      }\n      if (!authed && accountTab?.classList.contains(\"active\")) {\n        setMainTab(document.querySelector('.main-tabs .page-tab[data-main=\"quoteMain\"]'));\n      }\n      if (adminMain) {\n        adminMain.style.display = isAdmin ? \"\" : \"none\";\n      }\n      if (recordsMain) {\n        recordsMain.style.display = canSeeRecordsTab ? \"\" : \"none\";\n      }\n      if (accountMain) {\n        accountMain.style.display = authed ? \"\" : \"none\";\n      }\n\n      if (recordVisibilitySelect) {\n        recordVisibilitySelection = isAdmin ? recordVisibilitySelection : \"owner_only\";\n        recordVisibilitySelect.value = recordVisibilitySelection;\n        recordVisibilitySelect.disabled = !isAdmin;\n        if (recordVisibilityWrap) recordVisibilityWrap.style.display = isAdmin ? \"\" : \"none\";\n      }\n      if (recordsVisibilityFilterWrap) {\n        if (!isAdmin) recordsVisibilityFilter = \"all\";\n        recordsVisibilityFilterWrap.style.display = isAdmin ? \"\" : \"none\";\n        if (recordsVisibilitySelect) recordsVisibilitySelect.value = recordsVisibilityFilter;\n      }\n      if (authed && (isAdmin || canViewRecords)) {\n        populateRecordCreatorsOptions(isAdmin ? cachedUsers : []);\n      }\n      const activeAdminTab = document.querySelector('#adminTabs .page-tab.active');\n      if (isAdmin && activeAdminTab) {\n        const targetId = activeAdminTab.getAttribute(\"data-section\");\n        if (targetId === \"recordsSection\") {\n          renderQuoteRecords();\n          renderQuoteSummary();\n        }\n      }\n    }\n\n    async function refreshAuthState(forceReload = false) {\n      if (!sessionToken) {\n        isAdmin = false;\n        isUser = false;\n        sessionRole = null;\n        sessionUsername = \"\";\n        canViewRecords = false;\n        recordSavingEnabled = true;\n        updateAuthUI();\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/auth\/status`, { headers: authHeaders() });\n        if (!resp.ok) throw new Error(\"HTTP \" + resp.status);\n        const data = await resp.json();\n        if (data.authenticated) {\n          sessionRole = data.role;\n          sessionUsername = data.username || \"\";\n          isAdmin = sessionRole === \"admin\";\n          isUser = sessionRole === \"user\" || sessionRole === \"admin\";\n          canViewRecords = !!data.canViewRecords || isAdmin;\n          recordSavingEnabled = data.recordEnabled !== false;\n        } else {\n          sessionToken = null;\n          sessionRole = null;\n          sessionUsername = \"\";\n          localStorage.removeItem(SESSION_KEY);\n          isAdmin = false;\n          isUser = false;\n          canViewRecords = false;\n          recordSavingEnabled = true;\n        }\n      } catch (e) {\n        console.warn(\"\u68c0\u67e5\u767b\u5f55\u72b6\u6001\u5931\u8d25:\", e);\n        sessionToken = null;\n        sessionRole = null;\n        sessionUsername = \"\";\n        localStorage.removeItem(SESSION_KEY);\n        isAdmin = false;\n        isUser = false;\n        canViewRecords = false;\n        recordSavingEnabled = true;\n      }\n\n      updateAuthUI();\n      if (isAuthenticated() && (forceReload || !settingsLoaded)) {\n        await loadSettingsFromServer();\n      }\n      if (isAuthenticated()) {\n        await refreshProjects();\n      }\n      if (isAuthenticated()) {\n        renderQuoteRecords();\n      }\n      if (isAdmin) {\n        renderQuoteSummary();\n        renderUserTable();\n      }\n    }\n\n    async function fetchQuoteRecords() {\n      const month = document.getElementById(\"recordsMonth\")?.value || \"\";\n      const params = new URLSearchParams({ page: String(recordsPage), pageSize: String(recordsPageSize) });\n      if (month) params.append(\"month\", month);\n      if (recordsCreatorFilter) params.append(\"creator\", recordsCreatorFilter);\n      if (recordsProjectFilter) params.append(\"projectId\", recordsProjectFilter);\n      if (recordsAdoptedFilter !== \"all\") params.append(\"adopted\", recordsAdoptedFilter === \"true\");\n      if (isAdmin && recordsVisibilityFilter !== \"all\") params.append(\"visibility\", recordsVisibilityFilter);\n      const resp = await fetch(`${API_BASE}\/quotes?${params.toString()}`, {\n        headers: authHeaders()\n      });\n      if (!resp.ok) throw new Error(\"\u65e0\u6cd5\u83b7\u53d6\u62a5\u4ef7\u8bb0\u5f55\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u5e76\u5177\u5907\u67e5\u770b\u6743\u9650\");\n      return resp.json();\n    }\n\n    async function fetchQuoteSummary() {\n      const now = new Date();\n      const resp = await fetch(`${API_BASE}\/quotes\/summary?year=${now.getFullYear()}`, {\n        headers: authHeaders()\n      });\n      if (!resp.ok) throw new Error(\"\u65e0\u6cd5\u83b7\u53d6\u6c47\u603b\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u7ba1\u7406\u5458\");\n      return resp.json();\n    }\n\n    async function updateQuoteRecord(id, payload) {\n      const resp = await fetch(`${API_BASE}\/quotes\/${id}`, {\n        method: \"PATCH\",\n        headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n        body: JSON.stringify(payload)\n      });\n      const data = await resp.json().catch(() => ({}));\n      if (!resp.ok) throw new Error(data.detail || \"\u66f4\u65b0\u5931\u8d25\");\n      return data;\n    }\n\n    async function deleteQuoteRecord(id) {\n      const resp = await fetch(`${API_BASE}\/quotes\/${id}`, {\n        method: \"DELETE\",\n        headers: authHeaders()\n      });\n      const data = await resp.json().catch(() => ({}));\n      if (!resp.ok) throw new Error(data.detail || \"\u5220\u9664\u5931\u8d25\");\n      return data;\n    }\n\n    async function renderQuoteRecords() {\n      const listEl = document.getElementById(\"recordsList\");\n      const infoEl = document.getElementById(\"recordsPageInfo\");\n      const prevBtn = document.getElementById(\"recordsPrev\");\n      const nextBtn = document.getElementById(\"recordsNext\");\n      if (!listEl) return;\n      if (!isAdmin && !canViewRecords) {\n        listEl.textContent = \"\u5f53\u524d\u8d26\u53f7\u65e0\u67e5\u770b\u62a5\u4ef7\u8bb0\u5f55\u7684\u6743\u9650\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458\u5f00\u542f\u3002\";\n        if (infoEl) infoEl.textContent = \"\";\n        if (prevBtn) prevBtn.disabled = true;\n        if (nextBtn) nextBtn.disabled = true;\n        return;\n      }\n      listEl.textContent = \"\u52a0\u8f7d\u4e2d...\";\n      try {\n        const data = await fetchQuoteRecords();\n        recordsPage = data.page || recordsPage;\n        recordsMaxPage = Math.max(1, Math.ceil((data.total || 0) \/ (data.pageSize || recordsPageSize || 1)));\n        if (recordsPage > recordsMaxPage) {\n          recordsPage = recordsMaxPage;\n          renderQuoteRecords();\n          return;\n        }\n        listEl.innerHTML = \"\";\n        if (Array.isArray(data.items) && data.items.length) {\n          const visibilityLabels = {\n            admin_only: \"\u4ec5\u7ba1\u7406\u5458\",\n            all_users: \"\u5168\u90e8\u767b\u5f55\u7528\u6237\",\n            owner_only: \"\u4ec5\u521b\u5efa\u4eba\",\n          };\n          data.items.forEach(item => {\n            const div = document.createElement(\"div\");\n            div.className = \"record-card\";\n            const createdLabel = item.createdBy ? `\u521b\u5efa\u4eba\uff1a${item.createdBy}` : \"\u521b\u5efa\u4eba\uff1a-\";\n            const visibilityLabel = visibilityLabels[item.visibility] || \"\u672a\u6307\u5b9a\";\n            const adoptedLabel = item.adopted ? \"\u5df2\u91c7\u7528\" : \"\u672a\u91c7\u7528\";\n            div.innerHTML = `\n              <div><strong>${item.processLabel || item.processType}<\/strong> \u00b7 ${new Date(item.createdAt).toLocaleString()}<\/div>\n              <div>${createdLabel} \u00b7 \u53ef\u89c1\uff1a${visibilityLabel} \u00b7 \u72b6\u6001\uff1a${adoptedLabel}<\/div>\n              <div>\u9879\u76ee\uff1a${item.projectName || \"\u672a\u5173\u8054\"}<\/div>\n              <div>\u6750\u6599\uff1a${item.material?.vendor || ''} \/ ${item.material?.name || ''}<\/div>\n              <div>\u5355\u4ef7\uff1a${formatMoney(item.finalPricePerPart)} \u00d7 \u6570\u91cf ${item.quantity} = ${formatMoney(item.totalPrice)}<\/div>\n            `;\n\n            if (isAdmin) {\n              const actions = document.createElement(\"div\");\n              actions.className = \"pager\";\n\n              const adoptBtn = document.createElement(\"button\");\n              adoptBtn.type = \"button\";\n              adoptBtn.className = \"mini-btn\";\n              adoptBtn.textContent = item.adopted ? \"\u6807\u8bb0\u672a\u91c7\u7528\" : \"\u6807\u8bb0\u5df2\u91c7\u7528\";\n              adoptBtn.addEventListener(\"click\", async () => {\n                await updateQuoteRecord(item.id, { adopted: !item.adopted });\n                renderQuoteRecords();\n              });\n\n              const visibilitySelect = document.createElement(\"select\");\n              [\n                { value: \"admin_only\", label: \"\u4ec5\u7ba1\u7406\u5458\" },\n                { value: \"all_users\", label: \"\u5168\u90e8\u767b\u5f55\u7528\u6237\" },\n                { value: \"owner_only\", label: \"\u4ec5\u521b\u5efa\u4eba\" },\n              ].forEach(opt => {\n                const option = document.createElement(\"option\");\n                option.value = opt.value;\n                option.textContent = opt.label;\n                visibilitySelect.appendChild(option);\n              });\n              visibilitySelect.value = item.visibility || \"admin_only\";\n              visibilitySelect.addEventListener(\"change\", async () => {\n                await updateQuoteRecord(item.id, { visibility: visibilitySelect.value });\n                renderQuoteRecords();\n              });\n\n              const delBtn = document.createElement(\"button\");\n              delBtn.type = \"button\";\n              delBtn.className = \"mini-btn\";\n              delBtn.textContent = \"\u5220\u9664\";\n              delBtn.addEventListener(\"click\", async () => {\n                if (window.confirm(\"\u786e\u5b9a\u5220\u9664\u8be5\u8bb0\u5f55\u5417\uff1f\")) {\n                  await deleteQuoteRecord(item.id);\n                  renderQuoteRecords();\n                  renderQuoteSummary();\n                }\n              });\n\n              actions.append(\"\u72b6\u6001\uff1a\", adoptBtn, \" \u53ef\u89c1\u8303\u56f4\uff1a\", visibilitySelect, delBtn);\n              div.appendChild(actions);\n            }\n\n            listEl.appendChild(div);\n          });\n        } else {\n          listEl.textContent = \"\u6682\u65e0\u8bb0\u5f55\";\n        }\n        if (infoEl) infoEl.textContent = `${recordsPage}\/${recordsMaxPage}\uff08\u5171 ${data.total || 0} \u6761\uff09`;\n        if (prevBtn) prevBtn.disabled = recordsPage <= 1;\n        if (nextBtn) nextBtn.disabled = recordsPage >= recordsMaxPage;\n      } catch (e) {\n        listEl.textContent = e.message || \"\u83b7\u53d6\u8bb0\u5f55\u5931\u8d25\";\n        if (infoEl) infoEl.textContent = \"\";\n      }\n    }\n\n    async function renderQuoteSummary() {\n      const summaryEl = document.getElementById(\"recordsSummary\");\n      if (!summaryEl) return;\n      if (!isAdmin) {\n        summaryEl.textContent = \"\u4ec5\u7ba1\u7406\u5458\u53ef\u67e5\u770b\u6708\u5ea6\u6c47\u603b\";\n        return;\n      }\n      summaryEl.textContent = \"\u52a0\u8f7d\u4e2d...\";\n      try {\n        const data = await fetchQuoteSummary();\n        if (Array.isArray(data.items) && data.items.length) {\n          const lines = data.items.map(item => `${item.month}: ${item.count} \u5355 \u00b7 ${formatMoney(item.amount)}`);\n          summaryEl.innerHTML = lines.join(\"<br>\");\n        } else {\n          summaryEl.textContent = \"\u6682\u65e0\u6570\u636e\";\n        }\n      } catch (e) {\n        summaryEl.textContent = e.message || \"\u83b7\u53d6\u6c47\u603b\u5931\u8d25\";\n      }\n    }\n\n    async function fetchUsers() {\n      const resp = await fetch(`${API_BASE}\/admin\/users`, { headers: authHeaders() });\n      if (!resp.ok) throw new Error(\"\u65e0\u6cd5\u83b7\u53d6\u7528\u6237\u5217\u8868\uff0c\u8bf7\u786e\u8ba4\u5df2\u767b\u5f55\u7ba1\u7406\u5458\");\n      return resp.json();\n    }\n\n    function getCachedUser(username) {\n      return Array.isArray(cachedUsers) ? cachedUsers.find(u => u.username === username) : null;\n    }\n\n    function populateRecordCreatorsOptions(users = []) {\n      const select = document.getElementById(\"recordsCreatorFilter\");\n      if (!select) return;\n      const prev = select.value;\n      select.innerHTML = \"\";\n      const allOpt = document.createElement(\"option\");\n      allOpt.value = \"\";\n      allOpt.textContent = \"\u5168\u90e8\";\n      select.appendChild(allOpt);\n      if (sessionUsername) {\n        const selfOpt = document.createElement(\"option\");\n        selfOpt.value = sessionUsername;\n        selfOpt.textContent = `\u4ec5\u81ea\u5df1 (${sessionUsername})`;\n        select.appendChild(selfOpt);\n      }\n      if (Array.isArray(users) && users.length) {\n        users.forEach(u => {\n          const opt = document.createElement(\"option\");\n          opt.value = u.username;\n          opt.textContent = `${u.username}\uff08${u.role === \"admin\" ? \"\u7ba1\u7406\u5458\" : \"\u7528\u6237\"}\uff09`;\n          select.appendChild(opt);\n        });\n      }\n      const allowed = Array.from(select.options).map(o => o.value);\n      select.value = allowed.includes(prev) ? prev : \"\";\n    }\n\n    async function renderUserTable() {\n      if (!isAdmin) return;\n      const tbody = document.getElementById(\"userTableBody\");\n      if (!tbody) return;\n      tbody.innerHTML = \"<tr><td colspan='6'>\u52a0\u8f7d\u4e2d...<\/td><\/tr>\";\n      try {\n        const users = await fetchUsers();\n        cachedUsers = users;\n        populateRecordCreatorsOptions(users);\n        if (!Array.isArray(users) || users.length === 0) {\n          tbody.innerHTML = \"<tr><td colspan='6'>\u6682\u65e0\u7528\u6237<\/td><\/tr>\";\n          return;\n        }\n        tbody.innerHTML = \"\";\n        users.forEach(user => {\n          const tr = document.createElement(\"tr\");\n          tr.innerHTML = `\n            <td>${user.username}<\/td>\n            <td>${user.role === \"admin\" ? \"\u7ba1\u7406\u5458\" : \"\u666e\u901a\u7528\u6237\"}<\/td>\n            <td>${user.active ? \"\u542f\u7528\" : \"\u7981\u7528\"}<\/td>\n            <td>\n              <span class=\"badge\">${user.recordEnabled ? \"\u8bb0\u5f55\u4e2d\" : \"\u4e0d\u8bb0\u5f55\"}<\/span>\n              <button type=\"button\" class=\"mini-btn\" data-action=\"record\" data-username=\"${user.username}\" data-enabled=\"${user.recordEnabled}\">${user.recordEnabled ? \"\u6682\u505c\u8bb0\u5f55\" : \"\u5f00\u542f\u8bb0\u5f55\"}<\/button>\n            <\/td>\n            <td>\n              <span class=\"badge\">${user.canViewRecords ? \"\u53ef\u67e5\u770b\" : \"\u4e0d\u53ef\u67e5\u770b\"}<\/span>\n              <button type=\"button\" class=\"mini-btn\" data-action=\"view\" data-username=\"${user.username}\" data-view=\"${user.canViewRecords}\">${user.canViewRecords ? \"\u6536\u56de\u6743\u9650\" : \"\u5141\u8bb8\u67e5\u770b\"}<\/button>\n            <\/td>\n            <td>\n              <button type=\"button\" class=\"mini-btn\" data-action=\"reset\" data-username=\"${user.username}\">\u91cd\u7f6e\u5bc6\u7801<\/button>\n              <button type=\"button\" class=\"mini-btn\" data-action=\"edit\" data-username=\"${user.username}\" data-role=\"${user.role}\">\u7f16\u8f91<\/button>\n              <button type=\"button\" class=\"mini-btn\" data-action=\"toggle\" data-username=\"${user.username}\" data-active=\"${user.active}\">\n                ${user.active ? \"\u7981\u7528\" : \"\u542f\u7528\"}\n              <\/button>\n              <button type=\"button\" class=\"mini-btn\" data-action=\"delete\" data-username=\"${user.username}\">\u5220\u9664<\/button>\n            <\/td>\n          `;\n          tbody.appendChild(tr);\n        });\n      } catch (e) {\n        tbody.innerHTML = `<tr><td colspan='6'>${e.message || \"\u52a0\u8f7d\u5931\u8d25\"}<\/td><\/tr>`;\n      }\n    }\n\n    async function createUser() {\n      const name = document.getElementById(\"newUserName\").value.trim();\n      const pwd = document.getElementById(\"newUserPassword\").value;\n      const role = document.getElementById(\"newUserRole\").value;\n      const recordFlag = document.getElementById(\"newUserRecordEnabled\").value === \"true\";\n      const viewFlag = document.getElementById(\"newUserCanViewRecords\").value === \"true\";\n      if (!name || !pwd) {\n        alert(\"\u8bf7\u8f93\u5165\u7528\u6237\u540d\u548c\u521d\u59cb\u5bc6\u7801\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/admin\/users`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ username: name, password: pwd, role, active: true, recordEnabled: recordFlag, canViewRecords: viewFlag })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u521b\u5efa\u5931\u8d25\");\n        document.getElementById(\"newUserName\").value = \"\";\n        document.getElementById(\"newUserPassword\").value = \"\";\n        document.getElementById(\"newUserRole\").value = \"user\";\n        document.getElementById(\"newUserRecordEnabled\").value = \"true\";\n        document.getElementById(\"newUserCanViewRecords\").value = \"false\";\n        await renderUserTable();\n        alert(\"\u7528\u6237\u5df2\u521b\u5efa\");\n      } catch (e) {\n        alert(e.message || \"\u521b\u5efa\u5931\u8d25\");\n      }\n    }\n\n    async function toggleUser(username, active) {\n      try {\n        const resp = await fetch(`${API_BASE}\/admin\/users\/${encodeURIComponent(username)}\/toggle`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ active })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u64cd\u4f5c\u5931\u8d25\");\n        await renderUserTable();\n      } catch (e) {\n        alert(e.message || \"\u64cd\u4f5c\u5931\u8d25\");\n      }\n    }\n\n    async function resetUserPassword(username) {\n      const newPwd = window.prompt(`\u8bf7\u8f93\u5165\u4e3a ${username} \u8bbe\u7f6e\u7684\u65b0\u5bc6\u7801\uff1a`);\n      if (!newPwd) return;\n      try {\n        const resp = await fetch(`${API_BASE}\/admin\/users\/${encodeURIComponent(username)}\/reset-password`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ newPassword: newPwd })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u91cd\u7f6e\u5931\u8d25\");\n        alert(\"\u5bc6\u7801\u5df2\u91cd\u7f6e\");\n      } catch (e) {\n        alert(e.message || \"\u91cd\u7f6e\u5931\u8d25\");\n      }\n    }\n\n    async function updateUser(username, newUsername, newRole, extraPayload = {}) {\n      if (!newUsername) newUsername = username;\n      if (newRole !== \"admin\" && newRole !== \"user\") {\n        alert(\"\u89d2\u8272\u53ea\u80fd\u4e3a admin \u6216 user\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/admin\/users\/${encodeURIComponent(username)}`, {\n          method: \"PUT\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ newUsername, role: newRole, ...extraPayload })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u66f4\u65b0\u5931\u8d25\");\n        await renderUserTable();\n        alert(\"\u7528\u6237\u4fe1\u606f\u5df2\u66f4\u65b0\");\n      } catch (e) {\n        alert(e.message || \"\u66f4\u65b0\u5931\u8d25\");\n      }\n    }\n\n    async function deleteUser(username) {\n      try {\n        const resp = await fetch(`${API_BASE}\/admin\/users\/${encodeURIComponent(username)}`, {\n          method: \"DELETE\",\n          headers: authHeaders()\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u5220\u9664\u5931\u8d25\");\n        await renderUserTable();\n      } catch (e) {\n        alert(e.message || \"\u5220\u9664\u5931\u8d25\");\n      }\n    }\n\n    async function persistQuoteResult(quote) {\n      if (!recordSavingEnabled) return;\n      if (!isAuthenticated()) {\n        alert(\"\u5f53\u524d\u672a\u767b\u5f55\uff0c\u5df2\u8df3\u8fc7\u4fdd\u5b58\u62a5\u4ef7\u8bb0\u5f55\u3002\");\n        return;\n      }\n      try {\n        const recordVisibility = isAdmin\n          ? (document.getElementById(\"recordVisibilitySelect\")?.value || \"admin_only\")\n          : \"owner_only\";\n        const projectSelect = document.getElementById(\"projectSelect\");\n        const projectId = projectSelect?.value || \"\";\n        const projectName = cachedProjects.find(p => p.id === projectId)?.name || \"\";\n        const resp = await fetch(`${API_BASE}\/quotes`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({\n            processType: quote.processType,\n            processLabel: quote.processLabel,\n            quantity: quote.quantity,\n            totalPrice: quote.totalPrice,\n            finalPricePerPart: quote.finalPricePerPart,\n            costSumPerPart: quote.costSumPerPart,\n            profitMargin: quote.profitMargin,\n            materialCostPerPart: quote.materialCostPerPart,\n            machineCostPerPart: quote.machineCostPerPart,\n            postCostPerPart: quote.postCostPerPart,\n            setupCostPerPart: quote.setupCostPerPart,\n            material: quote.material,\n            machine: quote.machine,\n            postProcess: quote.post,\n            weight: quote.weight,\n            volume: quote.volume,\n            totalHours: quote.totalHours,\n            projectId: projectId || null,\n            projectName: projectName || undefined,\n            visibility: recordVisibility\n          })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || `HTTP ${resp.status}`);\n        alert(\"\u62a5\u4ef7\u8bb0\u5f55\u5df2\u4fdd\u5b58\u3002\");\n        if (isAdmin) {\n          renderQuoteRecords();\n          renderQuoteSummary();\n        }\n      } catch (e) {\n        console.warn(\"\u4fdd\u5b58\u62a5\u4ef7\u8bb0\u5f55\u5931\u8d25\", e);\n        alert(`\u4fdd\u5b58\u62a5\u4ef7\u8bb0\u5f55\u5931\u8d25\uff1a${e.message || \"\u672a\u77e5\u9519\u8bef\"}`);\n      }\n    }\n\n    async function userLoginFlow() {\n      const username = document.getElementById(\"userLoginName\").value.trim();\n      const password = document.getElementById(\"userLoginPassword\").value;\n      if (!username || !password) {\n        alert(\"\u8bf7\u8f93\u5165\u7528\u6237\u540d\u548c\u5bc6\u7801\");\n        return;\n      }\n\n      try {\n        const resp = await fetch(`${API_BASE}\/auth\/login`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\" },\n          body: JSON.stringify({ username, password })\n        });\n        if (!resp.ok) {\n          const err = await resp.json().catch(() => ({}));\n          throw new Error(err.detail || \"\u767b\u5f55\u5931\u8d25\");\n        }\n        const data = await resp.json();\n        sessionToken = data.token;\n        sessionUsername = data.username;\n        sessionRole = data.role;\n        isAdmin = sessionRole === \"admin\";\n        isUser = sessionRole === \"user\" || sessionRole === \"admin\";\n        canViewRecords = !!data.canViewRecords || isAdmin;\n        recordSavingEnabled = data.recordEnabled !== false;\n        localStorage.setItem(SESSION_KEY, sessionToken);\n        await refreshAuthState(true);\n        alert(`\u767b\u5f55\u6210\u529f\uff0c\u5f53\u524d\u89d2\u8272\uff1a${sessionRole === \"admin\" ? \"\u7ba1\u7406\u5458\" : \"\u666e\u901a\u7528\u6237\"}`);\n      } catch (e) {\n        alert(\"\u767b\u5f55\u5931\u8d25\uff1a\" + e.message);\n      }\n    }\n\n    async function logoutFlow() {\n      if (sessionToken) {\n        try {\n          await fetch(`${API_BASE}\/auth\/logout`, {\n            method: \"POST\",\n            headers: { ...authHeaders() },\n          });\n        } catch (e) {\n          console.warn(\"\u767b\u51fa\u5931\u8d25\uff1a\", e);\n        }\n      }\n      sessionToken = null;\n      sessionRole = null;\n      sessionUsername = \"\";\n      isUser = false;\n      isAdmin = false;\n      localStorage.removeItem(SESSION_KEY);\n      settingsLoaded = false;\n      cachedProjects = [];\n      populateProjectOptions(document.getElementById(\"projectSelect\"));\n      populateProjectOptions(document.getElementById(\"recordsProjectFilter\"), true, \"\", \"\u5168\u90e8\u9879\u76ee\");\n      updateAuthUI();\n    }\n\n    document.getElementById(\"userLoginBtn\").addEventListener(\"click\", userLoginFlow);\n    document.getElementById(\"userLogoutBtn\").addEventListener(\"click\", logoutFlow);\n    document.getElementById(\"selfChangePasswordBtn\").addEventListener(\"click\", selfChangePassword);\n    document.getElementById(\"selfChangeUsernameBtn\").addEventListener(\"click\", selfChangeUsername);\n\n    \/\/ ===== \u4fdd\u5b58\u8bbe\u7f6e & \u4fee\u6539\u5bc6\u7801 =====\n    async function saveSettingsToServer() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n\n      const materials = getMaterialsFromTable();\n      const machines = getMachinesFromTable();\n      if (materials.length === 0) {\n        alert(\"\u8bf7\u81f3\u5c11\u914d\u7f6e\u4e00\u4e2a\u6750\u6599\");\n        return;\n      }\n      if (machines.length === 0) {\n        alert(\"\u8bf7\u81f3\u5c11\u914d\u7f6e\u4e00\u53f0\u8bbe\u5907\");\n        return;\n      }\n\n      let pMargin = parseFloat(document.getElementById(\"configProfitMargin\").value);\n      if (isNaN(pMargin) || pMargin < 0) pMargin = 0;\n      const profit = pMargin \/ 100;\n\n      let minPrice = parseFloat(document.getElementById(\"configMinPrice\").value);\n      if (isNaN(minPrice) || minPrice < 0) minPrice = 0;\n\n      let setupFee = parseFloat(document.getElementById(\"configSetupFee\").value);\n      if (isNaN(setupFee) || setupFee < 0) setupFee = 0;\n\n      const eleInput = document.getElementById(\"configElectricityPrice\");\n      let electricityPrice = parseFloat(eleInput?.value);\n      if (isNaN(electricityPrice) || electricityPrice < 0) {\n        alert(\"\u7535\u4ef7\uff08\u5143\/kWh\uff09\u8bf7\u8f93\u5165 0 \u6216\u4ee5\u4e0a\u7684\u6570\u5b57\");\n        eleInput &#038;&#038; eleInput.focus();\n        return;\n      }\n\n\n      const postProcessRules = getPostProcessRulesFromTable();\n      if (!postProcessRules) {\n        alert(\"\u540e\u5904\u7406\u8bbe\u7f6e\u6709\u8bef\uff1akey\u3001\u540d\u79f0\u5fc5\u586b\uff0c\u65f6\u95f4\/\u6210\u672c\u9700\u5927\u4e8e\u7b49\u4e8e 0\u3002\");\n        return;\n      }\n\n      const processCosts = getProcessCostsFromTable();\n      if (!processCosts) return;\n\n      const firstProcessKey = Object.keys(processCosts)[0];\n      const fallbackProcessCost = firstProcessKey ? processCosts[firstProcessKey] : { laborHourlyCost: 0, machinesPerOperator: 1, overheadHourlyPerMachine: 0 };\n\n      const payload = {\n        materials,\n        machines,\n        defaultProfitMargin: profit,\n        defaultMinPricePerPart: minPrice,\n        setupFee: setupFee,\n        electricityPrice,\n        laborHourlyCost: fallbackProcessCost.laborHourlyCost,\n        machinesPerOperator: fallbackProcessCost.machinesPerOperator,\n        overheadHourlyPerMachine: fallbackProcessCost.overheadHourlyPerMachine,\n        processCosts,\n        postProcessRules\n      };\n\n\n      try {\n        const resp = await fetch(`${API_BASE}\/settings`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify(payload)\n        });\n        if (resp.status === 401) {\n          isAdmin = false;\n          isUser = false;\n          sessionUsername = \"\";\n          sessionRole = null;\n          sessionToken = null;\n          localStorage.removeItem(SESSION_KEY);\n          updateAuthUI();\n          throw new Error(\"\u672a\u767b\u5f55\u6216\u4f1a\u8bdd\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u3002\");\n        }\n        if (!resp.ok) {\n          const errData = await resp.json().catch(() => ({}));\n          throw new Error(errData.detail || \"\u4fdd\u5b58\u5931\u8d25\");\n        }\n        const saved = await resp.json();\n        applySettingsToRuntime(saved);\n        rebuildPostProcessOptions();\n        initSettingsFromRuntime();\n        alert(\"\u8bbe\u7f6e\u5df2\u4fdd\u5b58\u5230\u670d\u52a1\u5668\u3002\");\n      } catch (e) {\n        alert(\"\u4fdd\u5b58\u8bbe\u7f6e\u5931\u8d25\uff1a\" + e.message);\n      }\n    }\n\n    async function exportCurrentSettings() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/settings\/export`, { headers: authHeaders() });\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        const data = await resp.json();\n        const blob = new Blob([JSON.stringify(data, null, 2)], { type: \"application\/json\" });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = `quote_settings_${Date.now()}.json`;\n        a.click();\n        URL.revokeObjectURL(url);\n      } catch (e) {\n        alert(`\u5bfc\u51fa\u5931\u8d25\uff1a${e.message}`);\n      }\n    }\n\n    function triggerImportSettings() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      const fileInput = document.getElementById(\"importSettingsFile\");\n      fileInput.value = \"\";\n      fileInput.click();\n    }\n\n    async function handleImportSettingsFile(event) {\n      const file = event.target.files?.[0];\n      if (!file) return;\n\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        event.target.value = \"\";\n        return;\n      }\n\n      try {\n        const content = await file.text();\n        let parsed;\n        try {\n          parsed = JSON.parse(content);\n        } catch (err) {\n          throw new Error(\"\u6587\u4ef6\u4e0d\u662f\u6709\u6548\u7684 JSON \u914d\u7f6e\uff0c\u8bf7\u68c0\u67e5\u540e\u91cd\u8bd5\u3002\");\n        }\n\n        if (!window.confirm(`\u786e\u5b9a\u8981\u5bfc\u5165\u914d\u7f6e\u6587\u4ef6\u300c${file.name}\u300d\u5e76\u8986\u76d6\u5f53\u524d\u8bbe\u7f6e\u5417\uff1f`)) return;\n\n        const resp = await fetch(`${API_BASE}\/settings\/import`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify(parsed)\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u5bfc\u5165\u5931\u8d25\");\n\n        applySettingsToRuntime(data);\n        initSettingsFromRuntime();\n        alert(\"\u5df2\u5bfc\u5165\u5e76\u5e94\u7528\u914d\u7f6e\uff0c\u540c\u65f6\u751f\u6210\u65b0\u7684\u5907\u4efd\u6587\u4ef6\u3002\");\n      } catch (e) {\n        alert(`\u5bfc\u5165\u5931\u8d25\uff1a${e.message}`);\n      } finally {\n        event.target.value = \"\";\n      }\n    }\n\n    async function backupCurrentSettings() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/settings\/backup`, { method: \"POST\", headers: authHeaders() });\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        alert(\"\u5df2\u5907\u4efd\u5f53\u524d\u914d\u7f6e\u3002\");\n      } catch (e) {\n        alert(`\u5907\u4efd\u5931\u8d25\uff1a${e.message}`);\n      }\n    }\n\n    async function restoreBackupSettings() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      if (!window.confirm(\"\u786e\u5b9a\u8981\u6062\u590d\u5230\u6700\u8fd1\u7684\u5907\u4efd\u914d\u7f6e\u5417\uff1f\u5f53\u524d\u672a\u4fdd\u5b58\u7684\u4fee\u6539\u5c06\u4f1a\u4e22\u5931\u3002\")) return;\n      try {\n        const resp = await fetch(`${API_BASE}\/settings\/restore`, { method: \"POST\", headers: authHeaders() });\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        const data = await resp.json();\n        applySettingsToRuntime(data);\n        initSettingsFromRuntime();\n        alert(\"\u5df2\u6062\u590d\u5230\u5907\u4efd\u914d\u7f6e\u3002\");\n      } catch (e) {\n        alert(`\u6062\u590d\u5931\u8d25\uff1a${e.message}`);\n      }\n    }\n\n    async function backupAllData() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/backup\/full`, { method: \"POST\", headers: authHeaders() });\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        alert(\"\u5df2\u5b8c\u6210\u5168\u91cf\u5907\u4efd\uff08\u542b\u914d\u7f6e\u3001\u7528\u6237\u3001\u9879\u76ee\u4e0e\u62a5\u4ef7\u8bb0\u5f55\uff09\u3002\");\n      } catch (e) {\n        alert(`\u5168\u91cf\u5907\u4efd\u5931\u8d25\uff1a${e.message}`);\n      }\n    }\n\n    async function restoreAllData() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      if (!window.confirm(\"\u786e\u5b9a\u8981\u6062\u590d\u5168\u91cf\u5907\u4efd\u5417\uff1f\u5f53\u524d\u914d\u7f6e\u3001\u7528\u6237\u3001\u9879\u76ee\u4e0e\u62a5\u4ef7\u8bb0\u5f55\u90fd\u4f1a\u88ab\u8986\u76d6\u3002\")) return;\n      try {\n        const resp = await fetch(`${API_BASE}\/backup\/full\/restore`, { method: \"POST\", headers: authHeaders() });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || `HTTP ${resp.status}`);\n        if (data.settings) {\n          applySettingsToRuntime(data.settings);\n          initSettingsFromRuntime();\n        }\n        alert(\"\u5df2\u4ece\u5168\u91cf\u5907\u4efd\u6062\u590d\u5168\u90e8\u6570\u636e\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u4ee5\u5237\u65b0\u6743\u9650\u4e0e\u4f1a\u8bdd\u3002\");\n        await logoutFlow();\n      } catch (e) {\n        alert(`\u6062\u590d\u5168\u91cf\u5907\u4efd\u5931\u8d25\uff1a${e.message}`);\n      }\n    }\n\n    async function restoreFactorySettings() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      if (!window.confirm(\"\u786e\u5b9a\u8981\u6062\u590d\u5230\u51fa\u5382\u9ed8\u8ba4\u914d\u7f6e\u5417\uff1f\u5f53\u524d\u914d\u7f6e\u5c06\u88ab\u8986\u76d6\u3002\")) return;\n      try {\n        const resp = await fetch(`${API_BASE}\/settings\/restore-factory`, { method: \"POST\", headers: authHeaders() });\n        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);\n        const data = await resp.json();\n        applySettingsToRuntime(data);\n        initSettingsFromRuntime();\n        alert(\"\u5df2\u6062\u590d\u51fa\u5382\u8bbe\u7f6e\u5e76\u5e94\u7528\u5230\u754c\u9762\u3002\");\n      } catch (e) {\n        alert(`\u6062\u590d\u5931\u8d25\uff1a${e.message}`);\n      }\n    }\n\n    async function selfChangePassword() {\n      if (!isAuthenticated()) {\n        alert(\"\u8bf7\u5148\u767b\u5f55\u3002\");\n        return;\n      }\n      const oldPwd = document.getElementById(\"selfOldPassword\").value;\n      const newPwd = document.getElementById(\"selfNewPassword\").value;\n      if (!oldPwd || !newPwd) {\n        alert(\"\u8bf7\u586b\u5199\u5f53\u524d\u5bc6\u7801\u548c\u65b0\u5bc6\u7801\u3002\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/account\/change-password`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ oldPassword: oldPwd, newPassword: newPwd })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u4fee\u6539\u5931\u8d25\");\n        alert(\"\u5bc6\u7801\u5df2\u66f4\u65b0\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55\u3002\");\n        await logoutFlow();\n      } catch (e) {\n        alert(e.message || \"\u4fee\u6539\u5931\u8d25\");\n      }\n    }\n\n    async function selfChangeUsername() {\n      if (!isAuthenticated()) {\n        alert(\"\u8bf7\u5148\u767b\u5f55\u3002\");\n        return;\n      }\n      const newName = document.getElementById(\"selfNewUsername\").value.trim();\n      const password = document.getElementById(\"selfUsernamePassword\").value;\n      if (!newName || !password) {\n        alert(\"\u8bf7\u586b\u5199\u65b0\u7528\u6237\u540d\u548c\u5f53\u524d\u5bc6\u7801\u3002\");\n        return;\n      }\n      try {\n        const resp = await fetch(`${API_BASE}\/account\/change-username`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ newUsername: newName, password })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) throw new Error(data.detail || \"\u4fee\u6539\u5931\u8d25\");\n        alert(\"\u7528\u6237\u540d\u5df2\u66f4\u65b0\uff0c\u8bf7\u4f7f\u7528\u65b0\u7528\u6237\u540d\u91cd\u65b0\u767b\u5f55\u3002\");\n        await logoutFlow();\n      } catch (e) {\n        alert(e.message || \"\u4fee\u6539\u5931\u8d25\");\n      }\n    }\n\n    async function changeAdminPassword() {\n      if (!isAdmin || !sessionToken) {\n        alert(\"\u8bf7\u5148\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55\u3002\");\n        return;\n      }\n      const oldPwd = window.prompt(\"\u8bf7\u8f93\u5165\u5f53\u524d\u7ba1\u7406\u5458\u5bc6\u7801\uff1a\");\n      if (!oldPwd) return;\n      const newPwd = window.prompt(\"\u8bf7\u8f93\u5165\u65b0\u7684\u7ba1\u7406\u5458\u5bc6\u7801\uff1a\");\n      if (!newPwd) return;\n\n      try {\n        const resp = await fetch(`${API_BASE}\/admin\/change-password`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application\/json\", ...authHeaders() },\n          body: JSON.stringify({ oldPassword: oldPwd, newPassword: newPwd })\n        });\n        const data = await resp.json().catch(() => ({}));\n        if (!resp.ok) {\n          throw new Error(data.detail || \"\u4fee\u6539\u5931\u8d25\");\n        }\n        alert(\"\u5bc6\u7801\u5df2\u66f4\u65b0\uff0c\u8bf7\u4f7f\u7528\u65b0\u5bc6\u7801\u91cd\u65b0\u767b\u5f55\u3002\");\n        sessionToken = null;\n        sessionRole = null;\n        sessionUsername = \"\";\n        isAdmin = false;\n        isUser = false;\n        localStorage.removeItem(SESSION_KEY);\n        updateAuthUI();\n      } catch (e) {\n        alert(\"\u4fee\u6539\u5bc6\u7801\u5931\u8d25\uff1a\" + e.message);\n      }\n    }\n\n    document.getElementById(\"saveSettingsBtn\").addEventListener(\"click\", saveSettingsToServer);\n    document.getElementById(\"exportSettingsBtn\").addEventListener(\"click\", exportCurrentSettings);\n    document.getElementById(\"backupSettingsBtn\").addEventListener(\"click\", backupCurrentSettings);\n    document.getElementById(\"restoreBackupBtn\").addEventListener(\"click\", restoreBackupSettings);\n    document.getElementById(\"backupFullBtn\").addEventListener(\"click\", backupAllData);\n    document.getElementById(\"restoreFullBtn\").addEventListener(\"click\", restoreAllData);\n    document.getElementById(\"restoreFactoryBtn\").addEventListener(\"click\", restoreFactorySettings);\n    document.getElementById(\"changePasswordBtn\").addEventListener(\"click\", changeAdminPassword);\n    document.getElementById(\"saveSettingsBtnMaterials\").addEventListener(\"click\", saveSettingsToServer);\n    document.getElementById(\"saveSettingsBtnMachines\").addEventListener(\"click\", saveSettingsToServer);\n    document.getElementById(\"importSettingsBtn\").addEventListener(\"click\", triggerImportSettings);\n    document.getElementById(\"importSettingsFile\").addEventListener(\"change\", handleImportSettingsFile);\n    document.getElementById(\"addMaterialBtn\").addEventListener(\"click\", () => {\n      const vendor = currentMaterialsVendor || \"\u672a\u5206\u7c7b\";\n      appendMaterialRow(vendor, \"\", \"\", getCurrentProcessType(), \"WEIGHT\", \"\");\n      rebuildMaterialsVendorFilter();\n      rebuildMaterialOptions();\n    });\n    document.getElementById(\"addMachineBtn\").addEventListener(\"click\", () => {\n      const vendor = currentMachinesVendor || \"\u672a\u5206\u7c7b\";\n      appendMachineRow(vendor, \"\", \"\", \"\", \"\", \"\", \"\", getCurrentProcessType());\n      rebuildMachinesVendorFilter();\n      rebuildMachineOptions();\n    });\n    document.getElementById(\"addPostProcessBtn\").addEventListener(\"click\", () => {\n      appendPostProcessRow();\n      rebuildPostProcessOptions();\n    });\n\n    document.getElementById(\"projectSearchBtn\")?.addEventListener(\"click\", () => {\n      const kw = document.getElementById(\"projectSearch\")?.value || \"\";\n      refreshProjects(kw);\n    });\n    document.getElementById(\"projectSearch\")?.addEventListener(\"keypress\", (e) => {\n      if (e.key === \"Enter\") {\n        e.preventDefault();\n        document.getElementById(\"projectSearchBtn\")?.click();\n      }\n    });\n    document.getElementById(\"projectNewBtn\")?.addEventListener(\"click\", () => openProjectModal());\n    document.getElementById(\"projectCreateBtn\")?.addEventListener(\"click\", () => openProjectModal());\n    document.getElementById(\"projectAdminSearchBtn\")?.addEventListener(\"click\", () => {\n      const kw = document.getElementById(\"projectAdminSearch\")?.value || \"\";\n      refreshProjects(kw);\n    });\n    document.getElementById(\"projectAdminResetBtn\")?.addEventListener(\"click\", () => {\n      const input = document.getElementById(\"projectAdminSearch\");\n      if (input) input.value = \"\";\n      refreshProjects(\"\");\n    });\n    document.getElementById(\"projectTableBody\")?.addEventListener(\"click\", (e) => {\n      const btn = e.target.closest(\"button[data-action]\");\n      if (!btn) return;\n      const id = btn.getAttribute(\"data-id\");\n      const action = btn.getAttribute(\"data-action\");\n      const project = cachedProjects.find(p => p.id === id);\n      if (action === \"edit\" && project) {\n        openProjectModal(project);\n      } else if (action === \"delete\") {\n        deleteProject(id);\n      }\n    });\n    document.getElementById(\"projectModalSubmit\")?.addEventListener(\"click\", submitProjectModal);\n    document.getElementById(\"projectModalClose\")?.addEventListener(\"click\", closeProjectModal);\n    document.getElementById(\"projectModalCancel\")?.addEventListener(\"click\", closeProjectModal);\n\n    document.getElementById(\"createUserBtn\").addEventListener(\"click\", createUser);\n    document.getElementById(\"userTableBody\").addEventListener(\"click\", (e) => {\n      const btn = e.target.closest(\"button[data-action]\");\n      if (!btn) return;\n      const username = btn.getAttribute(\"data-username\");\n      const action = btn.getAttribute(\"data-action\");\n      if (action === \"reset\") {\n        resetUserPassword(username);\n      } else if (action === \"toggle\") {\n        const active = btn.getAttribute(\"data-active\") === \"true\";\n        toggleUser(username, !active);\n      } else if (action === \"edit\") {\n        const currentRole = btn.getAttribute(\"data-role\") || \"user\";\n        const newName = window.prompt(\"\u8bf7\u8f93\u5165\u65b0\u7684\u7528\u6237\u540d\uff08\u7559\u7a7a\u5219\u4e0d\u4fee\u6539\uff09\uff1a\", username) || username;\n        const newRole = window.prompt(\"\u8bf7\u8f93\u5165\u89d2\u8272 admin\/user\uff1a\", currentRole) || currentRole;\n        updateUser(username, newName.trim(), newRole.trim());\n      } else if (action === \"record\") {\n        const user = getCachedUser(username);\n        if (!user) return;\n        updateUser(username, user.username, user.role, { recordEnabled: !user.recordEnabled });\n      } else if (action === \"view\") {\n        const user = getCachedUser(username);\n        if (!user) return;\n        updateUser(username, user.username, user.role, { canViewRecords: !user.canViewRecords });\n      } else if (action === \"delete\") {\n        if (window.confirm(`\u786e\u5b9a\u8981\u5220\u9664\u7528\u6237 ${username} \u5417\uff1f`)) {\n          deleteUser(username);\n        }\n      }\n    });\n\n    \/\/ ===== \u62a5\u4ef7\u8ba1\u7b97 & \u5bfc\u51fa =====\n    document.getElementById(\"quote-form\").addEventListener(\"submit\", function (e) {\n      e.preventDefault();\n\n      if (!isAuthenticated()) {\n        alert(\"\u8bf7\u5148\u767b\u5f55\u540e\u518d\u8ba1\u7b97\u62a5\u4ef7\u3002\");\n        return;\n      }\n\n      const materials = getMaterialsFromTable();\n      const machines = getMachinesFromTable();\n      const processType = getCurrentProcessType();\n      const processLabel = PROCESS_TYPES.find(p => p.value === processType)?.label || processType;\n      if (materials.length === 0) {\n        alert(\"\u8bf7\u5148\u5728\u4e0b\u65b9\u201c\u6750\u6599\u8bbe\u7f6e\u201d\u4e2d\u6dfb\u52a0\u81f3\u5c11\u4e00\u4e2a\u6750\u6599\u3002\");\n        return;\n      }\n      if (machines.length === 0) {\n        alert(\"\u8bf7\u5148\u5728\u4e0b\u65b9\u201c\u8bbe\u5907\u8bbe\u7f6e\u201d\u4e2d\u6dfb\u52a0\u81f3\u5c11\u4e00\u53f0\u8bbe\u5907\u3002\");\n        return;\n      }\n\n      const matIndex = parseInt(document.getElementById(\"material\").value, 10);\n      const macIndex = parseInt(document.getElementById(\"machine\").value, 10);\n      const material = materials[matIndex];\n      const machine = machines[macIndex];\n\n      if (!material || !isProcessMatch(material.processType, processType)) {\n        alert(\"\u8bf7\u9009\u62e9\u4e0e\u5f53\u524d\u52a0\u5de5\u7c7b\u578b\u5339\u914d\u7684\u6750\u6599\u3002\");\n        return;\n      }\n      if (!machine || !isProcessMatch(machine.processType, processType)) {\n        alert(\"\u8bf7\u9009\u62e9\u4e0e\u5f53\u524d\u52a0\u5de5\u7c7b\u578b\u5339\u914d\u7684\u8bbe\u5907\u3002\");\n        return;\n      }\n\n      const weight = parseFloat(document.getElementById(\"weight\").value) || 0;\n      const volume = parseFloat(document.getElementById(\"volume\").value) || 0;\n      if (material.pricingMode === \"VOLUME\" && volume <= 0) {\n        alert(\"\u5f53\u524d\u6750\u6599\u6309\u4f53\u79ef\u8ba1\u4ef7\uff0c\u8bf7\u586b\u5199\u6709\u6548\u7684\u4f53\u79ef\u3002\");\n        return;\n      }\n      if (material.pricingMode !== \"VOLUME\" &#038;&#038; weight <= 0) {\n        alert(\"\u5f53\u524d\u6750\u6599\u6309\u91cd\u91cf\u8ba1\u4ef7\uff0c\u8bf7\u586b\u5199\u6709\u6548\u7684\u91cd\u91cf\u3002\");\n        return;\n      }\n\n      const days = parseFloat(document.getElementById(\"printDays\").value) || 0;\n      const hours = parseFloat(document.getElementById(\"printHours\").value) || 0;\n      const minutes = parseFloat(document.getElementById(\"printMinutes\").value) || 0;\n      const totalHours = days * 24 + hours + minutes \/ 60;\n      if (totalHours <= 0) {\n        alert(\"\u8bf7\u586b\u5199\u6253\u5370\u65f6\u95f4\uff08\u81f3\u5c11\u4e00\u4e2a\u5927\u4e8e 0 \u7684\u503c\uff09\u3002\");\n        return;\n      }\n\n      const postKey = document.getElementById(\"postProcess\").value;\n      const post = getPostRuleByKey(postKey);\n\n      const quantity = parseInt(document.getElementById(\"quantity\").value, 10) || 1;\n\n      const customMarginInput = document.getElementById(\"customMargin\").value;\n      const customMinInput = document.getElementById(\"customMin\").value;\n\n      const profitMargin = customMarginInput !== \"\"\n        ? (parseFloat(customMarginInput) || 0) \/ 100\n        : runtimeConfig.defaultProfitMargin;\n\n      const minPricePerPart = customMinInput !== \"\"\n        ? (parseFloat(customMinInput) || 0)\n        : runtimeConfig.defaultMinPricePerPart;\n\n      const processCosts = getProcessCostConfig(processType);\n      const materialCostPerPart = material.pricingMode === \"VOLUME\"\n        ? ((volume \/ 1000000) * (material.pricePerCubicMeter || 0))\n        : ((weight \/ 1000) * (material.pricePerKg || 0));\n      const operatorHourly = (processCosts.laborHourlyCost || 0) \/ (processCosts.machinesPerOperator || 1);\n      const overheadHourly = processCosts.overheadHourlyPerMachine || 0;\n      const electricityHourly = (machine.powerW ? (machine.powerW \/ 1000) * (runtimeConfig.electricityPrice || 0) : 0);\n      const baseMachineHourly = machine.hourlyRate || 0;\n      const depreciationHourly = computeMachineDepreciationHourly(machine);\n      const machineHourly = baseMachineHourly > 0 ? baseMachineHourly : depreciationHourly;\n      const operationalHourly = operatorHourly + overheadHourly + electricityHourly;\n      const hourlyRateIncludesOperational = machine.hourlyRateIncludesOperational;\n      const inferredAllIn = hourlyRateIncludesOperational === true\n        || (hourlyRateIncludesOperational == null && baseMachineHourly > 0 && depreciationHourly === 0);\n      const shouldAddOperational = !inferredAllIn;\n      const effectiveMachineHourly = machineHourly + (shouldAddOperational ? operationalHourly : 0);\n      const machineCostPerPart = totalHours * effectiveMachineHourly;\n      const totalPostMinutes = (post.baseMinutes || 0) + (post.minutesPerGram || 0) * weight;\n      const postLaborCost = (processCosts.laborHourlyCost || 0) * totalPostMinutes \/ 60;\n      const postMaterialCost = (post.extraMaterialCostPerGram || 0) * weight;\n      const postMultiplier = post.costMultiplier || 1;\n      const postCostPerPart = (postLaborCost + postMaterialCost) * postMultiplier;\n      const setupCostPerPart = quantity > 0 ? runtimeConfig.setupFee \/ quantity : runtimeConfig.setupFee;\n\n      const costSumPerPart = materialCostPerPart + machineCostPerPart + postCostPerPart + setupCostPerPart;\n      const withProfitPerPart = costSumPerPart * (1 + profitMargin);\n      const finalPricePerPart = Math.max(withProfitPerPart, minPricePerPart);\n      const totalPrice = finalPricePerPart * quantity;\n\n      const materialPriceLabel = material.pricingMode === \"VOLUME\"\n        ? `${material.pricePerCubicMeter ?? 0} \u5143\/m\u00b3`\n        : `${material.pricePerKg ?? 0} \u5143\/kg`;\n      const materialUsageLabel = material.pricingMode === \"VOLUME\"\n        ? `${volume.toFixed(2)} cm\u00b3`\n        : `${weight.toFixed(2)} g`;\n      const machineHourlyLabel = formatMoney(effectiveMachineHourly);\n\n      const resultEl = document.getElementById(\"result\");\n      const resultTotalEl = document.getElementById(\"result-total\");\n      const resultDetailEl = document.getElementById(\"result-detail\");\n\n      resultTotalEl.textContent = `${formatMoney(totalPrice)}  \uff08\u5171 ${quantity} \u4ef6\uff0c\u5355\u4ef6 ${formatMoney(finalPricePerPart)}\uff09`;\n\n      const lines = [\n        `\u25b6 \u52a0\u5de5\u7c7b\u578b\uff1a${processLabel}`,\n        `\u25b6 \u6750\u6599\uff1a${material.vendor} \/ ${material.name}  ${materialPriceLabel}`,\n        `   - \u5355\u4ef6\u8017\u6750\uff1a${materialUsageLabel} \u2192 \u6750\u6599\u6210\u672c\uff1a${formatMoney(materialCostPerPart)}`,\n        `\u25b6 \u8bbe\u5907\uff1a${machine.vendor} \/ ${machine.name}  ${machineHourlyLabel} \u5143\/\u5c0f\u65f6`,\n        `   - \u5355\u4ef6\u6253\u5370\u65f6\u95f4\uff1a${days} \u5929 ${hours} \u5c0f\u65f6 ${minutes} \u5206\u949f \u2248 ${totalHours.toFixed(2)} \u5c0f\u65f6`,\n        `   - \u8bbe\u5907\u6210\u672c\uff1a${formatMoney(machineCostPerPart)}`,\n        `\u25b6 \u540e\u5904\u7406\uff1a${post.name} \u2192 \u5355\u4ef6\u540e\u5904\u7406\u6210\u672c\uff1a${formatMoney(postCostPerPart)}\uff08\u65f6\u95f4 ${totalPostMinutes.toFixed(2)} \u5206\u949f\uff0c\u4eba\u5de5 ${formatMoney(postLaborCost)}\uff0c\u6750\u6599 ${formatMoney(postMaterialCost)}\uff0c\u7cfb\u6570 x${postMultiplier.toFixed(2)}\uff09`,\n        `\u25b6 \u4e0a\u673a\/\u8c03\u673a\u8d39\uff08\u8ba2\u5355\uff09\uff1a${formatMoney(runtimeConfig.setupFee)}  \u5206\u644a\u540e\u6bcf\u4ef6\uff1a${formatMoney(setupCostPerPart)}`,\n        \"\",\n        `\u25b6 \u5355\u4ef6\u6210\u672c\u5c0f\u8ba1\uff1a${formatMoney(costSumPerPart)}`,\n        `\u25b6 \u5229\u6da6\u7387\uff1a${(profitMargin * 100).toFixed(0)}%`,\n        `\u25b6 \u5355\u4ef6\u6700\u4f4e\u4ef7\uff1a${formatMoney(minPricePerPart)}`,\n        `\u25b6 \u542b\u5229\u6da6\u5355\u4ef7\uff1a${formatMoney(withProfitPerPart)}`,\n        `\u25b6 \u6700\u7ec8\u8ba1\u4ef7\u5355\u4ef6\uff1a${formatMoney(finalPricePerPart)}`,\n        \"\",\n        `\u25b6 \u6570\u91cf\uff1a${quantity} \u4ef6`,\n        `\u25b6 \u8ba2\u5355\u603b\u4ef7\uff1a${formatMoney(totalPrice)}`\n      ];\n\n      resultDetailEl.textContent = lines.join(\"\\n\");\n      resultEl.style.display = \"block\";\n\n      lastQuote = {\n        material,\n        machine,\n        post,\n        weight,\n        volume,\n        days,\n        hours,\n        minutes,\n        totalHours,\n        quantity,\n        profitMargin,\n        minPricePerPart,\n        materialCostPerPart,\n        machineCostPerPart,\n        postCostPerPart,\n        postMultiplier,\n        postLaborCost,\n        postMaterialCost,\n        totalPostMinutes,\n        setupCostPerPart,\n        machineHourlyRateUsed: effectiveMachineHourly,\n        machineOperationalHourly: baseMachineHourly > 0 ? 0 : operationalHourly,\n        costSumPerPart,\n        withProfitPerPart,\n        finalPricePerPart,\n        totalPrice,\n        processType,\n        processLabel\n      };\n\n    });\n\n    document.getElementById(\"resetBtn\").addEventListener(\"click\", () => {\n      document.getElementById(\"quote-form\").reset();\n      document.getElementById(\"result\").style.display = \"none\";\n      lastQuote = null;\n    });\n\n    document.getElementById(\"exportBtn\").addEventListener(\"click\", () => {\n      if (!lastQuote) {\n        alert(\"\u8bf7\u5148\u8ba1\u7b97\u4e00\u6b21\u62a5\u4ef7\uff0c\u518d\u5bfc\u51fa\u62a5\u4ef7\u5355\u3002\");\n        return;\n      }\n      persistQuoteResult(lastQuote);\n      const now = new Date();\n      const dateStr = now.toLocaleString(\"zh-CN\");\n      const lines = [\n        \"\u52a0\u5de5\u62a5\u4ef7\u5355\",\n        \"========================\",\n        `\u65e5\u671f\uff1a${dateStr}`,\n        `\u52a0\u5de5\u7c7b\u578b\uff1a${lastQuote.processLabel}`,\n        \"\",\n        `\u6750\u6599\uff1a${lastQuote.material.vendor} \/ ${lastQuote.material.name}`,\n        `\u6750\u6599\u5355\u4ef7\uff1a${lastQuote.material.pricingMode === \"VOLUME\" ? `${lastQuote.material.pricePerCubicMeter} \u5143\/m\u00b3` : `${lastQuote.material.pricePerKg} \u5143\/kg`}`,\n        `\u5355\u4ef6\u8017\u6750\uff1a${lastQuote.material.pricingMode === \"VOLUME\" ? `${lastQuote.volume.toFixed(2)} cm\u00b3` : `${lastQuote.weight.toFixed(2)} g`}`,\n        `\u6750\u6599\u6210\u672c\uff08\u5355\u4ef6\uff09\uff1a${formatMoney(lastQuote.materialCostPerPart)}`,\n        \"\",\n        `\u8bbe\u5907\uff1a${lastQuote.machine.vendor} \/ ${lastQuote.machine.name}`,\n        `\u8bbe\u5907\u5c0f\u65f6\u6210\u672c\uff1a${formatMoney(lastQuote.machineHourlyRateUsed || lastQuote.machine.hourlyRate)} \u5143\/\u5c0f\u65f6`,\n        `\u5355\u4ef6\u6253\u5370\u65f6\u95f4\uff1a${lastQuote.days} \u5929 ${lastQuote.hours} \u5c0f\u65f6 ${lastQuote.minutes} \u5206\u949f \u2248 ${lastQuote.totalHours.toFixed(2)} \u5c0f\u65f6`,\n        `\u8bbe\u5907\u6210\u672c\uff08\u5355\u4ef6\uff09\uff1a${formatMoney(lastQuote.machineCostPerPart)}`,\n        \"\",\n        `\u540e\u5904\u7406\u7b49\u7ea7\uff1a${lastQuote.post.name}`,\n        `\u540e\u5904\u7406\u6210\u672c\uff08\u5355\u4ef6\uff09\uff1a${formatMoney(lastQuote.postCostPerPart)}\uff08\u65f6\u95f4 ${lastQuote.totalPostMinutes.toFixed(2)} \u5206\u949f\uff0c\u4eba\u5de5 ${formatMoney(lastQuote.postLaborCost)}, \u6750\u6599 ${formatMoney(lastQuote.postMaterialCost)}, \u7cfb\u6570 x${(lastQuote.postMultiplier || 1).toFixed(2)}\uff09`,\n        \"\",\n        `\u4e0a\u673a\/\u8c03\u673a\u8d39\uff08\u8ba2\u5355\uff09\uff1a${formatMoney(runtimeConfig.setupFee)}`,\n        `\u5206\u644a\u540e\u4e0a\u673a\u8d39\uff08\u5355\u4ef6\uff09\uff1a${formatMoney(lastQuote.setupCostPerPart)}`,\n        \"\",\n        `\u5355\u4ef6\u6210\u672c\u5c0f\u8ba1\uff1a${formatMoney(lastQuote.costSumPerPart)}`,\n        `\u5229\u6da6\u7387\uff1a${(lastQuote.profitMargin * 100).toFixed(0)}%`,\n        `\u5355\u4ef6\u6700\u4f4e\u4ef7\uff1a${formatMoney(lastQuote.minPricePerPart)}`,\n        `\u542b\u5229\u6da6\u5355\u4ef7\uff1a${formatMoney(lastQuote.withProfitPerPart)}`,\n        `\u6700\u7ec8\u8ba1\u4ef7\u5355\u4ef7\uff1a${formatMoney(lastQuote.finalPricePerPart)}`,\n        \"\",\n        `\u6570\u91cf\uff1a${lastQuote.quantity} \u4ef6`,\n        `\u8ba2\u5355\u603b\u4ef7\uff1a${formatMoney(lastQuote.totalPrice)}`,\n        \"\",\n        \"\uff08\u672c\u62a5\u4ef7\u4ec5\u4f9b\u53c2\u8003\uff0c\u5b9e\u9645\u4ef7\u683c\u53ef\u6839\u636e\u6279\u91cf\u3001\u989c\u8272\u3001\u5de5\u671f\u7b49\u60c5\u51b5\u8c03\u6574\u3002\uff09\"\n      ];\n\n      const blob = new Blob([lines.join(\"\\n\")], { type: \"text\/plain;charset=utf-8\" });\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      const ts = now.toISOString().replace(\/[:.]\/g, \"-\");\n      a.href = url;\n      a.download = `process-quote-${lastQuote.processType}-${ts}.txt`;\n      document.body.appendChild(a);\n      a.click();\n      document.body.removeChild(a);\n      URL.revokeObjectURL(url);\n    });\n\n    \/\/ ===== \u9875\u9762\u521d\u59cb\u5316 =====\n    window.addEventListener(\"DOMContentLoaded\", async () => {\n      document.querySelectorAll(\".main-tabs .page-tab\").forEach(btn => {\n        btn.addEventListener(\"click\", () => {\n          if (btn.dataset.main === \"adminMain\" && !isAdmin) {\n            alert(\"\u4ec5\u7ba1\u7406\u5458\u53ef\u8bbf\u95ee\uff0c\u8bf7\u5148\u4f7f\u7528\u7ba1\u7406\u5458\u8d26\u6237\u767b\u5f55\u3002\");\n            return;\n          }\n          if (btn.dataset.main === \"recordsMain\") {\n            if (!isAuthenticated()) {\n              alert(\"\u8bf7\u5148\u767b\u5f55\u540e\u67e5\u770b\u62a5\u4ef7\u8bb0\u5f55\u3002\");\n              return;\n            }\n            if (!isAdmin && !canViewRecords) {\n              alert(\"\u5f53\u524d\u8d26\u53f7\u65e0\u67e5\u770b\u62a5\u4ef7\u8bb0\u5f55\u7684\u6743\u9650\uff0c\u8bf7\u8054\u7cfb\u7ba1\u7406\u5458\u5f00\u542f\u3002\");\n              return;\n            }\n          }\n          if (btn.dataset.main === \"accountMain\" && !isAuthenticated()) {\n            alert(\"\u8bf7\u5148\u767b\u5f55\u540e\u518d\u8fdb\u884c\u8d26\u6237\u8bbe\u7f6e\u3002\");\n            return;\n          }\n          setMainTab(btn);\n        });\n      });\n\n      document.querySelectorAll(\"#adminTabs .page-tab\").forEach(btn => {\n        btn.addEventListener(\"click\", () => {\n          setAdminSubTab(btn);\n          const targetId = btn.getAttribute(\"data-section\");\n          if (targetId === \"recordsSection\") {\n            renderQuoteRecords();\n            renderQuoteSummary();\n          }\n          if (targetId === \"userManagementSection\") {\n            renderUserTable();\n          }\n          if (targetId === \"projectSection\") {\n            refreshProjects();\n          }\n        });\n      });\n      initProcessTypeSelector();\n      updateAuthUI();\n      await refreshAuthState();\n    });\n  <\/script>\n\n\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>\u901a\u7528\u52a0\u5de5\u62a5\u4ef7\u8ba1\u7b97\u5668 \u901a\u7528\u52a0\u5de5\u62a5\u4ef7\u8ba1\u7b97\u5668 \u6839\u636e\u5de5\u827a\u9009\u62e9\u5bf9\u5e94\u6750\u6599\u4e0e\u8bbe\u5907\uff0c\u5feb\u901f\u4f30\u7b97\u6210\u672c\u4e0e\u5229\u6da6 \u8bf7\u5148\u767b\u5f55\u540e\u4f7f\u7528\u62a5\u4ef7\u529f\u80fd [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18],"tags":[],"class_list":["post-107","post","type-post","status-publish","format-standard","hentry","category-18"],"_links":{"self":[{"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/posts\/107","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/comments?post=107"}],"version-history":[{"count":6,"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/posts\/107\/revisions"}],"predecessor-version":[{"id":113,"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/posts\/107\/revisions\/113"}],"wp:attachment":[{"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/media?parent=107"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/categories?post=107"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/awakencraft.com\/index.php\/wp-json\/wp\/v2\/tags?post=107"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}