2049 lines
77 KiB
HTML
2049 lines
77 KiB
HTML
{% extends "ewoooc_base.html" %}
|
||
|
||
{% block title %}PChome 商品比價決策台 - EwoooC{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.price-tool-page {
|
||
color: var(--momo-text-primary);
|
||
}
|
||
|
||
.price-tool-head {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.82fr);
|
||
gap: 18px;
|
||
align-items: stretch;
|
||
padding: var(--momo-space-4) var(--momo-space-5);
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
}
|
||
|
||
.price-tool-head h2 {
|
||
margin: 0;
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-display);
|
||
font-size: var(--momo-text-headline);
|
||
font-weight: 800;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.price-tool-head p,
|
||
.price-tool-page .text-muted {
|
||
color: var(--momo-text-secondary) !important;
|
||
}
|
||
|
||
.price-hero-copy {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.price-hero-kpis {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.price-hero-kpi {
|
||
min-height: 92px;
|
||
padding: 12px;
|
||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||
border-radius: var(--momo-radius-md);
|
||
background: rgba(250, 247, 240, 0.72);
|
||
}
|
||
|
||
.price-hero-kpi strong {
|
||
display: block;
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: 1.55rem;
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
}
|
||
|
||
.price-hero-kpi span {
|
||
display: block;
|
||
margin-top: 7px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.76rem;
|
||
font-weight: 850;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.price-command-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 0.9fr);
|
||
gap: var(--momo-space-4);
|
||
margin-bottom: var(--momo-space-4);
|
||
}
|
||
|
||
.price-next-action,
|
||
.price-readiness-panel,
|
||
.price-result-panel {
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
}
|
||
|
||
.price-next-action {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||
gap: 14px;
|
||
align-items: center;
|
||
padding: 16px;
|
||
background: rgba(242, 178, 90, 0.16);
|
||
border-color: rgba(172, 92, 58, 0.2);
|
||
}
|
||
|
||
.price-next-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 999px;
|
||
background: rgba(172, 92, 58, 0.14);
|
||
color: var(--momo-warm-rust);
|
||
}
|
||
|
||
.price-next-title {
|
||
display: block;
|
||
color: var(--momo-text-primary);
|
||
font-size: 1rem;
|
||
font-weight: 900;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.price-next-reason {
|
||
display: block;
|
||
margin-top: 3px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.price-readiness-panel,
|
||
.price-result-panel {
|
||
padding: 14px;
|
||
}
|
||
|
||
.price-decision-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: var(--momo-space-4);
|
||
}
|
||
|
||
.price-decision-card {
|
||
display: grid;
|
||
gap: 8px;
|
||
min-height: 126px;
|
||
padding: 13px;
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
background: rgba(255, 255, 255, 0.82);
|
||
box-shadow: var(--momo-shadow-soft);
|
||
}
|
||
|
||
.price-decision-card.is-active {
|
||
border-color: rgba(172, 92, 58, 0.34);
|
||
background: rgba(242, 178, 90, 0.16);
|
||
}
|
||
|
||
.price-decision-card.is-ready {
|
||
border-color: rgba(46, 125, 91, 0.24);
|
||
background: rgba(235, 248, 241, 0.72);
|
||
}
|
||
|
||
.price-decision-card.is-blocked {
|
||
border-color: rgba(200, 81, 58, 0.24);
|
||
background: rgba(255, 244, 239, 0.78);
|
||
}
|
||
|
||
.price-decision-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.73rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.price-decision-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 999px;
|
||
background: rgba(42, 37, 32, 0.08);
|
||
color: var(--momo-warm-rust);
|
||
}
|
||
|
||
.price-decision-value {
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-display);
|
||
font-size: 0.98rem;
|
||
font-weight: 900;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.price-decision-detail {
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.76rem;
|
||
font-weight: 760;
|
||
line-height: 1.42;
|
||
}
|
||
|
||
.price-workflow-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-bottom: var(--momo-space-4);
|
||
}
|
||
|
||
.price-workflow-step {
|
||
display: grid;
|
||
grid-template-columns: auto minmax(0, 1fr);
|
||
gap: 10px;
|
||
align-items: center;
|
||
min-height: 70px;
|
||
padding: 11px;
|
||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||
border-radius: var(--momo-radius-md);
|
||
background: rgba(250, 247, 240, 0.6);
|
||
}
|
||
|
||
.price-workflow-step.is-current {
|
||
border-color: rgba(172, 92, 58, 0.34);
|
||
background: rgba(242, 178, 90, 0.16);
|
||
}
|
||
|
||
.price-workflow-step.is-done {
|
||
border-color: rgba(46, 125, 91, 0.24);
|
||
background: rgba(235, 248, 241, 0.72);
|
||
}
|
||
|
||
.price-workflow-step strong {
|
||
display: block;
|
||
color: var(--momo-text-primary);
|
||
font-size: 0.82rem;
|
||
font-weight: 900;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.price-workflow-step span:last-child {
|
||
display: block;
|
||
margin-top: 2px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.7rem;
|
||
font-weight: 780;
|
||
}
|
||
|
||
.price-workflow-index {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 999px;
|
||
background: rgba(172, 92, 58, 0.13);
|
||
color: var(--momo-warm-rust);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: 0.78rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.price-panel-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.78rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.price-readiness-row,
|
||
.price-risk-row {
|
||
display: grid;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.price-readiness-row:last-child,
|
||
.price-risk-row:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.price-readiness-meta,
|
||
.price-risk-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.78rem;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.price-readiness-track,
|
||
.price-risk-track {
|
||
overflow: hidden;
|
||
height: 8px;
|
||
border-radius: 999px;
|
||
background: rgba(42, 37, 32, 0.08);
|
||
}
|
||
|
||
.price-readiness-bar,
|
||
.price-risk-bar {
|
||
display: block;
|
||
width: 0;
|
||
height: 100%;
|
||
border-radius: inherit;
|
||
background: var(--momo-warm-caramel);
|
||
transition: width 0.2s ease;
|
||
}
|
||
|
||
.price-readiness-bar.is-pchome,
|
||
.price-risk-bar.is-good {
|
||
background: #2e7d5b;
|
||
}
|
||
|
||
.price-readiness-bar.is-momo,
|
||
.price-risk-bar.is-watch {
|
||
background: #d19a2a;
|
||
}
|
||
|
||
.price-risk-bar.is-urgent {
|
||
background: #c8513a;
|
||
}
|
||
|
||
.price-operation-card {
|
||
height: 100%;
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
}
|
||
|
||
.price-step-head {
|
||
background: var(--momo-bg-paper);
|
||
border-bottom: 1px solid var(--momo-border-light);
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-display);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.price-count-badge,
|
||
.price-result-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 22px;
|
||
padding: 3px 8px;
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-sm);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: var(--momo-text-label);
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
}
|
||
|
||
.price-count-badge.is-muted,
|
||
.price-result-badge.is-muted {
|
||
background: var(--momo-tag-muted-bg);
|
||
border-color: var(--momo-tag-muted-border);
|
||
color: var(--momo-tag-muted-text);
|
||
}
|
||
|
||
.price-count-badge.is-pchome,
|
||
.price-result-badge.is-pchome {
|
||
background: var(--momo-info-bg);
|
||
border-color: var(--momo-info-border);
|
||
color: var(--momo-info-text);
|
||
}
|
||
|
||
.price-count-badge.is-momo,
|
||
.price-result-badge.is-momo {
|
||
background: var(--momo-warning-bg);
|
||
border-color: var(--momo-warning-border);
|
||
color: var(--momo-warning-text);
|
||
}
|
||
|
||
.price-stat-card {
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
}
|
||
|
||
.price-stat-card h4 {
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-weight: 800;
|
||
}
|
||
|
||
.price-stat-card.is-pchome h4 {
|
||
color: var(--momo-info-text);
|
||
}
|
||
|
||
.price-stat-card.is-momo h4 {
|
||
color: var(--momo-warning-text);
|
||
}
|
||
|
||
.price-stat-card.is-diff h4 {
|
||
color: var(--momo-text-primary);
|
||
}
|
||
|
||
.price-result-summary-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(260px, 0.92fr) minmax(0, 1.35fr);
|
||
gap: 12px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.price-result-callout {
|
||
display: grid;
|
||
gap: 8px;
|
||
padding: 14px;
|
||
border: 1px solid rgba(172, 92, 58, 0.22);
|
||
border-radius: var(--momo-radius-md);
|
||
background: rgba(242, 178, 90, 0.14);
|
||
}
|
||
|
||
.price-result-callout strong {
|
||
color: var(--momo-text-primary);
|
||
font-size: 1rem;
|
||
font-weight: 900;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.price-result-callout span {
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.8rem;
|
||
font-weight: 760;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.price-result-matrix {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.price-result-matrix-card {
|
||
min-height: 92px;
|
||
padding: 11px;
|
||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||
border-radius: var(--momo-radius-md);
|
||
background: rgba(255, 255, 255, 0.72);
|
||
}
|
||
|
||
.price-result-matrix-card strong {
|
||
display: block;
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: 1.45rem;
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
}
|
||
|
||
.price-result-matrix-card span {
|
||
display: block;
|
||
margin-top: 7px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.74rem;
|
||
font-weight: 850;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.price-note {
|
||
padding: 10px 12px;
|
||
background: var(--momo-info-bg);
|
||
border: 1px solid var(--momo-info-border);
|
||
border-radius: var(--momo-radius-md);
|
||
color: var(--momo-info-text);
|
||
}
|
||
|
||
.price-note.is-unit {
|
||
background: var(--momo-success-bg);
|
||
border-color: var(--momo-success-border);
|
||
color: var(--momo-success-text);
|
||
}
|
||
|
||
.price-note.is-review {
|
||
background: rgba(255, 248, 231, 0.95);
|
||
border-color: rgba(210, 158, 58, 0.28);
|
||
color: #7a5209;
|
||
}
|
||
|
||
.price-unit-list,
|
||
.price-review-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.price-unit-item {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 10px;
|
||
align-items: center;
|
||
padding: 8px 10px;
|
||
border: 1px solid rgba(46, 125, 91, 0.2);
|
||
border-radius: var(--momo-radius-sm);
|
||
background: rgba(255, 255, 255, 0.45);
|
||
}
|
||
|
||
.price-unit-name {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
color: var(--momo-text-primary);
|
||
font-size: 0.82rem;
|
||
font-weight: 850;
|
||
line-height: 1.35;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.price-unit-metric {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: var(--momo-success-text);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: var(--momo-text-label);
|
||
font-weight: 900;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.price-review-item {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
gap: 12px;
|
||
align-items: start;
|
||
padding: 10px;
|
||
border: 1px solid rgba(210, 158, 58, 0.24);
|
||
border-radius: var(--momo-radius-sm);
|
||
background: rgba(255, 255, 255, 0.58);
|
||
}
|
||
|
||
.price-review-compare {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.price-review-store {
|
||
min-width: 0;
|
||
padding: 8px;
|
||
border: 1px solid rgba(42, 37, 32, 0.1);
|
||
border-radius: var(--momo-radius-sm);
|
||
background: rgba(250, 247, 240, 0.64);
|
||
}
|
||
|
||
.price-review-store.is-pchome {
|
||
border-color: rgba(46, 125, 91, 0.2);
|
||
background: rgba(235, 248, 241, 0.62);
|
||
}
|
||
|
||
.price-review-store.is-momo {
|
||
border-color: rgba(201, 100, 66, 0.2);
|
||
background: rgba(255, 244, 239, 0.58);
|
||
}
|
||
|
||
.price-review-platform {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 0.68rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.price-review-name {
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
min-height: 2.5em;
|
||
margin-top: 5px;
|
||
overflow: hidden;
|
||
color: var(--momo-text-primary);
|
||
font-size: 0.78rem;
|
||
font-weight: 850;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.price-review-price {
|
||
display: block;
|
||
margin-top: 6px;
|
||
color: var(--momo-text-primary);
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: 0.88rem;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.price-review-actions {
|
||
display: grid;
|
||
gap: 7px;
|
||
min-width: 128px;
|
||
}
|
||
|
||
.price-table-head th {
|
||
background: var(--momo-bg-paper) !important;
|
||
color: var(--momo-text-secondary) !important;
|
||
font-family: var(--momo-font-mono, monospace);
|
||
font-size: var(--momo-text-label);
|
||
font-weight: 800;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.price-action-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 24px;
|
||
padding: 4px 8px;
|
||
border-radius: var(--momo-radius-sm);
|
||
font-size: 0.72rem;
|
||
font-weight: 900;
|
||
line-height: 1.2;
|
||
white-space: normal;
|
||
}
|
||
|
||
.price-action-pill.is-urgent {
|
||
background: var(--momo-danger-bg);
|
||
color: var(--momo-danger-text);
|
||
border: 1px solid var(--momo-danger-border);
|
||
}
|
||
|
||
.price-action-pill.is-good {
|
||
background: var(--momo-success-bg);
|
||
color: var(--momo-success-text);
|
||
border: 1px solid var(--momo-success-border);
|
||
}
|
||
|
||
.price-action-pill.is-watch {
|
||
background: var(--momo-info-bg);
|
||
color: var(--momo-info-text);
|
||
border: 1px solid var(--momo-info-border);
|
||
}
|
||
|
||
.price-toast {
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
min-width: min(300px, calc(100vw - 40px));
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-md);
|
||
background: var(--momo-bg-elevated);
|
||
color: var(--momo-text-primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.price-toast--success {
|
||
background: var(--momo-success-bg);
|
||
border-color: var(--momo-success-border);
|
||
color: var(--momo-success-text);
|
||
}
|
||
|
||
.price-toast--danger {
|
||
background: var(--momo-danger-bg);
|
||
border-color: var(--momo-danger-border);
|
||
color: var(--momo-danger-text);
|
||
}
|
||
|
||
.price-toast--warning {
|
||
background: var(--momo-warning-bg);
|
||
border-color: var(--momo-warning-border);
|
||
color: var(--momo-warning-text);
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.price-tool-head {
|
||
grid-template-columns: 1fr;
|
||
padding: var(--momo-space-4);
|
||
}
|
||
|
||
.price-hero-kpis,
|
||
.price-decision-grid,
|
||
.price-workflow-strip,
|
||
.price-result-summary-grid,
|
||
.price-result-matrix {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.price-command-grid,
|
||
.price-next-action {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.price-next-action .btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.price-unit-item {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.price-review-item,
|
||
.price-review-compare,
|
||
.price-review-actions {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.price-unit-metric {
|
||
justify-content: flex-start;
|
||
white-space: normal;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid py-4 price-tool-page">
|
||
<header class="price-tool-head mb-4">
|
||
<div class="price-hero-copy">
|
||
<h2><i class="fas fa-balance-scale me-2"></i>PChome 商品比價決策台</h2>
|
||
<p class="text-muted mb-0">PChome 反查 MOMO:確認同款、判斷價差、決定下一步。</p>
|
||
</div>
|
||
<div class="price-hero-kpis" aria-label="比價核心狀態">
|
||
<div class="price-hero-kpi">
|
||
<strong id="heroPchomeCount">0</strong>
|
||
<span>PChome 商品</span>
|
||
</div>
|
||
<div class="price-hero-kpi">
|
||
<strong id="heroMomoCount">0</strong>
|
||
<span>MOMO 可用候選</span>
|
||
</div>
|
||
<div class="price-hero-kpi">
|
||
<strong id="heroDecisionCount">0</strong>
|
||
<span>已產生決策</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="price-command-grid" aria-label="比價操作總覽">
|
||
<div class="price-next-action">
|
||
<span class="price-next-icon"><i class="fas fa-location-arrow"></i></span>
|
||
<span>
|
||
<strong class="price-next-title" id="priceNextActionTitle">今天先做:選擇要檢查的商品範圍</strong>
|
||
<span class="price-next-reason" id="priceNextActionReason">請先選品牌或輸入關鍵字,系統才知道要抓哪一批 PChome 商品。</span>
|
||
</span>
|
||
<button type="button" class="btn btn-primary btn-sm" id="priceNextActionButton">
|
||
輸入關鍵字
|
||
</button>
|
||
</div>
|
||
<div class="price-readiness-panel" aria-label="資料準備狀態">
|
||
<div class="price-panel-title">
|
||
<span><i class="fas fa-clipboard-check me-1"></i>資料準備狀態</span>
|
||
<span id="priceReadySummary">尚未開始</span>
|
||
</div>
|
||
<div class="price-readiness-row">
|
||
<div class="price-readiness-meta"><span>PChome 商品</span><strong id="pricePchomeReadyText">0 筆</strong></div>
|
||
<div class="price-readiness-track"><span class="price-readiness-bar is-pchome" id="pricePchomeReadyBar"></span></div>
|
||
</div>
|
||
<div class="price-readiness-row">
|
||
<div class="price-readiness-meta"><span>MOMO 商品</span><strong id="priceMomoReadyText">0 筆</strong></div>
|
||
<div class="price-readiness-track"><span class="price-readiness-bar is-momo" id="priceMomoReadyBar"></span></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="price-decision-grid" id="priceDecisionGrid" aria-label="目前卡點與下一步">
|
||
<article class="price-decision-card is-active" id="decisionScopeCard">
|
||
<div class="price-decision-top">
|
||
<span>檢查範圍</span>
|
||
<span class="price-decision-icon"><i class="fas fa-magnifying-glass"></i></span>
|
||
</div>
|
||
<div class="price-decision-value" id="decisionScopeValue">尚未選擇</div>
|
||
<div class="price-decision-detail" id="decisionScopeDetail">先選品牌或輸入商品關鍵字。</div>
|
||
</article>
|
||
<article class="price-decision-card" id="decisionPchomeCard">
|
||
<div class="price-decision-top">
|
||
<span>PChome</span>
|
||
<span class="price-decision-icon"><i class="fas fa-cart-shopping"></i></span>
|
||
</div>
|
||
<div class="price-decision-value" id="decisionPchomeValue">等待商品</div>
|
||
<div class="price-decision-detail" id="decisionPchomeDetail">取得商品後,系統才知道要比哪一批。</div>
|
||
</article>
|
||
<article class="price-decision-card" id="decisionMomoCard">
|
||
<div class="price-decision-top">
|
||
<span>MOMO</span>
|
||
<span class="price-decision-icon"><i class="fas fa-store"></i></span>
|
||
</div>
|
||
<div class="price-decision-value" id="decisionMomoValue">等待候選</div>
|
||
<div class="price-decision-detail" id="decisionMomoDetail">會自動分成同款、單位價、需確認。</div>
|
||
</article>
|
||
<article class="price-decision-card" id="decisionResultCard">
|
||
<div class="price-decision-top">
|
||
<span>比價結果</span>
|
||
<span class="price-decision-icon"><i class="fas fa-chart-simple"></i></span>
|
||
</div>
|
||
<div class="price-decision-value" id="decisionResultValue">尚未判讀</div>
|
||
<div class="price-decision-detail" id="decisionResultDetail">結果會分成檢查售價、主推曝光、觀察賣點。</div>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="price-workflow-strip" aria-label="比價流程">
|
||
<div class="price-workflow-step is-current" id="workflowStepScope">
|
||
<span class="price-workflow-index">1</span>
|
||
<span><strong>選範圍</strong><span>品牌或關鍵字</span></span>
|
||
</div>
|
||
<div class="price-workflow-step" id="workflowStepPchome">
|
||
<span class="price-workflow-index">2</span>
|
||
<span><strong>抓 PChome</strong><span>取得主場商品</span></span>
|
||
</div>
|
||
<div class="price-workflow-step" id="workflowStepMomo">
|
||
<span class="price-workflow-index">3</span>
|
||
<span><strong>找 MOMO</strong><span>同款與單位價</span></span>
|
||
</div>
|
||
<div class="price-workflow-step" id="workflowStepResult">
|
||
<span class="price-workflow-index">4</span>
|
||
<span><strong>做決策</strong><span>價格與曝光</span></span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 操作區 -->
|
||
<div class="row mb-4">
|
||
<!-- 選擇檢查範圍 -->
|
||
<div class="col-lg-4 mb-3">
|
||
<div class="card price-operation-card">
|
||
<div class="card-header price-step-head">
|
||
<i class="fas fa-tag me-2"></i>選擇要檢查的範圍
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<select class="form-select" id="brandSelect">
|
||
<option value="">選擇品牌</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">或輸入品牌/關鍵字</label>
|
||
<input type="text" class="form-control" id="customKeyword" placeholder="如: 理膚寶水、雅漾">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 取得 PChome 商品 -->
|
||
<div class="col-lg-4 mb-3">
|
||
<div class="card price-operation-card">
|
||
<div class="card-header price-step-head">
|
||
<i class="fas fa-shopping-cart me-2"></i>補齊 PChome 商品
|
||
</div>
|
||
<div class="card-body">
|
||
<button class="btn btn-primary w-100 mb-2" id="fetchPchomeBtn">
|
||
<i class="fas fa-download me-1"></i>取得 PChome 商品
|
||
</button>
|
||
<div class="text-center my-2"><small class="text-muted">或</small></div>
|
||
<button class="btn btn-outline-info w-100" id="manualPchomeBtn" data-bs-toggle="modal" data-bs-target="#manualInputModal" data-source="pchome">
|
||
<i class="fas fa-keyboard me-1"></i>手動輸入
|
||
</button>
|
||
<div class="mt-3">
|
||
<span class="price-count-badge is-muted" id="pchomeCount">0 筆商品</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 取得 MOMO 商品 -->
|
||
<div class="col-lg-4 mb-3">
|
||
<div class="card price-operation-card">
|
||
<div class="card-header price-step-head">
|
||
<i class="fas fa-store me-2"></i>補齊 MOMO 商品
|
||
</div>
|
||
<div class="card-body">
|
||
<button class="btn btn-primary w-100 mb-2" id="fetchTargetedMomoBtn" disabled>
|
||
<i class="fas fa-magnifying-glass-dollar me-1"></i>自動找 MOMO 候選
|
||
</button>
|
||
<div class="text-center my-2"><small class="text-muted">或</small></div>
|
||
<button class="btn btn-primary w-100 mb-2" id="uploadMomoBtn" data-bs-toggle="modal" data-bs-target="#uploadModal">
|
||
<i class="fas fa-file-excel me-1"></i>匯入 MOMO 商品
|
||
</button>
|
||
<div class="text-center my-2"><small class="text-muted">或</small></div>
|
||
<button class="btn btn-outline-warning w-100" id="manualMomoBtn" data-bs-toggle="modal" data-bs-target="#manualInputModal" data-source="momo">
|
||
<i class="fas fa-keyboard me-1"></i>手動輸入
|
||
</button>
|
||
<div class="mt-3">
|
||
<span class="price-count-badge is-muted" id="momoCount">0 筆商品</span>
|
||
<span class="price-count-badge is-muted ms-1" id="momoUnitCompareCount">0 筆單位價</span>
|
||
<span class="price-count-badge is-muted ms-1" id="momoReviewCount">0 筆需確認</span>
|
||
</div>
|
||
<div class="price-note is-unit mt-3 py-2" id="momoUnitComparePanel" style="display:none;"></div>
|
||
<div class="price-note is-review mt-3 py-2" id="momoReviewPanel" style="display:none;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 比價按鈕 -->
|
||
<div class="row mb-4">
|
||
<div class="col text-center">
|
||
<button class="btn btn-lg btn-primary px-5" id="compareBtn" disabled>
|
||
<i class="fas fa-balance-scale me-2"></i>開始檢查價差
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 進度顯示 -->
|
||
<div class="row mb-4" id="progressSection" style="display: none;">
|
||
<div class="col">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div class="d-flex align-items-center">
|
||
<div class="spinner-border text-primary me-3" role="status"></div>
|
||
<div>
|
||
<strong id="progressText">處理中...</strong>
|
||
<br><small class="text-muted" id="progressDetail"></small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 比價結果 -->
|
||
<div class="row" id="resultSection" style="display: none;">
|
||
<div class="col">
|
||
<section class="price-result-panel mb-4" aria-label="比價結果判讀">
|
||
<div class="price-panel-title">
|
||
<span><i class="fas fa-chart-bar me-1"></i>比價結果判讀</span>
|
||
<span id="priceResultSummary">等待比價結果</span>
|
||
</div>
|
||
<div class="price-result-summary-grid">
|
||
<div class="price-result-callout">
|
||
<strong id="priceResultHeadline">尚未產生判讀</strong>
|
||
<span id="priceResultAdvice">取得 PChome 與 MOMO 商品後,會直接整理下一步。</span>
|
||
</div>
|
||
<div class="price-result-matrix" aria-label="比價決策分佈">
|
||
<div class="price-result-matrix-card">
|
||
<strong id="priceUrgentMetric">0</strong>
|
||
<span>需檢查售價或活動</span>
|
||
</div>
|
||
<div class="price-result-matrix-card">
|
||
<strong id="priceGoodMetric">0</strong>
|
||
<span>可主推曝光</span>
|
||
</div>
|
||
<div class="price-result-matrix-card">
|
||
<strong id="priceWatchMetric">0</strong>
|
||
<span>觀察賣點</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row g-3">
|
||
<div class="col-lg-4">
|
||
<div class="price-risk-row">
|
||
<div class="price-risk-meta"><span>需檢查價格</span><strong id="priceUrgentText">0 筆</strong></div>
|
||
<div class="price-risk-track"><span class="price-risk-bar is-urgent" id="priceUrgentBar"></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="price-risk-row">
|
||
<div class="price-risk-meta"><span>可主推曝光</span><strong id="priceGoodText">0 筆</strong></div>
|
||
<div class="price-risk-track"><span class="price-risk-bar is-good" id="priceGoodBar"></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="price-risk-row">
|
||
<div class="price-risk-meta"><span>價格接近</span><strong id="priceWatchText">0 筆</strong></div>
|
||
<div class="price-risk-track"><span class="price-risk-bar is-watch" id="priceWatchBar"></span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 統計卡片 -->
|
||
<div class="row mb-4">
|
||
<div class="col-md-3 mb-3">
|
||
<div class="card price-stat-card">
|
||
<div class="card-body text-center">
|
||
<h4 class="mb-0" id="matchedCount">0</h4>
|
||
<small class="text-muted">找到同款</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<div class="card price-stat-card is-pchome">
|
||
<div class="card-body text-center">
|
||
<h4 class="mb-0" id="pchomeCheaperCount">0</h4>
|
||
<small>PChome 較便宜</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<div class="card price-stat-card is-momo">
|
||
<div class="card-body text-center">
|
||
<h4 class="mb-0" id="momoCheaperCount">0</h4>
|
||
<small>MOMO 較便宜</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<div class="card price-stat-card is-diff">
|
||
<div class="card-body text-center">
|
||
<h4 class="mb-0" id="avgPriceDiff">$0</h4>
|
||
<small>平均價差</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 結果表格 -->
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="fas fa-list me-2"></i>比價結果</span>
|
||
<div>
|
||
<button class="btn btn-sm btn-outline-primary" id="exportResultBtn">
|
||
<i class="fas fa-file-excel me-1"></i>匯出 Excel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover table-striped mb-0">
|
||
<thead class="price-table-head">
|
||
<tr>
|
||
<th style="width: 120px;">下一步</th>
|
||
<th style="width: 35%;">PChome 商品</th>
|
||
<th style="width: 35%;">MOMO 商品</th>
|
||
<th class="text-center">相似度</th>
|
||
<th class="text-end">PChome 價</th>
|
||
<th class="text-end">MOMO 價</th>
|
||
<th class="text-end">價差</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="resultBody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上傳 Excel Modal -->
|
||
<div class="modal fade" id="uploadModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title"><i class="fas fa-file-excel me-2"></i>上傳 MOMO 商品 Excel</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label class="form-label">選擇 Excel 檔案</label>
|
||
<input type="file" class="form-control" id="momoExcelFile" accept=".xlsx,.xls,.csv">
|
||
</div>
|
||
<div class="price-note">
|
||
<i class="fas fa-info-circle me-1"></i>
|
||
Excel 至少要有商品名稱與售價;有賣場連結時會一起帶入雙開比對。
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" id="parseExcelBtn">
|
||
<i class="fas fa-upload me-1"></i>上傳並解析
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 手動輸入 Modal -->
|
||
<div class="modal fade" id="manualInputModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title"><i class="fas fa-keyboard me-2"></i>手動輸入商品</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="price-note py-2">
|
||
<i class="fas fa-bullseye me-1"></i>
|
||
貼上 MOMO 商品、售價與賣場連結;會整理成可比價清單。
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">商品資料</label>
|
||
<textarea class="form-control" id="manualInput" rows="10" placeholder="理膚寶水 多容安舒緩濕潤乳液 40ml,850,https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12345
|
||
理膚寶水 B5全面修復霜 100ml,680
|
||
La Roche-Posay 安得利防曬液 50ml,920
|
||
|
||
也可以直接從 Excel 複製貼上 (Tab 分隔)"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" id="parseManualBtn">
|
||
<i class="fas fa-check me-1"></i>確認
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
let pchomeProducts = [];
|
||
let momoProducts = [];
|
||
let momoUnitCompareCandidates = [];
|
||
let momoReviewCandidates = [];
|
||
let comparisonResult = null;
|
||
let currentManualSource = null;
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadBrands();
|
||
bindEvents();
|
||
renderPriceCommandDashboard();
|
||
});
|
||
|
||
async function loadBrands() {
|
||
try {
|
||
const response = await fetchWithCSRF('/api/price_comparison/brands');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const select = document.getElementById('brandSelect');
|
||
for (const brand of data.data.brands) {
|
||
const option = document.createElement('option');
|
||
option.value = brand.code;
|
||
option.textContent = brand.name;
|
||
select.appendChild(option);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('載入品牌失敗:', error);
|
||
}
|
||
}
|
||
|
||
function bindEvents() {
|
||
// 品牌選擇
|
||
document.getElementById('brandSelect').addEventListener('change', function() {
|
||
const selected = this.options[this.selectedIndex];
|
||
if (selected.value) {
|
||
document.getElementById('customKeyword').value = selected.textContent;
|
||
}
|
||
renderPriceCommandDashboard();
|
||
});
|
||
|
||
document.getElementById('customKeyword').addEventListener('input', renderPriceCommandDashboard);
|
||
|
||
// 爬取 PChome
|
||
document.getElementById('fetchPchomeBtn').addEventListener('click', fetchPchome);
|
||
|
||
// 上傳 Excel
|
||
document.getElementById('parseExcelBtn').addEventListener('click', parseMomoExcel);
|
||
|
||
document.getElementById('fetchTargetedMomoBtn').addEventListener('click', fetchTargetedMomoCandidates);
|
||
|
||
// 手動輸入來源
|
||
document.querySelectorAll('[data-bs-target="#manualInputModal"]').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
currentManualSource = this.dataset.source;
|
||
});
|
||
});
|
||
|
||
// 解析手動輸入
|
||
document.getElementById('parseManualBtn').addEventListener('click', parseManualInput);
|
||
|
||
// 比價
|
||
document.getElementById('compareBtn').addEventListener('click', runComparison);
|
||
|
||
// 匯出
|
||
document.getElementById('exportResultBtn').addEventListener('click', exportResult);
|
||
|
||
document.getElementById('priceNextActionButton').addEventListener('click', runPriceNextAction);
|
||
}
|
||
|
||
function getKeyword() {
|
||
const custom = document.getElementById('customKeyword').value.trim();
|
||
if (custom) return custom;
|
||
|
||
const select = document.getElementById('brandSelect');
|
||
if (!select.value) return '';
|
||
return select.options[select.selectedIndex]?.textContent || '';
|
||
}
|
||
|
||
async function fetchPchome() {
|
||
const keyword = getKeyword();
|
||
if (!keyword) {
|
||
showToast('請先選擇品牌或輸入關鍵字', 'warning');
|
||
return;
|
||
}
|
||
|
||
showProgress('取得 PChome 商品中...', `搜尋:${keyword}`);
|
||
|
||
try {
|
||
const response = await fetchWithCSRF('/api/price_comparison/fetch_pchome', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ keyword, limit: 100 })
|
||
});
|
||
|
||
const data = await response.json();
|
||
hideProgress();
|
||
|
||
if (data.success) {
|
||
pchomeProducts = data.data.products;
|
||
momoProducts = [];
|
||
momoUnitCompareCandidates = [];
|
||
momoReviewCandidates = [];
|
||
resetComparisonResult();
|
||
document.getElementById('pchomeCount').textContent = `${pchomeProducts.length} 筆商品`;
|
||
document.getElementById('pchomeCount').className = 'price-count-badge is-pchome';
|
||
document.getElementById('momoCount').textContent = '0 筆同款';
|
||
document.getElementById('momoCount').className = 'price-count-badge is-muted';
|
||
renderMomoUnitComparePanel();
|
||
renderMomoReviewPanel();
|
||
updateCompareButton();
|
||
updateTargetedMomoButton();
|
||
renderPriceCommandDashboard();
|
||
showToast(`成功取得 ${pchomeProducts.length} 筆 PChome 商品`, 'success');
|
||
} else {
|
||
showToast(data.message, 'danger');
|
||
}
|
||
} catch (error) {
|
||
hideProgress();
|
||
showToast('取得失敗:' + error.message, 'danger');
|
||
}
|
||
}
|
||
|
||
async function fetchTargetedMomoCandidates() {
|
||
if (!pchomeProducts.length) {
|
||
showToast('請先取得 PChome 商品,再搜尋 MOMO 候選', 'warning');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('fetchTargetedMomoBtn');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>搜尋中...';
|
||
showProgress('搜尋 MOMO 候選中...', '正在用 PChome 商品名稱找單品與組合候選');
|
||
|
||
try {
|
||
const response = await fetchWithCSRF('/api/price_comparison/fetch_momo_for_pchome', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
pchome_products: pchomeProducts,
|
||
sync_external_offers: true
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
hideProgress();
|
||
|
||
const payload = data.data || {};
|
||
momoProducts = Array.isArray(payload.products) ? payload.products : [];
|
||
momoUnitCompareCandidates = Array.isArray(payload.unit_compare_candidates) ? payload.unit_compare_candidates : [];
|
||
momoReviewCandidates = Array.isArray(payload.review_candidates) ? payload.review_candidates : [];
|
||
resetComparisonResult();
|
||
document.getElementById('momoCount').textContent = `${momoProducts.length} 筆同款`;
|
||
document.getElementById('momoCount').className = momoProducts.length ? 'price-count-badge is-momo' : 'price-count-badge is-muted';
|
||
renderMomoUnitComparePanel();
|
||
renderMomoReviewPanel();
|
||
updateCompareButton();
|
||
renderPriceCommandDashboard();
|
||
|
||
const syncResult = payload.external_offer_sync || {};
|
||
const syncedCount = Number(syncResult.written_count || 0);
|
||
if (data.success) {
|
||
const syncText = syncedCount ? `,已同步 ${syncedCount} 筆到作戰清單` : '';
|
||
showToast(`MOMO 候選完成:同款 ${momoProducts.length} 筆、單位價 ${momoUnitCompareCandidates.length} 筆、需確認 ${momoReviewCandidates.length} 筆${syncText}`, 'success');
|
||
} else if (momoUnitCompareCandidates.length) {
|
||
showToast(`已自動換算 ${momoUnitCompareCandidates.length} 筆單位價候選`, 'success');
|
||
} else if (momoReviewCandidates.length) {
|
||
showToast(`找到 ${momoReviewCandidates.length} 筆需人工確認候選,暫不進自動比價`, 'warning');
|
||
} else {
|
||
showToast(data.message || '目前沒有找到 MOMO 候選', 'warning');
|
||
}
|
||
} catch (error) {
|
||
hideProgress();
|
||
showToast('搜尋 MOMO 候選失敗:' + error.message, 'danger');
|
||
} finally {
|
||
updateTargetedMomoButton();
|
||
btn.innerHTML = '<i class="fas fa-magnifying-glass-dollar me-1"></i>自動找 MOMO 候選';
|
||
}
|
||
}
|
||
|
||
function renderMomoUnitComparePanel() {
|
||
const countBadge = document.getElementById('momoUnitCompareCount');
|
||
const panel = document.getElementById('momoUnitComparePanel');
|
||
countBadge.textContent = `${momoUnitCompareCandidates.length} 筆單位價`;
|
||
countBadge.className = momoUnitCompareCandidates.length
|
||
? 'price-count-badge is-pchome ms-1'
|
||
: 'price-count-badge is-muted ms-1';
|
||
|
||
if (!momoUnitCompareCandidates.length) {
|
||
panel.style.display = 'none';
|
||
panel.replaceChildren();
|
||
return;
|
||
}
|
||
|
||
panel.style.display = 'block';
|
||
const heading = document.createElement('strong');
|
||
heading.textContent = `已自動換算 ${momoUnitCompareCandidates.length} 筆單位價`;
|
||
|
||
const list = document.createElement('div');
|
||
list.className = 'price-unit-list';
|
||
momoUnitCompareCandidates.slice(0, 3).forEach(item => {
|
||
const evidence = item.target_unit_price_comparison || {};
|
||
const row = document.createElement('div');
|
||
row.className = 'price-unit-item';
|
||
|
||
const name = document.createElement('span');
|
||
name.className = 'price-unit-name';
|
||
name.textContent = item.name || item.title || '未命名商品';
|
||
|
||
const metric = document.createElement('span');
|
||
metric.className = 'price-unit-metric';
|
||
const gap = Number(evidence.unit_gap_pct ?? item.target_gap_pct ?? 0);
|
||
const unit = evidence.unit_label ? `/${evidence.unit_label}` : '';
|
||
const momoUnitPrice = Number(evidence.momo_unit_price || 0);
|
||
const pchomeUnitPrice = Number(evidence.competitor_unit_price || 0);
|
||
metric.textContent = momoUnitPrice && pchomeUnitPrice
|
||
? `MOMO $${momoUnitPrice.toFixed(2)}${unit}|PChome $${pchomeUnitPrice.toFixed(2)}${unit}|${gap >= 0 ? '+' : ''}${gap.toFixed(1)}%`
|
||
: `${gap >= 0 ? '+' : ''}${gap.toFixed(1)}%`;
|
||
|
||
row.append(name, metric);
|
||
list.appendChild(row);
|
||
});
|
||
|
||
panel.replaceChildren(heading, list);
|
||
}
|
||
|
||
function buildPchomeProductUrl(productId) {
|
||
const id = String(productId || '').trim();
|
||
return id ? `https://24h.pchome.com.tw/prod/${encodeURIComponent(id)}` : '';
|
||
}
|
||
|
||
function reviewCandidateReasonLabels(item) {
|
||
const labels = Array.isArray(item?.target_match_reason_labels)
|
||
? item.target_match_reason_labels.filter(Boolean)
|
||
: [];
|
||
return labels.length ? labels : ['需人工確認同款'];
|
||
}
|
||
|
||
function renderMomoReviewPanel() {
|
||
const countBadge = document.getElementById('momoReviewCount');
|
||
const panel = document.getElementById('momoReviewPanel');
|
||
countBadge.textContent = `${momoReviewCandidates.length} 筆需確認`;
|
||
countBadge.className = momoReviewCandidates.length
|
||
? 'price-count-badge is-momo ms-1'
|
||
: 'price-count-badge is-muted ms-1';
|
||
|
||
if (!momoReviewCandidates.length) {
|
||
panel.style.display = 'none';
|
||
panel.replaceChildren();
|
||
return;
|
||
}
|
||
|
||
panel.style.display = 'block';
|
||
const heading = document.createElement('strong');
|
||
heading.textContent = `待確認 ${momoReviewCandidates.length} 筆:先並排看兩家賣場`;
|
||
|
||
const list = document.createElement('div');
|
||
list.className = 'price-review-list';
|
||
momoReviewCandidates.slice(0, 5).forEach(item => {
|
||
const pchomeId = String(item.target_pchome_product_id || '').trim();
|
||
const pchomeName = item.target_pchome_name || pchomeId || 'PChome 商品';
|
||
const momoName = item.name || item.title || item.product_id || 'MOMO 候選';
|
||
const pchomeUrl = toSafeUrl(item.target_pchome_url || buildPchomeProductUrl(pchomeId));
|
||
const momoUrl = toSafeUrl(item.product_url || item.url || '');
|
||
const pchomePrice = item.target_pchome_price ? formatMoney(item.target_pchome_price) : '待補價格';
|
||
const momoPrice = item.price ? formatMoney(item.price) : '待補價格';
|
||
const reasons = reviewCandidateReasonLabels(item).slice(0, 2).join('、') || '請確認規格';
|
||
|
||
const row = document.createElement('div');
|
||
row.className = 'price-review-item';
|
||
row.innerHTML = `
|
||
<div class="price-review-compare">
|
||
<section class="price-review-store is-pchome">
|
||
<div class="price-review-platform">
|
||
<span>PChome</span>
|
||
${pchomeUrl ? `<a href="${escapeHtml(pchomeUrl)}" target="_blank" rel="noopener noreferrer">賣場</a>` : '<span>待補連結</span>'}
|
||
</div>
|
||
<div class="price-review-name" title="${escapeHtml(pchomeName)}">${escapeHtml(pchomeName)}</div>
|
||
<span class="price-review-price">${escapeHtml(pchomePrice)}</span>
|
||
</section>
|
||
<section class="price-review-store is-momo">
|
||
<div class="price-review-platform">
|
||
<span>MOMO</span>
|
||
${momoUrl ? `<a href="${escapeHtml(momoUrl)}" target="_blank" rel="noopener noreferrer">賣場</a>` : '<span>待補連結</span>'}
|
||
</div>
|
||
<div class="price-review-name" title="${escapeHtml(momoName)}">${escapeHtml(momoName)}</div>
|
||
<span class="price-review-price">${escapeHtml(momoPrice)}</span>
|
||
</section>
|
||
</div>
|
||
<div class="price-review-actions">
|
||
${pchomeUrl && momoUrl ? `<button type="button" class="btn btn-sm btn-outline-primary" data-pchome-url="${escapeHtml(pchomeUrl)}" data-momo-url="${escapeHtml(momoUrl)}" onclick="openComparisonStores(this)"><i class="fas fa-up-right-from-square me-1"></i>同時開兩家賣場</button>` : ''}
|
||
<span class="price-action-pill is-watch">${escapeHtml(reasons)}</span>
|
||
</div>
|
||
`;
|
||
list.appendChild(row);
|
||
});
|
||
|
||
panel.replaceChildren(heading, list);
|
||
}
|
||
|
||
async function parseMomoExcel() {
|
||
const fileInput = document.getElementById('momoExcelFile');
|
||
if (!fileInput.files.length) {
|
||
showToast('先選擇 MOMO 商品檔案。', 'warning');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', fileInput.files[0]);
|
||
|
||
try {
|
||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||
const response = await fetch('/api/price_comparison/parse_momo_excel', {
|
||
method: 'POST',
|
||
headers: { 'X-CSRFToken': csrfToken },
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
momoProducts = data.data.products;
|
||
momoUnitCompareCandidates = [];
|
||
momoReviewCandidates = [];
|
||
resetComparisonResult();
|
||
document.getElementById('momoCount').textContent = `${momoProducts.length} 筆商品`;
|
||
document.getElementById('momoCount').className = 'price-count-badge is-momo';
|
||
renderMomoUnitComparePanel();
|
||
renderMomoReviewPanel();
|
||
updateCompareButton();
|
||
renderPriceCommandDashboard();
|
||
bootstrap.Modal.getInstance(document.getElementById('uploadModal')).hide();
|
||
showToast(`成功解析 ${momoProducts.length} 筆 MOMO 商品`, 'success');
|
||
} else {
|
||
showToast(data.message, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showToast('解析失敗: ' + error.message, 'danger');
|
||
}
|
||
}
|
||
|
||
function parseManualInput() {
|
||
const input = document.getElementById('manualInput').value.trim();
|
||
if (!input) {
|
||
showToast('先貼上商品、售價與賣場連結。', 'warning');
|
||
return;
|
||
}
|
||
|
||
const lines = input.split('\n');
|
||
const products = [];
|
||
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i].trim();
|
||
if (!line) continue;
|
||
|
||
// 支援逗號或 Tab 分隔
|
||
let parts = line.split(',');
|
||
if (parts.length < 2) {
|
||
parts = line.split('\t');
|
||
}
|
||
if (parts.length < 2) {
|
||
showToast(`第 ${i + 1} 行缺商品名稱或售價,請補齊後再加入比價清單。`, 'warning');
|
||
return;
|
||
}
|
||
|
||
// 驗證價格是否為數字
|
||
const price = parseInt(parts[1].trim());
|
||
if (isNaN(price) || price <= 0) {
|
||
showToast(`第 ${i + 1} 行售價需為數字,例如:680。`, 'warning');
|
||
return;
|
||
}
|
||
|
||
const rawUrl = parts[2]?.trim() || '';
|
||
const productId = currentManualSource === 'momo'
|
||
? extractMomoCodeFromUrl(rawUrl)
|
||
: `manual_${i}`;
|
||
|
||
products.push({
|
||
name: parts[0].trim(),
|
||
price: price,
|
||
product_id: productId,
|
||
url: rawUrl
|
||
});
|
||
}
|
||
|
||
if (currentManualSource === 'pchome') {
|
||
pchomeProducts = products;
|
||
momoProducts = [];
|
||
momoUnitCompareCandidates = [];
|
||
momoReviewCandidates = [];
|
||
resetComparisonResult();
|
||
document.getElementById('pchomeCount').textContent = `${products.length} 筆商品`;
|
||
document.getElementById('pchomeCount').className = 'price-count-badge is-pchome';
|
||
document.getElementById('momoCount').textContent = '0 筆同款';
|
||
document.getElementById('momoCount').className = 'price-count-badge is-muted';
|
||
renderMomoUnitComparePanel();
|
||
renderMomoReviewPanel();
|
||
updateTargetedMomoButton();
|
||
} else {
|
||
momoProducts = products;
|
||
momoUnitCompareCandidates = [];
|
||
momoReviewCandidates = [];
|
||
resetComparisonResult();
|
||
document.getElementById('momoCount').textContent = `${products.length} 筆商品`;
|
||
document.getElementById('momoCount').className = 'price-count-badge is-momo';
|
||
renderMomoUnitComparePanel();
|
||
renderMomoReviewPanel();
|
||
}
|
||
|
||
updateCompareButton();
|
||
renderPriceCommandDashboard();
|
||
bootstrap.Modal.getInstance(document.getElementById('manualInputModal')).hide();
|
||
document.getElementById('manualInput').value = '';
|
||
showToast(`成功新增 ${products.length} 筆商品`, 'success');
|
||
}
|
||
|
||
function updateCompareButton() {
|
||
const btn = document.getElementById('compareBtn');
|
||
btn.disabled = !(pchomeProducts.length > 0 && momoProducts.length > 0);
|
||
}
|
||
|
||
function updateTargetedMomoButton() {
|
||
const btn = document.getElementById('fetchTargetedMomoBtn');
|
||
if (btn) btn.disabled = pchomeProducts.length === 0;
|
||
}
|
||
|
||
function resetComparisonResult() {
|
||
comparisonResult = null;
|
||
document.getElementById('resultSection').style.display = 'none';
|
||
setText('priceResultSummary', '等待比價結果');
|
||
setText('priceResultHeadline', '尚未產生判讀');
|
||
setText('priceResultAdvice', '取得 PChome 與 MOMO 商品後,會直接整理下一步。');
|
||
setText('priceUrgentMetric', '0');
|
||
setText('priceGoodMetric', '0');
|
||
setText('priceWatchMetric', '0');
|
||
}
|
||
|
||
function renderPriceCommandDashboard() {
|
||
const keyword = getKeyword();
|
||
const pchomeCount = pchomeProducts.length;
|
||
const momoCount = momoProducts.length;
|
||
const unitCount = momoUnitCompareCandidates.length;
|
||
const reviewCount = momoReviewCandidates.length;
|
||
const hasResult = Boolean(comparisonResult && Array.isArray(comparisonResult.matches));
|
||
const matchedCount = hasResult ? comparisonResult.matches.length : 0;
|
||
const stats = comparisonResult?.stats || {};
|
||
const urgentCount = Number(stats.momo_cheaper_count || 0);
|
||
const goodCount = Number(stats.pchome_cheaper_count || 0);
|
||
const watchCount = Math.max(0, matchedCount - urgentCount - goodCount);
|
||
const usableMomoCount = momoCount + unitCount;
|
||
|
||
setText('heroPchomeCount', pchomeCount.toLocaleString());
|
||
setText('heroMomoCount', usableMomoCount.toLocaleString());
|
||
setText('heroDecisionCount', matchedCount.toLocaleString());
|
||
setText('pricePchomeReadyText', `${pchomeCount} 筆`);
|
||
setText(
|
||
'priceMomoReadyText',
|
||
reviewCount || unitCount
|
||
? `${momoCount} 筆同款,${unitCount} 筆單位價,${reviewCount} 筆確認`
|
||
: `${momoCount} 筆`
|
||
);
|
||
setWidth('pricePchomeReadyBar', Math.min(100, pchomeCount));
|
||
setWidth('priceMomoReadyBar', Math.min(100, momoCount + unitCount));
|
||
renderPriceDecisionCards({ keyword, pchomeCount, momoCount, unitCount, reviewCount, matchedCount, urgentCount, goodCount, watchCount });
|
||
renderPriceWorkflow({ keyword, pchomeCount, momoCount, unitCount, matchedCount });
|
||
|
||
if (!keyword && !pchomeCount && !momoCount && !unitCount) {
|
||
setText('priceReadySummary', '請先選範圍');
|
||
setNextAction('今天先做:選擇要檢查的商品範圍', '請先選品牌或輸入關鍵字,系統才知道要抓哪一批 PChome 商品。', '輸入關鍵字', 'keyword');
|
||
return;
|
||
}
|
||
|
||
if (!pchomeCount) {
|
||
setText('priceReadySummary', '缺 PChome 商品');
|
||
const scopeText = keyword ? `已選「${keyword}」` : '已補 MOMO 商品';
|
||
setNextAction('今天先做:取得 PChome 商品', `${scopeText},先取得 PChome 商品,才有業績主場可以比。`, keyword ? '取得 PChome 商品' : '輸入關鍵字', keyword ? 'fetch-pchome' : 'keyword');
|
||
return;
|
||
}
|
||
|
||
if (!momoCount && unitCount) {
|
||
setText('priceReadySummary', '已有單位價比較');
|
||
setNextAction('今天先看:自動單位價比較', `系統已自動換算 ${unitCount} 筆候選,不需要先人工確認。`, '查看單位價', 'focus-momo-unit');
|
||
return;
|
||
}
|
||
|
||
if (!momoCount && reviewCount) {
|
||
setText('priceReadySummary', 'MOMO 候選待確認');
|
||
setNextAction('今天先做:確認 MOMO 單品/組合候選', `已找到 ${reviewCount} 筆可能候選,但需要確認單品、組合或單位價後才能比價。`, '查看候選提醒', 'focus-momo-review');
|
||
return;
|
||
}
|
||
|
||
if (!momoCount) {
|
||
setText('priceReadySummary', '缺 MOMO 商品');
|
||
setNextAction('今天先做:自動找 MOMO 候選', 'PChome 商品已準備好,先用 PChome 商品名稱反查 MOMO,同時保留單品與組合候選。', '自動找 MOMO 候選', 'fetch-momo');
|
||
return;
|
||
}
|
||
|
||
setText('priceReadySummary', '可以比價');
|
||
if (!hasResult) {
|
||
setNextAction('今天先做:開始檢查價差', `兩邊資料已齊:PChome ${pchomeCount} 筆、MOMO ${momoCount} 筆。`, '開始檢查價差', 'compare');
|
||
return;
|
||
}
|
||
|
||
if (urgentCount > 0) {
|
||
setNextAction('今天先做:處理 PChome 價格偏高商品', `已找到 ${urgentCount} 筆 MOMO 較便宜商品,先檢查售價、活動組合或曝光策略。`, '查看需檢查價格', 'focus-results');
|
||
return;
|
||
}
|
||
|
||
if (goodCount > 0) {
|
||
setNextAction('今天先做:主推 PChome 有價格優勢商品', `已找到 ${goodCount} 筆 PChome 較便宜商品,適合安排曝光、文案或活動位置。`, '查看可主推商品', 'focus-results');
|
||
return;
|
||
}
|
||
|
||
setNextAction('今天先做:檢查商品賣點與活動位置', '目前價格接近,差異不大,下一步看曝光、文案和活動組合。', '查看比價結果', 'focus-results');
|
||
}
|
||
|
||
function renderPriceDecisionCards(state) {
|
||
const keyword = state.keyword || '';
|
||
setDecisionCard(
|
||
'decisionScopeCard',
|
||
'decisionScopeValue',
|
||
'decisionScopeDetail',
|
||
keyword ? `檢查「${keyword}」` : '尚未選擇',
|
||
keyword ? '範圍已確認,可以取得 PChome 商品。' : '先選品牌或輸入商品關鍵字。',
|
||
keyword ? 'ready' : 'active'
|
||
);
|
||
setDecisionCard(
|
||
'decisionPchomeCard',
|
||
'decisionPchomeValue',
|
||
'decisionPchomeDetail',
|
||
state.pchomeCount ? `${state.pchomeCount.toLocaleString()} 筆商品` : '等待商品',
|
||
state.pchomeCount ? '已可用這批商品反查 MOMO。' : '取得商品後,系統才知道要比哪一批。',
|
||
state.pchomeCount ? 'ready' : (keyword ? 'active' : '')
|
||
);
|
||
|
||
const momoValue = state.momoCount
|
||
? `${state.momoCount.toLocaleString()} 筆同款`
|
||
: state.unitCount
|
||
? `${state.unitCount.toLocaleString()} 筆單位價`
|
||
: state.reviewCount
|
||
? `${state.reviewCount.toLocaleString()} 筆待確認`
|
||
: '等待候選';
|
||
const momoDetail = state.momoCount
|
||
? '同款可直接進入總價比對。'
|
||
: state.unitCount
|
||
? '單品與組合不同,已改用單位價判讀。'
|
||
: state.reviewCount
|
||
? '需要確認單品、組合或容量後再使用。'
|
||
: '會自動分成同款、單位價、需確認。';
|
||
setDecisionCard(
|
||
'decisionMomoCard',
|
||
'decisionMomoValue',
|
||
'decisionMomoDetail',
|
||
momoValue,
|
||
momoDetail,
|
||
state.momoCount || state.unitCount ? 'ready' : (state.pchomeCount ? 'active' : '')
|
||
);
|
||
|
||
const resultValue = state.matchedCount
|
||
? `${state.matchedCount.toLocaleString()} 筆判讀`
|
||
: '尚未判讀';
|
||
let resultDetail = '結果會分成檢查售價、主推曝光、觀察賣點。';
|
||
let resultState = state.momoCount ? 'active' : '';
|
||
if (state.matchedCount) {
|
||
if (state.urgentCount) {
|
||
resultDetail = `${state.urgentCount.toLocaleString()} 筆需先檢查售價或活動。`;
|
||
resultState = 'blocked';
|
||
} else if (state.goodCount) {
|
||
resultDetail = `${state.goodCount.toLocaleString()} 筆可主推曝光。`;
|
||
resultState = 'ready';
|
||
} else {
|
||
resultDetail = '價格接近,改看賣點、活動位置與庫存。';
|
||
resultState = 'ready';
|
||
}
|
||
}
|
||
setDecisionCard('decisionResultCard', 'decisionResultValue', 'decisionResultDetail', resultValue, resultDetail, resultState);
|
||
}
|
||
|
||
function setDecisionCard(cardId, valueId, detailId, value, detail, state) {
|
||
const card = document.getElementById(cardId);
|
||
if (card) {
|
||
card.className = 'price-decision-card' + (
|
||
state === 'ready' ? ' is-ready' :
|
||
state === 'blocked' ? ' is-blocked' :
|
||
state === 'active' ? ' is-active' : ''
|
||
);
|
||
}
|
||
setText(valueId, value);
|
||
setText(detailId, detail);
|
||
}
|
||
|
||
function renderPriceWorkflow(state) {
|
||
setWorkflowState('workflowStepScope', state.keyword ? 'done' : 'current');
|
||
setWorkflowState('workflowStepPchome', state.pchomeCount ? 'done' : (state.keyword ? 'current' : ''));
|
||
setWorkflowState('workflowStepMomo', state.momoCount || state.unitCount ? 'done' : (state.pchomeCount ? 'current' : ''));
|
||
setWorkflowState('workflowStepResult', state.matchedCount ? 'done' : (state.momoCount ? 'current' : ''));
|
||
}
|
||
|
||
function setWorkflowState(id, state) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.className = 'price-workflow-step' + (
|
||
state === 'done' ? ' is-done' :
|
||
state === 'current' ? ' is-current' : ''
|
||
);
|
||
}
|
||
|
||
function setNextAction(title, reason, label, action) {
|
||
setText('priceNextActionTitle', title);
|
||
setText('priceNextActionReason', reason);
|
||
const btn = document.getElementById('priceNextActionButton');
|
||
btn.textContent = label;
|
||
btn.dataset.action = action;
|
||
}
|
||
|
||
function runPriceNextAction() {
|
||
const action = document.getElementById('priceNextActionButton').dataset.action;
|
||
if (action === 'keyword') {
|
||
document.getElementById('customKeyword').focus();
|
||
return;
|
||
}
|
||
if (action === 'fetch-pchome') {
|
||
fetchPchome();
|
||
return;
|
||
}
|
||
if (action === 'upload-momo') {
|
||
bootstrap.Modal.getOrCreateInstance(document.getElementById('uploadModal')).show();
|
||
return;
|
||
}
|
||
if (action === 'fetch-momo') {
|
||
fetchTargetedMomoCandidates();
|
||
return;
|
||
}
|
||
if (action === 'compare') {
|
||
runComparison();
|
||
return;
|
||
}
|
||
if (action === 'focus-results') {
|
||
document.getElementById('resultSection')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
return;
|
||
}
|
||
if (action === 'focus-momo-unit') {
|
||
document.getElementById('momoUnitComparePanel')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
return;
|
||
}
|
||
if (action === 'focus-momo-review') {
|
||
document.getElementById('momoReviewPanel')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
}
|
||
|
||
function setText(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = value;
|
||
}
|
||
|
||
function setWidth(id, value) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.style.width = `${Math.max(0, Math.min(100, value))}%`;
|
||
}
|
||
|
||
async function runComparison() {
|
||
showProgress('檢查價差中...', '正在找兩邊可確認的同款商品');
|
||
|
||
try {
|
||
const response = await fetchWithCSRF('/api/price_comparison/quick_compare', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
pchome_products: pchomeProducts,
|
||
momo_products: momoProducts
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
hideProgress();
|
||
|
||
if (data.success) {
|
||
comparisonResult = data.data;
|
||
displayResult(comparisonResult);
|
||
renderPriceCommandDashboard();
|
||
showToast(`比價完成,找到 ${comparisonResult.matched_count} 筆同款`, 'success');
|
||
} else {
|
||
showToast(data.message, 'danger');
|
||
}
|
||
} catch (error) {
|
||
hideProgress();
|
||
showToast('比價失敗:' + error.message, 'danger');
|
||
}
|
||
}
|
||
|
||
function displayResult(result) {
|
||
result = result || {};
|
||
const stats = result.stats || {};
|
||
const matches = Array.isArray(result.matches) ? result.matches : [];
|
||
const urgentCount = Number(stats.momo_cheaper_count || 0);
|
||
const goodCount = Number(stats.pchome_cheaper_count || 0);
|
||
const watchCount = Math.max(0, matches.length - urgentCount - goodCount);
|
||
|
||
// 更新統計
|
||
document.getElementById('matchedCount').textContent = result.matched_count || matches.length;
|
||
document.getElementById('pchomeCheaperCount').textContent = goodCount;
|
||
document.getElementById('momoCheaperCount').textContent = urgentCount;
|
||
document.getElementById('avgPriceDiff').textContent = formatMoney(stats.avg_price_diff || 0);
|
||
setText('priceUrgentText', `${urgentCount} 筆`);
|
||
setText('priceGoodText', `${goodCount} 筆`);
|
||
setText('priceWatchText', `${watchCount} 筆`);
|
||
setText('priceUrgentMetric', urgentCount.toLocaleString());
|
||
setText('priceGoodMetric', goodCount.toLocaleString());
|
||
setText('priceWatchMetric', watchCount.toLocaleString());
|
||
setText('priceResultSummary', matches.length ? `共找到 ${matches.length} 筆同款` : '尚未找到同款');
|
||
if (urgentCount > 0) {
|
||
setText('priceResultHeadline', `先處理 ${urgentCount.toLocaleString()} 筆 PChome 價格偏高商品`);
|
||
setText('priceResultAdvice', '建議先檢查售價、折扣、組合內容與活動曝光,避免高業績商品被外部低價壓住。');
|
||
} else if (goodCount > 0) {
|
||
setText('priceResultHeadline', `可主推 ${goodCount.toLocaleString()} 筆 PChome 價格有利商品`);
|
||
setText('priceResultAdvice', '建議安排首頁曝光、搜尋關鍵字、活動文案或 EDM,把價格優勢轉成流量。');
|
||
} else if (matches.length) {
|
||
setText('priceResultHeadline', '價格差距不大,改看內容與曝光');
|
||
setText('priceResultAdvice', '下一步檢查商品頁賣點、圖片、庫存與活動位置,找出非價格因素。');
|
||
} else {
|
||
setText('priceResultHeadline', '尚未找到可判讀同款');
|
||
setText('priceResultAdvice', '請改用更精準的商品名稱、型號或容量搜尋,或先檢查 MOMO 候選。');
|
||
}
|
||
const denominator = Math.max(matches.length, 1);
|
||
setWidth('priceUrgentBar', (urgentCount / denominator) * 100);
|
||
setWidth('priceGoodBar', (goodCount / denominator) * 100);
|
||
setWidth('priceWatchBar', (watchCount / denominator) * 100);
|
||
|
||
// 建立表格
|
||
const tbody = document.getElementById('resultBody');
|
||
tbody.innerHTML = '';
|
||
|
||
for (const m of matches) {
|
||
const row = document.createElement('tr');
|
||
|
||
const similarityClass = m.similarity >= 0.8 ? 'text-success' : (m.similarity >= 0.6 ? 'text-warning' : 'text-danger');
|
||
const actionBadge = getResultActionBadge(m);
|
||
const gapText = formatPriceGap(m);
|
||
const pchomeName = String(m.pchome?.name || '未命名商品');
|
||
const momoName = String(m.momo?.name || '未命名商品');
|
||
const pchomeId = escapeHtml(String(m.pchome?.product_id || ''));
|
||
const momoId = escapeHtml(String(m.momo?.product_id || ''));
|
||
|
||
// 處理 URL (可能是 url 或 product_url)
|
||
const pchomeUrl = toSafeUrl(m.pchome?.url || m.pchome?.product_url || '');
|
||
const momoUrl = toSafeUrl(m.momo?.url || m.momo?.product_url || '');
|
||
const dualOpenButton = pchomeUrl && momoUrl
|
||
? `<button type="button"
|
||
class="btn btn-sm btn-outline-primary mt-2"
|
||
data-pchome-url="${escapeHtml(pchomeUrl)}"
|
||
data-momo-url="${escapeHtml(momoUrl)}"
|
||
onclick="openComparisonStores(this)">
|
||
<i class="fas fa-up-right-from-square me-1"></i>雙開賣場
|
||
</button>`
|
||
: '';
|
||
|
||
row.innerHTML = `
|
||
<td>
|
||
${actionBadge}
|
||
${dualOpenButton}
|
||
</td>
|
||
<td>
|
||
<div class="d-flex align-items-start">
|
||
<div class="flex-grow-1">
|
||
<small class="text-truncate d-block" style="max-width: 220px;" title="${escapeHtml(pchomeName)}">
|
||
${escapeHtml(pchomeName)}
|
||
</small>
|
||
<small class="text-muted">${pchomeId}</small>
|
||
</div>
|
||
${pchomeUrl ? `<a href="${escapeHtml(pchomeUrl)}" target="_blank" class="btn btn-sm btn-outline-info ms-1" title="前往 PChome 查看"><i class="fas fa-external-link-alt"></i></a>` : ''}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div class="d-flex align-items-start">
|
||
<div class="flex-grow-1">
|
||
<small class="text-truncate d-block" style="max-width: 220px;" title="${escapeHtml(momoName)}">
|
||
${escapeHtml(momoName)}
|
||
</small>
|
||
<small class="text-muted">${momoId}</small>
|
||
</div>
|
||
${momoUrl ? `<a
|
||
href="${escapeHtml(momoUrl)}"
|
||
target="_blank"
|
||
class="btn btn-sm btn-outline-warning ms-1 momo-tracked-link"
|
||
title="前往 MOMO 查看"
|
||
data-momo-original-url="${escapeHtml(momoUrl)}"
|
||
data-track-platform="momo"
|
||
data-track-source="price-comparison"
|
||
data-track-product-id="${momoId}"
|
||
data-track-icode="${momoId}"
|
||
data-track-product-name="${escapeHtml(momoName)}">
|
||
<i class="fas fa-external-link-alt"></i>
|
||
</a>` : ''}
|
||
</div>
|
||
</td>
|
||
<td class="text-center ${similarityClass}">
|
||
<strong>${Math.round(m.similarity * 100)}%</strong>
|
||
</td>
|
||
<td class="text-end ${m.cheaper_at === 'pchome' ? 'text-success fw-bold' : ''}">
|
||
${formatMoney(m.pchome?.price)}
|
||
</td>
|
||
<td class="text-end ${m.cheaper_at === 'momo' ? 'text-warning fw-bold' : ''}">
|
||
${formatMoney(m.momo?.price)}
|
||
</td>
|
||
<td class="text-end ${m.price_diff < 0 ? 'text-success' : (m.price_diff > 0 ? 'text-danger' : '')}">
|
||
${gapText}
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
|
||
document.getElementById('resultSection').style.display = 'block';
|
||
}
|
||
|
||
function openComparisonStores(button) {
|
||
const pchomeUrl = toSafeUrl(button?.dataset?.pchomeUrl || '');
|
||
const momoUrl = toSafeUrl(button?.dataset?.momoUrl || '');
|
||
const opened = [];
|
||
if (pchomeUrl) opened.push(window.open(pchomeUrl, `pchome_compare_${Date.now()}`));
|
||
if (momoUrl) opened.push(window.open(momoUrl, `momo_compare_${Date.now()}`));
|
||
for (const win of opened) {
|
||
if (win) {
|
||
try { win.opener = null; } catch (_) {}
|
||
}
|
||
}
|
||
if (opened.some((win) => !win)) {
|
||
showToast('瀏覽器擋住了其中一個視窗;請用商品旁的 PChome / MOMO 按鈕分別開啟。', 'warning');
|
||
}
|
||
}
|
||
|
||
function getResultActionBadge(match) {
|
||
if (match.cheaper_at === 'momo') {
|
||
return '<span class="price-action-pill is-urgent">檢查售價</span>';
|
||
}
|
||
if (match.cheaper_at === 'pchome') {
|
||
return '<span class="price-action-pill is-good">主推曝光</span>';
|
||
}
|
||
return '<span class="price-action-pill is-watch">觀察賣點</span>';
|
||
}
|
||
|
||
function formatPriceGap(match) {
|
||
const diff = Number(match.price_diff || 0);
|
||
const pct = Number(match.price_diff_percent || 0);
|
||
const label = diff > 0 ? 'PChome 貴' : (diff < 0 ? 'PChome 便宜' : '價格相同');
|
||
const amount = diff === 0 ? '$0' : formatMoney(Math.abs(diff));
|
||
const pctText = Number.isFinite(pct) ? `${Math.abs(pct).toFixed(1)}%` : '0.0%';
|
||
return `<strong>${label} ${amount}</strong><br><small>${pctText}</small>`;
|
||
}
|
||
|
||
function exportResult() {
|
||
if (!comparisonResult || !comparisonResult.matches.length) {
|
||
showToast('沒有資料可匯出', 'warning');
|
||
return;
|
||
}
|
||
|
||
const headers = ['下一步', 'PChome商品', 'MOMO商品', '相似度%', 'PChome價', 'MOMO價', '價差'];
|
||
const rows = comparisonResult.matches.map(m => [
|
||
`"${actionLabelForExport(m).replace(/"/g, '""')}"`,
|
||
`"${m.pchome.name.replace(/"/g, '""')}"`,
|
||
`"${m.momo.name.replace(/"/g, '""')}"`,
|
||
Math.round(m.similarity * 100),
|
||
m.pchome.price,
|
||
m.momo.price,
|
||
m.price_diff,
|
||
]);
|
||
|
||
const csv = '\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
||
downloadFile(csv, 'pchome_momo_price_actions.csv', 'text/csv;charset=utf-8');
|
||
}
|
||
|
||
function actionLabelForExport(match) {
|
||
if (match.cheaper_at === 'momo') return '檢查 PChome 售價或活動';
|
||
if (match.cheaper_at === 'pchome') return '安排 PChome 曝光或文案';
|
||
return '觀察賣點與活動位置';
|
||
}
|
||
|
||
function showProgress(title, detail) {
|
||
document.getElementById('progressText').textContent = title;
|
||
document.getElementById('progressDetail').textContent = detail;
|
||
document.getElementById('progressSection').style.display = 'block';
|
||
}
|
||
|
||
function hideProgress() {
|
||
document.getElementById('progressSection').style.display = 'none';
|
||
}
|
||
|
||
function downloadFile(content, filename, type) {
|
||
const blob = new Blob([content], { type });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function formatMoney(value) {
|
||
const number = Number(value || 0);
|
||
return `$${Math.round(number).toLocaleString('zh-TW')}`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function toSafeUrl(value) {
|
||
const target = String(value || '').trim();
|
||
if (!target) return '';
|
||
try {
|
||
const parsed = new URL(target, location.origin);
|
||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||
return parsed.href;
|
||
}
|
||
} catch (error) {
|
||
return '';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function extractMomoCodeFromUrl(url) {
|
||
const target = (url || '').trim();
|
||
if (!target) {
|
||
return '';
|
||
}
|
||
try {
|
||
const parsed = new URL(target, location.origin);
|
||
const iCode = parsed.searchParams.get('i_code');
|
||
if (iCode) {
|
||
return iCode.trim();
|
||
}
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
const match = /[?&]i_code=([^&#]+)/i.exec(target);
|
||
return match ? decodeURIComponent(match[1] || '').trim() : '';
|
||
}
|
||
|
||
function showToast(message, type = 'info') {
|
||
const toast = document.createElement('div');
|
||
toast.className = `price-toast price-toast--${type} position-fixed`;
|
||
const closeButton = document.createElement('button');
|
||
closeButton.type = 'button';
|
||
closeButton.className = 'btn-close float-end';
|
||
closeButton.addEventListener('click', () => toast.remove());
|
||
const text = document.createElement('span');
|
||
text.textContent = message;
|
||
toast.appendChild(closeButton);
|
||
toast.appendChild(text);
|
||
document.body.appendChild(toast);
|
||
setTimeout(() => toast.remove(), 5000);
|
||
}
|
||
</script>
|
||
{% endblock %}
|