feat(miniprogram): 温润东方风全面 UI 重设计

73 文件变更,覆盖全部 40 个页面 SCSS + TabBar 图标 + 组件样式。
统一赤陶主色 #C4623A + 暖米背景 + 衬线标题字体 + 12px 圆角体系。
This commit is contained in:
iven
2026-04-28 00:19:52 +08:00
parent fbb28e655d
commit 50eae8b809
97 changed files with 7633 additions and 2373 deletions

View File

@@ -1,5 +1,35 @@
@import '../../../styles/variables.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@mixin section-title {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px;
font-weight: bold;
color: $tx;
margin-bottom: 20px;
display: block;
}
@mixin tag($bg, $color) {
display: inline-block;
padding: 4px 12px;
border-radius: 8px;
font-size: 20px;
font-weight: 500;
background: $bg;
color: $color;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.detail-page {
min-height: 100vh;
background: $bg;
@@ -8,34 +38,34 @@
/* ===== 余额卡片 ===== */
.balance-card {
background: linear-gradient(135deg, $pri 0%, $pri-d 100%);
background: $card;
margin: 20px 24px 16px;
border-radius: $r-lg;
padding: 32px;
padding-top: 40px;
}
.balance-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
box-shadow: $shadow-sm;
}
.balance-label {
font-size: 26px;
color: rgba(255, 255, 255, 0.85);
font-size: 24px;
color: $tx2;
display: block;
margin-bottom: 8px;
}
.balance-value {
font-size: 56px;
@include serif-number;
font-size: 60px;
font-weight: bold;
color: white;
letter-spacing: 1px;
color: $pri;
display: block;
margin-bottom: 28px;
letter-spacing: -1px;
}
.balance-stats {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.12);
background: $bg;
border-radius: $r;
padding: 20px 0;
}
@@ -48,47 +78,46 @@
}
.stat-value {
@include serif-number;
font-size: 30px;
font-weight: bold;
color: white;
margin-bottom: 4px;
&.green {
color: #A7F3D0;
&.stat-earn {
color: $acc;
}
&.orange {
color: #FDE68A;
&.stat-spend {
color: $wrn;
}
&.gray {
color: rgba(255, 255, 255, 0.6);
&.stat-expired {
color: $tx3;
}
}
.stat-label {
font-size: 22px;
color: rgba(255, 255, 255, 0.7);
color: $tx3;
}
.stat-divider {
width: 1px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
background: $bd;
}
/* ===== 类型筛选标签 ===== */
.type-tabs {
display: flex;
gap: 0;
padding: 20px 24px 0;
background: $card;
padding: 0 24px;
margin-bottom: 16px;
}
.type-tab {
@include flex-center;
flex: 1;
text-align: center;
padding: 16px 0;
position: relative;
@@ -98,7 +127,7 @@
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48px;
width: 40px;
height: 4px;
background: $pri;
border-radius: 2px;
@@ -107,9 +136,9 @@
.type-tab-text {
font-size: 28px;
color: $tx2;
color: $tx3;
&.active {
.type-tab.active & {
color: $pri;
font-weight: bold;
}
@@ -127,45 +156,44 @@
border-radius: $r;
padding: 24px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
box-shadow: $shadow-sm;
}
.tx-icon {
width: 72px;
height: 72px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.tx-badge {
width: 64px;
height: 64px;
border-radius: $r;
@include flex-center;
margin-right: 20px;
flex-shrink: 0;
&.type-earn {
&.tx-badge-earn {
background: $acc-l;
}
&.type-spend {
background: $dan-l;
&.tx-badge-spend {
background: $wrn-l;
}
&.type-expired {
&.tx-badge-expired {
background: $bd-l;
}
}
.tx-icon-text {
font-size: 32px;
.tx-badge-text {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px;
font-weight: bold;
.type-earn & {
.tx-badge-earn & {
color: $acc;
}
.type-spend & {
color: $dan;
.tx-badge-spend & {
color: $wrn;
}
.type-expired & {
.tx-badge-expired & {
color: $tx3;
}
}
@@ -200,20 +228,22 @@
}
.tx-amount {
@include serif-number;
font-size: 32px;
font-weight: bold;
margin-bottom: 4px;
&.positive {
&.tx-amount-positive {
color: $acc;
}
&.negative {
color: $dan;
&.tx-amount-negative {
color: $tx2;
}
}
.tx-remaining {
@include serif-number;
font-size: 20px;
color: $tx3;
}

View File

@@ -13,12 +13,6 @@ const TYPE_TABS = [
{ key: 'spend', label: '支出' },
];
const TYPE_ICONS: Record<string, { icon: string; className: string }> = {
earn: { icon: '↑', className: 'type-earn' },
spend: { icon: '↓', className: 'type-spend' },
expired: { icon: '⏰', className: 'type-expired' },
};
export default function PointsDetail() {
const [account, setAccount] = useState<PointsAccount | null>(null);
const [transactions, setTransactions] = useState<PointsTransaction[]>([]);
@@ -48,7 +42,6 @@ export default function PointsDetail() {
page_size: 10,
});
let list = res.data || [];
// 前端按类型过滤(后端暂不支持 type 参数)
if (type) {
list = list.filter((t) => t.type === type);
}
@@ -99,8 +92,16 @@ export default function PointsDetail() {
fetchTransactions(1, key, true);
};
const getTypeConfig = (type: string) => {
return TYPE_ICONS[type] || { icon: '?', className: 'type-earn' };
const getTypeLabel = (type: string) => {
if (type === 'earn') return '收';
if (type === 'spend') return '支';
return '过';
};
const getTypeClass = (type: string) => {
if (type === 'earn') return 'earn';
if (type === 'spend') return 'spend';
return 'expired';
};
const formatAmount = (tx: PointsTransaction) => {
@@ -122,23 +123,21 @@ export default function PointsDetail() {
<View className='detail-page'>
{/* 余额卡片 */}
<View className='balance-card'>
<View className='balance-row'>
<Text className='balance-label'></Text>
<Text className='balance-value'>{balance.toLocaleString()}</Text>
</View>
<Text className='balance-label'></Text>
<Text className='balance-value'>{balance.toLocaleString()}</Text>
<View className='balance-stats'>
<View className='stat-item'>
<Text className='stat-value green'>{(account?.total_earned ?? 0).toLocaleString()}</Text>
<Text className='stat-value stat-earn'>{(account?.total_earned ?? 0).toLocaleString()}</Text>
<Text className='stat-label'></Text>
</View>
<View className='stat-divider' />
<View className='stat-item'>
<Text className='stat-value orange'>{(account?.total_spent ?? 0).toLocaleString()}</Text>
<Text className='stat-value stat-spend'>{(account?.total_spent ?? 0).toLocaleString()}</Text>
<Text className='stat-label'></Text>
</View>
<View className='stat-divider' />
<View className='stat-item'>
<Text className='stat-value gray'>{(account?.total_expired ?? 0).toLocaleString()}</Text>
<Text className='stat-value stat-expired'>{(account?.total_expired ?? 0).toLocaleString()}</Text>
<Text className='stat-label'></Text>
</View>
</View>
@@ -152,26 +151,21 @@ export default function PointsDetail() {
className={`type-tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text
className={`type-tab-text ${activeTab === tab.key ? 'active' : ''}`}
>
{tab.label}
</Text>
<Text className='type-tab-text'>{tab.label}</Text>
</View>
))}
</View>
{/* 交易列表 */}
{transactions.length === 0 && !loading ? (
<EmptyState icon='📊' text='暂无积分记录' hint='签到或兑换后将显示记录' />
<EmptyState icon='' text='暂无积分记录' hint='签到或兑换后将显示记录' />
) : (
<View className='transaction-list'>
{transactions.map((tx) => {
const typeCfg = getTypeConfig(tx.type);
return (
<View className='transaction-item' key={tx.id}>
<View className={`tx-icon ${typeCfg.className}`}>
<Text className='tx-icon-text'>{typeCfg.icon}</Text>
<View className={`tx-badge tx-badge-${getTypeClass(tx.type)}`}>
<Text className='tx-badge-text'>{getTypeLabel(tx.type)}</Text>
</View>
<View className='tx-info'>
<Text className='tx-desc'>
@@ -180,7 +174,7 @@ export default function PointsDetail() {
<Text className='tx-date'>{formatDate(tx.created_at)}</Text>
</View>
<View className='tx-amount-col'>
<Text className={`tx-amount ${tx.type === 'earn' ? 'positive' : 'negative'}`}>
<Text className={`tx-amount tx-amount-${tx.type === 'earn' ? 'positive' : 'negative'}`}>
{formatAmount(tx)}
</Text>
<Text className='tx-remaining'> {tx.balance_after.toLocaleString()}</Text>

View File

@@ -1,5 +1,35 @@
@import '../../../styles/variables.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@mixin section-title {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px;
font-weight: bold;
color: $tx;
margin-bottom: 20px;
display: block;
}
@mixin tag($bg, $color) {
display: inline-block;
padding: 4px 12px;
border-radius: 8px;
font-size: 20px;
font-weight: 500;
background: $bg;
color: $color;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.exchange-page {
min-height: 100vh;
background: $bg;
@@ -7,58 +37,69 @@
}
/* ===== 商品预览 ===== */
.product-preview {
.product-card {
display: flex;
align-items: center;
padding: 32px 24px;
background: $card;
margin-bottom: 16px;
margin: 20px 24px 16px;
border-radius: $r-lg;
box-shadow: $shadow-sm;
}
.preview-image {
width: 160px;
height: 160px;
.product-icon-wrap {
width: 128px;
height: 128px;
border-radius: $r;
display: flex;
align-items: center;
justify-content: center;
@include flex-center;
margin-right: 24px;
flex-shrink: 0;
}
.preview-icon {
font-size: 64px;
.product-icon-char {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 52px;
font-weight: bold;
color: #FFFFFF;
}
.preview-info {
.product-meta {
flex: 1;
min-width: 0;
}
.preview-name {
.product-name {
font-size: 32px;
font-weight: bold;
color: $tx;
display: block;
margin-bottom: 8px;
margin-bottom: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-type {
font-size: 24px;
color: $tx3;
display: block;
.product-type-tag {
@include tag($pri-l, $pri-d);
}
/* ===== 兑换详情 ===== */
.exchange-detail {
background: $card;
/* ===== 兑换明细 ===== */
.detail-section {
padding: 0 24px;
margin-bottom: 16px;
}
.detail-section-title {
@include section-title;
}
.detail-card {
background: $card;
border-radius: $r;
box-shadow: $shadow-sm;
padding: 0 24px;
}
.detail-row {
display: flex;
justify-content: space-between;
@@ -66,7 +107,7 @@
padding: 24px 0;
border-bottom: 1px solid $bd-l;
&:last-child {
&.last {
border-bottom: none;
}
}
@@ -77,35 +118,37 @@
}
.detail-value {
@include serif-number;
font-size: 28px;
color: $tx;
font-weight: bold;
&.cost {
color: $wrn;
font-size: 32px;
&.detail-cost {
color: $pri;
font-size: 34px;
}
&.sufficient {
&.detail-sufficient {
color: $acc;
}
&.insufficient {
&.detail-insufficient {
color: $dan;
}
}
/* ===== 温馨提示 ===== */
.exchange-notice {
.notice-section {
background: $card;
padding: 24px;
margin: 0 24px;
border-radius: $r;
box-shadow: $shadow-sm;
}
.notice-title {
@include section-title;
font-size: 28px;
font-weight: bold;
color: $tx;
display: block;
margin-bottom: 12px;
}
@@ -113,7 +156,7 @@
font-size: 24px;
color: $tx3;
display: block;
line-height: 1.6;
line-height: 1.7;
margin-bottom: 4px;
}
@@ -128,7 +171,7 @@
padding: 16px 24px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
background: $card;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
box-shadow: 0 -2px 12px rgba(45, 42, 38, 0.06);
z-index: 10;
}
@@ -143,31 +186,28 @@
color: $tx3;
}
.footer-cost-value {
display: flex;
align-items: center;
gap: 4px;
}
.footer-cost-icon {
font-size: 24px;
}
.footer-cost-num {
font-size: 36px;
@include serif-number;
font-size: 38px;
font-weight: bold;
color: $wrn;
color: $pri;
}
.footer-cost-unit {
font-size: 22px;
color: $tx2;
margin-left: 4px;
}
.confirm-btn {
background: $pri;
padding: 20px 48px;
border-radius: $r;
border-radius: $r-pill;
transition: opacity 0.2s;
&.disabled {
background: $tx3;
opacity: 0.6;
background: $bd;
opacity: 0.7;
}
}

View File

@@ -10,10 +10,22 @@ import type { PointsAccount, PointsProduct } from '../../../services/points';
import Loading from '../../../components/Loading';
import './index.scss';
const TYPE_ICONS: Record<string, string> = {
physical: '📦',
service: '🎫',
privilege: '👑',
const TYPE_INITIAL: Record<string, string> = {
physical: '',
service: '',
privilege: '',
};
const TYPE_LABEL: Record<string, string> = {
physical: '实物商品',
service: '服务券',
privilege: '权益卡',
};
const TYPE_COLOR: Record<string, string> = {
physical: '#5B7A5E',
service: '#C4623A',
privilege: '#8B3E1F',
};
export default function ExchangeConfirm() {
@@ -81,7 +93,6 @@ export default function ExchangeConfirm() {
const order = await exchangeProduct(product.id);
Taro.showToast({ title: '兑换成功', icon: 'success', duration: 2000 });
// 展示核销码弹窗
setTimeout(() => {
Taro.showModal({
title: '兑换成功',
@@ -115,62 +126,62 @@ export default function ExchangeConfirm() {
);
}
const productType = product?.product_type || 'physical';
const initial = TYPE_INITIAL[productType] || '礼';
const typeLabel = TYPE_LABEL[productType] || '商品';
const typeColor = TYPE_COLOR[productType] || '#C4623A';
return (
<View className='exchange-page'>
{/* 商品信息卡片 */}
<View className='product-preview'>
{/* 商品预览卡片 */}
<View className='product-card'>
<View
className='preview-image'
style={{ backgroundColor: '#0891B2' }}
className='product-icon-wrap'
style={{ backgroundColor: typeColor }}
>
<Text className='preview-icon'>
{product ? TYPE_ICONS[product.product_type] || '🎁' : '🎁'}
</Text>
<Text className='product-icon-char'>{initial}</Text>
</View>
<View className='preview-info'>
<Text className='preview-name'>{product?.name || ''}</Text>
<Text className='preview-type'>
{product?.product_type === 'physical'
? '实物商品'
: product?.product_type === 'service'
? '服务券'
: '权益卡'}
</Text>
<View className='product-meta'>
<Text className='product-name'>{product?.name || ''}</Text>
<Text className='product-type-tag'>{typeLabel}</Text>
</View>
</View>
{/* 兑换详情 */}
<View className='exchange-detail'>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value cost'>{cost.toLocaleString()}</Text>
</View>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text
className={`detail-value ${insufficient ? 'insufficient' : 'sufficient'}`}
>
{balance.toLocaleString()}
</Text>
</View>
{insufficient && (
{/* 兑换明细 */}
<View className='detail-section'>
<Text className='detail-section-title'></Text>
<View className='detail-card'>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value insufficient'>
-{(cost - balance).toLocaleString()}
<Text className='detail-label'></Text>
<Text className='detail-value detail-cost'>{cost.toLocaleString()}</Text>
</View>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text
className={`detail-value ${insufficient ? 'detail-insufficient' : 'detail-sufficient'}`}
>
{balance.toLocaleString()}
</Text>
</View>
{insufficient && (
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value detail-insufficient'>
-{(cost - balance).toLocaleString()}
</Text>
</View>
)}
<View className='detail-row last'>
<Text className='detail-label'></Text>
<Text className='detail-value'>
{product && product.stock > 0 ? `剩余 ${product.stock}` : '已兑完'}
</Text>
</View>
)}
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value'>
{product && product.stock > 0 ? `剩余 ${product.stock}` : '已兑完'}
</Text>
</View>
</View>
{/* 温馨提示 */}
<View className='exchange-notice'>
<View className='notice-section'>
<Text className='notice-title'></Text>
<Text className='notice-text'>
@@ -182,10 +193,8 @@ export default function ExchangeConfirm() {
<View className='exchange-footer'>
<View className='footer-cost'>
<Text className='footer-cost-label'></Text>
<View className='footer-cost-value'>
<Text className='footer-cost-icon'>🪙</Text>
<Text className='footer-cost-num'>{cost.toLocaleString()}</Text>
</View>
<Text className='footer-cost-num'>{cost.toLocaleString()}</Text>
<Text className='footer-cost-unit'></Text>
</View>
<View
className={`confirm-btn ${insufficient || (product?.stock ?? 0) <= 0 || submitting ? 'disabled' : ''}`}

View File

@@ -1,16 +1,16 @@
@import '../../styles/variables.scss';
@import '../../styles/mixins.scss';
.mall-page {
min-height: 100vh;
background: $bg;
padding-bottom: 40px;
padding-bottom: calc(120px + env(safe-area-inset-bottom));
}
/* ===== 积分余额卡片 ===== */
/* ─── 积分余额卡片 ─── */
.mall-header {
background: linear-gradient(135deg, $pri 0%, $pri-d 100%);
padding: 32px;
padding-top: 48px;
padding: 48px 32px 36px;
}
.points-card {
@@ -20,7 +20,7 @@
padding: 32px;
}
.points-card-top {
.points-top {
display: flex;
justify-content: space-between;
align-items: center;
@@ -36,9 +36,13 @@
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.4);
padding: 10px 28px;
border-radius: 32px;
border-radius: $r-pill;
transition: all 0.2s;
&:active {
opacity: 0.8;
}
&.checked {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
@@ -47,8 +51,8 @@
.checkin-btn-text {
font-size: 24px;
color: white;
font-weight: bold;
color: #fff;
font-weight: 600;
}
.checkin-btn.checked .checkin-btn-text {
@@ -56,12 +60,14 @@
}
.points-balance {
@include serif-number;
font-size: 72px;
font-weight: bold;
color: white;
color: #fff;
display: block;
margin-bottom: 8px;
letter-spacing: 2px;
line-height: 1;
}
.points-streak {
@@ -70,12 +76,10 @@
display: block;
}
/* ===== 商品类型切换 ===== */
/* ─── 商品类型切换 ─── */
.type-tabs {
display: flex;
gap: 0;
padding: 20px 24px 0;
background: transparent;
}
.type-tab {
@@ -103,15 +107,15 @@
&.active {
color: $pri;
font-weight: bold;
font-weight: 600;
}
}
/* ===== 商品网格 ===== */
/* ─── 商品网格 ─── */
.product-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
gap: 16px;
padding: 20px 24px;
}
@@ -119,19 +123,32 @@
background: $card;
border-radius: $r;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
box-shadow: $shadow-sm;
&:active {
opacity: 0.7;
}
}
.product-image {
width: 100%;
height: 240px;
display: flex;
align-items: center;
justify-content: center;
height: 200px;
@include flex-center;
&.type-physical { background: $pri-l; }
&.type-service { background: $acc-l; }
&.type-privilege { background: $wrn-l; }
}
.product-image-icon {
font-size: 64px;
.product-image-char {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 56px;
font-weight: bold;
color: $pri;
line-height: 1;
.type-service & { color: $acc; }
.type-privilege & { color: $wrn; }
}
.product-info {
@@ -140,7 +157,7 @@
.product-name {
font-size: 26px;
font-weight: bold;
font-weight: 600;
color: $tx;
display: block;
margin-bottom: 12px;
@@ -161,11 +178,15 @@
gap: 4px;
}
.product-points-icon {
.product-points-char {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 22px;
font-weight: bold;
color: $wrn;
}
.product-points-value {
@include serif-number;
font-size: 28px;
font-weight: bold;
color: $wrn;
@@ -174,15 +195,69 @@
.product-stock {
font-size: 20px;
padding: 2px 10px;
border-radius: 8px;
border-radius: $r-sm;
&.out {
color: $tx3;
background: $bd-l;
@include tag($bd-l, $tx3);
}
&.low {
color: $dan;
background: $dan-l;
@include tag($dan-l, $dan);
}
}
/* ─── 空状态 ─── */
.mall-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160px 40px;
}
.empty-icon {
width: 120px;
height: 120px;
border-radius: 50%;
background: $pri-l;
@include flex-center;
margin-bottom: 32px;
}
.empty-char {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 52px;
font-weight: bold;
color: $pri;
line-height: 1;
}
.empty-title {
font-size: 32px;
font-weight: 600;
color: $tx;
margin-bottom: 12px;
}
.empty-hint {
font-size: 26px;
color: $tx3;
text-align: center;
margin-bottom: 24px;
}
.empty-action {
background: $pri;
border-radius: $r;
padding: 16px 48px;
&:active {
opacity: 0.85;
}
}
.empty-action-text {
font-size: 28px;
color: #fff;
font-weight: 600;
}

View File

@@ -9,21 +9,20 @@ import {
} from '../../services/points';
import type { PointsAccount, PointsProduct, CheckinStatus } from '../../services/points';
import { useAuthStore } from '../../stores/auth';
import EmptyState from '../../components/EmptyState';
import Loading from '../../components/Loading';
import './index.scss';
const PRODUCT_TYPE_TABS = [
{ key: '', label: '全部' },
{ key: 'physical', label: '实物' },
{ key: 'service', label: '服务券' },
{ key: 'privilege', label: '权益' },
{ key: 'physical', label: '实物', char: '物' },
{ key: 'service', label: '服务券', char: '券' },
{ key: 'privilege', label: '权益', char: '权' },
];
const TYPE_COLORS: Record<string, string> = {
physical: '#0891B2',
service: '#059669',
privilege: '#D97706',
const TYPE_BG: Record<string, string> = {
physical: 'type-physical',
service: 'type-service',
privilege: 'type-privilege',
};
export default function Mall() {
@@ -117,14 +116,9 @@ export default function Mall() {
try {
const result = await dailyCheckin();
setCheckinStatus(result);
// 刷新积分余额
const acct = await getAccount();
setAccount(acct);
Taro.showToast({
title: '签到成功',
icon: 'success',
duration: 2000,
});
Taro.showToast({ title: '签到成功', icon: 'success', duration: 2000 });
} catch (err) {
Taro.showToast({
title: err instanceof Error ? err.message : '签到失败',
@@ -150,40 +144,36 @@ export default function Mall() {
const balance = account?.balance ?? 0;
if (noProfile) {
return (
<View className='mall-page'>
<View className='mall-empty-state'>
<View className='empty-icon'>
<Text className='empty-char'></Text>
</View>
<Text className='empty-title'></Text>
<Text className='empty-hint'>使</Text>
<View className='empty-action' onClick={() => Taro.navigateTo({ url: '/pages/profile/family-add/index' })}>
<Text className='empty-action-text'></Text>
</View>
</View>
</View>
);
}
return (
<View className='mall-page'>
{/* 未关联患者档案时显示引导 */}
{noProfile && (
<View className='mall-page'>
<EmptyState
icon='👤'
text='请先完善个人档案'
hint='建档后即可使用积分商城、签到等功能'
actionText='去建档'
onAction={() => Taro.navigateTo({ url: '/pages/profile/family-add/index' })}
/>
</View>
)}
{!noProfile && (
<>
{/* 积分余额卡片 */}
<View className='mall-header'>
<View className='points-card'>
<View className='points-card-top'>
<View className='points-top'>
<Text className='points-label'></Text>
<View
className={`checkin-btn ${
checkinStatus?.checked_in_today ? 'checked' : ''
}`}
className={`checkin-btn ${checkinStatus?.checked_in_today ? 'checked' : ''}`}
onClick={handleCheckin}
>
<Text className='checkin-btn-text'>
{checkinLoading
? '...'
: checkinStatus?.checked_in_today
? '已签到'
: '签到'}
{checkinLoading ? '...' : checkinStatus?.checked_in_today ? '已签到' : '签到'}
</Text>
</View>
</View>
@@ -204,11 +194,7 @@ export default function Mall() {
className={`type-tab ${productType === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text
className={`type-tab-text ${
productType === tab.key ? 'active' : ''
}`}
>
<Text className={`type-tab-text ${productType === tab.key ? 'active' : ''}`}>
{tab.label}
</Text>
</View>
@@ -217,35 +203,28 @@ export default function Mall() {
{/* 商品列表 */}
{products.length === 0 && !loading ? (
<EmptyState
icon='🎁'
text='暂无商品'
hint='更多好物即将上架'
/>
<View className='mall-empty-state'>
<View className='empty-icon'>
<Text className='empty-char'></Text>
</View>
<Text className='empty-title'></Text>
<Text className='empty-hint'></Text>
</View>
) : (
<View className='product-grid'>
{products.map((item) => (
<View className='product-card' key={item.id} onClick={() => handleProductClick(item)}>
<View
className='product-image'
style={{ backgroundColor: TYPE_COLORS[item.product_type] || '#94A3B8' }}
>
<Text className='product-image-icon'>
{item.product_type === 'physical'
? '📦'
: item.product_type === 'service'
? '🎫'
: '👑'}
<View className={`product-image ${TYPE_BG[item.product_type] || ''}`}>
<Text className='product-image-char'>
{item.product_type === 'physical' ? '物' : item.product_type === 'service' ? '券' : '权'}
</Text>
</View>
<View className='product-info'>
<Text className='product-name'>{item.name}</Text>
<View className='product-bottom'>
<View className='product-points'>
<Text className='product-points-icon'>🪙</Text>
<Text className='product-points-value'>
{item.points_cost}
</Text>
<Text className='product-points-char'>P</Text>
<Text className='product-points-value'>{item.points_cost}</Text>
</View>
{item.stock <= 0 ? (
<Text className='product-stock out'></Text>
@@ -262,8 +241,6 @@ export default function Mall() {
)}
</View>
)}
</>
)}
</View>
);
}

View File

@@ -1,5 +1,35 @@
@import '../../../styles/variables.scss';
@mixin serif-number {
font-family: 'Georgia', 'Times New Roman', serif;
font-variant-numeric: tabular-nums;
}
@mixin section-title {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 30px;
font-weight: bold;
color: $tx;
margin-bottom: 20px;
display: block;
}
@mixin tag($bg, $color) {
display: inline-block;
padding: 4px 12px;
border-radius: 8px;
font-size: 20px;
font-weight: 500;
background: $bg;
color: $color;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.orders-page {
min-height: 100vh;
background: $bg;
@@ -13,11 +43,12 @@
padding: 20px 24px 0;
background: $card;
margin-bottom: 16px;
border-radius: 0 0 $r-lg $r-lg;
}
.status-tab {
flex: 1;
text-align: center;
@include flex-center;
padding: 16px 0;
position: relative;
@@ -27,7 +58,7 @@
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48px;
width: 40px;
height: 4px;
background: $pri;
border-radius: 2px;
@@ -36,9 +67,9 @@
.status-tab-text {
font-size: 28px;
color: $tx2;
color: $tx3;
&.active {
.status-tab.active & {
color: $pri;
font-weight: bold;
}
@@ -54,7 +85,7 @@
border-radius: $r;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
box-shadow: $shadow-sm;
}
.order-header {
@@ -66,6 +97,7 @@
}
.order-product {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px;
font-weight: bold;
color: $tx;
@@ -75,43 +107,12 @@
white-space: nowrap;
}
.order-status {
.order-status-tag {
@include tag(transparent, $tx3);
padding: 4px 16px;
border-radius: 20px;
border-radius: $r-pill;
margin-left: 12px;
flex-shrink: 0;
&.status-pending {
background: $wrn-l;
.order-status-text {
color: $wrn;
}
}
&.status-verified {
background: $acc-l;
.order-status-text {
color: $acc;
}
}
&.status-cancelled {
background: $dan-l;
.order-status-text {
color: $dan;
}
}
&.status-expired {
background: $bd-l;
.order-status-text {
color: $tx3;
}
}
}
.order-status-text {
@@ -136,11 +137,12 @@
}
.order-row-value {
@include serif-number;
font-size: 26px;
color: $tx;
&.cost {
color: $wrn;
&.order-cost {
color: $pri;
font-weight: bold;
}
}
@@ -158,11 +160,13 @@
.qrcode-label {
font-size: 24px;
color: $tx3;
margin-right: 8px;
}
.qrcode-value {
@include serif-number;
font-size: 24px;
color: $pri;
color: $pri-d;
font-weight: bold;
flex: 1;
overflow: hidden;

View File

@@ -14,11 +14,11 @@ const STATUS_TABS = [
{ key: 'expired', label: '已过期' },
];
const STATUS_CONFIG: Record<string, { label: string; className: string }> = {
pending: { label: '待核销', className: 'status-pending' },
verified: { label: '已核销', className: 'status-verified' },
cancelled: { label: '已取消', className: 'status-cancelled' },
expired: { label: '已过期', className: 'status-expired' },
const STATUS_CONFIG: Record<string, { label: string; tagBg: string; tagColor: string }> = {
pending: { label: '待核销', tagBg: '#FFF3E0', tagColor: '#C4873A' },
verified: { label: '已核销', tagBg: '#E8F0E8', tagColor: '#5B7A5E' },
cancelled: { label: '已取消', tagBg: '#FDEAEA', tagColor: '#B54A4A' },
expired: { label: '已过期', tagBg: '#F0EBE5', tagColor: '#A8A29E' },
};
export default function MallOrders() {
@@ -40,7 +40,6 @@ export default function MallOrders() {
page_size: 10,
});
let list = res.data || [];
// 前端按状态过滤(后端暂不支持 status 参数)
if (status) {
list = list.filter((o) => o.status === status);
}
@@ -101,7 +100,7 @@ export default function MallOrders() {
};
const getStatusConfig = (status: string) => {
return STATUS_CONFIG[status] || { label: status, className: 'status-pending' };
return STATUS_CONFIG[status] || { label: status, tagBg: '#F0EBE5', tagColor: '#A8A29E' };
};
const formatDate = (dateStr: string) => {
@@ -120,11 +119,7 @@ export default function MallOrders() {
className={`status-tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text
className={`status-tab-text ${activeTab === tab.key ? 'active' : ''}`}
>
{tab.label}
</Text>
<Text className='status-tab-text'>{tab.label}</Text>
</View>
))}
</View>
@@ -132,7 +127,7 @@ export default function MallOrders() {
{/* 订单列表 */}
{orders.length === 0 && !loading ? (
<EmptyState
icon='📋'
icon=''
text='暂无订单'
hint='去商城兑换心仪商品吧'
actionText='去商城'
@@ -146,7 +141,10 @@ export default function MallOrders() {
<View className='order-card' key={order.id}>
<View className='order-header'>
<Text className='order-product'> {order.product_id.slice(0, 8)}</Text>
<View className={`order-status ${statusCfg.className}`}>
<View
className='order-status-tag'
style={{ background: statusCfg.tagBg, color: statusCfg.tagColor }}
>
<Text className='order-status-text'>{statusCfg.label}</Text>
</View>
</View>
@@ -154,8 +152,8 @@ export default function MallOrders() {
<View className='order-body'>
<View className='order-row'>
<Text className='order-row-label'></Text>
<Text className='order-row-value cost'>
🪙 {order.points_cost.toLocaleString()}
<Text className='order-row-value order-cost'>
{order.points_cost.toLocaleString()}
</Text>
</View>
<View className='order-row'>
@@ -166,9 +164,9 @@ export default function MallOrders() {
</View>
{order.status === 'pending' && (
<View className='order-qrcode' onClick={() => handleShowQrCode(order.qr_code)}>
<Text className='qrcode-label'>: </Text>
<Text className='qrcode-label'></Text>
<Text className='qrcode-value'>{order.qr_code}</Text>
<Text className='qrcode-tap'></Text>
<Text className='qrcode-tap'></Text>
</View>
)}
</View>