优化摸大红前台交互与主题按钮样式

列表改为滚动触底自动分页,详情与订单页铺满主内容区,按钮统一为橙色主题描边/实心样式。
This commit is contained in:
yml2213
2026-07-16 19:40:37 +08:00
parent 6d61027318
commit 094a91c985
5 changed files with 427 additions and 99 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue' import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant' import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue' import MobileBottomNav from '@/components/MobileBottomNav.vue'
@@ -24,12 +24,22 @@ const loadingMore = ref(false)
const keyword = ref('') const keyword = ref('')
const activeCategoryId = ref<number | null>(null) const activeCategoryId = ref<number | null>(null)
const failedCover = ref<Record<number, boolean>>({}) const failedCover = ref<Record<number, boolean>>({})
const listPanel = ref<HTMLElement | null>(null)
const loadMoreSentinel = ref<HTMLElement | null>(null)
let searchTimer: ReturnType<typeof setTimeout> | null = null let searchTimer: ReturnType<typeof setTimeout> | null = null
let loadMoreObserver: IntersectionObserver | null = null
onMounted(async () => { onMounted(async () => {
await loadCategories() await loadCategories()
applyRouteQuery() applyRouteQuery()
await loadProducts(true) await loadProducts(true)
setupLoadMoreObserver()
})
onBeforeUnmount(() => {
loadMoreObserver?.disconnect()
loadMoreObserver = null
if (searchTimer) clearTimeout(searchTimer)
}) })
watch( watch(
@@ -64,13 +74,13 @@ async function loadCategories() {
} }
async function loadProducts(reset = true) { async function loadProducts(reset = true) {
if (loadingMore.value) return
if (reset) { if (reset) {
if (loading.value) return
loading.value = true loading.value = true
page.value = 1 page.value = 1
hasMore.value = true hasMore.value = true
} else { } else {
if (!hasMore.value) return if (loading.value || loadingMore.value || !hasMore.value) return
loadingMore.value = true loadingMore.value = true
} }
try { try {
@@ -92,6 +102,25 @@ async function loadProducts(reset = true) {
} }
} }
function setupLoadMoreObserver() {
loadMoreObserver?.disconnect()
loadMoreObserver = new IntersectionObserver(
entries => {
if (!entries.some(item => item.isIntersecting)) return
if (loading.value || loadingMore.value || !hasMore.value) return
loadProducts(false)
},
{ root: listPanel.value, rootMargin: '160px 0px', threshold: 0 }
)
nextTick(() => {
if (loadMoreSentinel.value) loadMoreObserver?.observe(loadMoreSentinel.value)
})
}
watch([listPanel, loadMoreSentinel], () => {
setupLoadMoreObserver()
})
function selectCategory(id: number | null) { function selectCategory(id: number | null) {
activeCategoryId.value = id activeCategoryId.value = id
syncQuery() syncQuery()
@@ -166,7 +195,7 @@ function showCover(item: MohongProduct) {
</button> </button>
</aside> </aside>
<section class="list-panel"> <section ref="listPanel" class="list-panel">
<van-loading v-if="loading" class="state-loading" size="24px" vertical>加载中...</van-loading> <van-loading v-if="loading" class="state-loading" size="24px" vertical>加载中...</van-loading>
<van-empty v-else-if="products.length === 0" description="暂无商品" /> <van-empty v-else-if="products.length === 0" description="暂无商品" />
<div v-else class="product-list"> <div v-else class="product-list">
@@ -196,17 +225,14 @@ function showCover(item: MohongProduct) {
<span class="buy-icon"></span> <span class="buy-icon"></span>
</button> </button>
</div> </div>
<van-button <div
v-if="hasMore && products.length" v-if="products.length"
size="small" ref="loadMoreSentinel"
plain class="load-sentinel"
block aria-hidden="true"
:loading="loadingMore" />
class="load-more" <div v-if="loadingMore" class="load-tip">加载中...</div>
@click="loadProducts(false)" <div v-else-if="!loading && products.length && !hasMore" class="load-tip">已经到底了</div>
>
加载更多
</van-button>
</section> </section>
</div> </div>
<MobileBottomNav /> <MobileBottomNav />
@@ -379,8 +405,14 @@ function showCover(item: MohongProduct) {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
} }
.load-more { .load-sentinel {
margin: 12px; width: 100%;
width: calc(100% - 24px); height: 1px;
}
.load-tip {
padding: 12px;
text-align: center;
color: #b0b8c4;
font-size: 12px;
} }
</style> </style>
@@ -214,7 +214,9 @@ async function handleBuy() {
> >
{{ canBuy ? '立即购买' : '暂时缺货' }} {{ canBuy ? '立即购买' : '暂时缺货' }}
</el-button> </el-button>
<el-button size="large" @click="router.push('/mohong')">返回列表</el-button> <el-button size="large" class="btn-outline" @click="router.push('/mohong')">
返回列表
</el-button>
</div> </div>
<div class="tips"> <div class="tips">
@@ -288,14 +290,15 @@ async function handleBuy() {
<style scoped> <style scoped>
.mohong-detail-page { .mohong-detail-page {
width: 100%; width: 100%;
max-width: 1200px;
} }
.detail-layout { .detail-layout {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr); /* 左图固定合理宽度,右侧信息铺满剩余空间,避免整页右侧大片空白 */
gap: 28px; grid-template-columns: minmax(320px, 420px) minmax(0, 1fr);
gap: 24px;
align-items: start; align-items: start;
width: 100%;
} }
.gallery-panel, .gallery-panel,
@@ -304,13 +307,15 @@ async function handleBuy() {
border: 1px solid #eef1f5; border: 1px solid #eef1f5;
border-radius: 16px; border-radius: 16px;
padding: 18px; padding: 18px;
min-width: 0;
} }
.main-image { .main-image {
width: 100%;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
background: #f3f6fb; background: #111;
} }
.main-image img { .main-image img {
@@ -436,10 +441,27 @@ async function handleBuy() {
.actions { .actions {
display: flex; display: flex;
flex-wrap: wrap;
gap: 12px; gap: 12px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.actions :deep(.el-button--primary) {
--el-button-bg-color: #ff6a00;
--el-button-border-color: #ff6a00;
--el-button-hover-bg-color: #e55d00;
--el-button-hover-border-color: #e55d00;
}
.actions :deep(.btn-outline) {
--el-button-bg-color: #fff8f2;
--el-button-border-color: rgba(255, 106, 0, 0.32);
--el-button-text-color: #ff6a00;
--el-button-hover-bg-color: #fff3e8;
--el-button-hover-border-color: #ff6a00;
--el-button-hover-text-color: #ff6a00;
}
.tips { .tips {
padding-top: 16px; padding-top: 16px;
border-top: 1px solid #eef1f5; border-top: 1px solid #eef1f5;
@@ -527,5 +549,9 @@ async function handleBuy() {
.detail-layout { .detail-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.gallery-panel {
max-width: 420px;
}
} }
</style> </style>
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue' import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Tickets } from '@element-plus/icons-vue' import { Search, Tickets } from '@element-plus/icons-vue'
@@ -23,12 +23,21 @@ const page = ref(1)
const hasMore = ref(true) const hasMore = ref(true)
const keyword = ref('') const keyword = ref('')
const activeCategoryId = ref<number | null>(null) const activeCategoryId = ref<number | null>(null)
const loadMoreSentinel = ref<HTMLElement | null>(null)
let searchTimer: ReturnType<typeof setTimeout> | null = null let searchTimer: ReturnType<typeof setTimeout> | null = null
let loadMoreObserver: IntersectionObserver | null = null
onMounted(async () => { onMounted(async () => {
await loadCategories() await loadCategories()
applyRouteQuery() applyRouteQuery()
await loadProducts(true) await loadProducts(true)
setupLoadMoreObserver()
})
onBeforeUnmount(() => {
loadMoreObserver?.disconnect()
loadMoreObserver = null
if (searchTimer) clearTimeout(searchTimer)
}) })
watch( watch(
@@ -64,13 +73,13 @@ async function loadCategories() {
} }
async function loadProducts(reset = true) { async function loadProducts(reset = true) {
if (loadingMore.value) return
if (reset) { if (reset) {
if (loading.value) return
loading.value = true loading.value = true
page.value = 1 page.value = 1
hasMore.value = true hasMore.value = true
} else { } else {
if (!hasMore.value) return if (loading.value || loadingMore.value || !hasMore.value) return
loadingMore.value = true loadingMore.value = true
} }
try { try {
@@ -93,6 +102,26 @@ async function loadProducts(reset = true) {
} }
} }
function setupLoadMoreObserver() {
loadMoreObserver?.disconnect()
loadMoreObserver = new IntersectionObserver(
entries => {
if (!entries.some(item => item.isIntersecting)) return
if (loading.value || loadingMore.value || !hasMore.value) return
loadProducts(false)
},
{ root: null, rootMargin: '200px 0px', threshold: 0 }
)
nextTick(() => {
if (loadMoreSentinel.value) loadMoreObserver?.observe(loadMoreSentinel.value)
})
}
watch(loadMoreSentinel, el => {
loadMoreObserver?.disconnect()
if (el) loadMoreObserver?.observe(el)
})
function selectCategory(id: number | null) { function selectCategory(id: number | null) {
activeCategoryId.value = id activeCategoryId.value = id
syncQuery() syncQuery()
@@ -138,10 +167,13 @@ const activeCategoryName = () => {
@input="onKeywordInput" @input="onKeywordInput"
/> />
</div> </div>
<el-button size="small" :icon="Tickets" @click="router.push('/mohong/orders')"> <button type="button" class="toolbar-btn outline" @click="router.push('/mohong/orders')">
我的订单 <el-icon><Tickets /></el-icon>
</el-button> <span>我的订单</span>
<el-button size="small" plain @click="router.push('/')">租号大厅</el-button> </button>
<button type="button" class="toolbar-btn outline" @click="router.push('/')">
租号大厅
</button>
</div> </div>
</header> </header>
@@ -176,11 +208,17 @@ const activeCategoryName = () => {
<div v-else v-loading="loading" class="product-grid"> <div v-else v-loading="loading" class="product-grid">
<MohongProductCard v-for="item in products" :key="item.id" :product="item" /> <MohongProductCard v-for="item in products" :key="item.id" :product="item" />
</div> </div>
<div v-if="!loading && products.length" class="load-state"> <div
<el-button v-if="hasMore" :loading="loadingMore" @click="loadProducts(false)"> v-if="products.length"
加载更多 ref="loadMoreSentinel"
</el-button> class="load-sentinel"
<span v-else class="end">已经到底了</span> aria-hidden="true"
/>
<div v-if="loadingMore" class="load-state">
<span class="loading-more">加载中...</span>
</div>
<div v-else-if="!loading && products.length && !hasMore" class="load-state">
<span class="end">已经到底了</span>
</div> </div>
</div> </div>
</div> </div>
@@ -244,12 +282,20 @@ const activeCategoryName = () => {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
width: 220px; width: 220px;
height: 32px; height: 36px;
padding: 0 10px; padding: 0 12px;
border: 1px solid #e4e7ed; border: 1px solid #eef1f5;
border-radius: 8px; border-radius: 10px;
background: #fff; background: #fff;
color: #94a3b8; color: #94a3b8;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.search-box:focus-within {
border-color: rgba(255, 106, 0, 0.45);
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.08);
} }
.search-box input { .search-box input {
@@ -262,6 +308,36 @@ const activeCategoryName = () => {
font-size: 13px; font-size: 13px;
} }
/* 对齐顶栏客服/发布账号的主题按钮风格 */
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.toolbar-btn.outline {
border-color: rgba(255, 106, 0, 0.32);
background: #fff8f2;
color: #ff6a00;
}
.toolbar-btn.outline:hover {
border-color: #ff6a00;
background: #fff3e8;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
transform: translateY(-1px);
}
.shop-layout { .shop-layout {
display: grid; display: grid;
grid-template-columns: 140px minmax(0, 1fr); grid-template-columns: 140px minmax(0, 1fr);
@@ -357,6 +433,11 @@ const activeCategoryName = () => {
color: #17233d; color: #17233d;
} }
.load-sentinel {
width: 100%;
height: 1px;
}
.load-state { .load-state {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -364,6 +445,7 @@ const activeCategoryName = () => {
padding: 12px 0 8px; padding: 12px 0 8px;
} }
.loading-more,
.end { .end {
color: #b0b8c4; color: #b0b8c4;
font-size: 12px; font-size: 12px;
@@ -141,7 +141,9 @@ function openChat() {
<h1>{{ statusText }}</h1> <h1>{{ statusText }}</h1>
<p class="sub">订单号 {{ order.order_no }} · {{ formatDateTime(order.created_at) }}</p> <p class="sub">订单号 {{ order.order_no }} · {{ formatDateTime(order.created_at) }}</p>
</div> </div>
<el-button @click="router.push('/mohong/orders')">返回列表</el-button> <button type="button" class="toolbar-btn outline" @click="router.push('/mohong/orders')">
返回列表
</button>
</header> </header>
<div class="layout"> <div class="layout">
@@ -164,7 +166,7 @@ function openChat() {
<div v-if="order.copy_text" class="copy-box"> <div v-if="order.copy_text" class="copy-box">
<div class="copy-head"> <div class="copy-head">
<h3>订单信息发给客服</h3> <h3>订单信息发给客服</h3>
<el-button type="primary" plain size="small" @click="copyText">一键复制</el-button> <button type="button" class="btn outline sm" @click="copyText">一键复制</button>
</div> </div>
<pre>{{ order.copy_text }}</pre> <pre>{{ order.copy_text }}</pre>
</div> </div>
@@ -177,27 +179,38 @@ function openChat() {
<aside class="side-card"> <aside class="side-card">
<h3>操作</h3> <h3>操作</h3>
<el-button <button
v-if="order.status === 'pending_payment'" v-if="order.status === 'pending_payment'"
type="primary" type="button"
color="#ff6a00" class="btn solid"
:loading="acting" :disabled="acting"
@click="handlePay" @click="handlePay"
> >
去支付 {{ acting ? '处理中...' : '去支付' }}
</el-button> </button>
<el-button <button
v-if="order.status === 'pending_payment'" v-if="order.status === 'pending_payment'"
:loading="acting" type="button"
class="btn outline"
:disabled="acting"
@click="handleCancel" @click="handleCancel"
> >
取消订单 取消订单
</el-button> </button>
<el-button v-if="order.conversation_id" type="primary" plain @click="openChat"> <button
v-if="order.conversation_id"
type="button"
class="btn outline"
@click="openChat"
>
进入订单群 进入订单群
</el-button> </button>
<el-button v-if="order.copy_text" plain @click="copyText">复制订单信息</el-button> <button v-if="order.copy_text" type="button" class="btn outline" @click="copyText">
<el-button plain @click="router.push('/mohong')">继续选购</el-button> 复制订单信息
</button>
<button type="button" class="btn outline" @click="router.push('/mohong')">
继续选购
</button>
</aside> </aside>
</div> </div>
</template> </template>
@@ -213,9 +226,11 @@ function openChat() {
<div class="pay-way-options"> <div class="pay-way-options">
<button type="button" class="pay-way-option" @click="choosePayWay('WXZF')"> <button type="button" class="pay-way-option" @click="choosePayWay('WXZF')">
<strong>微信支付</strong> <strong>微信支付</strong>
<small>使用微信扫码完成支付</small>
</button> </button>
<button type="button" class="pay-way-option" @click="choosePayWay('ZFBZF')"> <button type="button" class="pay-way-option" @click="choosePayWay('ZFBZF')">
<strong>支付宝支付</strong> <strong>支付宝支付</strong>
<small>使用支付宝扫码完成支付</small>
</button> </button>
</div> </div>
</el-dialog> </el-dialog>
@@ -225,6 +240,7 @@ function openChat() {
title="订单支付" title="订单支付"
width="520px" width="520px"
append-to-body append-to-body
class="mohong-pay-dialog"
@closed="cashier.stopPaymentPolling" @closed="cashier.stopPaymentPolling"
> >
<div v-if="cashier.activePayment.value" class="pay-dialog-body"> <div v-if="cashier.activePayment.value" class="pay-dialog-body">
@@ -238,13 +254,14 @@ function openChat() {
</div> </div>
</div> </div>
<template #footer> <template #footer>
<el-button <button
type="primary" type="button"
:loading="cashier.checkingPayment.value" class="btn solid"
:disabled="cashier.checkingPayment.value"
@click="cashier.refreshPaymentStatus(false)" @click="cashier.refreshPaymentStatus(false)"
> >
我已支付刷新状态 {{ cashier.checkingPayment.value ? '查询中...' : '我已支付,刷新状态' }}
</el-button> </button>
</template> </template>
</el-dialog> </el-dialog>
</section> </section>
@@ -253,7 +270,6 @@ function openChat() {
<style scoped> <style scoped>
.order-detail-page { .order-detail-page {
width: 100%; width: 100%;
max-width: 1100px;
display: grid; display: grid;
gap: 18px; gap: 18px;
} }
@@ -263,6 +279,7 @@ function openChat() {
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 12px;
width: 100%;
} }
.eyebrow { .eyebrow {
@@ -274,7 +291,8 @@ function openChat() {
.page-header h1 { .page-header h1 {
margin: 0 0 6px; margin: 0 0 6px;
font-size: 28px; font-size: 24px;
font-weight: 800;
color: #17233d; color: #17233d;
} }
@@ -284,11 +302,65 @@ function openChat() {
font-size: 13px; font-size: 13px;
} }
.toolbar-btn,
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.toolbar-btn.outline,
.btn.outline {
border-color: rgba(255, 106, 0, 0.32);
background: #fff8f2;
color: #ff6a00;
}
.toolbar-btn.outline:hover,
.btn.outline:hover:not(:disabled) {
border-color: #ff6a00;
background: #fff3e8;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
}
.btn.solid {
background: #ff6a00;
border-color: #ff6a00;
color: #fff;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.2);
}
.btn.solid:hover:not(:disabled) {
background: #e55d00;
border-color: #e55d00;
}
.btn.sm {
height: 30px;
padding: 0 12px;
font-size: 12px;
}
.btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.layout { .layout {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) 240px; grid-template-columns: minmax(0, 1fr) 280px;
gap: 16px; gap: 16px;
align-items: start; align-items: start;
width: 100%;
} }
.main-card, .main-card,
@@ -388,11 +460,11 @@ function openChat() {
.side-card { .side-card {
display: grid; display: grid;
gap: 10px; gap: 10px;
align-content: start;
} }
.side-card :deep(.el-button) { .side-card .btn {
width: 100%; width: 100%;
margin: 0;
} }
.pay-way-options { .pay-way-options {
@@ -401,6 +473,8 @@ function openChat() {
} }
.pay-way-option { .pay-way-option {
display: grid;
gap: 4px;
width: 100%; width: 100%;
padding: 14px 16px; padding: 14px 16px;
border: 1px solid #e7edf6; border: 1px solid #e7edf6;
@@ -408,6 +482,29 @@ function openChat() {
background: #fff; background: #fff;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
transition:
border-color 0.2s,
background 0.2s;
}
.pay-way-option:hover {
border-color: #ff6a00;
background: #fff8f2;
}
.pay-way-option strong {
color: #17233d;
font-size: 15px;
}
.pay-way-option small {
color: #8a94a6;
font-size: 12px;
}
.mohong-pay-dialog :deep(.el-dialog__footer) {
display: flex;
justify-content: center;
} }
.pay-dialog-body { .pay-dialog-body {
@@ -34,61 +34,152 @@ async function loadOrders() {
<section class="mohong-orders-page"> <section class="mohong-orders-page">
<header class="page-header"> <header class="page-header">
<div> <div>
<p class="eyebrow">My Orders</p>
<h1>摸大红订单</h1> <h1>摸大红订单</h1>
<p class="sub">查看支付状态复制订单信息进入客服群</p>
</div> </div>
<el-button @click="router.push('/mohong')">返回商品列表</el-button> <button type="button" class="toolbar-btn outline" @click="router.push('/mohong')">
返回商品列表
</button>
</header> </header>
<el-table v-loading="loading" :data="orders" border stripe empty-text="暂无订单"> <div class="table-wrap">
<el-table-column prop="order_no" label="订单号" min-width="180" /> <el-table
<el-table-column prop="product_title" label="商品" min-width="160" /> v-loading="loading"
<el-table-column label="数量" width="80" prop="quantity" /> :data="orders"
<el-table-column label="金额" width="110"> border
<template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template> stripe
</el-table-column> empty-text="暂无订单"
<el-table-column label="状态" width="110"> style="width: 100%"
<template #default="{ row }">{{ mohongOrderStatusLabel(row.status) }}</template> >
</el-table-column> <el-table-column prop="order_no" label="订单号" min-width="200" />
<el-table-column label="下单时间" min-width="170"> <el-table-column prop="product_title" label="商品" min-width="180" />
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template> <el-table-column label="数量" width="90" prop="quantity" align="center" />
</el-table-column> <el-table-column label="金额" width="120" align="right">
<el-table-column label="操作" width="120" fixed="right"> <template #default="{ row }">¥{{ row.amount || formatCent(row.amount_cent) }}</template>
<template #default="{ row }"> </el-table-column>
<el-button link type="primary" @click="router.push(`/mohong/orders/${row.id}`)"> <el-table-column label="状态" width="110" align="center">
详情 <template #default="{ row }">
</el-button> <span class="status-tag" :class="row.status">
</template> {{ mohongOrderStatusLabel(row.status) }}
</el-table-column> </span>
</el-table> </template>
</el-table-column>
<el-table-column label="下单时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<button type="button" class="link-btn" @click="router.push(`/mohong/orders/${row.id}`)">
详情
</button>
</template>
</el-table-column>
</el-table>
</div>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.mohong-orders-page { .mohong-orders-page {
display: grid; display: grid;
gap: 18px; gap: 16px;
width: 100%; width: 100%;
max-width: 1200px;
} }
.page-header { .page-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: center;
gap: 12px; gap: 12px;
} width: 100%;
.eyebrow {
margin: 0 0 8px;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
} }
.page-header h1 { .page-header h1 {
margin: 0; margin: 0 0 4px;
font-size: 28px; font-size: 22px;
font-weight: 800;
color: #17233d; color: #17233d;
} }
.sub {
margin: 0;
color: #8a94a6;
font-size: 13px;
}
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 14px;
border-radius: 10px;
border: 1px solid rgba(255, 106, 0, 0.32);
background: #fff8f2;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.toolbar-btn:hover {
border-color: #ff6a00;
background: #fff3e8;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.1);
}
.table-wrap {
width: 100%;
background: #fff;
border: 1px solid #eef1f5;
border-radius: 14px;
overflow: hidden;
}
.status-tag {
display: inline-flex;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #f1f5f9;
color: #64748b;
}
.status-tag.pending_payment {
background: #fff7ed;
color: #ff6a00;
}
.status-tag.paid {
background: #ecfdf5;
color: #059669;
}
.status-tag.completed {
background: #eff6ff;
color: #2563eb;
}
.status-tag.cancelled,
.status-tag.refunded {
background: #f8fafc;
color: #94a3b8;
}
.link-btn {
border: 0;
background: transparent;
color: #ff6a00;
font-size: 13px;
font-weight: 700;
cursor: pointer;
padding: 0;
}
.link-btn:hover {
color: #e55d00;
text-decoration: underline;
}
</style> </style>