diff --git a/assets/css/routes/orders.css b/assets/css/routes/orders.css index 916d1d66..5d6fed71 100644 --- a/assets/css/routes/orders.css +++ b/assets/css/routes/orders.css @@ -1,1692 +1,102 @@ -/** - * AKSRT Orders — user-center embedded + legacy shell - * 订单中心:卡片 / 状态 tab / 统计 / 售后 - */ - -/* ==================== Shell ==================== */ +/* Knowledge-payment order center */ .aksrt-orders-page { - padding: 24px 0; - color: var(--color-ink, #1f2329); -} - -.aksrt-orders-page__header h1 { - font-size: 24px; - font-weight: 700; - margin: 0 0 8px; - color: var(--color-ink); -} - -.aksrt-orders-page__header p { - color: var(--color-ink-3, #8b8f97); - margin: 0 0 20px; -} - -/* UC embed: no outer page padding */ -.uc-orders-panel.aksrt-orders-page { + --orders-ink: var(--color-ink, #1d2738); + --orders-muted: var(--color-ink-3, #758096); + --orders-line: color-mix(in srgb, var(--color-line, #e1e6ef) 88%, transparent); padding: 0; - max-width: 100%; + color: var(--orders-ink); } .uc-orders-panel__head { - gap: 12px; - margin-bottom: 4px; - padding: 2px 2px 12px; - border-bottom: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 80%, transparent); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(285px, 42%); + gap: 28px; + align-items: end; + padding: 4px 0 22px; + border-bottom: 1px solid var(--orders-line); } -.uc-orders-panel__head .font-bold { - letter-spacing: 0.01em; +.aksrt-orders-page__eyebrow { + margin: 0 0 7px; + color: var(--color-primary, #3b82f6); + font-size: 11px; + font-weight: 750; + letter-spacing: .14em; + text-transform: uppercase; } -/* ==================== Stats ==================== */ +.uc-orders-panel__title { display: flex; align-items: center; gap: 9px; font-size: 21px; } +.uc-orders-panel__title-icon { color: var(--color-primary, #3b82f6); font-size: 17px; } +.uc-orders-panel__title-wrap > p:last-child { margin-bottom: 0; } + .aksrt-orders-page__stats { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin: 0 0 18px; -} - -.aksrt-orders-page__stat { - position: relative; - padding: 16px 18px; - border-radius: var(--radius-xl, 14px); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 90%, transparent); - background: - linear-gradient( - 145deg, - color-mix(in srgb, var(--surface-1, #fff) 92%, var(--color-primary, #3b82f6) 8%), - var(--surface-1, #fff) 55% - ); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); - display: flex; - flex-direction: column; - gap: 8px; - overflow: hidden; - transition: transform 0.18s ease, box-shadow 0.18s ease; -} - -.aksrt-orders-page__stat::after { - content: ""; - position: absolute; - right: -12px; - top: -12px; - width: 64px; - height: 64px; - border-radius: 50%; - background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, transparent); - pointer-events: none; -} - -.aksrt-orders-page__stat:hover { - transform: translateY(-1px); - box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06); -} - -.aksrt-orders-page__stat-label { - font-size: 12px; - font-weight: 500; - color: var(--color-ink-3, #8b8f97); - letter-spacing: 0.02em; -} - -.aksrt-orders-page__stat-value { - font-size: 24px; - font-weight: 750; - color: var(--color-ink, #1f2329); - line-height: 1.1; - font-variant-numeric: tabular-nums; -} - -.aksrt-orders-page__stat-value.c-red { - color: var(--color-error, #ef4444); -} - -.aksrt-orders-page__stat-value.c-green { - color: var(--color-success, #16a34a); -} - -/* ==================== Extra nav (sections) ==================== */ -.aksrt-orders-page__extra-nav { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: 0 0 14px; -} - -.aksrt-orders-page__extra-link { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 7px 14px; - border-radius: 999px; - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-4, var(--surface-2, #f6f7f9)); - color: var(--color-ink-2, #4b5563); - font-size: 13px; - font-weight: 550; - text-decoration: none; - transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; -} - -.aksrt-orders-page__extra-link:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 35%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); - background: color-mix(in srgb, var(--color-primary, #3b82f6) 8%, var(--surface-1, #fff)); -} - -.aksrt-orders-page__extra-link.is-active { - background: color-mix(in srgb, var(--color-primary, #3b82f6) 12%, var(--surface-1, #fff)); - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 40%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary, #3b82f6) 12%, transparent); -} - -/* ==================== Type filter chips ==================== */ -.aksrt-orders-page__filters, -.aksrt-orders-page__type-filters { - display: flex; - gap: 8px; - flex-wrap: wrap; - margin: 0 0 14px; -} - -.aksrt-orders-page__filter-chip { - padding: 6px 14px; - border-radius: 999px; - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-1, #fff); - font-size: 12.5px; - font-weight: 550; - text-decoration: none; - color: var(--color-ink-2, #4b5563); - transition: all 0.15s ease; - line-height: 1.4; -} - -.aksrt-orders-page__filter-chip:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 40%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); -} - -.aksrt-orders-page__filter-chip.is-active { - background: var(--color-primary, #3b82f6); - color: var(--color-on-primary, #fff); - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 55%, #000 0%); - box-shadow: 0 4px 12px color-mix(in srgb, var(--color-primary, #3b82f6) 28%, transparent); -} - -/* ==================== Status tabs ==================== */ -.aksrt-orders-page__tabs { - display: flex; - gap: 4px; - flex-wrap: nowrap; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: thin; - margin: 0 0 16px; - padding: 4px; - border-radius: 12px; - background: var(--surface-2, #f3f4f6); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent); -} - -.aksrt-orders-page__tab { - position: relative; - flex: 1 0 auto; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - min-width: 72px; - padding: 9px 14px; - border-radius: 9px; - font-size: 13px; - font-weight: 600; - color: var(--color-ink-2, #4b5563); - text-decoration: none; - white-space: nowrap; - transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; -} - -.aksrt-orders-page__tab:hover { - color: var(--color-ink, #1f2329); - background: color-mix(in srgb, var(--surface-1, #fff) 70%, transparent); -} - -.aksrt-orders-page__tab.is-active { - background: var(--surface-1, #fff); - color: var(--color-primary, #3b82f6); - box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08), 0 0 0 1px color-mix(in srgb, var(--color-line, #e5e7eb) 60%, transparent); -} - -.aksrt-orders-page__tab-badge { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 18px; - height: 18px; - padding: 0 5px; - border-radius: 999px; - background: var(--color-error, #ef4444); - color: #fff; - font-size: 11px; - font-weight: 700; - line-height: 1; -} - -.uc-orders-panel .aksrt-orders-page__tab-badge { - margin-left: 2px; -} - -/* ==================== Order cards ==================== */ -.aksrt-orders-page__list { - display: grid; - grid-template-columns: 1fr; - gap: 14px; - width: 100%; -} - -.aksrt-orders-page__card { - background: var(--surface-1, #fff); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 88%, transparent); - border-radius: var(--radius-xl, 14px); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); - overflow: hidden; - transition: box-shadow 0.18s ease, border-color 0.18s ease, transform 0.18s ease; -} - -.aksrt-orders-page__card:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 22%, var(--color-line, #e5e7eb)); - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.07); -} - -.aksrt-orders-page__card-head { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 10px; - padding: 12px 16px; - background: color-mix(in srgb, var(--surface-2, #f6f7f9) 88%, transparent); - border-bottom: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 75%, transparent); -} - -.aksrt-orders-page__card-type { - display: inline-flex; - align-items: center; - padding: 2px 9px; - border-radius: 999px; - background: color-mix(in srgb, var(--color-primary, #3b82f6) 12%, transparent); - color: var(--color-primary, #3b82f6); - font-size: 12px; - font-weight: 650; - letter-spacing: 0.01em; -} - -.aksrt-orders-page__card-number { - font-size: 12.5px; - font-weight: 600; - color: var(--color-ink-2, #4b5563); - font-variant-numeric: tabular-nums; - font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); -} - -.aksrt-orders-page__card-time { - margin-left: auto; - font-size: 12px; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-orders-page__card-status { - font-size: 12.5px; - font-weight: 700; - color: var(--color-primary, #3b82f6); - padding: 2px 0; -} - -/* line items */ -.aksrt-orders-page__card-items { - display: grid; - gap: 12px; - padding: 14px 16px 6px; -} - -.aksrt-orders-page__card-item { - display: grid; - grid-template-columns: 64px minmax(0, 1fr) auto; - gap: 12px; - align-items: center; -} - -/* Item without a thumbnail: drop the fixed 64px image column so the title - * and price fill the row instead of being crushed into the missing image cell. */ -.aksrt-orders-page__card-item--no-image { - grid-template-columns: minmax(0, 1fr) auto; -} - -.aksrt-orders-page__card-img { - width: 64px; - height: 64px; - border-radius: 10px; - object-fit: cover; - background: var(--surface-3, #eef0f3); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent); -} - -.aksrt-orders-page__card-item-info { - min-width: 0; - display: grid; - gap: 4px; -} - -.aksrt-orders-page__card-item-title { - font-size: 14px; - font-weight: 650; - color: var(--color-ink, #1f2329); - line-height: 1.4; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.aksrt-orders-page__card-item-opts { - font-size: 12px; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-orders-page__card-item-price { - text-align: right; - display: grid; - gap: 2px; - justify-items: end; - font-variant-numeric: tabular-nums; -} - -.aksrt-orders-page__card-item-price strong { - font-size: 14px; - font-weight: 700; - color: var(--color-ink, #1f2329); -} - -.aksrt-orders-page__card-item-price span { - font-size: 12px; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-orders-page__card-summary { - display: flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: flex-end; - gap: 8px; - padding: 8px 16px 4px; - font-size: 13px; - color: var(--color-ink-2, #4b5563); -} - -.aksrt-orders-page__card-summary strong { - font-size: 18px; - font-weight: 750; - color: var(--color-error, #ef4444); - font-variant-numeric: tabular-nums; -} - -.aksrt-orders-page__card-summary small { - color: var(--color-ink-3, #8b8f97); - font-size: 12px; -} - -/* logistics */ -.aksrt-orders-page__timeline { - margin: 6px 16px 0; - padding: 10px 12px; - border-radius: 10px; - background: var(--surface-2, #f6f7f9); - border: 1px dashed color-mix(in srgb, var(--color-line, #e5e7eb) 90%, transparent); -} - -.aksrt-orders-page__timeline-toggle { - margin: 0; - font-size: 13px; - font-weight: 600; - color: var(--color-ink-2, #4b5563); - cursor: pointer; - display: flex; - align-items: center; - gap: 8px; - user-select: none; -} - -.aksrt-orders-page__timeline-toggle .fa-chevron-down { - margin-left: auto; - opacity: 0.55; - transition: transform 0.15s ease; -} - -.aksrt-orders-page__timeline.is-open .aksrt-orders-page__timeline-toggle .fa-chevron-down { - transform: rotate(180deg); -} - -.aksrt-orders-page__timeline-body { - margin-top: 10px; - display: grid; - gap: 8px; -} - -.aksrt-orders-page__timeline-item { - display: grid; - grid-template-columns: 120px minmax(0, 1fr); - gap: 10px; - font-size: 12.5px; - color: var(--color-ink-2, #4b5563); -} - -.aksrt-orders-page__timeline-item time { - color: var(--color-ink-3, #8b8f97); - font-variant-numeric: tabular-nums; -} - -/* actions */ -.aksrt-orders-page__card-actions { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - align-items: center; - gap: 8px; - padding: 12px 16px 14px; - border-top: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent); - margin-top: 8px; - background: color-mix(in srgb, var(--surface-1, #fff) 70%, var(--surface-2, #f6f7f9)); -} - -.aksrt-orders-page__card-actions .btn-sm, -.aksrt-orders-page__card-actions .btn-primary, -.aksrt-orders-page__card-actions .btn-outline, -.aksrt-orders-page__card-actions .btn-ghost, -.aksrt-orders-page__card-actions a.btn-outline, -.aksrt-orders-page__card-actions a.btn-ghost { - min-height: 32px; - padding: 0 12px; - border-radius: 999px; - font-size: 12.5px; - font-weight: 600; - line-height: 30px; - display: inline-flex; - align-items: center; - justify-content: center; - text-decoration: none; - cursor: pointer; - transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; -} - -.aksrt-orders-page__card-actions .btn-primary, -.aksrt-orders-page__card-actions button.btn-primary { - border: 1px solid transparent; - background: var(--color-primary, #3b82f6); - color: var(--color-on-primary, #fff); - box-shadow: 0 4px 10px color-mix(in srgb, var(--color-primary, #3b82f6) 25%, transparent); -} - -.aksrt-orders-page__card-actions .btn-primary:hover { - filter: brightness(1.05); -} - -.aksrt-orders-page__card-actions .btn-primary.c-green { - background: var(--color-success, #16a34a); - box-shadow: 0 4px 10px color-mix(in srgb, var(--color-success, #16a34a) 22%, transparent); -} - -.aksrt-orders-page__card-actions .btn-outline, -.aksrt-orders-page__card-actions a.btn-outline { - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-1, #fff); - color: var(--color-ink-2, #4b5563); -} - -.aksrt-orders-page__card-actions .btn-outline:hover, -.aksrt-orders-page__card-actions a.btn-outline:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 40%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); -} - -.aksrt-orders-page__card-actions .btn-ghost, -.aksrt-orders-page__card-actions a.btn-ghost { - border: 1px solid transparent; - background: transparent; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-orders-page__card-actions .btn-ghost:hover { - color: var(--color-ink, #1f2329); - background: color-mix(in srgb, var(--surface-2, #f3f4f6) 80%, transparent); -} - -.aksrt-orders-page__countdown { - font-size: 12.5px; - color: var(--color-warning, #d97706); - font-weight: 650; - margin-right: auto; - font-variant-numeric: tabular-nums; -} - -/* Order detail modal content */ -html.aksrt-modal-open { - overflow: hidden; -} - -.aksrt-order-detail-dialog { - display: flex; - flex-direction: column; - width: min(520px, 100%); - max-height: min(85vh, 760px); - border-radius: 20px; - overflow: hidden; - background: var(--surface-1, #fff); - box-shadow: - 0 24px 60px rgba(15, 23, 42, 0.18), - 0 0 0 1px color-mix(in srgb, var(--color-line, #e5e7eb) 60%, transparent); -} - -.aksrt-order-detail-dialog .aksrt-modal__header { - background: transparent; - border-bottom: none; - padding: 16px 18px 4px; -} - -.aksrt-order-detail-dialog .aksrt-modal__header h3 { - font-size: 16px; - font-weight: 750; - letter-spacing: 0.01em; -} - -.aksrt-order-detail-dialog .aksrt-modal__close { - width: 34px; - height: 34px; - background: color-mix(in srgb, var(--surface-2, #f3f4f6) 88%, transparent); - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-order-detail-dialog .aksrt-modal__close:hover { - color: var(--color-ink, #1f2329); - background: var(--surface-3, #e8eaed); -} - -.aksrt-order-detail-dialog .aksrt-modal__body { - min-height: 0; - padding: 8px 18px 16px; - gap: 14px; - overflow-x: hidden; - overflow-y: auto; - overscroll-behavior: contain; -} - -/* Hero: status + amount */ -.aksrt-order-detail__hero { - position: relative; - margin: 0 -18px; - padding: 16px 18px 18px; - background: - radial-gradient(120% 90% at 100% 0%, color-mix(in srgb, var(--color-primary, #3b82f6) 16%, transparent) 0%, transparent 55%), - linear-gradient(165deg, - color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-1, #fff)) 0%, - var(--surface-1, #fff) 70%); - border-bottom: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 65%, transparent); -} - -.aksrt-order-detail__hero.is-pending, -.aksrt-order-detail__hero.is-created { - background: - radial-gradient(120% 90% at 100% 0%, color-mix(in srgb, var(--color-warning, #d97706) 18%, transparent) 0%, transparent 55%), - linear-gradient(165deg, - color-mix(in srgb, var(--color-warning, #d97706) 10%, var(--surface-1, #fff)) 0%, - var(--surface-1, #fff) 70%); -} - -.aksrt-order-detail__hero.is-paid, -.aksrt-order-detail__hero.is-delivered, -.aksrt-order-detail__hero.is-completed { - background: - radial-gradient(120% 90% at 100% 0%, color-mix(in srgb, var(--color-success, #16a34a) 16%, transparent) 0%, transparent 55%), - linear-gradient(165deg, - color-mix(in srgb, var(--color-success, #16a34a) 10%, var(--surface-1, #fff)) 0%, - var(--surface-1, #fff) 70%); -} - -.aksrt-order-detail__hero.is-cancelled, -.aksrt-order-detail__hero.is-expired, -.aksrt-order-detail__hero.is-refunded, -.aksrt-order-detail__hero.is-refunding { - background: - radial-gradient(120% 90% at 100% 0%, color-mix(in srgb, var(--color-ink-3, #8b8f97) 14%, transparent) 0%, transparent 55%), - linear-gradient(165deg, - color-mix(in srgb, var(--color-ink-3, #8b8f97) 8%, var(--surface-1, #fff)) 0%, - var(--surface-1, #fff) 70%); -} - -.aksrt-order-detail__hero-top { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin-bottom: 14px; -} - -.aksrt-order-detail__status-pill { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 5px 12px; - border-radius: 999px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.02em; - background: color-mix(in srgb, var(--color-primary, #3b82f6) 14%, transparent); - color: var(--color-primary, #3b82f6); -} - -.aksrt-order-detail__status-pill::before { - content: ""; - width: 6px; - height: 6px; - border-radius: 50%; - background: currentColor; - box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 18%, transparent); -} - -.aksrt-order-detail__status-pill.is-pending, -.aksrt-order-detail__status-pill.is-created, -.aksrt-order-detail__status-pill.is-pending-review { - background: color-mix(in srgb, var(--color-warning, #d97706) 14%, transparent); - color: var(--color-warning, #d97706); -} - -.aksrt-order-detail__status-pill.is-paid, -.aksrt-order-detail__status-pill.is-delivered, -.aksrt-order-detail__status-pill.is-completed { - background: color-mix(in srgb, var(--color-success, #16a34a) 14%, transparent); - color: var(--color-success, #16a34a); -} - -.aksrt-order-detail__status-pill.is-cancelled, -.aksrt-order-detail__status-pill.is-expired, -.aksrt-order-detail__status-pill.is-refunded, -.aksrt-order-detail__status-pill.is-refunding { - background: color-mix(in srgb, var(--color-ink-3, #8b8f97) 14%, transparent); - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-order-detail__type-chip { - display: inline-flex; - align-items: center; - padding: 4px 10px; - border-radius: 999px; - font-size: 11.5px; - font-weight: 600; - color: var(--color-ink-2, #4b5563); - background: color-mix(in srgb, var(--surface-2, #f3f4f6) 90%, transparent); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 80%, transparent); -} - -.aksrt-order-detail__hero-amount-wrap { - display: flex; - flex-direction: column; - gap: 4px; -} - -.aksrt-order-detail__hero-label { - font-size: 12px; - color: var(--color-ink-3, #8b8f97); - letter-spacing: 0.02em; -} - -.aksrt-order-detail__hero-amount { - font-size: 28px; - font-weight: 800; - line-height: 1.15; - color: var(--color-error, #ef4444); - font-variant-numeric: tabular-nums; - letter-spacing: -0.02em; -} - -/* Product card */ -.aksrt-order-detail__product { - display: grid; - grid-template-columns: 64px minmax(0, 1fr); - gap: 12px; - align-items: center; - padding: 12px; - border-radius: 14px; - background: color-mix(in srgb, var(--surface-2, #f6f7f9) 70%, var(--surface-1, #fff)); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 80%, transparent); -} - -.aksrt-order-detail__thumb { - width: 64px; - height: 64px; - border-radius: 12px; - object-fit: cover; - background: var(--surface-3, #eef0f3); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent); -} - -.aksrt-order-detail__thumb--empty { - display: inline-flex; - align-items: center; - justify-content: center; - color: color-mix(in srgb, var(--color-primary, #3b82f6) 55%, var(--color-ink-4, #c0c4cc)); - font-size: 20px; - background: - radial-gradient(circle at 30% 30%, color-mix(in srgb, var(--color-primary, #3b82f6) 12%, transparent), transparent 65%), - var(--surface-3, #eef0f3); -} - -.aksrt-order-detail__product-info { - min-width: 0; - display: grid; - gap: 6px; -} - -.aksrt-order-detail__product-title { - font-size: 14.5px; - font-weight: 700; - color: var(--color-ink, #1f2329); - line-height: 1.4; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.aksrt-order-detail__product-meta { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px 8px; - font-size: 12.5px; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-order-detail__dot { - width: 3px; - height: 3px; - border-radius: 50%; - background: var(--color-ink-4, #c0c4cc); -} - -/* Sections */ -.aksrt-order-detail__section { - display: grid; - gap: 10px; -} - -.aksrt-order-detail__section-title { - margin: 0; - font-size: 13px; - font-weight: 700; - color: var(--color-ink, #1f2329); - display: flex; - align-items: center; - gap: 8px; -} - -.aksrt-order-detail__section-title::before { - content: ""; - width: 3px; - height: 13px; - border-radius: 2px; - background: linear-gradient(180deg, var(--color-primary, #3b82f6), color-mix(in srgb, var(--color-primary, #3b82f6) 40%, #fff)); - display: inline-block; -} - -.aksrt-order-detail__list { - margin: 0; - padding: 0; - display: grid; - gap: 0; + grid-template-columns: repeat(3, 1fr); + border: 1px solid var(--orders-line); border-radius: 14px; background: var(--surface-1, #fff); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 88%, transparent); overflow: hidden; } - -.aksrt-order-detail__row { - display: grid; - grid-template-columns: 88px minmax(0, 1fr); - gap: 10px 12px; - align-items: center; - padding: 11px 14px; - font-size: 13.5px; - border-bottom: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 65%, transparent); - background: transparent; - transition: background 0.12s ease; -} - -.aksrt-order-detail__row:last-child { - border-bottom: none; -} - -.aksrt-order-detail__row:hover { - background: color-mix(in srgb, var(--surface-2, #f6f7f9) 55%, transparent); -} - -.aksrt-order-detail__row dt { - margin: 0; - color: var(--color-ink-3, #8b8f97); - font-weight: 500; - font-size: 12.5px; -} - -.aksrt-order-detail__row dd { - margin: 0; - color: var(--color-ink, #1f2329); - font-weight: 600; - word-break: break-all; - font-variant-numeric: tabular-nums; - text-align: right; - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 6px; - min-width: 0; -} - -.aksrt-order-detail__value { - min-width: 0; -} - -.aksrt-order-detail__row.is-discount dd { - color: var(--color-success, #16a34a); -} - -.aksrt-order-detail__copy { - flex: 0 0 auto; - width: 28px; - height: 28px; - border: none; - border-radius: 8px; - background: color-mix(in srgb, var(--surface-2, #f3f4f6) 90%, transparent); - color: var(--color-ink-3, #8b8f97); - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 12px; - transition: background 0.15s ease, color 0.15s ease, transform 0.15s ease; -} - -.aksrt-order-detail__copy:hover { - color: var(--color-primary, #3b82f6); - background: color-mix(in srgb, var(--color-primary, #3b82f6) 12%, transparent); -} - -.aksrt-order-detail__copy.is-copied { - color: var(--color-success, #16a34a); - background: color-mix(in srgb, var(--color-success, #16a34a) 14%, transparent); - transform: scale(1.05); -} - -/* Multi-item list */ -.aksrt-order-detail__items { - margin: 0; - padding: 0; - list-style: none; - display: grid; - gap: 0; - border-radius: 14px; - background: var(--surface-1, #fff); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 88%, transparent); - overflow: hidden; -} - -.aksrt-order-detail__item { - display: grid; - grid-template-columns: 40px minmax(0, 1fr) auto auto; - gap: 10px; - align-items: center; - padding: 10px 12px; - font-size: 13px; - color: var(--color-ink-2, #4b5563); - border-bottom: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 65%, transparent); -} - -.aksrt-order-detail__item:last-child { - border-bottom: none; -} - -.aksrt-order-detail__item-img { - width: 40px; - height: 40px; - border-radius: 8px; - object-fit: cover; - background: var(--surface-3, #eef0f3); -} - -.aksrt-order-detail__item-title { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-weight: 600; - color: var(--color-ink, #1f2329); -} - -.aksrt-order-detail__item-qty { - font-style: normal; - color: var(--color-ink-3, #8b8f97); - font-variant-numeric: tabular-nums; -} - -.aksrt-order-detail__item-price { - font-weight: 700; - color: var(--color-ink, #1f2329); - font-variant-numeric: tabular-nums; - min-width: 64px; - text-align: right; -} - -/* Timeline */ -.aksrt-order-detail__timeline { - position: relative; - margin: 0; - padding: 4px 0 2px 18px; - list-style: none; - display: grid; - gap: 12px; -} - -.aksrt-order-detail__timeline::before { - content: ""; - position: absolute; - left: 5px; - top: 10px; - bottom: 10px; - width: 2px; - border-radius: 2px; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-primary, #3b82f6) 55%, transparent), - color-mix(in srgb, var(--color-line, #e5e7eb) 90%, transparent) - ); -} - -.aksrt-order-detail__timeline-item { - position: relative; - display: grid; -} - -.aksrt-order-detail__timeline-dot { - position: absolute; - left: -18px; - top: 6px; - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--color-primary, #3b82f6); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary, #3b82f6) 14%, transparent); -} - -.aksrt-order-detail__timeline-item.is-latest .aksrt-order-detail__timeline-dot { - background: var(--color-success, #16a34a); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success, #16a34a) 16%, transparent); -} - -.aksrt-order-detail__timeline-body { - display: grid; - gap: 2px; -} - -.aksrt-order-detail__timeline-label { - font-size: 13.5px; - font-weight: 650; - color: var(--color-ink, #1f2329); -} - -.aksrt-order-detail__timeline-item.is-latest .aksrt-order-detail__timeline-label { - color: var(--color-success, #16a34a); -} - -.aksrt-order-detail__timeline-time { - font-size: 12px; - color: var(--color-ink-3, #8b8f97); - font-variant-numeric: tabular-nums; -} - -/* Footer actions */ -.aksrt-order-detail-dialog .aksrt-modal__footer { - padding: 14px 18px 16px; - background: color-mix(in srgb, var(--surface-2, #f6f7f9) 72%, transparent); - border-top: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent); - gap: 8px; - justify-content: flex-end; -} - -.aksrt-order-detail-dialog .aksrt-modal__footer:empty { - display: none; -} - -.aksrt-modal__footer .btn-primary, -.aksrt-modal__footer .btn-outline, -.aksrt-modal__footer .btn-ghost, -.aksrt-modal__footer a.btn-ghost, -.aksrt-modal__footer a.btn-outline { - min-height: 38px; - padding: 0 18px; - border-radius: 11px; - font-size: 13.5px; - font-weight: 650; - line-height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - text-decoration: none; - cursor: pointer; - transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; -} - -.aksrt-modal__footer .btn-primary { - border: 1px solid transparent; - background: linear-gradient(180deg, color-mix(in srgb, var(--color-primary, #3b82f6) 92%, #fff), var(--color-primary, #3b82f6)); - color: var(--color-on-primary, #fff); - box-shadow: 0 8px 18px color-mix(in srgb, var(--color-primary, #3b82f6) 28%, transparent); -} - -.aksrt-modal__footer .btn-primary:hover { - filter: brightness(1.04); - transform: translateY(-1px); -} - -.aksrt-modal__footer .btn-primary.c-green { - background: linear-gradient(180deg, color-mix(in srgb, var(--color-success, #16a34a) 90%, #fff), var(--color-success, #16a34a)); - box-shadow: 0 8px 18px color-mix(in srgb, var(--color-success, #16a34a) 28%, transparent); -} - -.aksrt-modal__footer .btn-outline { - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-1, #fff); - color: var(--color-ink-2, #4b5563); -} - -.aksrt-modal__footer .btn-outline:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); - transform: translateY(-1px); -} - -.aksrt-modal__footer .btn-ghost { - border: 1px solid transparent; - background: transparent; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-modal__footer .btn-ghost:hover { - color: var(--color-ink, #1f2329); - background: color-mix(in srgb, var(--surface-2, #f3f4f6) 80%, transparent); -} - -/* Dark mode polish */ -html.dark .aksrt-order-detail-dialog { - background: var(--surface-1, #1a1d24); - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45); -} - -html.dark .aksrt-order-detail__product, -html.dark .aksrt-order-detail__list, -html.dark .aksrt-order-detail__items { - background: color-mix(in srgb, var(--surface-2, #12151b) 55%, var(--surface-1, #1a1d24)); - border-color: color-mix(in srgb, var(--color-line, #333) 90%, transparent); -} - -html.dark .aksrt-order-detail__type-chip, -html.dark .aksrt-order-detail__copy { - background: var(--surface-2, #12151b); - border-color: var(--color-line, #333); -} - -html.dark .aksrt-order-detail-dialog .aksrt-modal__footer { - background: color-mix(in srgb, var(--surface-2, #12151b) 80%, transparent); -} - -@media (max-width: 767px) { - .aksrt-order-detail-dialog { - width: 100%; - max-width: 100%; - border-radius: 18px 18px 0 0; - margin-top: auto; - max-height: 92vh; - } - - .aksrt-order-detail__hero-amount { - font-size: 26px; - } - - .aksrt-order-detail__product { - grid-template-columns: 56px minmax(0, 1fr); - } - - .aksrt-order-detail__thumb { - width: 56px; - height: 56px; - } - - .aksrt-order-detail__row { - grid-template-columns: 76px minmax(0, 1fr); - padding: 10px 12px; - } - - .aksrt-order-detail__item { - grid-template-columns: 36px minmax(0, 1fr) auto auto; - } - - .aksrt-modal__footer { - flex-wrap: wrap; - } - - .aksrt-modal__footer .btn-primary, - .aksrt-modal__footer .btn-outline, - .aksrt-modal__footer .btn-ghost, - .aksrt-modal__footer a { - flex: 1 1 calc(50% - 8px); - min-width: 0; - } -} - -/* legacy item classes (if any remain) */ -.aksrt-orders-page__item { - background: var(--surface-1); - border-radius: var(--radius-2xl, 16px); - box-shadow: var(--shadow-sm); - overflow: hidden; -} - -/* ==================== Pagination ==================== */ -.aksrt-orders-page__pagination { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 8px; - margin-top: 22px; -} - -.aksrt-orders-page__page-chip { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 36px; - height: 36px; - padding: 0 10px; - border-radius: 10px; - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-1, #fff); - font-size: 13px; - font-weight: 600; - text-decoration: none; - color: var(--color-ink-2, #4b5563); - transition: all 0.15s ease; -} - -.aksrt-orders-page__page-chip:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 45%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); -} - -.aksrt-orders-page__page-chip.is-active { - background: var(--color-primary, #3b82f6); - color: var(--color-on-primary, #fff); - border-color: transparent; - box-shadow: 0 4px 12px color-mix(in srgb, var(--color-primary, #3b82f6) 28%, transparent); -} - -/* ==================== Empty ==================== */ -.aksrt-orders-page__empty { - text-align: center; - padding: 56px 20px; - border-radius: var(--radius-xl, 14px); - border: 1px dashed color-mix(in srgb, var(--color-line, #e5e7eb) 90%, transparent); - background: - radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--color-primary, #3b82f6) 8%, transparent), transparent 55%), - var(--surface-2, #f6f7f9); -} - -.aksrt-orders-page__empty i { - font-size: 42px; - color: color-mix(in srgb, var(--color-primary, #3b82f6) 45%, var(--color-ink-4, #c0c4cc)); - margin-bottom: 14px; - display: block; - opacity: 0.9; -} - -.aksrt-orders-page__empty p { - margin: 0; - font-size: 14px; - color: var(--color-ink-3, #8b8f97); -} - -/* ==================== After-sale / coupons / favorites ==================== */ -.aksrt-orders-page__aftersale, -.aksrt-orders-page__coupons, -.aksrt-orders-page__favorites { - margin-top: 4px; -} - -.aksrt-orders-page__aftersale h2, -.aksrt-orders-page__coupons h2, -.aksrt-orders-page__favorites h2 { - margin: 0 0 14px; - font-size: 16px; - font-weight: 700; -} - -.aksrt-as-list { - display: grid; - gap: 12px; -} - -.aksrt-as-card { - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 88%, transparent); - border-radius: 12px; - background: var(--surface-1, #fff); - padding: 14px 16px; - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); -} - -.aksrt-as-card__head { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px 12px; - margin-bottom: 6px; -} - -.aksrt-as-card__order { - font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); - font-size: 12px; - color: var(--color-ink-3, #8b8f97); -} - -.aksrt-as-card__status { - margin-left: auto; - font-size: 12.5px; - font-weight: 700; - color: var(--color-warning, #d97706); -} - -.aksrt-as-card__meta { - font-size: 12px; - margin-bottom: 8px; -} - -.aksrt-as-card__reason { - font-size: 13.5px; - color: var(--color-ink-2, #4b5563); - line-height: 1.5; - margin-bottom: 10px; -} - -.aksrt-as-timeline { - margin: 0 0 12px; - padding: 0 0 0 14px; - border-left: 2px solid color-mix(in srgb, var(--color-primary, #3b82f6) 25%, var(--color-line, #e5e7eb)); - list-style: none; - display: grid; - gap: 8px; -} - -.aksrt-as-timeline li { - display: grid; - gap: 2px; - font-size: 12.5px; - color: var(--color-ink-2, #4b5563); -} - -.aksrt-as-timeline time { - color: var(--color-ink-3, #8b8f97); - font-size: 11.5px; -} - -.aksrt-as-card__actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: flex-end; -} - -.aksrt-orders-page__fav-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 12px; -} - -.aksrt-orders-page__fav-card { - display: grid; - gap: 8px; - padding: 10px; - border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 88%, transparent); - background: var(--surface-1, #fff); - text-decoration: none; - color: inherit; - transition: box-shadow 0.15s ease, transform 0.15s ease; -} - -.aksrt-orders-page__fav-card:hover { - box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08); - transform: translateY(-1px); -} - -.aksrt-orders-page__fav-card img { - width: 100%; - aspect-ratio: 1; - object-fit: cover; - border-radius: 8px; - background: var(--surface-3, #eef0f3); -} - -.aksrt-orders-page__fav-card h3 { - margin: 0; - font-size: 13px; - font-weight: 600; - line-height: 1.35; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.aksrt-orders-page__fav-card strong { - color: var(--color-error, #ef4444); - font-size: 14px; -} - -/* ==================== Form fields + modal ==================== */ -.aksrt-orders-page__field { - display: grid; - gap: 6px; -} - -.aksrt-orders-page__field label { - font-size: 13.5px; - font-weight: 600; - color: var(--color-ink, #1f2329); -} - -.aksrt-orders-page__select, -.aksrt-orders-page__textarea, -.aksrt-orders-page__input { - width: 100%; - border: 1px solid var(--color-line, #e5e7eb); - border-radius: 10px; - padding: 10px 12px; - font-size: 14px; - color: var(--color-ink, #1f2329); - background: var(--surface-1, #fff); - transition: border-color 0.15s ease, box-shadow 0.15s ease; - box-sizing: border-box; -} - -.aksrt-orders-page__select:focus, -.aksrt-orders-page__textarea:focus, -.aksrt-orders-page__input:focus { - outline: none; - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 50%, var(--color-line, #e5e7eb)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary, #3b82f6) 16%, transparent); -} - -.aksrt-modal[hidden] { - display: none !important; -} - -.aksrt-modal { - position: fixed; - inset: 0; - z-index: var(--z-modal, 10050); - display: none; - align-items: center; - justify-content: center; - padding: 16px; -} - -.aksrt-modal:not([hidden]) { - display: flex; -} - -.aksrt-modal.is-open { - display: flex; -} - -.aksrt-modal__overlay { - position: absolute; - inset: 0; - background: rgba(15, 23, 42, 0.48); - backdrop-filter: blur(5px); - -webkit-backdrop-filter: blur(5px); -} - -.aksrt-modal__dialog { - position: relative; - background: var(--surface-1, #fff); - border-radius: 16px; - width: min(520px, 100%); - max-height: 85vh; - overflow-y: auto; - box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22); - border: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 70%, transparent); -} - -.aksrt-modal__dialog.aksrt-order-detail-dialog { - overflow: hidden; -} - -.aksrt-modal__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 18px; - border-bottom: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 80%, transparent); - position: sticky; - top: 0; - background: var(--surface-1, #fff); - z-index: 1; -} - -.aksrt-modal__header h3 { - margin: 0; - font-size: 16px; - font-weight: 700; - color: var(--color-ink, #1f2329); -} - -.aksrt-modal__close { - border: none; - background: var(--surface-2, #f3f4f6); - width: 32px; - height: 32px; - border-radius: 999px; - font-size: 20px; - cursor: pointer; - color: var(--color-ink-3, #8b8f97); - line-height: 1; - transition: color 0.15s ease, background 0.15s ease; -} - -.aksrt-modal__close:hover { - color: var(--color-ink, #1f2329); - background: var(--surface-3, #e8eaed); -} - -.aksrt-modal__body { - padding: 16px 18px; - display: grid; - gap: 14px; -} - -.aksrt-modal__footer { - display: flex; - justify-content: flex-end; - gap: 10px; - padding: 14px 18px 16px; - border-top: 1px solid color-mix(in srgb, var(--color-line, #e5e7eb) 80%, transparent); -} - -/* ==================== Responsive ==================== */ -@media (max-width: 767px) { - .aksrt-orders-page { - padding: 16px 0; - } - - .aksrt-orders-page__stats { - grid-template-columns: 1fr; - } - - .aksrt-orders-page__filters, - .aksrt-orders-page__type-filters { - flex-wrap: nowrap; - overflow-x: auto; - padding-bottom: 2px; - -webkit-overflow-scrolling: touch; - } - - .aksrt-orders-page__filter-chip { - flex-shrink: 0; - } - - .aksrt-orders-page__card-item { - grid-template-columns: 56px minmax(0, 1fr); - } - - .aksrt-orders-page__card-img { - width: 56px; - height: 56px; - } - - .aksrt-orders-page__card-item-price { - grid-column: 2; - text-align: left; - justify-items: start; - flex-direction: row; - display: flex; - gap: 10px; - align-items: baseline; - } - - .aksrt-orders-page__card-item--no-image { - grid-template-columns: minmax(0, 1fr); - } - - .aksrt-orders-page__card-item--no-image .aksrt-orders-page__card-item-price { - grid-column: auto; - } - - .aksrt-orders-page__card-time { - margin-left: 0; - width: 100%; - order: 5; - } - - .aksrt-orders-page__card-head { - gap: 6px 8px; - } - - .aksrt-orders-page__card-actions { - justify-content: stretch; - } - - .aksrt-orders-page__card-actions .btn-sm, - .aksrt-orders-page__card-actions .btn-primary, - .aksrt-orders-page__card-actions .btn-outline, - .aksrt-orders-page__card-actions .btn-ghost, - .aksrt-orders-page__card-actions a { - flex: 1 1 auto; - } - - .aksrt-orders-page__timeline-item { - grid-template-columns: 1fr; - gap: 2px; - } - - .aksrt-orders-page__countdown { - width: 100%; - margin-right: 0; - margin-bottom: 4px; - } -} - -@media (max-width: 480px) { - .aksrt-orders-page__stat { - padding: 14px; - } - - .aksrt-orders-page__stat-value { - font-size: 20px; - } - - .aksrt-orders-page__tab { - min-width: 64px; - padding: 8px 10px; - font-size: 12.5px; - } - - .aksrt-modal__dialog { - max-height: 92vh; - } -} - -/* ==================== Dark mode ==================== */ -html.dark .aksrt-orders-page__stat, -html.dark .aksrt-orders-page__card, -html.dark .aksrt-as-card, -html.dark .aksrt-orders-page__fav-card { - background: var(--surface-1, #1a1d24); - border-color: color-mix(in srgb, var(--color-line, #333) 90%, transparent); - box-shadow: none; -} - -html.dark .aksrt-orders-page__stat { - background: linear-gradient( - 145deg, - color-mix(in srgb, var(--surface-1, #1a1d24) 88%, var(--color-primary, #3b82f6) 12%), - var(--surface-1, #1a1d24) 60% - ); -} - -html.dark .aksrt-orders-page__tabs { - background: var(--surface-2, #12151b); -} - -html.dark .aksrt-orders-page__tab.is-active { - background: var(--surface-1, #1a1d24); -} - -html.dark .aksrt-orders-page__card-head, -html.dark .aksrt-orders-page__card-actions { - background: color-mix(in srgb, var(--surface-2, #12151b) 80%, transparent); -} - -html.dark .aksrt-orders-page__empty { - background: - radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--color-primary, #3b82f6) 14%, transparent), transparent 55%), - var(--surface-2, #12151b); -} - -html.dark .aksrt-orders-page__filter-chip { - background: var(--surface-2, #12151b); - border-color: var(--color-line, #333); -} - -html.dark .aksrt-modal__dialog { - background: var(--surface-1, #1a1d24); - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45); -} - -html.dark .aksrt-modal__header, -html.dark .aksrt-modal__close { - background: var(--surface-1, #1a1d24); -} - -html.dark .aksrt-modal__close { - background: var(--surface-2, #12151b); -} - -/* UC panel title / cart chip */ -.uc-orders-panel__title { - display: inline-flex; - align-items: center; - gap: 8px; - font-size: 16px; -} -.uc-orders-panel__title-icon { - width: 28px; - height: 28px; - border-radius: 8px; - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 13px; - color: var(--color-primary, #3b82f6); - background: color-mix(in srgb, var(--color-primary, #3b82f6) 12%, transparent); -} -.uc-orders-panel__cart-link { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 7px 12px; - border-radius: 999px; - border: 1px solid var(--color-line, #e5e7eb); - background: var(--surface-1, #fff); - color: var(--color-ink-2, #4b5563); - font-size: 12.5px; - font-weight: 600; - text-decoration: none; - transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; -} -.uc-orders-panel__cart-link:hover { - border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 40%, var(--color-line, #e5e7eb)); - color: var(--color-primary, #3b82f6); - background: color-mix(in srgb, var(--color-primary, #3b82f6) 8%, var(--surface-1, #fff)); -} -.aksrt-orders-page__empty-hint { - margin: 8px 0 0; - max-width: 280px; - margin-left: auto; - margin-right: auto; - line-height: 1.5; -} +.aksrt-orders-page__stat { display: grid; gap: 5px; padding: 13px 15px; border-right: 1px solid var(--orders-line); } +.aksrt-orders-page__stat:last-child { border-right: 0; } +.aksrt-orders-page__stat span { color: var(--orders-muted); font-size: 11px; font-weight: 600; } +.aksrt-orders-page__stat strong { font-size: 21px; line-height: 1; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__stat strong.is-pending { color: var(--color-warning, #c87819); } +.aksrt-orders-page__stat strong.is-completed { color: var(--color-success, #168560); } + +.aksrt-orders-page__search { display: flex; gap: 8px; align-items: center; margin: 20px 0 12px; padding: 7px; border: 1px solid var(--orders-line); border-radius: 12px; background: var(--surface-1, #fff); } +.aksrt-orders-page__search > i { margin-left: 7px; color: var(--orders-muted); font-size: 13px; } +.aksrt-orders-page__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--orders-ink); font-size: 13px; } +.aksrt-orders-page__search button, .aksrt-orders-page__card-actions .btn-primary, .aksrt-orders-page__card-actions .btn-outline, .aksrt-orders-page__card-actions .btn-ghost, .aksrt-orders-page__empty .btn-outline, .aksrt-modal__footer .btn-primary, .aksrt-modal__footer .btn-outline, .aksrt-modal__footer .btn-ghost { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; padding: 0 13px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 650; text-decoration: none; cursor: pointer; } +.aksrt-orders-page .btn-primary, .aksrt-modal__footer .btn-primary { background: var(--color-primary, #3b82f6); color: var(--color-on-primary, #fff); } +.aksrt-orders-page .btn-outline, .aksrt-modal__footer .btn-outline { border-color: var(--orders-line); background: var(--surface-1, #fff); color: var(--orders-ink); } +.aksrt-orders-page .btn-ghost, .aksrt-modal__footer .btn-ghost { background: transparent; color: var(--orders-muted); } +.aksrt-orders-page__filter-toggle { background: var(--surface-2, #f4f6f9); color: var(--orders-ink); white-space: nowrap; } + +.aksrt-orders-page__filter-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: -2px 0 14px; padding: 12px; border-radius: 10px; background: var(--surface-2, #f4f6f9); } +.aksrt-orders-page__filter-label { color: var(--orders-muted); font-size: 12px; font-weight: 650; } +.aksrt-orders-page__type-filters { display: flex; flex-wrap: wrap; gap: 6px; } +.aksrt-orders-page__filter-chip { padding: 5px 9px; border: 1px solid var(--orders-line); border-radius: 999px; background: var(--surface-1, #fff); color: var(--orders-muted); font-size: 12px; text-decoration: none; } +.aksrt-orders-page__filter-chip.is-active { border-color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-1, #fff)); color: var(--color-primary, #3b82f6); font-weight: 700; } +.aksrt-orders-page__clear { margin-left: auto; color: var(--color-primary, #3b82f6); font-size: 12px; text-decoration: none; } + +.aksrt-orders-page__tabs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin: 0 0 14px; border-bottom: 1px solid var(--orders-line); } +.aksrt-orders-page__tab { position: relative; padding: 11px 8px; color: var(--orders-muted); text-align: center; font-size: 13px; font-weight: 650; text-decoration: none; } +.aksrt-orders-page__tab.is-active { color: var(--orders-ink); } +.aksrt-orders-page__tab.is-active::after { position: absolute; right: 28%; bottom: -1px; left: 28%; height: 2px; border-radius: 2px; background: var(--color-primary, #3b82f6); content: ""; } +.aksrt-orders-page__tab-badge { display: inline-grid; min-width: 17px; height: 17px; place-items: center; border-radius: 50%; background: var(--color-warning, #c87819); color: #fff; font-size: 10px; } + +.aksrt-orders-page__list { display: grid; gap: 10px; } +.aksrt-orders-page__card { overflow: hidden; border: 1px solid var(--orders-line); border-radius: 13px; background: var(--surface-1, #fff); transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease; } +.aksrt-orders-page__card:hover { border-color: color-mix(in srgb, var(--color-primary, #3b82f6) 36%, var(--orders-line)); box-shadow: 0 8px 22px rgba(30, 48, 79, .07); transform: translateY(-1px); } +.aksrt-orders-page__card-rail { display: flex; gap: 10px; align-items: center; padding: 9px 14px; background: var(--surface-2, #f7f8fb); color: var(--orders-muted); font-size: 11px; } +.aksrt-orders-page__card-rail time { margin-left: auto; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__card-status { font-weight: 700; } +.aksrt-orders-page__card-status.is-pending { color: var(--color-warning, #c87819); } +.aksrt-orders-page__card-status.is-completed { color: var(--color-success, #168560); } +.aksrt-orders-page__card-main { display: grid; grid-template-columns: 52px minmax(0, 1fr) auto; gap: 13px; align-items: center; padding: 14px; } +.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 52px; height: 52px; border-radius: 10px; object-fit: cover; } +.aksrt-orders-page__card-art { display: grid; place-items: center; color: var(--color-primary, #3b82f6); background: color-mix(in srgb, var(--color-primary, #3b82f6) 10%, var(--surface-2, #f4f6f9)); } +.aksrt-orders-page__card-copy { min-width: 0; } +.aksrt-orders-page__card-copy h3 { display: -webkit-box; margin: 0 0 5px; overflow: hidden; color: var(--orders-ink); font-size: 14px; line-height: 1.35; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } +.aksrt-orders-page__card-copy p { margin: 0; color: var(--orders-muted); font-family: var(--font-mono, monospace); font-size: 11px; } +.aksrt-orders-page__card-value { display: grid; gap: 3px; text-align: right; } +.aksrt-orders-page__card-value strong { color: var(--orders-ink); font-size: 15px; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__card-value span { color: var(--orders-muted); font-size: 11px; } +.aksrt-orders-page__card-actions { display: flex; gap: 7px; align-items: center; justify-content: flex-end; padding: 9px 14px; border-top: 1px solid var(--orders-line); } +.aksrt-orders-page__countdown { margin-right: auto; color: var(--color-warning, #c87819); font-size: 11px; font-variant-numeric: tabular-nums; } +.aksrt-orders-page__card-actions .is-entitlement { background: var(--color-success, #168560); } + +.aksrt-orders-page__pagination { display: flex; justify-content: center; gap: 6px; margin-top: 20px; } +.aksrt-orders-page__page-chip { display: grid; width: 31px; height: 31px; place-items: center; border: 1px solid var(--orders-line); border-radius: 8px; color: var(--orders-muted); font-size: 12px; text-decoration: none; } +.aksrt-orders-page__page-chip.is-active { border-color: var(--color-primary, #3b82f6); background: var(--color-primary, #3b82f6); color: #fff; } +.aksrt-orders-page__empty { padding: 52px 20px; border: 1px dashed var(--orders-line); border-radius: 13px; background: var(--surface-2, #f7f8fb); text-align: center; } +.aksrt-orders-page__empty i { display: block; margin-bottom: 12px; color: var(--color-primary, #3b82f6); font-size: 30px; } +.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; } + +.aksrt-modal[hidden] { display: none !important; }.aksrt-modal { position: fixed; inset: 0; z-index: var(--z-modal, 10050); display: grid; place-items: center; padding: 16px; }.aksrt-modal__overlay { position: absolute; inset: 0; background: rgba(18, 31, 50, .5); backdrop-filter: blur(4px); }.aksrt-modal__dialog { position: relative; width: min(470px, 100%); overflow: hidden; border: 1px solid var(--orders-line); border-radius: 16px; background: var(--surface-1, #fff); box-shadow: 0 24px 62px rgba(16, 25, 42, .25); }.aksrt-modal__header { display: flex; align-items: center; justify-content: space-between; padding: 15px 17px; border-bottom: 1px solid var(--orders-line); }.aksrt-modal__header h3 { margin: 0; font-size: 15px; }.aksrt-modal__close { width: 28px; height: 28px; border: 0; border-radius: 50%; background: var(--surface-2, #f4f6f9); color: var(--orders-muted); font-size: 19px; cursor: pointer; }.aksrt-modal__body { display: grid; gap: 14px; padding: 16px; }.aksrt-modal__footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; padding: 12px 16px; border-top: 1px solid var(--orders-line); background: var(--surface-2, #f7f8fb); } +.aksrt-order-detail__hero { display: flex; align-items: center; justify-content: space-between; padding: 15px; border-radius: 11px; background: color-mix(in srgb, var(--color-primary, #3b82f6) 9%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__hero.is-pending { background: color-mix(in srgb, var(--color-warning, #c87819) 11%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__hero.is-completed { background: color-mix(in srgb, var(--color-success, #168560) 10%, var(--surface-2, #f7f8fb)); }.aksrt-order-detail__status-pill { font-size: 12px; font-weight: 700; }.aksrt-order-detail__hero-amount { font-size: 23px; font-variant-numeric: tabular-nums; }.aksrt-order-detail__product { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 11px; align-items: center; }.aksrt-order-detail__thumb { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; }.aksrt-order-detail__thumb--empty { display: grid; place-items: center; background: var(--surface-2, #f4f6f9); color: var(--color-primary, #3b82f6); }.aksrt-order-detail__product-title { display: block; line-height: 1.4; }.aksrt-order-detail__product-meta { display: block; margin-top: 5px; color: var(--orders-muted); font-size: 12px; }.aksrt-order-detail__list { margin: 0; border: 1px solid var(--orders-line); border-radius: 10px; overflow: hidden; }.aksrt-order-detail__list > div { display: grid; grid-template-columns: 86px minmax(0, 1fr); padding: 10px 12px; border-bottom: 1px solid var(--orders-line); font-size: 12px; }.aksrt-order-detail__list > div:last-child { border-bottom: 0; }.aksrt-order-detail__list dt { color: var(--orders-muted); }.aksrt-order-detail__list dd { margin: 0; overflow-wrap: anywhere; text-align: right; font-variant-numeric: tabular-nums; } + +@media (max-width: 767px) { .uc-orders-panel__head { grid-template-columns: 1fr; gap: 17px; }.aksrt-orders-page__stats { max-width: none; }.aksrt-orders-page__search { flex-wrap: wrap; }.aksrt-orders-page__search input { min-width: 150px; }.aksrt-orders-page__search .btn-primary { margin-left: auto; }.aksrt-orders-page__filter-toggle { margin-left: auto; }.aksrt-orders-page__card-rail time { width: 100%; margin-left: 0; order: 3; }.aksrt-orders-page__card-main { grid-template-columns: 48px minmax(0, 1fr); }.aksrt-orders-page__card-img, .aksrt-orders-page__card-art { width: 48px; height: 48px; }.aksrt-orders-page__card-value { grid-column: 2; text-align: left; }.aksrt-orders-page__card-actions > :not(.aksrt-orders-page__countdown) { flex: 1 1 auto; }.aksrt-orders-page__countdown { width: 100%; }.aksrt-modal { align-items: end; padding: 0; }.aksrt-modal__dialog { width: 100%; border-radius: 16px 16px 0 0; }.aksrt-modal__footer > * { flex: 1; } } +@media (prefers-reduced-motion: reduce) { .aksrt-orders-page__card { transition: none; } } +html.dark .aksrt-orders-page__stat, html.dark .aksrt-orders-page__card, html.dark .aksrt-modal__dialog { background: var(--surface-1, #1a1d24); } html.dark .aksrt-orders-page__card-rail, html.dark .aksrt-orders-page__filter-panel, html.dark .aksrt-modal__footer { background: var(--surface-2, #12151b); } diff --git a/assets/js/order-center.js b/assets/js/order-center.js index d6e09ed3..0cede03f 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -1,10 +1,5 @@ /** - * AKSRT Order Center — v2.0 - * - * 前台订单中心交互: - * - 支付 / 取消 / 售后:操作成功后局部刷新订单卡片,不再整页 reload - * - 倒计时 - * - Toast 通知替代 window.alert + * Knowledge-payment order center interactions. * * @package AKSRT */ @@ -13,914 +8,269 @@ "use strict"; var page = document.querySelector("[data-aksrt-order-center]"); - if (!page || !window.aksrtData) { - return; - } + if (!page || !window.aksrtData) return; - /* ── Toast helper ── */ - function showToast(text, duration) { - duration = duration || 2500; - var container = document.getElementById("toast-container"); - if (!container) { - container = document.createElement("div"); - container.id = "toast-container"; - container.style.cssText = "position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column-reverse;gap:10px;pointer-events:none;"; - document.body.appendChild(container); - } - var toast = document.createElement("div"); - toast.style.cssText = "pointer-events:auto;display:flex;align-items:center;gap:8px;padding:10px 20px;background:rgba(0,0,0,0.78);backdrop-filter:blur(12px);color:#fff;font-family:var(--font-ui);font-size:14px;font-weight:500;border-radius:100px;box-shadow:0 4px 20px rgba(0,0,0,0.2);animation:aksrtToastIn 0.35s cubic-bezier(0.22,1,0.36,1) forwards;white-space:nowrap;"; - toast.textContent = text; - container.appendChild(toast); - setTimeout(function () { - toast.style.animation = "aksrtToastOut 0.4s ease forwards"; - setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 400); - }, duration); - } - - // Inject toast keyframes if not present - if (!document.getElementById("aksrt-toast-keyframes")) { - var style = document.createElement("style"); - style.id = "aksrt-toast-keyframes"; - style.textContent = "@keyframes aksrtToastIn{0%{opacity:0;transform:translateY(16px) scale(0.92)}100%{opacity:1;transform:translateY(0) scale(1)}}@keyframes aksrtToastOut{0%{opacity:1;transform:translateY(0) scale(1)}100%{opacity:0;transform:translateY(8px) scale(0.95)}}"; - document.head.appendChild(style); - } - - /* ── REST helper ── */ - function restRoot() { - return ((aksrtData.siteUrl || "").replace(/\/$/, "")) + "/wp-json/aksrt/v1"; - } - - function request(path, body, method) { - var opts = { - method: method || "POST", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - "X-WP-Nonce": (aksrtData && aksrtData.restNonce) || "" - } - }; - if (body !== undefined) { - opts.body = JSON.stringify(body || {}); - } - return fetch(restRoot() + path, opts).then(function (response) { - return response.text().then(function (text) { - var data = {}; - try { - data = text ? JSON.parse(text) : {}; - } catch (e) { - data = { - success: false, - message: response.status === 404 - ? "支付接口未找到(404),请刷新页面或联系管理员。" - : ("请求失败(HTTP " + response.status + ")") - }; - } - if (typeof data.success === "undefined" && response.ok === false) { - data.success = false; - data.message = data.message || data.code || ("HTTP " + response.status); - } - return data; - }); - }); - } - - function orderPath(orderNumber, suffix) { - var base = "/shop/frontend/orders/" + encodeURIComponent(orderNumber || ""); - return suffix ? base + suffix : base; - } - - /* ── Order review modal (zibll 1:1) ── */ - var STAR_SVG = - ''; - var SCORE_LABELS = ["", "非常差", "较差", "一般", "较好", "非常好"]; - - function formatRemain(sec) { - sec = Math.max(0, sec | 0); - var d = Math.floor(sec / 86400); - var h = Math.floor((sec % 86400) / 3600); - var m = Math.floor((sec % 3600) / 60); - var s = sec % 60; - function z(n) { return n < 10 ? "0" + n : String(n); } - if (d > 0) return d + "天" + z(h) + ":" + z(m) + ":" + z(s); - return z(h) + ":" + z(m) + ":" + z(s); - } - - function bindShopScore(root) { - if (!root) return; - root.querySelectorAll("[data-aksrt-score-box], .shop-score-box").forEach(function (box) { - if (box.dataset.scoreBound === "1") return; - box.dataset.scoreBound = "1"; - var iconBox = box.querySelector(".score-icon"); - var textBox = box.querySelector(".score-text"); - var input = box.querySelector(".score-input"); - if (!iconBox) return; - var value = parseInt(box.getAttribute("score-value") || (input && input.value) || "0", 10) || 0; - var html = ""; - for (var i = 1; i <= 5; i++) { - html += '' + STAR_SVG + ""; - } - iconBox.innerHTML = html; - function paint(score, hover) { - iconBox.querySelectorAll(".icon-star").forEach(function (star) { - var s = parseInt(star.getAttribute("data-score"), 10) || 0; - star.classList.toggle("active", !hover && s <= score); - star.classList.toggle("hover", !!hover && s <= score); - }); - if (textBox) textBox.textContent = SCORE_LABELS[score] || ""; - } - paint(value, false); - iconBox.addEventListener("mouseover", function (e) { - var star = e.target.closest(".icon-star"); - if (!star || !iconBox.contains(star)) return; - paint(parseInt(star.getAttribute("data-score"), 10) || 0, true); - }); - iconBox.addEventListener("mouseleave", function () { - var cur = parseInt(box.getAttribute("score-value") || "0", 10) || 0; - paint(cur, false); - }); - iconBox.addEventListener("click", function (e) { - var star = e.target.closest(".icon-star"); - if (!star || !iconBox.contains(star)) return; - var score = parseInt(star.getAttribute("data-score"), 10) || 0; - box.setAttribute("score-value", String(score)); - if (input) input.value = String(score); - paint(score, false); - }); - }); - } - - function bindReviewImages(form) { - var preview = form.querySelector("[data-aksrt-review-preview]"); - var fileInput = form.querySelector("[data-aksrt-review-file]"); - var addBtn = form.querySelector("[data-aksrt-review-add]"); - if (!preview || !fileInput) return; - var max = parseInt(preview.getAttribute("data-max") || "6", 10) || 6; - var files = []; - - function render() { - preview.querySelectorAll(".preview-item").forEach(function (n) { n.remove(); }); - files.forEach(function (item, idx) { - var node = document.createElement("div"); - node.className = "preview-item"; - node.innerHTML = ''; - node.querySelector("img").src = item.url; - node.querySelector(".remove").addEventListener("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - if (item.url && item.url.indexOf("blob:") === 0) { - try { URL.revokeObjectURL(item.url); } catch (err) {} - } - files.splice(idx, 1); - render(); - }); - preview.insertBefore(node, addBtn); - }); - if (addBtn) addBtn.style.display = files.length >= max ? "none" : ""; - form._aksrtReviewFiles = files.slice(); - } - - function onPick() { - if (files.length >= max) return; - fileInput.click(); - } - if (addBtn) { - addBtn.addEventListener("click", onPick); - addBtn.addEventListener("keydown", function (e) { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onPick(); - } - }); - } - fileInput.addEventListener("change", function () { - var list = Array.prototype.slice.call(fileInput.files || []); - list.forEach(function (file) { - if (files.length >= max) return; - if (!file.type || file.type.indexOf("image/") !== 0) return; - files.push({ file: file, url: URL.createObjectURL(file) }); - }); - fileInput.value = ""; - render(); - }); - form._aksrtReviewFiles = []; - render(); - } - - function bindReviewCountdown(root) { - var el = root.querySelector("[data-aksrt-review-countdown]"); - if (!el) return; - var remain = parseInt(el.getAttribute("data-aksrt-review-countdown") || "0", 10) || 0; - if (remain <= 0) return; - el.textContent = formatRemain(remain); - var timer = window.setInterval(function () { - remain -= 1; - if (remain <= 0) { - window.clearInterval(timer); - el.textContent = "交易已关闭"; - return; - } - el.textContent = formatRemain(remain); - }, 1000); - root._aksrtReviewTimer = timer; - } - - function fileToDataUrl(file) { - return new Promise(function (resolve, reject) { - var reader = new FileReader(); - reader.onload = function () { resolve(String(reader.result || "")); }; - reader.onerror = function () { reject(new Error("read failed")); }; - reader.readAsDataURL(file); - }); - } - - function openOrderReviewModal(orderNumber, triggerBtn) { - if (!window.AksrtRemoteModal || typeof window.AksrtRemoteModal.openRemote !== "function") { - showToast("评价组件未加载"); - return; - } - if (triggerBtn) triggerBtn.disabled = true; - var url = restRoot() + orderPath(orderNumber, "/review-modal"); - window.AksrtRemoteModal.openRemote({ - url: url, - className: "modal-mini full-sm", - height: 380, - mobileBottom: true - }).then(function (shell) { - if (triggerBtn) triggerBtn.disabled = false; - if (!shell) return; - var form = shell.querySelector("[data-aksrt-order-review-form]"); - if (!form) return; - bindShopScore(shell); - bindReviewImages(form); - bindReviewCountdown(shell); - - form.addEventListener("submit", function (e) { - e.preventDefault(); - var errBox = form.querySelector("[data-aksrt-review-error]"); - function showErr(msg) { - if (errBox) { - errBox.hidden = false; - errBox.textContent = msg; - } else { - showToast(msg); - } - } - if (errBox) { - errBox.hidden = true; - errBox.textContent = ""; - } - - var productId = parseInt(form.getAttribute("data-product-id") || "0", 10) || 0; - var needShipping = form.getAttribute("data-need-shipping") === "1"; - var scoreProduct = parseInt((form.querySelector('input[name="score[product]"]') || {}).value || "0", 10) || 0; - var scoreService = parseInt((form.querySelector('input[name="score[service]"]') || {}).value || "0", 10) || 0; - var scoreShipping = parseInt((form.querySelector('input[name="score[shipping]"]') || {}).value || "0", 10) || 0; - if (scoreProduct < 1 || scoreService < 1) { - showErr("请对商品进行评分"); - return; - } - if (needShipping && scoreShipping < 1) { - showErr("请对物流服务进行评分"); - return; - } - - var comment = (form.querySelector('textarea[name="comment"]') || {}).value || ""; - var submitBtn = form.querySelector("[data-aksrt-review-submit]"); - if (submitBtn) { - submitBtn.disabled = true; - submitBtn.textContent = "提交中…"; - } - - var pending = form._aksrtReviewFiles || []; - Promise.all(pending.map(function (item) { - return item.file ? fileToDataUrl(item.file) : Promise.resolve(item.url || ""); - })).then(function (images) { - images = images.filter(Boolean); - return request(orderPath(orderNumber, "/review"), { - product_id: productId, - content: comment, - rating_product: scoreProduct, - rating_service: scoreService, - rating_shipping: needShipping ? scoreShipping : scoreService, - images: images - }); - }).then(function (result) { - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.innerHTML = ' 确认提交'; - } - if (!result || !result.success) { - showErr((result && result.message) || "评价失败"); - return; - } - showToast((result.message) || "评价成功"); - if (window.AksrtRemoteModal) window.AksrtRemoteModal.close(); - var card = findOrderCard(orderNumber); - updateCardStatus(card, "completed", "已完成"); - if (card) { - var btn = card.querySelector("[data-aksrt-order-review]"); - if (btn) btn.remove(); - } - }).catch(function () { - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.innerHTML = ' 确认提交'; - } - showErr("网络错误"); - }); - }); - }).catch(function (err) { - if (triggerBtn) triggerBtn.disabled = false; - showToast((err && err.message) || "无法打开评价"); - }); - } - - /* ── 扫码支付弹层(微信 Native code_url 等)── */ - function showPaymentQr(content, orderNumber) { - var existing = document.getElementById("aksrt-pay-qr-overlay"); - if (existing) existing.remove(); - - var overlay = document.createElement("div"); - overlay.id = "aksrt-pay-qr-overlay"; - overlay.style.cssText = "position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);"; - - var qrImg = ""; - if (window.AksrtShare && typeof window.AksrtShare.buildQrDataUrl === "function") { - var dataUrl = window.AksrtShare.buildQrDataUrl(content, 220); - if (dataUrl) { - qrImg = '支付二维码'; - } - } - if (!qrImg) { - qrImg = '

' + String(content).replace(/[<>&]/g, "") + "

"; - } - - var card = document.createElement("div"); - card.style.cssText = "background:#fff;border-radius:12px;padding:24px;max-width:300px;text-align:center;box-shadow:0 10px 40px rgba(0,0,0,0.2);"; - card.innerHTML = - '

请扫码完成支付

' + - qrImg + - '

支付完成后本页将自动更新

' + - ''; - overlay.appendChild(card); - document.body.appendChild(overlay); - - function close() { overlay.remove(); } - overlay.addEventListener("click", function (e) { if (e.target === overlay) close(); }); - card.querySelector("#aksrt-pay-qr-close").addEventListener("click", function () { - close(); - // 关闭后查询订单是否已到账 - if (orderNumber) { - request(orderPath(orderNumber), {}, "GET").then(function () { - window.location.reload(); - }).catch(function () {}); - } - }); - } - - /* ── 第三方支付返回提示(?payment=success|pending|cancelled)── */ - (function paymentReturnNotice() { - var params = new URLSearchParams(window.location.search); - var pay = params.get("payment"); - if (!pay) return; - var map = { success: "支付成功!", pending: "支付处理中,到账后将自动更新。", cancelled: "支付已取消。", fail: "支付失败,请重试。" }; - showToast(map[pay] || "支付状态:" + pay, 3500); - })(); - - /* ── Local card refresh helpers ── */ - function findOrderCard(orderNumber) { - return page.querySelector('[data-aksrt-order-card="' + orderNumber + '"]'); - } - - function escapeHtml(value) { - return String(value == null ? "" : value) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - function formatMoney(order) { - var amount = Number(order && order.amount != null ? order.amount : 0); - if (order && order.is_points) { - var symbol = order.currency_symbol || "积分"; - return escapeHtml(symbol) + " " + Math.round(amount); - } - return "¥" + amount.toFixed(2); - } - - /** Plain money for table cells (formatMoney already escapes). */ - function formatMoneyText(order) { - var amount = Number(order && order.amount != null ? order.amount : 0); - if (order && order.is_points) { - var symbol = order.currency_symbol || "积分"; - return String(symbol) + " " + Math.round(amount); - } - return "¥" + amount.toFixed(2); - } - - function updateCardStatus(card, newStatus, statusLabel) { - if (!card) return; - if (newStatus) { - card.setAttribute("data-order-status", newStatus); - } - var badge = card.querySelector(".aksrt-orders-page__card-status, .aksrt-orders-page__item-badge"); - if (badge) { - badge.textContent = statusLabel || newStatus; - } - // Hide action buttons that are no longer relevant - var actions = card.querySelectorAll("[data-aksrt-order-pay], [data-aksrt-order-cancel]"); - for (var i = 0; i < actions.length; i++) { - actions[i].style.display = "none"; - } - // Hide countdown if present - var countdown = card.querySelector("[data-aksrt-order-countdown]"); - if (countdown) { - countdown.style.display = "none"; - } - } - - /* ── Order detail modal ── */ var detailModal = page.querySelector("[data-aksrt-order-detail-modal]"); var detailBody = page.querySelector("[data-aksrt-order-detail-body]"); var detailFooter = page.querySelector("[data-aksrt-order-detail-footer]"); var detailOrderNumber = ""; - function closeOrderDetailModal() { + function toast(message) { + if (window.AksrtToast && typeof window.AksrtToast.show === "function") { + window.AksrtToast.show(message); + return; + } + window.alert(message); + } + + function restRoot() { + return String(aksrtData.siteUrl || "").replace(/\/$/, "") + "/wp-json/aksrt/v1"; + } + + function request(path, body, method) { + var options = { + method: method || "POST", + credentials: "same-origin", + headers: { "X-WP-Nonce": aksrtData.restNonce || "" } + }; + if (body !== undefined) { + options.headers["Content-Type"] = "application/json"; + options.body = JSON.stringify(body || {}); + } + return fetch(restRoot() + path, options).then(function (response) { + return response.json().catch(function () { + return { success: false, message: "请求失败(HTTP " + response.status + ")" }; + }); + }); + } + + function orderPath(orderNumber, suffix) { + return "/orders/" + encodeURIComponent(orderNumber || "") + (suffix || ""); + } + + function escapeHtml(value) { + return String(value == null ? "" : value) + .replace(/&/g, "&").replace(//g, ">").replace(/\"/g, """) + .replace(/'/g, "'"); + } + + function formatMoney(order) { + var amount = Number(order && order.amount || 0); + return order && order.is_points + ? escapeHtml(order.currency_symbol || "积分") + " " + Math.round(amount) + : "¥" + amount.toFixed(2); + } + + function closeDetail() { if (detailModal) detailModal.hidden = true; detailOrderNumber = ""; document.documentElement.classList.remove("aksrt-modal-open"); } - document.addEventListener("keydown", function (event) { - if (event.key === "Escape" && detailModal && !detailModal.hidden) { - closeOrderDetailModal(); - } - }); + function renderDetail(order) { + if (!detailBody || !detailFooter) return; + var actions = order.actions || {}; + var image = order.image || ""; + var title = order.product_name || "订单"; + var status = order.status_group || "closed"; + var html = '
'; + html += '' + escapeHtml(order.status_label || "") + ""; + html += '' + formatMoney(order) + "
"; + html += '
'; + html += image ? '' : ''; + html += '
' + escapeHtml(title) + ""; + html += '' + escapeHtml(order.product_type_label || "") + "
"; + html += '
'; + [["订单编号", order.order_number], ["创建时间", order.created_at], ["支付时间", order.paid_at || "—"]].forEach(function (row) { + html += "
" + escapeHtml(row[0]) + "
" + escapeHtml(row[1] || "—") + "
"; + }); + html += "
"; + detailBody.innerHTML = html; - function openOrderDetailModal(orderNumber) { - if (!detailModal || !detailBody || !detailFooter) { - showToast("订单详情组件未加载"); - return; - } - detailOrderNumber = orderNumber || ""; + var footer = ''; + if (actions.can_cancel) footer += ''; + if (actions.can_pay) footer += ''; + if (actions.can_open && order.permalink) footer += '' + escapeHtml(order.entitlement_label || "打开已购内容") + ""; + detailFooter.innerHTML = footer; + } + + function openDetail(orderNumber) { + if (!detailModal || !detailBody) return; + detailOrderNumber = orderNumber; detailBody.innerHTML = '

加载中…

'; detailFooter.innerHTML = ""; detailModal.hidden = false; document.documentElement.classList.add("aksrt-modal-open"); - request(orderPath(orderNumber), undefined, "GET").then(function (result) { - var order = null; - if (result && result.data && result.data.order_number) { - order = result.data; - } else if (result && result.data && result.data.data && result.data.data.order_number) { - order = result.data.data; - } else if (result && result.order_number) { - order = result; - } - if (result && result.success === false) { - detailBody.innerHTML = '

' + escapeHtml((result && result.message) || "加载失败") + "

"; + if (orderNumber !== detailOrderNumber) return; + if (!result.success || !result.data) { + detailBody.innerHTML = '

' + escapeHtml(result.message || "订单不存在") + "

"; return; } - if (!order || !order.order_number) { - detailBody.innerHTML = '

订单不存在

'; - return; - } - renderOrderDetail(order); + renderDetail(result.data); }).catch(function () { detailBody.innerHTML = '

网络错误,请稍后重试

'; }); } - function renderOrderDetail(order) { - if (!detailBody || !detailFooter) return; - var actions = (order && order.actions) || {}; - var payment = order.payment_method || {}; - var paymentLabel = payment.label || payment.value || "—"; - var qty = order.quantity || 1; - if ((!qty || qty < 1) && order.items && order.items.length) { - qty = 0; - order.items.forEach(function (item) { - qty += Math.max(1, parseInt(item.quantity || item.qty || 1, 10) || 1); - }); - } - if (!qty || qty < 1) qty = 1; + function currentViewPath() { + var params = new URLSearchParams(window.location.search); + var allowed = ["order_status", "order_type", "order_number", "order_page"]; + var query = new URLSearchParams(); + allowed.forEach(function (key) { if (params.get(key)) query.set(key, params.get(key)); }); + return "/orders/view" + (query.toString() ? "?" + query.toString() : ""); + } - var status = order.status || ""; - var statusLabel = order.status_label || status || "—"; - var statusClass = "is-" + String(status).replace(/[^a-z0-9_-]/gi, "-"); - var title = order.product_name || (order.product && order.product.title) || "订单"; - var image = order.image || (order.product && order.product.image) || ""; - if (!image && order.items && order.items.length && order.items[0].image) { - image = order.items[0].image; - } - var orderNo = order.order_number || "—"; + function actionHtml(order) { + var actions = order.actions || {}; + var html = ''; + if (actions.can_cancel) html += ''; + if (actions.can_pay) html += ''; + if (actions.can_open && order.permalink) html += '' + escapeHtml(order.entitlement_label || '打开已购内容') + ''; + return html; + } - // Meta fields only — amount lives in hero; type is already on the list card. - var rows = [ - { key: "qty", label: "数量", value: "×" + String(qty) }, - { key: "no", label: "订单编号", value: orderNo, copyable: true }, - { key: "created", label: "创建时间", value: order.created_at || "—" }, - { key: "paid", label: "支付时间", value: order.paid_at || "—" }, - { key: "pay", label: "支付方式", value: paymentLabel } - ]; + function cardHtml(order) { + var image = order.image + ? '' + : ''; + var countdown = Number(order.time_left || 0) > 0 + ? '' + : ''; + return '
' + + '
' + escapeHtml(order.product_type_label || '') + '' + escapeHtml(order.status_label || '') + '
' + + '
' + image + '

' + escapeHtml(order.product_name || '订单') + '

订单号 ' + escapeHtml(order.order_number) + '

' + formatMoney(order) + '实付
' + + '
'; + } - if (Number(order.shipping_fee || 0) > 0) { - rows.push({ key: "ship", label: "运费", value: "¥" + Number(order.shipping_fee || 0).toFixed(2) }); - } - if (Number(order.discount || 0) > 0) { - rows.push({ key: "disc", label: "优惠", value: "-¥" + Number(order.discount || 0).toFixed(2), accent: "discount" }); - } - if (order.customer_note) { - rows.push({ key: "note", label: "备注", value: order.customer_note }); - } + function renderStats(stats) { + var root = page.querySelector('[data-aksrt-order-stats]'); + if (!root) return; + root.innerHTML = '
全部' + Number(stats.total || 0) + '
' + + '
待支付' + Number(stats.pending || 0) + '
' + + '
已生效' + Number(stats.completed || 0) + '
'; + } - var html = ""; - html += '
'; - html += '
'; - html += ' ' + escapeHtml(statusLabel) + ""; - if (order.product_type_label) { - html += ' ' + escapeHtml(order.product_type_label) + ""; + function renderPagination(view) { + var existing = page.querySelector('.aksrt-orders-page__pagination'); + if (existing) existing.remove(); + if (Number(view.total_pages || 1) <= 1) return; + var root = document.createElement('nav'); + root.className = 'aksrt-orders-page__pagination'; + root.setAttribute('aria-label', '订单分页'); + var base = new URL(window.location.href); + for (var i = 1; i <= Number(view.total_pages); i++) { + base.searchParams.set('order_page', String(i)); + var link = document.createElement('a'); + link.className = 'aksrt-orders-page__page-chip' + (i === Number(view.page) ? ' is-active' : ''); + link.href = base.toString(); + link.textContent = String(i); + root.appendChild(link); } - html += "
"; - html += '
'; - html += ' 应付金额'; - html += ' ' + escapeHtml(formatMoneyText(order)) + ""; - html += "
"; - html += "
"; + var list = page.querySelector('[data-aksrt-order-list]'); + if (list) list.after(root); + } - html += '
'; - if (image) { - html += ''; + function renderView(view) { + renderStats(view.stats || {}); + var list = page.querySelector('[data-aksrt-order-list]'); + if (!list) return; + var items = Array.isArray(view.items) ? view.items : []; + if (!items.length) { + list.innerHTML = '

没有匹配的订单

换个条件试试,或清除当前筛选。

'; + list.classList.add('is-empty'); } else { - html += ''; + list.classList.remove('is-empty'); + list.innerHTML = items.map(cardHtml).join(''); } - html += '
'; - html += '
' + escapeHtml(title) + "
"; - html += '
'; - html += ' 数量 ' + escapeHtml(String(qty)) + ""; - if (paymentLabel && paymentLabel !== "—") { - html += ' '; - html += ' ' + escapeHtml(paymentLabel) + ""; + renderPagination(view); + if (detailOrderNumber) { + var detail = items.filter(function (item) { return item.order_number === detailOrderNumber; })[0]; + if (detail) renderDetail(detail); else closeDetail(); } - html += "
"; - html += "
"; - html += "
"; + updateCountdowns(); + } - html += '
'; - html += '

订单信息

'; - html += '
'; - rows.forEach(function (row) { - html += '
'; - html += '
' + escapeHtml(row.label) + '
'; - html += '
'; - html += '' + escapeHtml(row.value) + ""; - if (row.copyable && orderNo && orderNo !== "—") { - html += '"; - } - html += "
"; - html += "
"; + function refreshView() { + return request(currentViewPath(), undefined, "GET").then(function (result) { + if (!result.success || !result.data) throw new Error(result.message || "刷新失败"); + renderView(result.data); + return result.data; + }).catch(function (error) { + toast(error.message || "订单状态已变更,请刷新后查看"); }); - html += "
"; - - if (order.items && order.items.length > 1) { - html += '
'; - html += '

商品明细

'; - html += '
"; - } - - if (order.timeline && order.timeline.length) { - // Newest first for a natural "latest activity on top" feel. - var timeline = order.timeline.slice().reverse(); - html += '
'; - html += '

订单进度

'; - html += '
    '; - timeline.forEach(function (tl, idx) { - html += '
  1. '; - html += ''; - html += '
    '; - html += '' + escapeHtml(tl.label || "") + ""; - html += '"; - html += "
  2. "; - }); - html += "
"; - } - - detailBody.innerHTML = html; - - var footer = ""; - // Status-aware footer. Top-right ✕ is the only generic dismiss control. - if (actions.can_cancel) { - // Two-step cancel inside the modal — no window.confirm. - footer += ''; - } - if (actions.can_pay) { - footer += ''; - } - if (actions.can_view_product && order.permalink) { - footer += '查看商品'; - } - if (footer === "") { - footer = ''; - } - detailFooter.innerHTML = footer; } - function copyOrderNumber(code, trigger) { - if (!code) return; - function done() { - showToast("已复制订单号"); - if (trigger) { - trigger.classList.add("is-copied"); - window.setTimeout(function () { trigger.classList.remove("is-copied"); }, 1200); - } - } - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(code).then(done).catch(function () { - showToast(code); - }); - return; - } - try { - var ta = document.createElement("textarea"); - ta.value = code; - ta.setAttribute("readonly", ""); - ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); - ta.select(); - document.execCommand("copy"); - document.body.removeChild(ta); - done(); - } catch (e) { - showToast(code); - } - } - - /* ── Countdown timer ── */ - function updateCountdowns() { - var nodes = page.querySelectorAll("[data-aksrt-order-countdown]"); - for (var i = 0; i < nodes.length; i++) { - var node = nodes[i]; - var value = Number(node.getAttribute("data-aksrt-order-countdown") || 0); - if (!value) continue; - - value -= 1; - node.setAttribute("data-aksrt-order-countdown", String(value)); - node.textContent = value > 0 ? "剩余支付时间 " + value + " 秒" : "待支付已超时,请重新发起。"; - } - } - - setInterval(updateCountdowns, 1000); - - /* ── Event delegation ── */ - page.addEventListener("click", function (event) { - // Order detail open - var detailBtn = event.target.closest("[data-aksrt-order-detail]"); - if (detailBtn && !event.target.closest("[data-aksrt-order-detail-modal]")) { - var detailOrder = detailBtn.getAttribute("data-aksrt-order-detail"); - if (detailOrder) openOrderDetailModal(detailOrder); - return; - } - - // Copy order number inside detail modal - var copyBtn = event.target.closest("[data-aksrt-copy-order]"); - if (copyBtn) { - event.preventDefault(); - copyOrderNumber(copyBtn.getAttribute("data-aksrt-copy-order") || "", copyBtn); - return; - } - - // Order detail close - if (event.target.closest("[data-aksrt-order-detail-close]")) { - closeOrderDetailModal(); - return; - } - - // Timeline toggle - var timelineToggle = event.target.closest("[data-aksrt-toggle-timeline]"); - if (timelineToggle) { - var timeline = timelineToggle.closest("[data-aksrt-timeline]"); - var body = timeline ? timeline.querySelector(".aksrt-orders-page__timeline-body") : null; - if (body) body.hidden = !body.hidden; - return; - } - - // Review modal (zibll order_comment_modal 1:1 UX) - var reviewBtn = event.target.closest("[data-aksrt-order-review]"); - if (reviewBtn) { - var reviewOrder = reviewBtn.getAttribute("data-aksrt-order-review"); - if (!reviewOrder) return; - closeOrderDetailModal(); - openOrderReviewModal(reviewOrder, reviewBtn); - return; - } - - // Pay - var payButton = event.target.closest("[data-aksrt-order-pay]"); - if (payButton) { - var payOrderNumber = payButton.getAttribute("data-aksrt-order-pay"); - // 支付方式取订单创建时选定的方式(按钮 data 属性可覆盖); - // 留空则由服务端使用订单存储的 payment_method。 - var payMethod = payButton.getAttribute("data-aksrt-order-method") || ""; - var payLabel = payButton.textContent || "去支付"; - payButton.disabled = true; - payButton.textContent = "处理中..."; - request(orderPath(payOrderNumber, "/pay"), payMethod ? { payment_method: payMethod } : {}) - .then(function (result) { - if (!result.success) { - payButton.disabled = false; - payButton.textContent = payLabel; - showToast(result.message || "支付失败。"); - return; - } - - var data = result.data || {}; - - // 跳转/扫码型网关:到第三方支付页或展示二维码,订单仍为待支付, - // 由回调确认到账。 - if (data.redirect) { - if (data.payment_url && /^https?:\/\//i.test(data.payment_url)) { - window.location.href = data.payment_url; - return; - } - if (data.qr_content || data.payment_url) { - showPaymentQr(data.qr_content || data.payment_url, payOrderNumber); - payButton.disabled = false; - payButton.textContent = "已下单,待支付"; - return; - } - } - - // 同步型网关(站内余额):已到账。 - showToast("支付成功!"); - var card = findOrderCard(payOrderNumber); - updateCardStatus(card, "paid", "已支付"); - if (detailOrderNumber === payOrderNumber) { - closeOrderDetailModal(); - } - }) - .catch(function () { - payButton.disabled = false; - payButton.textContent = payLabel; - showToast("网络错误,请稍后重试。"); - }); - return; - } - - // Cancel — two-step in detail modal (关闭订单 → 确认取消), list card same. - var cancelButton = event.target.closest("[data-aksrt-order-cancel]"); - if (cancelButton) { - var cancelOrderNumber = cancelButton.getAttribute("data-aksrt-order-cancel"); - if (!cancelOrderNumber) return; - - // First click: transform into "确认取消" (no browser confirm dialog). - var step = cancelButton.getAttribute("data-aksrt-cancel-step") || "1"; - if (step !== "2") { - cancelButton.setAttribute("data-aksrt-cancel-step", "2"); - cancelButton.classList.remove("btn-ghost"); - cancelButton.classList.add("btn-primary", "is-cancel-confirm"); - cancelButton.textContent = "确认取消"; - // Auto-reset if user doesn't confirm within a few seconds. - if (cancelButton._aksrtCancelReset) { - window.clearTimeout(cancelButton._aksrtCancelReset); - } - cancelButton._aksrtCancelReset = window.setTimeout(function () { - if (!cancelButton.isConnected) return; - if (cancelButton.getAttribute("data-aksrt-cancel-step") !== "2") return; - cancelButton.setAttribute("data-aksrt-cancel-step", "1"); - cancelButton.classList.add("btn-ghost"); - cancelButton.classList.remove("btn-primary", "is-cancel-confirm"); - // Detail modal uses "关闭订单"; list cards use "取消". - var inDetail = !!(detailModal && detailModal.contains(cancelButton)); - cancelButton.textContent = inDetail ? "关闭订单" : "取消"; - }, 5000); + function startPayment(button) { + var number = button.getAttribute("data-aksrt-order-pay") || ""; + var method = button.getAttribute("data-aksrt-order-method") || ""; + if (!number) return; + button.disabled = true; + request(orderPath(number, "/pay"), method ? { payment_method: method } : {}).then(function (result) { + if (!result.success) throw new Error(result.message || "支付失败"); + var data = result.data || {}; + if (data.redirect && data.payment_url) { + window.location.assign(data.payment_url); return; } + toast(data.message || "支付成功"); + refreshView(); + }).catch(function (error) { + button.disabled = false; + toast(error.message || "网络错误,请稍后重试"); + }); + } - if (cancelButton._aksrtCancelReset) { - window.clearTimeout(cancelButton._aksrtCancelReset); - cancelButton._aksrtCancelReset = null; - } - cancelButton.disabled = true; - cancelButton.textContent = "取消中..."; - request(orderPath(cancelOrderNumber, "/cancel")) - .then(function (result) { - if (!result.success) { - cancelButton.disabled = false; - cancelButton.setAttribute("data-aksrt-cancel-step", "1"); - cancelButton.classList.add("btn-ghost"); - cancelButton.classList.remove("btn-primary", "is-cancel-confirm"); - var inDetailFail = !!(detailModal && detailModal.contains(cancelButton)); - cancelButton.textContent = inDetailFail ? "关闭订单" : "取消"; - showToast(result.message || "取消失败。"); - return; - } - showToast("订单已取消。"); - var card = findOrderCard(cancelOrderNumber); - updateCardStatus(card, "cancelled", "已取消"); - if (detailOrderNumber === cancelOrderNumber) { - // Refresh modal body with cancelled timeline, then close shortly. - if (result.data && result.data.order) { - renderOrderDetail(result.data.order); - } - window.setTimeout(function () { - closeOrderDetailModal(); - }, 600); - } - }) - .catch(function () { - cancelButton.disabled = false; - cancelButton.setAttribute("data-aksrt-cancel-step", "1"); - cancelButton.classList.add("btn-ghost"); - cancelButton.classList.remove("btn-primary", "is-cancel-confirm"); - var inDetailErr = !!(detailModal && detailModal.contains(cancelButton)); - cancelButton.textContent = inDetailErr ? "关闭订单" : "取消"; - showToast("网络错误,请稍后重试。"); - }); + function closeOrder(button) { + var number = button.getAttribute("data-aksrt-order-cancel") || ""; + if (!number) return; + if (button.getAttribute("data-aksrt-confirm") !== "true") { + button.setAttribute("data-aksrt-confirm", "true"); + button.textContent = "确认关闭"; return; } + button.disabled = true; + request(orderPath(number, "/cancel")).then(function (result) { + if (!result.success) throw new Error(result.message || "关闭订单失败"); + toast("订单已关闭"); + refreshView(); + }).catch(function (error) { + button.disabled = false; + button.removeAttribute("data-aksrt-confirm"); + button.textContent = "关闭订单"; + toast(error.message || "网络错误,请稍后重试"); + }); + } + + function updateCountdowns() { + page.querySelectorAll("[data-aksrt-order-countdown]").forEach(function (node) { + var value = Math.max(0, Number(node.getAttribute("data-aksrt-order-countdown") || 0) - 1); + node.setAttribute("data-aksrt-order-countdown", String(value)); + node.textContent = value ? "剩余支付时间 " + value + " 秒" : "订单即将关闭"; + }); + } + + page.addEventListener("click", function (event) { + var filterToggle = event.target.closest("[data-aksrt-orders-filter-toggle]"); + if (filterToggle) { + var panel = page.querySelector("[data-aksrt-orders-filter-panel]"); + var expanded = filterToggle.getAttribute("aria-expanded") === "true"; + filterToggle.setAttribute("aria-expanded", String(!expanded)); + if (panel) panel.hidden = expanded; + return; + } + var detail = event.target.closest("[data-aksrt-order-detail]"); + if (detail) { openDetail(detail.getAttribute("data-aksrt-order-detail")); return; } + if (event.target.closest("[data-aksrt-order-detail-close]")) { closeDetail(); return; } + var pay = event.target.closest("[data-aksrt-order-pay]"); + if (pay) { startPayment(pay); return; } + var cancel = event.target.closest("[data-aksrt-order-cancel]"); + if (cancel) closeOrder(cancel); }); - /* ── Coupon wallet ── */ - var couponRoot = page.querySelector("[data-aksrt-coupon-wallet-list]"); - if (couponRoot) { - request("/shop/frontend/coupons").then(function (result) { - var items = (result && result.data && result.data.items) || (result && result.items) || []; - // normalize success envelope - if (result && result.data && result.data.data && result.data.data.items) { - items = result.data.data.items; - } - if (result && result.success && result.data && result.data.items) { - items = result.data.items; - } - // handle this success() wrapper: {success,data:{items}} - if (Array.isArray(result && result.data)) items = result.data; - if (!items.length && result && result.data && Array.isArray(result.data.items)) { - items = result.data.items; - } - // fallback parse - try { - if (!items.length && result && result.data && result.data.data) { - items = result.data.data.items || []; - } - } catch (e) {} - if (!items || !items.length) { - couponRoot.innerHTML = '

暂无可用优惠券

'; - return; - } - var html = '
'; - items.forEach(function (c) { - var val = c.type === "percent" ? (c.value + "%") : ("¥" + Number(c.value || 0).toFixed(2)); - html += '
'; - html += '
' + val + '
'; - html += '
' + (c.code || "") + '
'; - if (c.min_amount > 0) { - html += '
满 ¥' + Number(c.min_amount).toFixed(2) + ' 可用
'; - } - if (c.expires_at) { - html += '
有效期至 ' + c.expires_at + '
'; - } - html += ''; - html += '
'; - }); - html += '
'; - couponRoot.innerHTML = html; - }).catch(function () { - couponRoot.innerHTML = '

加载失败

'; - }); - page.addEventListener("click", function (e) { - var b = e.target.closest("[data-aksrt-copy-coupon]"); - if (!b) return; - var code = b.getAttribute("data-aksrt-copy-coupon") || ""; - if (!code) return; - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(code).then(function () { showToast("已复制 " + code); }); - } else { - showToast(code); - } - }); - } -})(); + document.addEventListener("keydown", function (event) { + if (event.key === "Escape" && detailModal && !detailModal.hidden) closeDetail(); + }); + window.setInterval(updateCountdowns, 1000); +}()); diff --git a/inc/helpers/aksrt-transactions.php b/inc/helpers/aksrt-transactions.php index 8217a609..dc67ef56 100644 --- a/inc/helpers/aksrt-transactions.php +++ b/inc/helpers/aksrt-transactions.php @@ -17,13 +17,14 @@ function aksrt_get_order_status_labels(): array { return []; } -function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = '', string $product_type = ''): array { +function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = 'all', string $product_type = '', string $order_number = ''): array { $empty = [ 'items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, - 'stats' => ['total' => 0, 'pending' => 0, 'paid' => 0], + 'query' => [], + 'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0], 'product_types' => class_exists('\AKSRT\Core\DomainModel') ? array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()) : [], @@ -38,6 +39,7 @@ function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page 'per_page' => $per_page, 'status' => $status, 'product_type' => $product_type, + 'order_number' => $order_number, ]; if (class_exists('\AKSRT\Payment\UserOrders')) { diff --git a/inc/helpers/user-center-renderers/profile-orders.php b/inc/helpers/user-center-renderers/profile-orders.php index 293e873b..7e90e058 100644 --- a/inc/helpers/user-center-renderers/profile-orders.php +++ b/inc/helpers/user-center-renderers/profile-orders.php @@ -1,6 +1,6 @@ ' . esc_html__('请先登录', 'aksrt') . ''; - return; + echo '
' . esc_html__('请先登录', 'aksrt') . '
'; + return; } -$per_page = 20; -$paged = max(1, (int) ($_GET['order_page'] ?? 1)); -$tab = sanitize_key((string) ($_GET['order_tab'] ?? ($_GET['otab'] ?? ''))); -$status = sanitize_key((string) ($_GET['order_status'] ?? '')); -$type_filter = sanitize_key((string) ($_GET['order_type'] ?? '')); -$section = sanitize_key((string) ($_GET['section'] ?? '')); - -// Accept legacy standalone-page tab keys. -if ($tab === '' && isset($_GET['tab']) && sanitize_key((string) $_GET['tab']) !== 'orders') { - // When rendered as standalone legacy include, outer tab may be status. - $maybe = sanitize_key((string) $_GET['tab']); - if (in_array($maybe, ['pending', 'after'], true)) { - $tab = $maybe; - } -} - -$tab_aliases = []; -if (isset($tab_aliases[$tab])) { - $tab = $tab_aliases[$tab]; -} - -$tab_status_map = [ - 'pending' => \AKSRT\Payment\Order::STATUS_PENDING, - 'after' => \AKSRT\Payment\Order::STATUS_REFUNDING, +$raw_query = [ + 'status' => $_GET['order_status'] ?? '', + 'type' => $_GET['order_type'] ?? '', + 'order_number' => $_GET['order_number'] ?? '', + 'page' => $_GET['order_page'] ?? 1, + 'per_page' => 20, ]; -if ($tab !== '' && isset($tab_status_map[$tab])) { - $status = $tab_status_map[$tab]; -} - +$query = \AKSRT\Payment\OrderCenterQuery::from_array($raw_query); $orders_view = function_exists('aksrt_get_orders_page_view') - ? aksrt_get_orders_page_view($user_id, $paged, $per_page, $status, $type_filter) - : ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []]; + ? aksrt_get_orders_page_view($user_id, $query->page(), $query->per_page(), $query->status(), $query->product_type(), $query->order_number()) + : ['items' => [], 'total' => 0, 'total_pages' => 1, 'page' => 1, 'stats' => [], 'product_types' => []]; -$orders = (array) ($orders_view['items'] ?? []); -$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1)); -$stats = is_array($orders_view['stats'] ?? null) ? $orders_view['stats'] : []; +$orders = (array) ($orders_view['items'] ?? []); +$stats = is_array($orders_view['stats'] ?? null) ? $orders_view['stats'] : []; $type_options = is_array($orders_view['product_types'] ?? null) ? $orders_view['product_types'] : []; -if ($type_options === [] && class_exists('\AKSRT\Core\DomainModel')) { - $type_options = array_merge(['' => __('全部类型', 'aksrt')], \AKSRT\Core\DomainModel::order_product_type_labels()); -} +$page = max(1, (int) ($orders_view['page'] ?? 1)); +$total_pages = max(1, (int) ($orders_view['total_pages'] ?? 1)); +$status = $query->status(); +$type = $query->product_type(); +$order_number = $query->order_number(); -$tabs = [ - '' => __('全部', 'aksrt'), - 'pending' => __('待支付', 'aksrt'), - 'after' => __('退款中', 'aksrt'), -]; - -$base_query = ['tab' => 'orders']; -if ($type_filter !== '') { - $base_query['order_type'] = $type_filter; -} -if ($section !== '') { - $base_query['section'] = $section; -} - -/** - * Build URL staying inside user center orders tab. - * - * @param array $args - */ -$orders_url = static function (array $args = []) use ($base_query): string { - $q = array_merge($base_query, $args); - $base = function_exists('aksrt_get_user_center_url') - ? aksrt_get_user_center_url('orders') - : home_url('/profile/'); - $url = remove_query_arg( - ['order_tab', 'otab', 'order_status', 'order_page', 'order_type', 'section', 'tab'], - $base - ); - // Always keep tab=orders. - $q['tab'] = 'orders'; - // Map status filter to order_tab (avoid clashing with UC tab=). - if (isset($q['tab_status'])) { - $q['order_tab'] = $q['tab_status']; - unset($q['tab_status']); - } - foreach ($q as $k => $v) { - if ($v === null || $v === '') { - unset($q[$k]); - } - } - return add_query_arg($q, $url); +$orders_url = static function (array $override = []) use ($status, $type, $order_number, $page): string { + $values = array_merge([ + 'order_status' => $status, + 'order_type' => $type, + 'order_number' => $order_number, + 'order_page' => $page, + ], $override); + $base = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url('orders') : home_url('/profile/'); + $values['tab'] = 'orders'; + foreach ($values as $key => $value) { + if ($value === '' || $value === null || ($key === 'order_page' && (int) $value <= 1) || ($key === 'order_status' && $value === 'all')) { + unset($values[$key]); + } + } + return add_query_arg($values, remove_query_arg(['order_tab', 'otab', 'section'], $base)); }; -$pending_count = (int) ($stats['pending'] ?? 0); +$tabs = [ + 'all' => __('全部', 'aksrt'), + 'pending' => __('待支付', 'aksrt'), + 'completed' => __('已完成', 'aksrt'), + 'closed' => __('已关闭', 'aksrt'), +]; ?> -
-
-
-
- - -
-
-
+
+
+
+

+
+ + +
+

+
+
+
+
+
+
+
- -
-
- - -
-
- - -
-
- - -
-
- + -
- - -
+ - -
- $type_label) : - $is_type_active = (string) $type_key === (string) $type_filter; - $type_args = [ - 'order_type' => $type_key !== '' ? $type_key : null, - 'order_tab' => $tab !== '' ? $tab : null, - 'order_page' => 1, - ]; - $type_url = $orders_url($type_args); - if ($type_key === '') { - $type_url = remove_query_arg('order_type', $type_url); - } - ?> - - - - -
- + - -
- $tab_label) : - $is_active = $tab === $tab_key; - $tab_url = $tab_key !== '' - ? $orders_url(['order_tab' => $tab_key, 'order_page' => 1]) - : remove_query_arg(['order_tab', 'otab', 'order_status', 'order_page'], $orders_url()); - ?> - - - 0) : ?> - - - - -
- + +
+ +
+
+
+ +

