Add cart exports and sample request workflow
This commit is contained in:
137
app/globals.css
137
app/globals.css
@@ -49,6 +49,10 @@ a { color: inherit; text-decoration: none; }
|
||||
.site-header nav { display: flex; gap: 34px; font-size: 14px; color: #536078; }
|
||||
.site-header nav a:hover { color: var(--blue); }
|
||||
.header-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.cart-header-button { position: relative; min-height: 38px; padding: 7px 12px 7px 9px; display: inline-flex; align-items: center; gap: 5px; border: 1px solid #dbe4f1; border-radius: 10px; color: #536078; background: white; cursor: pointer; font-size: 10px; font-weight: 750; }
|
||||
.cart-header-button:hover { border-color: #a9c4f2; color: var(--blue); background: #f7faff; }
|
||||
.cart-header-button > span { font-size: 15px; line-height: 1; }
|
||||
.cart-header-button b { min-width: 17px; height: 17px; padding: 0 4px; display: inline-grid; place-items: center; border-radius: 999px; color: white; background: var(--blue); font-size: 9px; }
|
||||
.header-cta {
|
||||
border: 0; border-radius: 10px; padding: 11px 17px;
|
||||
background: var(--ink); color: white; cursor: pointer; font-weight: 650;
|
||||
@@ -124,8 +128,8 @@ a { color: inherit; text-decoration: none; }
|
||||
.step-kicker { margin: 0 0 7px; color: var(--blue); font-size: 11px; letter-spacing: .08em; font-weight: 800; text-transform: uppercase; }
|
||||
.question-step h2, .result-heading h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
|
||||
.step-help { margin: 7px 0 19px; color: #7c8799; font-size: 12px; }
|
||||
.choice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.choice-card { position: relative; min-height: 76px; padding: 14px 35px 14px 15px; display: grid; gap: 5px; text-align: left; border: 1px solid #dfe5ee; border-radius: 11px; background: white; cursor: pointer; }
|
||||
.choice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
|
||||
.choice-card { position: relative; min-height: 68px; padding: 11px 30px 11px 13px; display: grid; align-content: center; gap: 3px; text-align: left; border: 1px solid #dfe5ee; border-radius: 11px; background: white; cursor: pointer; }
|
||||
.choice-card:hover { border-color: #a8c3f4; background: #fbfdff; }
|
||||
.choice-card.selected { border-color: var(--blue); background: var(--blue-soft); box-shadow: inset 0 0 0 1px var(--blue); }
|
||||
.choice-card b { font-size: 13px; }
|
||||
@@ -375,12 +379,102 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.catalog-card-body > .product-save-button { margin-top: 5px; padding: 8px 10px; border: 1px solid #d8e3f3; border-radius: 8px; color: #185ee8; background: #f7faff; }
|
||||
.product-save-button { width: 100%; min-height: 33px; margin-top: 10px; padding: 8px 10px; display: inline-flex; align-items: center; justify-content: space-between; border: 1px solid #d8e3f3; border-radius: 8px; color: #185ee8; background: #f7faff; cursor: pointer; font-size: 10px; font-weight: 750; }
|
||||
.product-save-button:hover { border-color: #a9c4f2; color: #104ec6; background: #eef5ff; }
|
||||
.product-save-button span { font-size: 13px; line-height: 1; }
|
||||
.product-save-button.is-saving { visibility: hidden; }
|
||||
.product-save-toast { position: fixed; left: 50%; bottom: 24px; z-index: 120; transform: translateX(-50%); max-width: min(90vw, 360px); padding: 10px 15px; border: 1px solid #cbdaf2; border-radius: 999px; color: #174baf; background: rgba(248,251,255,.96); box-shadow: 0 12px 32px rgba(24,64,126,.18); font-size: 11px; font-weight: 750; text-align: center; backdrop-filter: blur(10px); }
|
||||
.add-cart-button { width: 100%; min-height: 33px; margin-top: 10px; padding: 8px 10px; display: inline-flex; align-items: center; justify-content: space-between; border: 1px solid #c9dafa; border-radius: 8px; color: #185ee8; background: #f4f8ff; cursor: pointer; font-size: 10px; font-weight: 750; }
|
||||
.add-cart-button:hover { border-color: #8eb2f2; color: #104ec6; background: #eaf2ff; }
|
||||
.add-cart-button span { font-size: 14px; line-height: 1; }
|
||||
.catalog-card-body > .add-cart-button { margin-top: 5px; }
|
||||
.precision-gear-card-body .add-cart-button { margin-top: 10px; }
|
||||
.recommendation-content .add-cart-button { margin-top: 10px; }
|
||||
.cart-toast { position: fixed; left: 50%; bottom: 24px; z-index: 120; transform: translateX(-50%); max-width: min(90vw, 380px); padding: 10px 15px; border: 1px solid #cbdaf2; border-radius: 999px; color: #174baf; background: rgba(248,251,255,.96); box-shadow: 0 12px 32px rgba(24,64,126,.18); font-size: 11px; font-weight: 750; text-align: center; backdrop-filter: blur(10px); }
|
||||
.cart-drawer-backdrop { position: fixed; inset: 0; z-index: 110; display: flex; justify-content: flex-end; background: rgba(7,14,29,.45); backdrop-filter: blur(4px); }
|
||||
.cart-drawer { width: min(430px, 100%); height: 100%; padding: 28px 24px 22px; display: flex; flex-direction: column; overflow: hidden; background: white; box-shadow: -18px 0 50px rgba(20,43,82,.2); }
|
||||
.cart-drawer-header { display: flex; align-items: flex-start; justify-content: space-between; padding-bottom: 17px; border-bottom: 1px solid #e5eaf1; }
|
||||
.cart-drawer-header .step-kicker { margin-bottom: 4px; }
|
||||
.cart-drawer-header h2 { margin: 0; color: var(--ink); font-size: 24px; letter-spacing: -.03em; }
|
||||
.cart-drawer-header h2 span { color: var(--blue); font-size: 16px; }
|
||||
.cart-close-button { width: 34px; height: 34px; border: 0; border-radius: 50%; color: #627086; background: #eef2f7; cursor: pointer; font-size: 20px; }
|
||||
.cart-list { flex: 1; display: grid; align-content: start; gap: 10px; padding: 16px 2px; overflow-y: auto; }
|
||||
.cart-item { min-width: 0; padding: 10px; display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 11px; border: 1px solid #dce5f2; border-radius: 11px; background: #f9fbfe; }
|
||||
.cart-item-image { width: 64px; height: 64px; display: grid; place-items: center; overflow: hidden; border-radius: 8px; background: #eef3fa; }
|
||||
.cart-item-image img { display: block; width: 100%; height: 100%; object-fit: contain; mix-blend-mode: multiply; }
|
||||
.cart-item-image span { color: #7ea1dd; font-weight: 800; }
|
||||
.cart-item-copy { min-width: 0; display: grid; align-content: start; gap: 3px; }
|
||||
.cart-item-copy small { color: var(--blue); font-size: 8px; font-weight: 750; }
|
||||
.cart-item-copy strong { overflow: hidden; color: var(--ink); font-size: 11px; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cart-item-copy code { overflow: hidden; color: #8995a6; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cart-item-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 5px; }
|
||||
.quantity-control { display: inline-flex; align-items: center; border: 1px solid #d7e1ee; border-radius: 7px; overflow: hidden; background: white; }
|
||||
.quantity-control button { width: 25px; height: 24px; padding: 0; border: 0; color: var(--blue); background: white; cursor: pointer; font-size: 15px; }
|
||||
.quantity-control span { min-width: 24px; color: #52617a; font-size: 10px; text-align: center; }
|
||||
.cart-remove-button { padding: 3px 0; border: 0; color: #a27b7b; background: transparent; cursor: pointer; font-size: 9px; }
|
||||
.cart-remove-button:hover { color: #d34f4f; }
|
||||
.cart-drawer-footer { padding-top: 14px; border-top: 1px solid #e5eaf1; }
|
||||
.cart-drawer-footer p { margin: 0 0 12px; color: #7f8b9e; font-size: 9px; line-height: 1.45; }
|
||||
.cart-drawer-footer > div { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
|
||||
.cart-drawer-footer .outline-button, .cart-drawer-footer .panel-primary { width: 100%; min-width: 0; min-height: 39px; padding: 0 6px; flex: none; font-size: 10px; line-height: 1; white-space: nowrap; }
|
||||
.cart-drawer-footer .panel-primary span { margin-left: 5px; font-size: 9px; }
|
||||
.cart-drawer-footer .cart-sample-button { grid-column: 1 / -1; color: white; background: #185ee8; }
|
||||
.cart-drawer-footer .cart-sample-button:hover { color: white; background: #124fc9; }
|
||||
.cart-drawer-footer .cart-sample-button:disabled, .cart-empty-actions .cart-sample-button:disabled { color: #aeb9c9; border-color: #dfe5ee; background: #f6f8fb; box-shadow: none; cursor: not-allowed; }
|
||||
.cart-export-button { color: var(--blue); border-color: #b9cdf1; }
|
||||
.cart-export-button:hover { color: #124ac0; border-color: var(--blue); background: #f4f8ff; }
|
||||
.cart-export-button:disabled { color: #aeb9c9; border-color: #dfe5ee; background: #f6f8fb; cursor: not-allowed; }
|
||||
.cart-pdf-button { color: #a85d2a; border-color: #edc9ac; }
|
||||
.cart-pdf-button:hover { color: #874317; border-color: #d99565; background: #fff8f2; }
|
||||
.cart-pdf-button:disabled { color: #bdb4ad; border-color: #e9e2dc; background: #faf8f6; cursor: not-allowed; }
|
||||
.cart-empty { margin: auto 0; padding: 30px 12px; display: grid; justify-items: center; gap: 9px; text-align: center; }
|
||||
.cart-empty > span { font-size: 36px; }
|
||||
.cart-empty strong { color: var(--ink); font-size: 16px; }
|
||||
.cart-empty p { max-width: 270px; margin: 0 0 8px; color: #7f8b9e; font-size: 10px; line-height: 1.6; }
|
||||
.cart-empty-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); width: 100%; gap: 7px; }
|
||||
.cart-empty-actions .panel-primary, .cart-empty-actions .cart-export-button { min-height: 42px; }
|
||||
.cart-empty-actions .cart-pdf-button { min-height: 42px; }
|
||||
.cart-empty-actions .outline-button, .cart-empty-actions .panel-primary { min-width: 0; padding: 0 6px; font-size: 10px; line-height: 1; white-space: nowrap; }
|
||||
.cart-empty-actions .cart-sample-button { grid-column: 1 / -1; }
|
||||
.cart-sample-scroll { flex: 1; overflow-y: auto; padding: 16px 2px; }
|
||||
.cart-sample-form { margin-top: 0; }
|
||||
.cart-sample-products { display: grid; gap: 6px; margin: 10px 0 14px; padding: 9px; border: 1px solid #dbe5f2; border-radius: 9px; background: #f8faff; }
|
||||
.cart-sample-products > div { min-width: 0; display: grid; gap: 2px; padding-bottom: 6px; border-bottom: 1px solid #e8eef6; }
|
||||
.cart-sample-products > div:last-child { padding-bottom: 0; border-bottom: 0; }
|
||||
.cart-sample-products span { overflow: hidden; color: var(--ink); font-size: 10px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cart-sample-products code { overflow: hidden; color: #8995a6; font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cart-sample-form-actions { display: grid; grid-template-columns: 1fr 1.4fr; gap: 8px; margin-top: 10px; }
|
||||
.cart-sample-form-actions .outline-button, .cart-sample-form-actions .panel-primary { width: 100%; min-width: 0; min-height: 39px; padding: 0 8px; font-size: 10px; white-space: nowrap; }
|
||||
.cart-sample-success { margin-top: 18px; }
|
||||
.cart-print-sheet { display: none; }
|
||||
|
||||
.cart-print-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; padding-bottom: 18px; border-bottom: 2px solid #185ee8; }
|
||||
.cart-print-header p { margin: 0 0 7px; color: #185ee8; font-size: 10pt; font-weight: 800; letter-spacing: .08em; }
|
||||
.cart-print-header h1 { margin: 0 0 6px; color: #17233a; font-size: 24pt; line-height: 1.1; }
|
||||
.cart-print-header span, .cart-print-header strong { color: #71809a; font-size: 9pt; }
|
||||
.cart-print-header strong { padding-top: 25px; font-weight: 600; }
|
||||
.cart-print-summary { width: 100%; margin-top: 18px; border-collapse: collapse; table-layout: fixed; color: #17233a; font-size: 8.5pt; }
|
||||
.cart-print-summary th, .cart-print-summary td { padding: 6px; border: 1px solid #dbe4ef; vertical-align: middle; overflow-wrap: anywhere; }
|
||||
.cart-print-summary th { color: white; background: #185ee8; font-weight: 800; text-align: center; }
|
||||
.cart-print-summary th:nth-child(1) { width: 12%; }
|
||||
.cart-print-summary th:nth-child(2) { width: 19%; }
|
||||
.cart-print-summary th:nth-child(3) { width: 14%; }
|
||||
.cart-print-summary th:nth-child(4) { width: 12%; }
|
||||
.cart-print-summary th:nth-child(5) { width: 7%; }
|
||||
.cart-print-summary th:nth-child(6) { width: 24%; }
|
||||
.cart-print-summary th:nth-child(7) { width: 12%; }
|
||||
.cart-print-summary td:nth-child(5) { text-align: center; font-weight: 800; }
|
||||
.cart-print-image-cell { height: 66px; text-align: center; }
|
||||
.cart-print-image-cell img { width: 78px; height: 54px; object-fit: contain; }
|
||||
.cart-print-preline { white-space: pre-line; }
|
||||
.cart-print-sheet > h2 { margin: 25px 0 10px; padding-bottom: 6px; border-bottom: 1px solid #cbd8ea; color: #185ee8; font-size: 15pt; }
|
||||
.cart-print-details { display: grid; gap: 12px; }
|
||||
.cart-print-product { break-inside: avoid; page-break-inside: avoid; border: 1px solid #dbe4ef; }
|
||||
.cart-print-product-heading { padding: 9px 11px; background: #f3f7fd; }
|
||||
.cart-print-product-heading h3 { margin: 0 0 4px; color: #17233a; font-size: 11pt; }
|
||||
.cart-print-product-heading span { color: #71809a; font-size: 8pt; }
|
||||
.cart-print-spec-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.cart-print-spec-grid div { min-height: 32px; padding: 6px 10px; border-top: 1px solid #e3eaf3; }
|
||||
.cart-print-spec-grid div:nth-child(4n + 1), .cart-print-spec-grid div:nth-child(4n + 2), .cart-print-spec-grid div:nth-child(4n + 3) { border-right: 1px solid #e3eaf3; }
|
||||
.cart-print-spec-grid span, .cart-print-spec-grid strong { display: block; overflow-wrap: anywhere; }
|
||||
.cart-print-spec-grid span { margin-bottom: 3px; color: #8492a7; font-size: 7.5pt; }
|
||||
.cart-print-spec-grid strong { color: #17233a; font-size: 8.5pt; line-height: 1.35; }
|
||||
.cart-print-footer { margin: 20px 0 0; padding-top: 10px; border-top: 1px solid #dbe4ef; color: #8492a7; font-size: 7.5pt; }
|
||||
.precision-gear-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, .8fr) 1.2fr;
|
||||
@@ -431,7 +525,6 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
|
||||
.precision-gear-card-body dl div { display: flex; justify-content: space-between; gap: 5px; padding: 2px 0; font-size: 8px; }
|
||||
.precision-gear-card-body dt { color: #8a95a7; }
|
||||
.precision-gear-card-body dd { max-width: 62%; margin: 0; overflow: hidden; color: #52617a; text-overflow: ellipsis; white-space: nowrap; text-align: right; font-weight: 700; }
|
||||
.precision-gear-card-body .product-save-button { margin-top: 10px; }
|
||||
.precision-gear-empty { margin-top: 15px; }
|
||||
.precision-gear-load-more { margin-top: 22px; }
|
||||
.catalog-empty { padding: 60px 20px; text-align: center; border: 1px dashed #cbd5e3; border-radius: 15px; background: white; }
|
||||
@@ -507,7 +600,7 @@ select:focus, input:focus, textarea:focus { border-color: var(--blue); box-shado
|
||||
.catalog-spec-table strong { font-size: 10.5px; line-height: 1.2; }
|
||||
.catalog-modal-actions { display: flex; align-items: center; gap: 18px; margin-top: 20px; }
|
||||
.catalog-modal-actions .panel-primary { flex: 0 0 auto; }
|
||||
.catalog-modal-actions .modal-save-button { width: auto; min-width: 124px; margin-top: 0; flex: 0 0 auto; }
|
||||
.catalog-modal-actions .modal-cart-button { width: auto; min-width: 124px; margin-top: 0; flex: 0 0 auto; }
|
||||
.catalog-modal-actions p { margin: 0; color: #8b95a5; font-size: 9px; line-height: 1.6; }
|
||||
.how-section { padding: 100px max(32px, calc((100vw - 1180px) / 2)); display: grid; grid-template-columns: .9fr 1.1fr; gap: 100px; color: white; background: linear-gradient(135deg, #0c1528, #14284e); }
|
||||
.eyebrow.light { color: #77a5ff; }
|
||||
@@ -535,6 +628,7 @@ footer > span { color: #9aa4b2; font-size: 10px; }
|
||||
.precision-gear-panel { grid-template-columns: 1fr; }
|
||||
.precision-gear-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.catalog-spec-table { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -548,6 +642,9 @@ footer > span { color: #9aa4b2; font-size: 10px; }
|
||||
.site-header nav a:hover { background: var(--blue-soft); }
|
||||
.header-cta { padding: 9px 11px; font-size: 11px; }
|
||||
.header-cta span { margin-left: 4px; }
|
||||
.cart-header-button { min-height: 36px; padding: 7px 7px; }
|
||||
.cart-header-button small { display: none; }
|
||||
.cart-header-button > span { font-size: 16px; }
|
||||
.mobile-menu-toggle { min-width: 52px; padding: 7px 8px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; border: 1px solid #dce5f2; border-radius: 10px; color: var(--blue); background: white; cursor: pointer; }
|
||||
.mobile-menu-toggle span { font-size: 18px; line-height: 1; }
|
||||
.mobile-menu-toggle small { font-size: 10px; font-weight: 700; }
|
||||
@@ -559,7 +656,12 @@ footer > span { color: #9aa4b2; font-size: 10px; }
|
||||
.selector-shell { border-radius: 16px; }
|
||||
.question-panel, .result-panel { padding: 20px 16px; }
|
||||
.welcome-panel { padding: 55px 20px 35px; }
|
||||
.choice-grid, .two-fields, .parameter-grid, .form-grid, .recommendation-grid { grid-template-columns: 1fr; }
|
||||
.two-fields, .parameter-grid, .form-grid, .recommendation-grid { grid-template-columns: 1fr; }
|
||||
.choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
||||
.choice-card { min-height: 62px; padding: 9px 27px 9px 10px; border-radius: 9px; }
|
||||
.choice-card b { font-size: 11px; }
|
||||
.choice-card small { font-size: 8px; line-height: 1.3; }
|
||||
.choice-check { right: 8px; top: 8px; width: 16px; height: 16px; font-size: 9px; }
|
||||
.torque-presets { grid-template-columns: repeat(4, 1fr); }
|
||||
.recommendation-card { min-height: 0; }
|
||||
.capability-strip { grid-template-columns: 1fr 1fr; gap: 24px 0; padding: 28px 16px; }
|
||||
@@ -619,7 +721,7 @@ footer > span { color: #9aa4b2; font-size: 10px; }
|
||||
.catalog-spec-table span { font-size: 7.5px; }
|
||||
.catalog-spec-table strong { font-size: 8.5px; line-height: 1.15; }
|
||||
.catalog-modal-actions { align-items: stretch; flex-direction: column; }
|
||||
.catalog-modal-actions .modal-save-button { width: 100%; }
|
||||
.catalog-modal-actions .modal-cart-button { width: 100%; }
|
||||
.products-section { padding: 75px 16px; }
|
||||
.how-section { grid-template-columns: 1fr; padding: 75px 20px; }
|
||||
footer { grid-template-columns: 1fr; justify-items: center; gap: 15px; }
|
||||
@@ -631,8 +733,11 @@ footer > span { color: #9aa4b2; font-size: 10px; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
.site-header, .hero-copy, .capability-strip, .products-section, .how-section, footer, .selector-topbar, .result-actions, .restart-button { display: none !important; }
|
||||
.hero { display: block; min-height: auto; padding: 0; background: white; }
|
||||
.selector-shell { box-shadow: none; border: 0; }
|
||||
.result-panel { padding: 20px; }
|
||||
@page { size: A4; margin: 12mm 10mm; }
|
||||
body.printing-cart { margin: 0; background: white; }
|
||||
body.printing-cart > * { visibility: hidden !important; }
|
||||
body.printing-cart .cart-print-sheet,
|
||||
body.printing-cart .cart-print-sheet * { visibility: visible !important; }
|
||||
body.printing-cart .cart-print-sheet { position: absolute; inset: 0; display: block !important; width: 100%; color: #17233a; background: white; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; }
|
||||
body.printing-cart .site-header, body.printing-cart .hero-copy, body.printing-cart .capability-strip, body.printing-cart .products-section, body.printing-cart .how-section, body.printing-cart footer, body.printing-cart .selector-topbar, body.printing-cart .result-actions, body.printing-cart .restart-button { display: none !important; }
|
||||
}
|
||||
|
||||
633
app/page.tsx
633
app/page.tsx
@@ -350,6 +350,216 @@ function Metric({ label, value }: { label: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
type CartItem = {
|
||||
product: CatalogProduct;
|
||||
quantity: number;
|
||||
source: string;
|
||||
};
|
||||
|
||||
const CART_STORAGE_KEY = "gaudelo-product-cart";
|
||||
|
||||
type CartExportImage = {
|
||||
base64: string;
|
||||
extension: "jpeg";
|
||||
};
|
||||
|
||||
const CART_EXPORT_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
const CART_IMAGE_SIZE_PX = 189; // 约 5 cm,按 96 DPI 换算
|
||||
|
||||
function getCartExportFilename() {
|
||||
const now = new Date();
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
return `高徳乐产品清单_${date}.xlsx`;
|
||||
}
|
||||
|
||||
function getFullSpecEntries(product: CatalogProduct) {
|
||||
return Object.entries(product.specs).filter(([, value]) => value);
|
||||
}
|
||||
|
||||
function getKeySpecText(product: CatalogProduct) {
|
||||
const operationAngle = product.specs["可操作角度"] ?? product.specs["操作角度范围"];
|
||||
return [
|
||||
...getFeaturedSpecs(product),
|
||||
...(operationAngle ? [["可操作角度", operationAngle] as const] : []),
|
||||
]
|
||||
.map(([label, value]) => `${label}:${value}`)
|
||||
.join("\n") || "暂无主要参数";
|
||||
}
|
||||
|
||||
function getFullSpecText(product: CatalogProduct) {
|
||||
return getFullSpecEntries(product)
|
||||
.map(([label, value]) => `${label}:${value}`)
|
||||
.join("\n") || "暂无详细参数";
|
||||
}
|
||||
|
||||
function compressCartImage(src: string) {
|
||||
return new Promise<CartExportImage | null>((resolve) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = CART_IMAGE_SIZE_PX;
|
||||
canvas.height = CART_IMAGE_SIZE_PX;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
context.fillStyle = "#ffffff";
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const padding = 10;
|
||||
const scale = Math.min(
|
||||
(canvas.width - padding * 2) / image.naturalWidth,
|
||||
(canvas.height - padding * 2) / image.naturalHeight,
|
||||
);
|
||||
const imageWidth = image.naturalWidth * scale;
|
||||
const imageHeight = image.naturalHeight * scale;
|
||||
context.drawImage(
|
||||
image,
|
||||
(canvas.width - imageWidth) / 2,
|
||||
(canvas.height - imageHeight) / 2,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
);
|
||||
resolve({
|
||||
base64: canvas.toDataURL("image/jpeg", 0.78),
|
||||
extension: "jpeg",
|
||||
});
|
||||
};
|
||||
image.onerror = () => resolve(null);
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function buildCartWorkbook(cartItems: CartItem[]) {
|
||||
const { default: ExcelJS } = await import("exceljs");
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
workbook.creator = "高徳乐 AI 舵机选型助手";
|
||||
workbook.created = new Date();
|
||||
|
||||
const summarySheet = workbook.addWorksheet("产品清单");
|
||||
const detailsSheet = workbook.addWorksheet("详细参数");
|
||||
summarySheet.views = [{ state: "frozen", ySplit: 1 }];
|
||||
detailsSheet.views = [{ state: "frozen", ySplit: 1 }];
|
||||
summarySheet.columns = [
|
||||
{ header: "产品图片", key: "image", width: 27 },
|
||||
{ header: "产品名称", key: "name", width: 34 },
|
||||
{ header: "产品编码", key: "code", width: 24 },
|
||||
{ header: "产品分类", key: "category", width: 18 },
|
||||
{ header: "购买数量", key: "quantity", width: 12 },
|
||||
{ header: "详细参数", key: "detailSpecs", width: 48 },
|
||||
];
|
||||
detailsSheet.columns = [
|
||||
{ header: "产品名称", key: "name", width: 34 },
|
||||
{ header: "产品编码", key: "code", width: 24 },
|
||||
{ header: "产品分类", key: "category", width: 18 },
|
||||
{ header: "购买数量", key: "quantity", width: 12 },
|
||||
{ header: "参数名称", key: "label", width: 22 },
|
||||
{ header: "参数值", key: "value", width: 48 },
|
||||
];
|
||||
|
||||
const headerStyle = {
|
||||
font: { bold: true, color: { argb: "FFFFFFFF" } },
|
||||
fill: { type: "pattern" as const, pattern: "solid" as const, fgColor: { argb: "FF185EE8" } },
|
||||
alignment: { vertical: "middle" as const, horizontal: "center" as const, wrapText: true },
|
||||
};
|
||||
summarySheet.getRow(1).eachCell((cell) => {
|
||||
Object.assign(cell, headerStyle);
|
||||
});
|
||||
detailsSheet.getRow(1).eachCell((cell) => {
|
||||
Object.assign(cell, headerStyle);
|
||||
});
|
||||
summarySheet.getRow(1).height = 26;
|
||||
detailsSheet.getRow(1).height = 26;
|
||||
|
||||
const imageCache = new Map<string, Promise<CartExportImage | null>>();
|
||||
const getImageAsset = (src: string) => {
|
||||
const cached = imageCache.get(src);
|
||||
if (cached) return cached;
|
||||
const pending = compressCartImage(src);
|
||||
imageCache.set(src, pending);
|
||||
return pending;
|
||||
};
|
||||
|
||||
for (const item of cartItems) {
|
||||
const product = item.product;
|
||||
const specEntries = getFullSpecEntries(product);
|
||||
const summaryRow = summarySheet.addRow({
|
||||
image: product.image ? "" : "暂无图片",
|
||||
name: product.name,
|
||||
code: product.code,
|
||||
category: product.category,
|
||||
quantity: item.quantity,
|
||||
detailSpecs: getFullSpecText(product),
|
||||
});
|
||||
summaryRow.height = Math.max(142, Math.min(240, 24 + specEntries.length * 15));
|
||||
summaryRow.eachCell((cell) => {
|
||||
cell.alignment = { vertical: "middle", wrapText: true };
|
||||
cell.border = {
|
||||
top: { style: "thin", color: { argb: "FFDDE6F3" } },
|
||||
bottom: { style: "thin", color: { argb: "FFDDE6F3" } },
|
||||
left: { style: "thin", color: { argb: "FFDDE6F3" } },
|
||||
right: { style: "thin", color: { argb: "FFDDE6F3" } },
|
||||
};
|
||||
});
|
||||
if (product.image) {
|
||||
const imageAsset = await getImageAsset(product.image);
|
||||
if (imageAsset) {
|
||||
const imageId = workbook.addImage(imageAsset);
|
||||
summarySheet.addImage(imageId, {
|
||||
tl: { col: 0.1, row: summaryRow.number - 1 + 0.08 },
|
||||
ext: { width: CART_IMAGE_SIZE_PX, height: CART_IMAGE_SIZE_PX },
|
||||
});
|
||||
summaryRow.getCell(1).value = null;
|
||||
} else {
|
||||
summaryRow.getCell(1).value = "图片加载失败";
|
||||
}
|
||||
}
|
||||
|
||||
for (const [label, value] of specEntries.length > 0 ? specEntries : [["暂无参数", ""]]) {
|
||||
const detailRow = detailsSheet.addRow({
|
||||
name: product.name,
|
||||
code: product.code,
|
||||
category: product.category,
|
||||
quantity: item.quantity,
|
||||
label,
|
||||
value,
|
||||
});
|
||||
detailRow.height = Math.max(24, Math.min(60, 18 + Math.ceil(String(value).length / 28) * 15));
|
||||
detailRow.eachCell((cell) => {
|
||||
cell.alignment = { vertical: "middle", wrapText: true };
|
||||
cell.border = {
|
||||
top: { style: "thin", color: { argb: "FFE5EBF3" } },
|
||||
bottom: { style: "thin", color: { argb: "FFE5EBF3" } },
|
||||
left: { style: "thin", color: { argb: "FFE5EBF3" } },
|
||||
right: { style: "thin", color: { argb: "FFE5EBF3" } },
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
summarySheet.autoFilter = { from: "A1", to: `F${Math.max(1, summarySheet.rowCount)}` };
|
||||
detailsSheet.autoFilter = { from: "A1", to: `F${Math.max(1, detailsSheet.rowCount)}` };
|
||||
return workbook;
|
||||
}
|
||||
|
||||
function downloadCartWorkbook(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
function isMobileDevice() {
|
||||
return /Android|iPhone|iPad|iPod|Mobi/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
}
|
||||
|
||||
/*
|
||||
type SaveProductRequest = {
|
||||
product: CatalogProduct;
|
||||
badge?: string;
|
||||
@@ -536,6 +746,7 @@ async function renderProductCardPng({ product, badge, subtitle, extraSpecs = []
|
||||
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("PNG生成失败"))), "image/png");
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
export default function Home() {
|
||||
const [stage, setStage] = useState(0);
|
||||
@@ -558,8 +769,22 @@ export default function Home() {
|
||||
const [precisionGearFiltersOpen, setPrecisionGearFiltersOpen] = useState(false);
|
||||
const [selectedCatalogProduct, setSelectedCatalogProduct] = useState<CatalogProduct | null>(null);
|
||||
const [catalogVisualMode, setCatalogVisualMode] = useState<CatalogVisualMode>("main");
|
||||
const [savingProductKey, setSavingProductKey] = useState("");
|
||||
const [saveProductMessage, setSaveProductMessage] = useState("");
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>(() => {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const storedCart = window.localStorage.getItem(CART_STORAGE_KEY);
|
||||
const parsedCart = storedCart ? JSON.parse(storedCart) as CartItem[] : [];
|
||||
return Array.isArray(parsedCart) ? parsedCart : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [cartOpen, setCartOpen] = useState(false);
|
||||
const [cartMessage, setCartMessage] = useState("");
|
||||
const [exportingCart, setExportingCart] = useState(false);
|
||||
const [cartSampleFormOpen, setCartSampleFormOpen] = useState(false);
|
||||
const [cartSampleSubmitted, setCartSampleSubmitted] = useState(false);
|
||||
const [cartSampleLeadId, setCartSampleLeadId] = useState("");
|
||||
|
||||
const matches = useMemo(() => getMatches(requirements), [requirements]);
|
||||
const topMatches = matches.slice(0, 3);
|
||||
@@ -613,6 +838,7 @@ export default function Home() {
|
||||
const selectedCatalogDimensionImage = selectedCatalogProduct
|
||||
? getCatalogProductDimensionImage(selectedCatalogProduct)
|
||||
: null;
|
||||
const cartCount = cartItems.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
useEffect(() => {
|
||||
function updateCatalogToolbarState() {
|
||||
@@ -642,65 +868,144 @@ export default function Home() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cartItems));
|
||||
} catch (error) {
|
||||
console.warn("保存购物车失败", error);
|
||||
}
|
||||
}, [cartItems]);
|
||||
|
||||
function openCatalogProduct(product: CatalogProduct) {
|
||||
setSelectedCatalogProduct(product);
|
||||
setCatalogVisualMode("main");
|
||||
}
|
||||
|
||||
async function saveProductCard(request: SaveProductRequest) {
|
||||
if (savingProductKey) return;
|
||||
const saveKey = `${request.product.id}-${request.badge ?? "product"}`;
|
||||
setSavingProductKey(saveKey);
|
||||
setSaveProductMessage("正在生成产品图片…");
|
||||
function addToCart(product: CatalogProduct, source = product.category) {
|
||||
setCartItems((current) => {
|
||||
const existingItem = current.find((item) => item.product.id === product.id);
|
||||
if (existingItem) {
|
||||
return current.map((item) => item.product.id === product.id
|
||||
? { ...item, quantity: item.quantity + 1, source }
|
||||
: item);
|
||||
}
|
||||
return [...current, { product, quantity: 1, source }];
|
||||
});
|
||||
setCartMessage(`${product.name} 已加入购物车`);
|
||||
setCartOpen(true);
|
||||
window.setTimeout(() => setCartMessage(""), 2200);
|
||||
}
|
||||
|
||||
function addTopMatchesToCart() {
|
||||
if (topMatches.length === 0) return;
|
||||
setCartItems((current) => {
|
||||
const next = [...current];
|
||||
topMatches.forEach((match) => {
|
||||
const existingIndex = next.findIndex((item) => item.product.id === match.product.id);
|
||||
if (existingIndex >= 0) {
|
||||
const existingItem = next[existingIndex];
|
||||
next[existingIndex] = {
|
||||
...existingItem,
|
||||
quantity: existingItem.quantity + 1,
|
||||
source: "AI初选结果",
|
||||
};
|
||||
} else {
|
||||
next.push({ product: match.product, quantity: 1, source: "AI初选结果" });
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
setCartMessage(`${topMatches.length} 款 AI 推荐型号已加入购物车`);
|
||||
setCartOpen(true);
|
||||
window.setTimeout(() => setCartMessage(""), 2400);
|
||||
}
|
||||
|
||||
function updateCartQuantity(productId: string, quantity: number) {
|
||||
if (quantity <= 0) {
|
||||
setCartItems((current) => current.filter((item) => item.product.id !== productId));
|
||||
return;
|
||||
}
|
||||
setCartItems((current) => current.map((item) => item.product.id === productId ? { ...item, quantity } : item));
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
setCartItems([]);
|
||||
setCartSampleFormOpen(false);
|
||||
setCartSampleSubmitted(false);
|
||||
}
|
||||
|
||||
function openCartSampleForm() {
|
||||
if (cartItems.length === 0) return;
|
||||
setCartSampleSubmitted(false);
|
||||
setCartSampleFormOpen(true);
|
||||
}
|
||||
|
||||
function submitCartSampleRequest(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const id = `GDL-S-${new Date().toISOString().slice(5, 10).replace("-", "")}-${String(Date.now()).slice(-4)}`;
|
||||
setCartSampleLeadId(id);
|
||||
setCartSampleSubmitted(true);
|
||||
}
|
||||
|
||||
async function saveCartList() {
|
||||
if (cartItems.length === 0 || exportingCart) return;
|
||||
|
||||
setExportingCart(true);
|
||||
setCartMessage("正在生成产品清单…");
|
||||
try {
|
||||
const blob = await renderProductCardPng(request);
|
||||
const safeName = `${request.product.code}-${request.product.name}`
|
||||
.replace(/[\\/:*?"<>|]/g, "-")
|
||||
.replace(/\s+/g, "-")
|
||||
.slice(0, 90);
|
||||
const fileName = `${safeName || request.product.id}.png`;
|
||||
const file = new File([blob], fileName, { type: "image/png" });
|
||||
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
|
||||
(navigator.maxTouchPoints > 0 && window.innerWidth <= 900);
|
||||
const canShareFile = typeof navigator.share === "function" &&
|
||||
(!navigator.canShare || navigator.canShare({ files: [file] }));
|
||||
const workbook = await buildCartWorkbook(cartItems);
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const filename = getCartExportFilename();
|
||||
const file = new File([buffer as BlobPart], filename, { type: CART_EXPORT_MIME });
|
||||
|
||||
if (isMobile && canShareFile) {
|
||||
if (isMobileDevice() && typeof navigator.share === "function") {
|
||||
const canShareFiles = typeof navigator.canShare !== "function" || navigator.canShare({ files: [file] });
|
||||
if (canShareFiles) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: `${request.product.name} 产品资料`,
|
||||
text: `高徳乐${request.product.category} · ${request.product.code}`,
|
||||
title: "高徳乐产品清单",
|
||||
text: "购物车产品清单",
|
||||
files: [file],
|
||||
});
|
||||
setSaveProductMessage("已打开系统分享面板");
|
||||
setCartMessage("已打开系统分享面板");
|
||||
return;
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
setSaveProductMessage("已取消保存");
|
||||
} else {
|
||||
throw error;
|
||||
if (error instanceof DOMException && error.name === "AbortError") return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
setSaveProductMessage("PNG图片已下载");
|
||||
}
|
||||
|
||||
downloadCartWorkbook(file, filename);
|
||||
setCartMessage("产品清单已下载");
|
||||
} catch (error) {
|
||||
console.error("保存产品参数图片失败", error);
|
||||
setSaveProductMessage("图片生成失败,请稍后重试");
|
||||
console.error("生成购物车清单失败", error);
|
||||
setCartMessage("产品清单生成失败,请稍后重试");
|
||||
} finally {
|
||||
setSavingProductKey("");
|
||||
window.setTimeout(() => setSaveProductMessage(""), 2800);
|
||||
setExportingCart(false);
|
||||
window.setTimeout(() => setCartMessage(""), 2400);
|
||||
}
|
||||
}
|
||||
|
||||
function exportCartPdf() {
|
||||
if (cartItems.length === 0) return;
|
||||
const previousTitle = document.title;
|
||||
document.body.classList.add("printing-cart");
|
||||
document.title = getCartExportFilename().replace(/\.xlsx$/, ".pdf");
|
||||
setCartOpen(false);
|
||||
let fallbackCleanupTimer = 0;
|
||||
const cleanupPrintMode = () => {
|
||||
window.clearTimeout(fallbackCleanupTimer);
|
||||
window.removeEventListener("afterprint", cleanupPrintMode);
|
||||
document.body.classList.remove("printing-cart");
|
||||
document.title = previousTitle;
|
||||
};
|
||||
window.addEventListener("afterprint", cleanupPrintMode);
|
||||
fallbackCleanupTimer = window.setTimeout(cleanupPrintMode, 30000);
|
||||
window.setTimeout(() => {
|
||||
window.print();
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function startSelection() {
|
||||
setMobileMenuOpen(false);
|
||||
setStage(1);
|
||||
@@ -731,11 +1036,196 @@ export default function Home() {
|
||||
|
||||
return (
|
||||
<main>
|
||||
{saveProductMessage && (
|
||||
<div className="product-save-toast" role="status" aria-live="polite">
|
||||
{saveProductMessage}
|
||||
{cartMessage && (
|
||||
<div className="cart-toast" role="status" aria-live="polite">
|
||||
{cartMessage}
|
||||
</div>
|
||||
)}
|
||||
{cartOpen && (
|
||||
<div
|
||||
className="cart-drawer-backdrop"
|
||||
role="presentation"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) setCartOpen(false);
|
||||
}}
|
||||
>
|
||||
<aside className="cart-drawer" role="dialog" aria-modal="true" aria-labelledby="cart-title">
|
||||
<div className="cart-drawer-header">
|
||||
<div>
|
||||
<p className="step-kicker">产品清单</p>
|
||||
<h2 id="cart-title">购物车 <span>{cartCount}</span></h2>
|
||||
</div>
|
||||
<button className="cart-close-button" type="button" onClick={() => setCartOpen(false)} aria-label="关闭购物车">×</button>
|
||||
</div>
|
||||
{cartItems.length > 0 ? (
|
||||
cartSampleFormOpen ? (
|
||||
<div className="cart-sample-scroll">
|
||||
{!cartSampleSubmitted ? (
|
||||
<form className="sample-form cart-sample-form" onSubmit={submitCartSampleRequest}>
|
||||
<div>
|
||||
<p className="step-kicker">购买样品申请</p>
|
||||
<h3>确认购物车样品</h3>
|
||||
<p>以下 {cartItems.length} 个型号、共 {cartCount} 件产品将作为本次样品申请内容。</p>
|
||||
</div>
|
||||
<div className="cart-sample-products">
|
||||
{cartItems.map((item) => (
|
||||
<div key={`sample-${item.product.id}`}>
|
||||
<span>{item.product.name}</span>
|
||||
<code>{item.product.code} · {item.quantity}件</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-grid">
|
||||
<label><span>公司名称</span><input required name="company" placeholder="请输入公司名称" /></label>
|
||||
<label><span>联系人</span><input required name="name" placeholder="姓名" /></label>
|
||||
<label><span>手机号</span><input required name="phone" inputMode="tel" placeholder="用于样品确认" /></label>
|
||||
<label><span>邮箱(选填)</span><input name="email" type="email" placeholder="接收申请结果" /></label>
|
||||
</div>
|
||||
<label className="full-field"><span>补充说明(选填)</span><textarea name="notes" placeholder="收货信息、使用场景、测试时间或其他要求" /></label>
|
||||
<div className="cart-sample-form-actions">
|
||||
<button className="outline-button" type="button" onClick={() => setCartSampleFormOpen(false)}>返回购物车</button>
|
||||
<button className="panel-primary" type="submit">提交购买样品申请 <span>→</span></button>
|
||||
</div>
|
||||
<small className="demo-disclaimer">当前为内部演示,不会向外部系统发送信息。</small>
|
||||
</form>
|
||||
) : (
|
||||
<div className="success-card cart-sample-success">
|
||||
<div className="success-icon">✓</div>
|
||||
<p className="step-kicker">样品申请已生成</p>
|
||||
<h3>申请编号 {cartSampleLeadId}</h3>
|
||||
<p>已记录购物车中的 {cartCount} 件样品。正式版上线后将自动通知销售与工程师跟进。</p>
|
||||
<button type="button" className="outline-button" onClick={() => setCartSampleFormOpen(false)}>返回购物车</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="cart-list">
|
||||
{cartItems.map((item) => (
|
||||
<div className="cart-item" key={item.product.id}>
|
||||
<div className="cart-item-image">
|
||||
{item.product.image ? <img src={item.product.image} alt="" /> : <span>G</span>}
|
||||
</div>
|
||||
<div className="cart-item-copy">
|
||||
<small>{item.source}</small>
|
||||
<strong>{item.product.name}</strong>
|
||||
<code>{item.product.code}</code>
|
||||
<div className="cart-item-actions">
|
||||
<div className="quantity-control" aria-label={`${item.product.name}数量`}>
|
||||
<button type="button" onClick={() => updateCartQuantity(item.product.id, item.quantity - 1)} aria-label="减少数量">−</button>
|
||||
<span>{item.quantity}</span>
|
||||
<button type="button" onClick={() => updateCartQuantity(item.product.id, item.quantity + 1)} aria-label="增加数量">+</button>
|
||||
</div>
|
||||
<button className="cart-remove-button" type="button" onClick={() => updateCartQuantity(item.product.id, 0)}>移除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="cart-drawer-footer">
|
||||
<p>共 {cartCount} 件产品,提交需求时可一并让工程师确认。</p>
|
||||
<div>
|
||||
<button className="outline-button" type="button" onClick={clearCart}>清空购物车</button>
|
||||
<button
|
||||
className="outline-button cart-export-button"
|
||||
type="button"
|
||||
onClick={saveCartList}
|
||||
disabled={cartItems.length === 0 || exportingCart}
|
||||
aria-label={exportingCart ? "正在生成产品清单" : "保存购物车产品列表"}
|
||||
>
|
||||
{exportingCart ? "生成中…" : "保存列表"}
|
||||
</button>
|
||||
<button
|
||||
className="outline-button cart-pdf-button"
|
||||
type="button"
|
||||
onClick={exportCartPdf}
|
||||
disabled={cartItems.length === 0}
|
||||
aria-label="导出购物车 PDF"
|
||||
>
|
||||
导出 PDF
|
||||
</button>
|
||||
<button className="panel-primary" type="button" onClick={() => { setCartOpen(false); startSelection(); }}>继续选型 <span>→</span></button>
|
||||
<button className="panel-primary cart-sample-button" type="button" onClick={openCartSampleForm}>购买样品申请 <span>→</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<div className="cart-empty">
|
||||
<span>🛒</span>
|
||||
<strong>购物车还是空的</strong>
|
||||
<p>浏览产品目录或完成 AI 初选,把需要确认的型号加入这里。</p>
|
||||
<div className="cart-empty-actions">
|
||||
<button className="panel-primary" type="button" onClick={() => setCartOpen(false)}>继续浏览产品 <span>→</span></button>
|
||||
<button className="outline-button cart-export-button" type="button" onClick={saveCartList} disabled aria-label="购物车为空,保存列表不可用">保存列表</button>
|
||||
<button className="outline-button cart-pdf-button" type="button" onClick={exportCartPdf} disabled aria-label="购物车为空,导出 PDF 不可用">导出 PDF</button>
|
||||
<button className="panel-primary cart-sample-button" type="button" onClick={openCartSampleForm} disabled aria-label="购物车为空,购买样品申请不可用">购买样品申请 <span>→</span></button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
<section className="cart-print-sheet" aria-hidden="true">
|
||||
<div className="cart-print-header">
|
||||
<div>
|
||||
<p>GAUDELOT · 高徳乐</p>
|
||||
<h1>产品清单</h1>
|
||||
<span>购物车共 {cartItems.length} 个型号 / {cartCount} 件产品</span>
|
||||
</div>
|
||||
<strong>{getCartExportFilename().replace(/\.xlsx$/, ".pdf")}</strong>
|
||||
</div>
|
||||
<table className="cart-print-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>产品图片</th>
|
||||
<th>产品名称</th>
|
||||
<th>产品编码</th>
|
||||
<th>产品分类</th>
|
||||
<th>数量</th>
|
||||
<th>主要参数</th>
|
||||
<th>产品尺寸</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cartItems.map((item) => (
|
||||
<tr key={`print-summary-${item.product.id}`}>
|
||||
<td className="cart-print-image-cell">
|
||||
{item.product.image ? <img src={item.product.image} alt={item.product.name} /> : "暂无图片"}
|
||||
</td>
|
||||
<td>{item.product.name}</td>
|
||||
<td>{item.product.code}</td>
|
||||
<td>{item.product.category}</td>
|
||||
<td>{item.quantity}</td>
|
||||
<td className="cart-print-preline">{getKeySpecText(item.product)}</td>
|
||||
<td>{item.product.specs["产品尺寸"] ?? item.product.specs["外形尺寸"] ?? "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>详细参数</h2>
|
||||
<div className="cart-print-details">
|
||||
{cartItems.map((item) => (
|
||||
<section className="cart-print-product" key={`print-detail-${item.product.id}`}>
|
||||
<div className="cart-print-product-heading">
|
||||
<div>
|
||||
<h3>{item.product.name}</h3>
|
||||
<span>{item.product.code} · {item.product.category} · 数量 {item.quantity}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="cart-print-spec-grid">
|
||||
{getFullSpecEntries(item.product).map(([label, value]) => (
|
||||
<div key={`${item.product.id}-${label}`}>
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
<p className="cart-print-footer">高徳乐 AI 舵机选型助手 · 本清单用于客户沟通、选型确认和打印留档,最终参数以正式技术文件为准。</p>
|
||||
</section>
|
||||
<header className="site-header">
|
||||
<a className="brand" href="#top" aria-label="高德乐 AI选型首页">
|
||||
<span className="brand-wordmark">
|
||||
@@ -750,6 +1240,11 @@ export default function Home() {
|
||||
<a href="#how" onClick={() => setMobileMenuOpen(false)}>服务流程</a>
|
||||
</nav>
|
||||
<div className="header-actions">
|
||||
<button className="cart-header-button" type="button" onClick={() => setCartOpen(true)} aria-label={`打开购物车,当前${cartCount}件产品`}>
|
||||
<span aria-hidden="true">🛒</span>
|
||||
<small>购物车</small>
|
||||
<b>{cartCount}</b>
|
||||
</button>
|
||||
<button className="header-cta" type="button" onClick={startSelection}>
|
||||
开始选型 <span>→</span>
|
||||
</button>
|
||||
@@ -1185,17 +1680,12 @@ export default function Home() {
|
||||
<small>价格与交期按用量确认</small>
|
||||
</div>
|
||||
<button
|
||||
className={`product-save-button ${savingProductKey.startsWith(`${match.product.id}-`) ? "is-saving" : ""}`}
|
||||
className="add-cart-button"
|
||||
type="button"
|
||||
onClick={() => saveProductCard({
|
||||
product: match.product,
|
||||
badge: index === 0 ? (match.coreCompatible ? "首选推荐" : "待复核候选") : `${match.confidence}% 匹配`,
|
||||
subtitle: `${match.product.category} · ${match.product.size || "尺寸待复核"}`,
|
||||
extraSpecs: [["AI匹配度", `${match.confidence}%`]],
|
||||
})}
|
||||
aria-label={`保存${match.product.name}产品参数图片`}
|
||||
onClick={() => addToCart(match.product, "AI初选结果")}
|
||||
aria-label={`将${match.product.name}加入购物车`}
|
||||
>
|
||||
{savingProductKey.startsWith(`${match.product.id}-`) ? "正在生成…" : "保存产品参数图片"} <span>↓</span>
|
||||
加入到购物车 <span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1216,7 +1706,7 @@ export default function Home() {
|
||||
<button className="panel-primary" type="button" onClick={() => setShowSampleForm(true)}>
|
||||
申请样品与工程师复核 <span>→</span>
|
||||
</button>
|
||||
<button className="outline-button" type="button" onClick={() => window.print()}>打印 / 保存方案</button>
|
||||
<button className="outline-button" type="button" onClick={addTopMatchesToCart}>三款加入购物车</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1385,16 +1875,12 @@ export default function Home() {
|
||||
查看完整参数 <span>→</span>
|
||||
</button>
|
||||
<button
|
||||
className={`product-save-button ${savingProductKey === `${product.id}-${product.category}` ? "is-saving" : ""}`}
|
||||
className="add-cart-button"
|
||||
type="button"
|
||||
onClick={() => saveProductCard({
|
||||
product,
|
||||
badge: product.category,
|
||||
subtitle: "完整产品参数",
|
||||
})}
|
||||
aria-label={`保存${product.name}产品参数图片`}
|
||||
onClick={() => addToCart(product)}
|
||||
aria-label={`将${product.name}加入购物车`}
|
||||
>
|
||||
{savingProductKey === `${product.id}-${product.category}` ? "正在生成…" : "保存产品参数图片"} <span>↓</span>
|
||||
加入到购物车 <span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1493,17 +1979,12 @@ export default function Home() {
|
||||
))}
|
||||
</dl>
|
||||
<button
|
||||
className={`product-save-button ${savingProductKey === `${product.id}-${product.subCategory}` ? "is-saving" : ""}`}
|
||||
className="add-cart-button"
|
||||
type="button"
|
||||
onClick={() => saveProductCard({
|
||||
product,
|
||||
badge: "精密齿轮配件",
|
||||
subtitle: product.subCategory,
|
||||
extraSpecs: [["产品子类", product.subCategory]],
|
||||
})}
|
||||
aria-label={`保存${product.name}产品参数图片`}
|
||||
onClick={() => addToCart(product, "精密齿轮配件")}
|
||||
aria-label={`将${product.name}加入购物车`}
|
||||
>
|
||||
{savingProductKey === `${product.id}-${product.subCategory}` ? "正在生成…" : "保存产品参数图片"} <span>↓</span>
|
||||
加入到购物车 <span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
@@ -1611,16 +2092,12 @@ export default function Home() {
|
||||
用AI继续确认需求 <span>→</span>
|
||||
</button>
|
||||
<button
|
||||
className={`product-save-button modal-save-button ${savingProductKey === `${selectedCatalogProduct.id}-${selectedCatalogProduct.category}` ? "is-saving" : ""}`}
|
||||
className="add-cart-button modal-cart-button"
|
||||
type="button"
|
||||
onClick={() => saveProductCard({
|
||||
product: selectedCatalogProduct,
|
||||
badge: selectedCatalogProduct.category,
|
||||
subtitle: "完整产品参数",
|
||||
})}
|
||||
aria-label={`保存${selectedCatalogProduct.name}产品参数图片`}
|
||||
onClick={() => addToCart(selectedCatalogProduct)}
|
||||
aria-label={`将${selectedCatalogProduct.name}加入购物车`}
|
||||
>
|
||||
{savingProductKey === `${selectedCatalogProduct.id}-${selectedCatalogProduct.category}` ? "正在生成…" : "保存产品参数图片"} <span>↓</span>
|
||||
加入到购物车 <span>+</span>
|
||||
</button>
|
||||
<p>具体价格、交期及最终适配结果以工程师确认和正式文件为准。</p>
|
||||
</div>
|
||||
|
||||
985
package-lock.json
generated
985
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "0.45.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user