+
+
+
+ + + + + +
+
+ +
+ 1) : ?> + +

+ - -
- -
-
- - - - - - -
- -
- - -
- - - -
- - - - -
-
- - × -
-
- - - -
- - - -
- -
-
- -
-
- -
- -
- - - 0 && !$is_points) : ?> - () - -
- - -
-

- -

- -
- - -
- - - - - - - - - - - - - - - -
-
- -
- - 1) : ?> -
- $tab !== '' ? $tab : null, - 'order_page' => $i, - ], static fn($v) => $v !== null && $v !== '')); - ?> - - - - - - -
- - -
- -

-

-
- - - - - - -
-

-
-

-
-
- +
diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index c5518ac4..92591163 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -668,21 +668,10 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js'; ['aksrt-tokens'], $version ); - // 微信扫码支付需要本地二维码生成器(AksrtShare.buildQrDataUrl)。 - $weixin_share_orders = get_template_directory() . '/assets/js/weixin-share.js'; - if (file_exists($weixin_share_orders)) { - wp_enqueue_script( - 'aksrt-weixin-share', - $base_uri . '/assets/js/weixin-share.js', - ['aksrt-script'], - (string) filemtime($weixin_share_orders), - true - ); - } wp_enqueue_script( 'aksrt-order-center', $base_uri . '/assets/js/order-center.js', - ['aksrt-script', 'aksrt-weixin-share', 'aksrt-core-remote-modal'], + ['aksrt-script'], file_exists(get_template_directory() . '/assets/js/order-center.js') ? (string) filemtime(get_template_directory() . '/assets/js/order-center.js') : $version, diff --git a/src/Payment/OrderCenterQuery.php b/src/Payment/OrderCenterQuery.php new file mode 100644 index 00000000..dd367861 --- /dev/null +++ b/src/Payment/OrderCenterQuery.php @@ -0,0 +1,124 @@ + */ + private array $values; + + /** + * @param array $values + */ + private function __construct(array $values) { + $this->values = $values; + } + + /** + * @param array $raw + */ + public static function from_array(array $raw, int $default_per_page = 20): self { + $status = sanitize_key((string) ($raw['status'] ?? self::STATUS_ALL)); + if (!in_array($status, self::STATUS_GROUPS, true)) { + $status = self::STATUS_ALL; + } + + $product_type = sanitize_key((string) ($raw['type'] ?? $raw['product_type'] ?? '')); + $types = DomainModel::order_product_type_labels(); + if ($product_type !== '' && !isset($types[$product_type])) { + $product_type = ''; + } + + $order_number = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($raw['order_number'] ?? '')) ?? ''); + $order_number = substr($order_number, 0, 32); + + $page = max(1, absint($raw['page'] ?? 1)); + $per_page = max(1, min(50, absint($raw['per_page'] ?? $default_per_page))); + + return new self([ + 'status' => $status, + 'product_type' => $product_type, + 'order_number' => $order_number, + 'page' => $page, + 'per_page' => $per_page, + ]); + } + + public function status(): string { + return (string) $this->values['status']; + } + + public function product_type(): string { + return (string) $this->values['product_type']; + } + + public function order_number(): string { + return (string) $this->values['order_number']; + } + + public function page(): int { + return (int) $this->values['page']; + } + + public function per_page(): int { + return (int) $this->values['per_page']; + } + + /** @return string[] */ + public function database_statuses(): array { + return match ($this->status()) { + self::STATUS_PENDING => [Order::STATUS_CREATED, Order::STATUS_PENDING], + self::STATUS_COMPLETED => [Order::STATUS_PAID], + self::STATUS_CLOSED => [Order::STATUS_CANCELLED, Order::STATUS_EXPIRED], + default => [ + Order::STATUS_CREATED, + Order::STATUS_PENDING, + Order::STATUS_PAID, + Order::STATUS_CANCELLED, + Order::STATUS_EXPIRED, + ], + }; + } + + /** @return array */ + public function url_values(): array { + return array_filter([ + 'order_status' => $this->status() !== self::STATUS_ALL ? $this->status() : '', + 'order_type' => $this->product_type(), + 'order_number' => $this->order_number(), + 'order_page' => $this->page() > 1 ? $this->page() : '', + ], static fn($value): bool => $value !== ''); + } + + /** @return array */ + public function repository_args(): array { + return [ + 'status_in' => $this->database_statuses(), + 'product_type' => $this->product_type(), + 'order_number_prefix' => $this->order_number(), + 'limit' => $this->per_page(), + 'offset' => ($this->page() - 1) * $this->per_page(), + ]; + } +} diff --git a/src/Payment/OrderCenterRest.php b/src/Payment/OrderCenterRest.php index 2fb736e6..b3e83b15 100644 --- a/src/Payment/OrderCenterRest.php +++ b/src/Payment/OrderCenterRest.php @@ -1,9 +1,6 @@ [A-Za-z0-9_-]+)'; + public function register_routes(): void { + $namespace = 'aksrt/v1'; + $order_number = '(?P[A-Za-z0-9_-]+)'; + $query_args = [ + 'order_status' => ['sanitize_callback' => 'sanitize_key'], + 'order_type' => ['sanitize_callback' => 'sanitize_key'], + 'order_number' => ['sanitize_callback' => 'sanitize_text_field'], + 'order_page' => ['sanitize_callback' => 'absint'], + ]; - register_rest_route($ns, '/shop/frontend/orders/' . $on . '/pay', [ - 'methods' => 'POST', - 'callback' => [$this, 'pay'], - 'permission_callback' => [$this, 'require_logged_in'], - ]); + register_rest_route($namespace, '/orders/view', [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'get_view'], + 'permission_callback' => [$this, 'require_logged_in'], + 'args' => $query_args, + ]); + $route_args = [ + 'order_number' => [ + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => static function ($value): bool { + return is_string($value) && (bool) preg_match('/^[A-Za-z0-9_-]+$/', $value); + }, + ], + ]; + register_rest_route($namespace, '/orders/' . $order_number . '/pay', [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [$this, 'pay'], + 'permission_callback' => [$this, 'require_logged_in'], + 'args' => $route_args, + ]); + register_rest_route($namespace, '/orders/' . $order_number . '/cancel', [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [$this, 'cancel'], + 'permission_callback' => [$this, 'require_logged_in'], + 'args' => $route_args, + ]); + register_rest_route($namespace, '/orders/' . $order_number, [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'get_order'], + 'permission_callback' => [$this, 'require_logged_in'], + 'args' => $route_args, + ]); + } - register_rest_route($ns, '/shop/frontend/orders/' . $on . '/cancel', [ - 'methods' => 'POST', - 'callback' => [$this, 'cancel'], - 'permission_callback' => [$this, 'require_logged_in'], - ]); + /** @return true|\WP_Error */ + public function require_logged_in() { + if (!is_user_logged_in()) { + return new \WP_Error('aksrt_rest_forbidden', __('请先登录', 'aksrt'), ['status' => 401]); + } + return true; + } - register_rest_route($ns, '/shop/frontend/orders/' . $on, [ - [ - 'methods' => 'GET', - 'callback' => [$this, 'get_order'], - 'permission_callback' => [$this, 'require_logged_in'], - ], - [ - // order-center.js historically POSTed for refresh — accept both. - 'methods' => 'POST', - 'callback' => [$this, 'get_order'], - 'permission_callback' => [$this, 'require_logged_in'], - ], - ]); - } + public function get_view(\WP_REST_Request $request): \WP_REST_Response { + if (!class_exists(UserOrders::class)) { + return $this->error(__('订单服务不可用', 'aksrt'), 500); + } + $view = UserOrders::get_instance()->get_order_center_view(get_current_user_id(), $this->query_args($request)); + return $this->success(['data' => $view]); + } - /** - * @return true|\WP_Error - */ - public function require_logged_in() { - if (!is_user_logged_in()) { - return new \WP_Error( - 'aksrt_rest_forbidden', - __('请先登录', 'aksrt'), - ['status' => 401] - ); - } - return true; - } + public function pay(\WP_REST_Request $request): \WP_REST_Response { + $params = $request->get_json_params(); + $params = is_array($params) ? $params : []; + $result = UserOrders::get_instance()->pay_order( + get_current_user_id(), + sanitize_text_field((string) $request->get_param('order_number')), + sanitize_key((string) ($params['payment_method'] ?? '')) + ); + return !empty($result['success']) + ? $this->success(['data' => $result['data'] ?? []]) + : $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400); + } - public function pay(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $params = $request->get_json_params(); - if (!is_array($params)) { - $params = []; - } - $payment_method = sanitize_key((string) ($params['payment_method'] ?? '')); + public function cancel(\WP_REST_Request $request): \WP_REST_Response { + $result = UserOrders::get_instance()->cancel_order( + get_current_user_id(), + sanitize_text_field((string) $request->get_param('order_number')) + ); + return !empty($result['success']) + ? $this->success(['data' => $result['data'] ?? []]) + : $this->error((string) ($result['message'] ?? __('关闭订单失败', 'aksrt')), 400); + } - if (!class_exists(UserOrders::class)) { - return $this->error(__('订单服务不可用', 'aksrt'), 500); - } - $result = UserOrders::get_instance()->pay_order(get_current_user_id(), $order_number, $payment_method); + public function get_order(\WP_REST_Request $request): \WP_REST_Response { + $order = UserOrders::get_instance()->get_order_detail_view( + get_current_user_id(), + sanitize_text_field((string) $request->get_param('order_number')) + ); + return $order ? $this->success(['data' => $order]) : $this->error(__('订单不存在', 'aksrt'), 404); + } - if (empty($result['success'])) { - return $this->error((string) ($result['message'] ?? __('支付失败', 'aksrt')), 400); - } + /** @return array */ + private function query_args(\WP_REST_Request $request): array { + return [ + 'status' => $request->get_param('order_status'), + 'type' => $request->get_param('order_type'), + 'order_number' => $request->get_param('order_number'), + 'page' => $request->get_param('order_page'), + ]; + } - return $this->success(['data' => $result['data'] ?? []]); - } + private function success(array $data = [], int $status = 200): \WP_REST_Response { + return new \WP_REST_Response(array_merge(['success' => true], $data), $status); + } - public function cancel(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - - if (!class_exists(UserOrders::class)) { - return $this->error(__('订单服务不可用', 'aksrt'), 500); - } - $result = UserOrders::get_instance()->cancel_order(get_current_user_id(), $order_number); - - if (empty($result['success'])) { - return $this->error((string) ($result['message'] ?? __('取消订单失败', 'aksrt')), 400); - } - - return $this->success(['data' => $result['data'] ?? []]); - } - - public function get_order(\WP_REST_Request $request): \WP_REST_Response { - $order_number = sanitize_text_field((string) $request->get_param('order_number')); - $order = null; - - if (!class_exists(UserOrders::class)) { - return $this->error(__('订单服务不可用', 'aksrt'), 500); - } - $order = UserOrders::get_instance()->get_order_detail_view(get_current_user_id(), $order_number); - - if (!$order) { - return $this->error(__('订单不存在', 'aksrt'), 404); - } - - return $this->success(['data' => $order]); - } - - private function success(array $data = [], int $status = 200): \WP_REST_Response { - return new \WP_REST_Response(array_merge(['success' => true], $data), $status); - } - - private function error(string $message, int $status = 400): \WP_REST_Response { - return new \WP_REST_Response([ - 'success' => false, - 'message' => $message, - ], $status); - } + private function error(string $message, int $status = 400): \WP_REST_Response { + return new \WP_REST_Response(['success' => false, 'message' => $message], $status); + } } diff --git a/src/Payment/OrderRepositoryCore.php b/src/Payment/OrderRepositoryCore.php index 3d84655c..709faf6a 100644 --- a/src/Payment/OrderRepositoryCore.php +++ b/src/Payment/OrderRepositoryCore.php @@ -108,10 +108,16 @@ trait OrderRepositoryCore { $params[] = $user_id; } - $status = sanitize_key((string) ($args['status'] ?? '')); - if ($status !== '') { - $where[] = 'status = %s'; - $params[] = $status; + $statuses = array_values(array_filter(array_map('sanitize_key', (array) ($args['status_in'] ?? [])))); + if ($statuses !== []) { + $where[] = 'status IN (' . implode(', ', array_fill(0, count($statuses), '%s')) . ')'; + array_push($params, ...$statuses); + } else { + $status = sanitize_key((string) ($args['status'] ?? '')); + if ($status !== '') { + $where[] = 'status = %s'; + $params[] = $status; + } } $product_type = sanitize_key((string) ($args['product_type'] ?? '')); @@ -120,6 +126,12 @@ trait OrderRepositoryCore { $params[] = $product_type; } + $order_number_prefix = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($args['order_number_prefix'] ?? '')) ?? ''); + if ($order_number_prefix !== '') { + $where[] = 'order_number LIKE %s'; + $params[] = $wpdb->esc_like($order_number_prefix) . '%'; + } + $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); @@ -166,10 +178,16 @@ trait OrderRepositoryCore { $where = ['user_id = %d']; $params = [$user_id]; - $status = sanitize_key((string) ($args['status'] ?? '')); - if ($status !== '') { - $where[] = 'status = %s'; - $params[] = $status; + $statuses = array_values(array_filter(array_map('sanitize_key', (array) ($args['status_in'] ?? [])))); + if ($statuses !== []) { + $where[] = 'status IN (' . implode(', ', array_fill(0, count($statuses), '%s')) . ')'; + array_push($params, ...$statuses); + } else { + $status = sanitize_key((string) ($args['status'] ?? '')); + if ($status !== '') { + $where[] = 'status = %s'; + $params[] = $status; + } } $product_type = sanitize_key((string) ($args['product_type'] ?? '')); @@ -178,6 +196,12 @@ trait OrderRepositoryCore { $params[] = $product_type; } + $order_number_prefix = strtoupper(preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($args['order_number_prefix'] ?? '')) ?? ''); + if ($order_number_prefix !== '') { + $where[] = 'order_number LIKE %s'; + $params[] = $wpdb->esc_like($order_number_prefix) . '%'; + } + $where_sql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; diff --git a/src/Payment/UserOrders.php b/src/Payment/UserOrders.php index 246bb5c5..19e75a46 100644 --- a/src/Payment/UserOrders.php +++ b/src/Payment/UserOrders.php @@ -1,14 +1,14 @@ $args + * @return array */ public function get_order_center_view(int $user_id, array $args = []): array { + $query = OrderCenterQuery::from_array($args); + $empty = $this->empty_view($query); if ($user_id <= 0 || !class_exists(OrderRepository::class)) { - return [ - 'items' => [], - 'total' => 0, - 'total_pages' => 1, - 'page' => 1, - 'stats' => $this->empty_stats(), - 'product_types' => $this->product_type_filter_options(), - ]; + return $empty; } - $page = max(1, (int) ($args['page'] ?? 1)); - $per_page = max(1, min(50, (int) ($args['per_page'] ?? 20))); - $status = sanitize_key((string) ($args['status'] ?? '')); - $product_type = sanitize_key((string) ($args['product_type'] ?? '')); - - $repo_args = [ - 'limit' => $per_page, - 'offset' => ($page - 1) * $per_page, - ]; - if ($status !== '') { - $repo_args['status'] = $status; - } - if ($product_type !== '' && $product_type !== 'all') { - $repo_args['product_type'] = $product_type; - } - - $repo = OrderRepository::get_instance(); + $repo_args = $query->repository_args(); + $repo = OrderRepository::get_instance(); $orders = $repo->get_user_orders($user_id, $repo_args); - $total = $repo->count_user_orders($user_id, $repo_args); + $total = $repo->count_user_orders($user_id, $repo_args); + $total_pages = max(1, (int) ceil($total / $query->per_page())); + $page = min($query->page(), $total_pages); - $items = []; - foreach ($orders as $order) { - $items[] = $this->decorate_order_view($order, true); + if ($page !== $query->page() && $total > 0) { + $repo_args['offset'] = ($page - 1) * $query->per_page(); + $orders = $repo->get_user_orders($user_id, $repo_args); } return [ - 'items' => $items, - 'total' => $total, - 'total_pages' => max(1, (int) ceil(max(0, $total) / $per_page)), - 'page' => $page, - 'stats' => $this->get_user_order_stats($user_id), + 'items' => array_map([$this, 'decorate_order_view'], $orders), + 'total' => $total, + 'total_pages' => $total_pages, + 'page' => $page, + 'query' => array_merge($query->url_values(), ['order_page' => $page]), + 'stats' => $this->get_user_order_stats($user_id), 'product_types' => $this->product_type_filter_options(), ]; } + /** @return array|null */ public function get_order_detail_view(int $user_id, string $order_number): ?array { if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { return null; } + $order = OrderRepository::get_instance()->get_user_order($user_id, $order_number); if (!$order) { return null; } + if (class_exists(Gateway::class)) { $order = Gateway::get_instance()->reconcile_order($order_number, $order) ?: $order; } - return $this->decorate_order_view($order, true); + + return $this->decorate_order_view($order); } - /** - * @return array{total:int,pending:int,paid:int} - */ + /** @return array */ public function get_user_order_stats(int $user_id): array { if ($user_id <= 0 || !class_exists(OrderRepository::class)) { - return $this->empty_stats(); + return ['total' => 0, 'pending' => 0, 'completed' => 0]; } + $repo = OrderRepository::get_instance(); return [ - 'total' => $repo->count_user_orders($user_id, []), - 'pending' => $repo->count_user_orders($user_id, ['status' => Order::STATUS_PENDING]), - 'paid' => $repo->count_user_orders($user_id, ['status' => Order::STATUS_PAID]), + 'total' => $repo->count_user_orders($user_id, [ + 'status_in' => OrderCenterQuery::from_array([])->database_statuses(), + ]), + 'pending' => $repo->count_user_orders($user_id, [ + 'status_in' => [Order::STATUS_CREATED, Order::STATUS_PENDING], + ]), + 'completed' => $repo->count_user_orders($user_id, [ + 'status_in' => [Order::STATUS_PAID], + ]), ]; } - /** - * @return array - */ + /** @return array */ public function product_type_filter_options(): array { - $labels = \AKSRT\Core\DomainModel::order_product_type_labels(); - return array_merge(['' => __('全部类型', 'aksrt')], $labels); - } - - /** - * @param array $order Normalized order row. - * @return array - */ - public function decorate_order_view(array $order, bool $with_detail = false): array { - $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; - $order_number = (string) ($order['order_number'] ?? ''); - $product_type = sanitize_key((string) ($order['product_type'] ?? '')); - $status = (string) ($order['status'] ?? Order::STATUS_PENDING); - $product_id = (int) ($order['product_id'] ?? 0); - $currency = sanitize_key((string) ($order['currency'] ?? 'CNY')); - $is_points = $currency === 'points' - || sanitize_key((string) ($order['payment_method'] ?? '')) === 'points' - || (!empty($meta['pay_modo']) && (string) $meta['pay_modo'] === 'points'); - - $type_labels = \AKSRT\Core\DomainModel::order_product_type_labels(); - $type_label = $type_labels[$product_type] ?? ($product_type !== '' ? $product_type : __('订单', 'aksrt')); - - $title = $this->resolve_order_title($order, $meta, $product_type, $product_id, $type_label); - $image = $this->resolve_order_image($order, $meta, $product_type, $product_id); - $permalink = $this->resolve_order_permalink($product_type, $product_id, $meta); - - $order_items = []; - $has_structured_items = !empty($order_items); - - $product = []; - // Shop product cards were removed with the shop feature; content/VIP - // orders fall through to the shared meta-based fields below. - - if (!empty($product['title'])) { - $title = (string) $product['title']; - } - if (!empty($product['image'])) { - $image = (string) $product['image']; - } - if (!empty($product['permalink'])) { - $permalink = (string) $product['permalink']; - } - - $shipping_records = []; - $after_sale_items = []; - $subtotal = (float) ($meta['items'][0]['line_total'] ?? $order['amount'] ?? 0); - $shipping_fee = (float) ($meta['shipping_fee'] ?? 0); - $discount = (float) ($meta['discount'] ?? 0); - $expires_at = (string) ($order['expires_at'] ?? ''); - $payment_value = sanitize_key((string) ($meta['payment_method']['value'] ?? $order['payment_method'] ?? 'balance')); - - $can_pay = in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true) - && $payment_value !== 'manual_review'; - $can_cancel = in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true); - $can_after_sale = false; - $can_receipt = false; - $can_review = false; - $can_rebuy = false; - // Order detail modal is always available; product permalink is optional. - $can_view = true; - $can_view_product = $permalink !== '' && in_array($status, [ - Order::STATUS_PAID, - Order::STATUS_DELIVERED, - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - ], true); - - $view = [ - 'id' => (int) ($order['id'] ?? 0), - 'order_number' => $order_number, - 'amount' => (float) ($order['amount'] ?? 0), - 'currency' => $currency, - 'currency_symbol' => $is_points - ? (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt')) - : '¥', - 'is_points' => $is_points, - 'status' => $status, - 'status_label' => Order::get_valid_statuses()[$status] ?? $status, - 'product_type' => $product_type, - 'product_type_label' => $type_label, - 'product_id' => $product_id, - 'product_name' => $title, - 'product' => $product ?: [ - 'title' => $title, - 'image' => $image, - 'permalink' => $permalink, - ], - 'image' => $image, - 'permalink' => $permalink, - 'created_at' => (string) ($order['created_at'] ?? ''), - 'paid_at' => (string) ($order['paid_at'] ?? ''), - 'expires_at' => $expires_at, - 'subtotal' => $subtotal, - 'shipping_fee' => $shipping_fee, - 'discount' => $discount, - 'address' => $meta['address'] ?? [], - 'payment_method' => $meta['payment_method'] ?? [ - 'value' => (string) ($order['payment_method'] ?? ''), - 'label' => $this->payment_method_label((string) ($order['payment_method'] ?? '')), - ], - 'coupon' => $meta['coupon'] ?? [], - 'items' => $has_structured_items ? $order_items : ($meta['items'] ?? []), - 'shipping_method' => $meta['shipping_method'] ?? [], - 'customer_note' => $meta['customer_note'] ?? ($order['note_text'] ?? ''), - 'shipping_records' => $shipping_records, - 'pay_url' => '#', - 'actions' => [ - 'can_pay' => $can_pay, - 'can_cancel' => $can_cancel, - 'can_after_sale' => $can_after_sale, - 'can_receipt' => $can_receipt, - 'can_review' => $can_review, - 'can_rebuy' => $can_rebuy, - 'can_view' => $can_view, - 'can_view_product' => $can_view_product, - ], - 'quantity' => $this->resolve_order_quantity($order_items, $meta), - 'time_left' => $this->get_order_countdown($expires_at, $status), - 'secondary_url' => $this->resolve_secondary_url($product_type), - 'secondary_label' => $this->resolve_secondary_label($product_type), - ]; - - if ($with_detail) { - $view['after_sale_records'] = $after_sale_items; - $view['timeline'] = $this->build_order_timeline($order, $shipping_records, $after_sale_items); - } - - return $view; - } - - /** - * Cancel pending order (any product_type). - * - * @return array{success:bool,message?:string,data?:array} - */ - public function cancel_order(int $user_id, string $order_number): array { - if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $repo = OrderRepository::get_instance(); - $order = $repo->get_user_order($user_id, $order_number); - if (!$order) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $status = (string) ($order['status'] ?? ''); - if (!in_array($status, [Order::STATUS_PENDING, Order::STATUS_CREATED, Order::STATUS_EXPIRED], true)) { - return ['success' => false, 'message' => __('当前订单不可取消。', 'aksrt')]; - } - - $repo->update_status($order_number, Order::STATUS_CANCELLED, [ - 'force' => true, - 'note' => __('用户在前台取消订单。', 'aksrt'), - ]); - - return [ - 'success' => true, - 'data' => [ - 'order' => $this->get_order_detail_view($user_id, $order_number), - ], - ]; - } - - /** - * Resume payment via configured gateway (any product_type). - * - * @return array{success:bool,message?:string,data?:array} - */ - public function pay_order(int $user_id, string $order_number, string $payment_method = ''): array { - if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - // Resume via shared gateway stack (any product_type; shop removed). - $repo = OrderRepository::get_instance(); - $order = $repo->get_user_order($user_id, $order_number); - if (!$order) { - return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; - } - - $status = (string) ($order['status'] ?? ''); - if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true)) { - return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')]; - } - - $stored_method = sanitize_key((string) ($order['payment_method'] ?? '')); - $payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance')); - if ($stored_method !== '' && $payment_method !== $stored_method) { - return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')]; - } - - $gateway_manager = class_exists(Gateway::class) ? Gateway::get_instance() : null; - $gateway = $gateway_manager ? $gateway_manager->get_gateway($payment_method) : null; - if (!$gateway) { - return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')]; - } - if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { - return ['success' => false, 'message' => __('该支付方式尚未配置,请联系站长。', 'aksrt')]; - } - - $order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number); - $order['payment_method'] = $payment_method; - - $payment_result = PaymentOrchestrator::pay($gateway, $order, [ - 'paid_note' => __('用户在前台完成支付。', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return ['success' => false, 'message' => $payment_result['message']]; - } - - if (!empty($payment_result['redirect'])) { - return [ - 'success' => true, - 'data' => [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'message' => $payment_result['message'], - ], - ]; - } - - return [ - 'success' => true, - 'data' => [ - 'order' => $this->get_order_detail_view($user_id, $order_number), - 'message' => $payment_result['message'], - ], - ]; - } - - private function empty_stats(): array { - return ['total' => 0, 'pending' => 0, 'paid' => 0]; + return array_merge(['' => __('全部类型', 'aksrt')], DomainModel::order_product_type_labels()); } /** * @param array $order - * @param array $meta + * @return array */ - private function resolve_order_title(array $order, array $meta, string $product_type, int $product_id, string $type_label): string { + public function decorate_order_view(array $order): array { + $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; + $product_type = sanitize_key((string) ($order['product_type'] ?? '')); + $product_id = (int) ($order['product_id'] ?? 0); + $status = sanitize_key((string) ($order['status'] ?? Order::STATUS_PENDING)); + $currency = sanitize_key((string) ($order['currency'] ?? 'CNY')); + $is_points = $currency === 'points' + || sanitize_key((string) ($order['payment_method'] ?? '')) === 'points' + || sanitize_key((string) ($meta['pay_modo'] ?? '')) === 'points'; + $type_labels = DomainModel::order_product_type_labels(); + $type_label = $type_labels[$product_type] ?? __('订单', 'aksrt'); + $title = $this->resolve_title($meta, $product_type, $product_id, $type_label); + $image = $this->resolve_image($meta, $product_id); + $permalink = $this->resolve_entitlement_url($product_type, $product_id, $meta); + $group = $this->status_group($status); + $payment_method = sanitize_key((string) ($order['payment_method'] ?? '')); + $can_pay = $group === OrderCenterQuery::STATUS_PENDING && $payment_method !== 'manual_review'; + $can_cancel = $group === OrderCenterQuery::STATUS_PENDING; + $can_open = $group === OrderCenterQuery::STATUS_COMPLETED && $permalink !== ''; + + return [ + 'id' => (int) ($order['id'] ?? 0), + 'order_number' => (string) ($order['order_number'] ?? ''), + 'amount' => (float) ($order['amount'] ?? 0), + 'currency' => $currency, + 'currency_symbol' => $is_points + ? (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt')) + : '¥', + 'is_points' => $is_points, + 'status' => $status, + 'status_group' => $group, + 'status_label' => $this->status_label($group), + 'product_type' => $product_type, + 'product_type_label' => $type_label, + 'product_id' => $product_id, + 'product_name' => $title, + 'image' => $image, + 'permalink' => $permalink, + 'created_at' => (string) ($order['created_at'] ?? ''), + 'paid_at' => (string) ($order['paid_at'] ?? ''), + 'expires_at' => (string) ($order['expires_at'] ?? ''), + 'payment_method' => [ + 'value' => $payment_method, + 'label' => $this->payment_method_label($payment_method), + ], + 'actions' => [ + 'can_pay' => $can_pay, + 'can_cancel' => $can_cancel, + 'can_open' => $can_open, + ], + 'time_left' => $this->get_order_countdown((string) ($order['expires_at'] ?? ''), $status), + 'entitlement_label' => $this->entitlement_label($product_type), + ]; + } + + /** @return array */ + private function empty_view(OrderCenterQuery $query): array { + return [ + 'items' => [], + 'total' => 0, + 'total_pages' => 1, + 'page' => 1, + 'query' => $query->url_values(), + 'stats' => ['total' => 0, 'pending' => 0, 'completed' => 0], + 'product_types' => $this->product_type_filter_options(), + ]; + } + + private function status_group(string $status): string { + return match ($status) { + Order::STATUS_CREATED, Order::STATUS_PENDING => OrderCenterQuery::STATUS_PENDING, + Order::STATUS_PAID => OrderCenterQuery::STATUS_COMPLETED, + Order::STATUS_CANCELLED, Order::STATUS_EXPIRED => OrderCenterQuery::STATUS_CLOSED, + default => OrderCenterQuery::STATUS_CLOSED, + }; + } + + private function status_label(string $group): string { + return match ($group) { + OrderCenterQuery::STATUS_PENDING => __('待支付', 'aksrt'), + OrderCenterQuery::STATUS_COMPLETED => __('权益已生效', 'aksrt'), + default => __('已关闭', 'aksrt'), + }; + } + + /** @param array $meta */ + private function resolve_title(array $meta, string $type, int $product_id, string $fallback): string { foreach (['product_title', 'title', 'package_title'] as $key) { if (!empty($meta[$key]) && is_string($meta[$key])) { return (string) $meta[$key]; } } - - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { - $pkg = (string) ($meta['package_title'] ?? ''); - if ($pkg !== '') { - return $pkg; - } + if ($type === DomainModel::ORDER_PRODUCT_VIP) { return __('VIP 会员', 'aksrt'); } - - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { - return (string) ($meta['title'] ?? __('余额充值', 'aksrt')); + if ($type === DomainModel::ORDER_PRODUCT_RECHARGE) { + return __('余额充值', 'aksrt'); } - - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { - return (string) ($meta['title'] ?? __('购买积分', 'aksrt')); + if ($type === DomainModel::ORDER_PRODUCT_POINTS_BUY) { + return __('购买积分', 'aksrt'); } - if ($product_id > 0) { - $post_title = get_the_title($product_id); - if (is_string($post_title) && $post_title !== '') { - return $post_title; + $title = get_the_title($product_id); + if (is_string($title) && $title !== '') { + return $title; } } - - return $type_label; + return $fallback; } - /** - * @param array $order - * @param array $meta - */ - private function resolve_order_image(array $order, array $meta, string $product_type, int $product_id): string { + /** @param array $meta */ + private function resolve_image(array $meta, int $product_id): string { if (!empty($meta['thumb']) && is_string($meta['thumb'])) { - return esc_url_raw((string) $meta['thumb']); + return esc_url_raw($meta['thumb']); } if ($product_id > 0 && has_post_thumbnail($product_id)) { - $url = get_the_post_thumbnail_url($product_id, 'thumbnail'); - if (is_string($url) && $url !== '') { - return $url; - } + return (string) get_the_post_thumbnail_url($product_id, 'thumbnail'); } return ''; } - /** - * @param array $meta - */ - private function resolve_order_permalink(string $product_type, int $product_id, array $meta): string { - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { - return function_exists('aksrt_get_user_center_tab_url') - ? aksrt_get_user_center_tab_url('vip') - : ''; - } - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE - || $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { - return function_exists('aksrt_get_user_center_tab_url') - ? aksrt_get_user_center_tab_url('credits') - : ''; - } - $post_id = $product_id > 0 ? $product_id : (int) ($meta['post_id'] ?? 0); - if ($post_id > 0) { - $link = get_permalink($post_id); - return is_string($link) ? $link : ''; - } - return ''; - } - - private function resolve_secondary_url(string $product_type): string { - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { + /** @param array $meta */ + private function resolve_entitlement_url(string $type, int $product_id, array $meta): string { + if ($type === DomainModel::ORDER_PRODUCT_VIP) { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : ''; } - if (in_array($product_type, [ - \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE, - \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY, - ], true)) { + if (in_array($type, [DomainModel::ORDER_PRODUCT_RECHARGE, DomainModel::ORDER_PRODUCT_POINTS_BUY], true)) { return function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : ''; } - return ''; + $post_id = $product_id ?: (int) ($meta['post_id'] ?? 0); + return $post_id > 0 ? (string) get_permalink($post_id) : ''; } - private function resolve_secondary_label(string $product_type): string { - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) { - return __('我的会员', 'aksrt'); - } - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { - return __('我的余额', 'aksrt'); - } - if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { - return class_exists('\AKSRT\Settings\Settings') - ? sprintf(__('我的%s', 'aksrt'), \AKSRT\Settings\Settings::get_currency_name()) - : __('我的积分', 'aksrt'); - } - return ''; + private function entitlement_label(string $type): string { + return match ($type) { + DomainModel::ORDER_PRODUCT_VIP => __('查看会员权益', 'aksrt'), + DomainModel::ORDER_PRODUCT_RECHARGE => __('查看余额', 'aksrt'), + DomainModel::ORDER_PRODUCT_POINTS_BUY => __('查看积分', 'aksrt'), + default => __('打开已购内容', 'aksrt'), + }; } private function payment_method_label(string $method): string { - $method = sanitize_key($method); - $map = [ - 'balance' => __('站内余额', 'aksrt'), - 'points' => __('积分支付', 'aksrt'), - 'alipay' => __('支付宝', 'aksrt'), - 'wechatpay' => __('微信支付', 'aksrt'), - 'wechat' => __('微信支付', 'aksrt'), - 'paypal' => 'PayPal', - 'stripe' => 'Stripe', - 'epay_alipay' => __('易支付·支付宝', 'aksrt'), - 'epay_wxpay' => __('易支付·微信', 'aksrt'), + $labels = [ + 'balance' => __('站内余额', 'aksrt'), + 'points' => __('积分支付', 'aksrt'), + 'alipay' => __('支付宝', 'aksrt'), + 'wechatpay' => __('微信支付', 'aksrt'), + 'paypal' => 'PayPal', + 'stripe' => 'Stripe', ]; - return $map[$method] ?? ($method !== '' ? $method : __('—', 'aksrt')); - } - - /** - * Build chronological order progress for the detail modal. - * - * Driven by current status + available timestamps / related records so the - * chain advances even when historical rows lack paid_at/delivered_at. - * - * @param array $order - * @param array $shipping_records - * @param array $after_sale_records - * @return array - */ - private function build_order_timeline(array $order, array $shipping_records, array $after_sale_records): array { - $status = sanitize_key((string) ($order['status'] ?? Order::STATUS_PENDING)); - $created_at = $this->timeline_time((string) ($order['created_at'] ?? '')); - $paid_at = $this->timeline_time((string) ($order['paid_at'] ?? '')); - $delivered_at = $this->timeline_time((string) ($order['delivered_at'] ?? '')); - $completed_at = $this->timeline_time((string) ($order['completed_at'] ?? '')); - $cancelled_at = $this->timeline_time((string) ($order['cancelled_at'] ?? '')); - $refunded_at = $this->timeline_time((string) ($order['refunded_at'] ?? '')); - - $shipping_created = ''; - foreach ($shipping_records as $record) { - $t = $this->timeline_time((string) ($record['created_at'] ?? '')); - if ($t !== '' && ($shipping_created === '' || strcmp($t, $shipping_created) < 0)) { - $shipping_created = $t; - } - } - if ($delivered_at === '' && $shipping_created !== '') { - $delivered_at = $shipping_created; - } - - $paid_like = [ - Order::STATUS_PAID, - Order::STATUS_DELIVERED, - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - Order::STATUS_REFUNDING, - Order::STATUS_REFUNDED, - ]; - $ship_like = [ - Order::STATUS_DELIVERED, - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - ]; - $done_like = [ - Order::STATUS_COMPLETED, - Order::STATUS_PENDING_REVIEW, - ]; - - $timeline = []; - $push = static function (array &$out, string $key, string $label, string $time): void { - if ($label === '') { - return; - } - $out[] = [ - 'key' => $key, - 'label' => $label, - 'time' => $time, - ]; - }; - - // 1) Always: order created. - $push( - $timeline, - 'created', - __('订单创建', 'aksrt'), - $created_at !== '' ? $created_at : '' - ); - - // 2) Paid (status-driven; fall back to created_at when paid_at missing). - if ($paid_at !== '' || in_array($status, $paid_like, true)) { - $push( - $timeline, - 'paid', - __('完成支付', 'aksrt'), - $paid_at !== '' ? $paid_at : $created_at - ); - } - - // 3) Shipped / delivered — only when logistics exists or status reached it. - $has_shipping = $shipping_records !== []; - if ( - $delivered_at !== '' - || $has_shipping - || in_array($status, $ship_like, true) - ) { - // Virtual / content flows often jump paid → completed without logistics; - // only show the ship step when we have evidence of shipping. - if ($delivered_at !== '' || $has_shipping || $status === Order::STATUS_DELIVERED) { - $push( - $timeline, - 'delivered', - __('订单发货', 'aksrt'), - $delivered_at !== '' ? $delivered_at : ($paid_at !== '' ? $paid_at : $created_at) - ); - } - } - - // 4) Completed / pending review. - if ($completed_at !== '' || in_array($status, $done_like, true)) { - $done_label = $status === Order::STATUS_PENDING_REVIEW - ? __('待评价', 'aksrt') - : __('交易完成', 'aksrt'); - $push( - $timeline, - 'completed', - $done_label, - $completed_at !== '' - ? $completed_at - : ($delivered_at !== '' ? $delivered_at : ($paid_at !== '' ? $paid_at : $created_at)) - ); - } - - // 5) Terminal cancel / expire / refund. - if ($status === Order::STATUS_CANCELLED || $cancelled_at !== '') { - $push( - $timeline, - 'cancelled', - __('订单取消', 'aksrt'), - $cancelled_at !== '' ? $cancelled_at : $created_at - ); - } - if ($status === Order::STATUS_EXPIRED) { - $push( - $timeline, - 'expired', - __('订单过期', 'aksrt'), - $cancelled_at !== '' ? $cancelled_at : $created_at - ); - } - if ($status === Order::STATUS_REFUNDING) { - $push( - $timeline, - 'refunding', - __('退款处理中', 'aksrt'), - $refunded_at !== '' ? $refunded_at : ($paid_at !== '' ? $paid_at : $created_at) - ); - } - if ($status === Order::STATUS_REFUNDED || $refunded_at !== '') { - $push( - $timeline, - 'refunded', - __('完成退款', 'aksrt'), - $refunded_at !== '' ? $refunded_at : ($paid_at !== '' ? $paid_at : $created_at) - ); - } - - // Extra logistics / after-sale events (detail lines). - foreach ($shipping_records as $record) { - $tracking = trim((string) ($record['tracking_no'] ?? '')); - $carrier = trim((string) ($record['carrier_label'] ?? $record['carrier'] ?? '')); - $status_l = trim((string) ($record['status_label'] ?? $record['status'] ?? '')); - $parts = array_filter([$carrier, $tracking, $status_l], static fn($v) => $v !== ''); - $label = $parts !== [] - ? sprintf(__('物流更新:%s', 'aksrt'), implode(' / ', $parts)) - : __('物流更新', 'aksrt'); - $push( - $timeline, - 'shipping', - $label, - $this->timeline_time((string) ($record['created_at'] ?? $record['updated_at'] ?? '')) - ); - } - foreach ($after_sale_records as $record) { - $reason = trim((string) ($record['reason'] ?? '')); - $type_l = trim((string) ($record['type_label'] ?? $record['type'] ?? '')); - $as_status = trim((string) ($record['status_label'] ?? $record['status'] ?? '')); - $parts = array_filter([$type_l, $as_status, $reason], static fn($v) => $v !== ''); - $label = $parts !== [] - ? sprintf(__('售后:%s', 'aksrt'), implode(' — ', $parts)) - : __('售后申请', 'aksrt'); - $push( - $timeline, - 'after_sale', - $label, - $this->timeline_time((string) ($record['created_at'] ?? $record['updated_at'] ?? '')) - ); - } - - usort($timeline, static function ($a, $b) { - $ta = (string) ($a['time'] ?? ''); - $tb = (string) ($b['time'] ?? ''); - if ($ta === $tb) { - // Keep canonical pipeline order when times collide / missing. - $rank = [ - 'created' => 10, - 'paid' => 20, - 'delivered' => 30, - 'shipping' => 35, - 'completed' => 40, - 'refunding' => 50, - 'refunded' => 60, - 'cancelled' => 70, - 'expired' => 80, - 'after_sale'=> 90, - ]; - $ra = $rank[(string) ($a['key'] ?? '')] ?? 99; - $rb = $rank[(string) ($b['key'] ?? '')] ?? 99; - return $ra <=> $rb; - } - if ($ta === '') { - return 1; - } - if ($tb === '') { - return -1; - } - return strcmp($ta, $tb); - }); - - return $timeline; - } - - /** - * Normalize a mysql datetime for timeline display (empty when invalid/zero). - */ - private function timeline_time(string $value): string { - $value = trim($value); - if ($value === '' || str_starts_with($value, '0000-00-00')) { - return ''; - } - return $value; + return $labels[$method] ?? ($method !== '' ? $method : __('—', 'aksrt')); } private function get_order_countdown(string $expires_at, string $status): int { - if ($expires_at === '' || !in_array($status, [Order::STATUS_PENDING, Order::STATUS_EXPIRED, Order::STATUS_CREATED], true)) { + if (!in_array($status, [Order::STATUS_CREATED, Order::STATUS_PENDING], true) || $expires_at === '') { return 0; } - $left = strtotime($expires_at) - time(); - return max(0, (int) $left); + $expires = strtotime($expires_at); + return $expires ? max(0, $expires - current_time('timestamp')) : 0; } - /** - * Sum line quantities for display (shop multi-item or single meta item). - * - * @param array> $order_items - * @param array $meta - */ - private function resolve_order_quantity(array $order_items, array $meta): int { - $qty = 0; - $source = !empty($order_items) ? $order_items : (is_array($meta['items'] ?? null) ? $meta['items'] : []); - foreach ($source as $row) { - if (!is_array($row)) { - continue; - } - $qty += max(1, (int) ($row['quantity'] ?? $row['qty'] ?? 1)); + /** @return array{success:bool,message?:string,data?:array} */ + public function cancel_order(int $user_id, string $order_number): array { + if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { + return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; } - if ($qty <= 0) { - $qty = max(1, (int) ($meta['quantity'] ?? $meta['qty'] ?? 1)); + $repo = OrderRepository::get_instance(); + $order = $repo->get_user_order($user_id, $order_number); + if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) { + return ['success' => false, 'message' => __('当前订单不可关闭。', 'aksrt')]; } - return $qty; + if (!$repo->update_status($order_number, Order::STATUS_CANCELLED, [ + 'force' => true, + 'note' => __('用户在前台关闭订单。', 'aksrt'), + ])) { + return ['success' => false, 'message' => __('关闭订单失败,请稍后重试。', 'aksrt')]; + } + return ['success' => true, 'data' => ['order' => $this->get_order_detail_view($user_id, $order_number)]]; + } + + /** @return array{success:bool,message?:string,data?:array} */ + public function pay_order(int $user_id, string $order_number, string $payment_method = ''): array { + if ($user_id <= 0 || $order_number === '' || !class_exists(OrderRepository::class)) { + return ['success' => false, 'message' => __('订单不存在。', 'aksrt')]; + } + $repo = OrderRepository::get_instance(); + $order = $repo->get_user_order($user_id, $order_number); + if (!$order || $this->status_group((string) ($order['status'] ?? '')) !== OrderCenterQuery::STATUS_PENDING) { + return ['success' => false, 'message' => __('当前订单状态不可支付。', 'aksrt')]; + } + $stored_method = sanitize_key((string) ($order['payment_method'] ?? '')); + $payment_method = sanitize_key($payment_method ?: ($stored_method ?: 'balance')); + if ($stored_method !== '' && $payment_method !== $stored_method) { + return ['success' => false, 'message' => __('支付方式与订单不一致,请重新下单。', 'aksrt')]; + } + $manager = class_exists(Gateway::class) ? Gateway::get_instance() : null; + $gateway = $manager ? $manager->get_gateway($payment_method) : null; + if (!$gateway || (method_exists($gateway, 'is_configured') && !$gateway->is_configured())) { + return ['success' => false, 'message' => __('当前支付方式不可用。', 'aksrt')]; + } + $order['subject'] = $order['subject'] ?? sprintf(__('订单 %s', 'aksrt'), $order_number); + $order['payment_method'] = $payment_method; + $result = PaymentOrchestrator::pay($gateway, $order, ['paid_note' => __('用户在前台完成支付。', 'aksrt')]); + if (empty($result['success'])) { + return ['success' => false, 'message' => (string) ($result['message'] ?? __('支付失败。', 'aksrt'))]; + } + return ['success' => true, 'data' => [ + 'redirect' => !empty($result['redirect']), + 'payment_url' => (string) ($result['payment_url'] ?? ''), + 'order_number' => $order_number, + 'order' => empty($result['redirect']) ? $this->get_order_detail_view($user_id, $order_number) : null, + 'message' => (string) ($result['message'] ?? ''), + ]]; } } diff --git a/templates/pages/page-orders.php b/templates/pages/page-orders.php index d1724bea..bb14135a 100644 --- a/templates/pages/page-orders.php +++ b/templates/pages/page-orders.php @@ -3,41 +3,29 @@ * Template Name: AKSRT - Orders * Template Post Type: page * - * Legacy standalone orders URL — redirects into user center tab (zibll-style). - * Kept as template slug so old page assignments resolve; content lives in UC. - * * @package AKSRT */ defined('ABSPATH') || exit; -if (!is_user_logged_in()) { - $login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(home_url('/profile/')); - wp_safe_redirect($login_url); - exit; -} - $target = function_exists('aksrt_get_user_center_url') - ? aksrt_get_user_center_url('orders') - : home_url('/profile/?tab=orders'); + ? aksrt_get_user_center_url('orders') + : home_url('/profile/?tab=orders'); -// Preserve filter/query from standalone page. -$keep = []; -foreach (['order_tab', 'otab', 'order_status', 'order_page', 'order_type', 'section'] as $key) { - if (isset($_GET[$key]) && (string) wp_unslash($_GET[$key]) !== '') { - $keep[$key] = sanitize_text_field(wp_unslash((string) $_GET[$key])); - } -} -// Legacy ?tab=pending on standalone page → order_tab. -if (isset($_GET['tab'])) { - $legacy_tab = sanitize_key((string) wp_unslash($_GET['tab'])); - if ($legacy_tab !== '' && $legacy_tab !== 'orders' && !isset($keep['order_tab'])) { - $keep['order_tab'] = $legacy_tab; - } -} -if ($keep !== []) { - $target = add_query_arg($keep, $target); +if (!is_user_logged_in()) { + $login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url($target); + wp_safe_redirect($login_url); + exit; } -wp_safe_redirect($target, 302); +$raw = [ + 'status' => $_GET['order_status'] ?? '', + 'type' => $_GET['order_type'] ?? '', + 'order_number' => $_GET['order_number'] ?? '', + 'page' => $_GET['order_page'] ?? 1, +]; +$query = \AKSRT\Payment\OrderCenterQuery::from_array($raw); +$params = array_merge(['tab' => 'orders'], $query->url_values()); + +wp_safe_redirect(add_query_arg($params, $target), 302); exit;