feat(commission): 收益中心新增30天保活状态与秒级动态预警倒计时看板

这个提交包含在:
chen
2026-09-17 20:06:38 +08:00
父节点 29aeb63026
当前提交 7fc169fb6e
共有 3 个文件被更改,包括 673 次插入 和 32 次删除

查看文件

@@ -111,6 +111,35 @@ class IdentityService
}
$available = max(0, (float) $released - (float) $used);
$windowDays = PartnerConfig::activeDays();
$isActive = (new ActiveService())->isActive($agent);
$now = time();
$lastTime = $last ? intval($last) : 0;
$expireTime = 0;
$remainingSeconds = 0;
$daysRemaining = 0;
$activeStatus = 'inactive';
if ($isActive) {
if ($lastTime > 0) {
$orderDay = strtotime(date('Y-m-d 00:00:00', $lastTime));
$expireTime = $orderDay + $windowDays * 86400;
$remainingSeconds = max(0, $expireTime - $now);
$daysRemaining = intval(ceil($remainingSeconds / 86400));
$activeStatus = ($daysRemaining <= 5) ? 'expiring' : 'active';
} else {
$activeStatus = 'active';
$daysRemaining = $windowDays;
$expireTime = $now + $windowDays * 86400;
$remainingSeconds = $windowDays * 86400;
}
} else {
$activeStatus = 'inactive';
$daysRemaining = 0;
$remainingSeconds = 0;
}
$data = is_object($agent) ? $agent->toArray() : $agent;
return array_merge($data, $identity, [
'is_venue' => intval($data['is_venue'] ?? 0),
@@ -124,8 +153,12 @@ class IdentityService
'voucher_released' => self::formatMoney($released),
'voucher_used' => self::formatMoney($used),
'voucher_available' => self::formatMoney($available),
'active_30d' => (new ActiveService())->isActive($agent),
'last_self_order_time' => $last ? intval($last) : 0,
'active_30d' => $isActive,
'active_status' => $activeStatus,
'days_remaining' => $daysRemaining,
'expire_time' => $expireTime,
'expire_seconds' => $remainingSeconds,
'last_self_order_time' => $lastTime,
]);
}
@@ -147,6 +180,11 @@ class IdentityService
'madow_value' => '0.00',
'voucher_available' => '0.00',
'active_30d' => true,
'active_status' => 'inactive',
'days_remaining' => 0,
'expire_time' => 0,
'expire_seconds' => 0,
'last_self_order_time' => 0,
'agent_form' => self::agentFormEnabled(),
'partner_mode' => PartnerConfig::isPartnerMode(),
];
@@ -166,6 +204,11 @@ class IdentityService
'madow_value' => $full['madow_value'],
'voucher_available' => $full['voucher_available'],
'active_30d' => $full['active_30d'],
'active_status' => $full['active_status'] ?? 'inactive',
'days_remaining' => $full['days_remaining'] ?? 0,
'expire_time' => $full['expire_time'] ?? 0,
'expire_seconds' => $full['expire_seconds'] ?? 0,
'last_self_order_time' => $full['last_self_order_time'] ?? 0,
'agent_form' => self::agentFormEnabled(),
'partner_mode' => PartnerConfig::isPartnerMode(),
];

查看文件

@@ -0,0 +1,210 @@
/**
* 30天收益保活倒计时看板与状态机综合测试用例
* 覆盖:
* 1. 后端 IdentityService 保活到期时间与状态判定(正常活跃、临期预警、奖励暂停)
* 2. 前端 30 天秒级倒计时时分秒换算逻辑
* 3. 前端 index.vue 模板节点及样式定义完整性
* 4. mp-weixin 编译产物 index.wxml / index.wxss / index.js 完整性
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
let passCount = 0;
let failCount = 0;
function it(desc, fn) {
try {
fn();
console.log(` [PASS] ${desc}`);
passCount++;
} catch (err) {
console.error(` [FAIL] ${desc}`);
console.error(` Error: ${err.message}`);
failCount++;
}
}
console.log('========================================================');
console.log(' 30天收益保活倒计时看板与状态机覆盖性测试');
console.log('========================================================\n');
// 1. 测试 IdentityService.php
const identityServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/IdentityService.php');
const identityContent = fs.readFileSync(identityServicePath, 'utf8');
it('IdentityService.php 存在并包含 active_status 字段逻辑', () => {
assert(fs.existsSync(identityServicePath), 'IdentityService.php 不存在');
assert(identityContent.includes("'active_status'"), "未在 attachAgent 中计算 'active_status'");
});
it('IdentityService.php 正确区分 active / expiring / inactive 三种状态', () => {
assert(identityContent.includes("'inactive'"), "缺少 inactive 状态判定");
assert(identityContent.includes("'expiring'"), "缺少 expiring 状态判定");
assert(identityContent.includes("'active'"), "缺少 active 状态判定");
});
it('IdentityService.php 导出 expire_time / expire_seconds / days_remaining', () => {
assert(identityContent.includes("'expire_time'"), "缺少 expire_time");
assert(identityContent.includes("'expire_seconds'"), "缺少 expire_seconds");
assert(identityContent.includes("'days_remaining'"), "缺少 days_remaining");
});
// 2. 模拟前端状态计算与倒计时逻辑
function calculateActiveState(agent, user, nowSec) {
const lastTime = Number(agent.last_self_order_time || user.last_self_order_time || 0);
const isExplicitInactive = agent.active_30d === false || user.active_30d === false || agent.is_active_30d === false || user.is_active_30d === false;
if (isExplicitInactive || (lastTime <= 0 && (agent.active_30d === false || user.active_30d === false))) {
return {
status: 'inactive',
title: '收益奖励已暂停',
badge: '暂缓计奖',
btnText: '去选购恢复'
};
}
let expireTime = Number(agent.expire_time || 0);
if (!expireTime) {
expireTime = lastTime > 0 ? (lastTime + 30 * 86400) : (nowSec + 30 * 86400);
}
const remainingSeconds = Math.max(0, expireTime - nowSec);
const remainingDays = Math.ceil(remainingSeconds / 86400);
if (remainingSeconds <= 0) {
return {
status: 'inactive',
title: '收益奖励已暂停',
badge: '暂缓计奖',
btnText: '去选购恢复'
};
}
if (remainingDays <= 5) {
return {
status: 'expiring',
title: '30天保活临期预警',
badge: `仅剩 ${remainingDays} 天`,
btnText: '立即自购保活'
};
}
return {
status: 'active',
title: '30天收益资格生效中',
badge: '正常活跃',
btnText: '去自购续期'
};
}
function calculateCountdown(expireTime, nowSec) {
const remaining = Math.max(0, expireTime - nowSec);
const d = Math.floor(remaining / 86400);
const h = Math.floor((remaining % 86400) / 3600);
const m = Math.floor((remaining % 3600) / 60);
const s = remaining % 60;
return {
days: d < 10 ? '0' + d : String(d),
hours: h < 10 ? '0' + h : String(h),
minutes: m < 10 ? '0' + m : String(m),
seconds: s < 10 ? '0' + s : String(s),
totalSeconds: remaining
};
}
// 3. 正常场景:自购在第 2 天(剩余 28 天) -> 正常活跃
it('正常场景:剩余28天 -> 状态为 active 且倒计时天数格式为 28', () => {
const now = 1700000000;
const expire = now + 28 * 86400 + 3600 * 5 + 60 * 12 + 30; // 28天5小时12分30秒
const state = calculateActiveState({ expire_time: expire, last_self_order_time: now - 2 * 86400 }, {}, now);
const cd = calculateCountdown(expire, now);
assert.strictEqual(state.status, 'active');
assert.strictEqual(state.badge, '正常活跃');
assert.strictEqual(cd.days, '28');
assert.strictEqual(cd.hours, '05');
assert.strictEqual(cd.minutes, '12');
assert.strictEqual(cd.seconds, '30');
});
// 4. 临期场景:剩余 3 天(<= 5 天) -> 临期预警
it('临期预警场景:剩余3天 -> 状态为 expiring,提示立即自购保活', () => {
const now = 1700000000;
const expire = now + 3 * 86400 + 3600 * 2 + 60 * 1 + 5;
const state = calculateActiveState({ expire_time: expire, last_self_order_time: now - 27 * 86400 }, {}, now);
const cd = calculateCountdown(expire, now);
assert.strictEqual(state.status, 'expiring');
assert.strictEqual(state.badge, '仅剩 4 天'); // ceil
assert.strictEqual(state.btnText, '立即自购保活');
assert.strictEqual(cd.days, '03');
assert.strictEqual(cd.hours, '02');
assert.strictEqual(cd.minutes, '01');
assert.strictEqual(cd.seconds, '05');
});
// 5. 业务失败/过期场景:超过30天未自购 -> 奖励暂停
it('过期场景:剩余时间为0 -> 状态为 inactive,按钮为去选购恢复', () => {
const now = 1700000000;
const expire = now - 100;
const state = calculateActiveState({ expire_time: expire, last_self_order_time: now - 31 * 86400 }, {}, now);
const cd = calculateCountdown(expire, now);
assert.strictEqual(state.status, 'inactive');
assert.strictEqual(state.title, '收益奖励已暂停');
assert.strictEqual(state.btnText, '去选购恢复');
assert.strictEqual(cd.totalSeconds, 0);
assert.strictEqual(cd.days, '00');
});
// 6. 前端源码 index.vue 结构检查
const indexVuePath = path.resolve(__dirname, '../uniapp/pages/commission/index.vue');
const indexVueContent = fs.readFileSync(indexVuePath, 'utf8');
it('uniapp/pages/commission/index.vue 包含 active-countdown-card 卡片', () => {
assert(indexVueContent.includes('active-countdown-card'), '未找到 active-countdown-card 类');
assert(indexVueContent.includes('countdown.days'), '未找到倒计时天数绑定');
assert(indexVueContent.includes('countdown.hours'), '未找到倒计时小时绑定');
assert(indexVueContent.includes('countdown.minutes'), '未找到倒计时分钟绑定');
assert(indexVueContent.includes('countdown.seconds'), '未找到倒计时秒数绑定');
});
it('uniapp/pages/commission/index.vue 包含定时器生命周期管理', () => {
assert(indexVueContent.includes('setInterval(updateCountdown, 1000)'), '缺少 1 秒定时器');
assert(indexVueContent.includes('clearInterval(timer)'), '缺少定时器清理');
assert(indexVueContent.includes('onShow'), '缺少 onShow 挂载');
assert(indexVueContent.includes('onHide'), '缺少 onHide 清理');
assert(indexVueContent.includes('onUnload'), '缺少 onUnload 清理');
});
// 7. 微信小程序已编译产物检查
const wxmlPath = path.resolve(__dirname, '../uniapp/unpackage/dist/dev/mp-weixin/pages/commission/index.wxml');
const wxssPath = path.resolve(__dirname, '../uniapp/unpackage/dist/dev/mp-weixin/pages/commission/index.wxss');
const jsPath = path.resolve(__dirname, '../uniapp/unpackage/dist/dev/mp-weixin/pages/commission/index.js');
it('mp-weixin 产物完整且包含 active-countdown-card 节点与样式', () => {
const wxml = fs.readFileSync(wxmlPath, 'utf8');
const wxss = fs.readFileSync(wxssPath, 'utf8');
const js = fs.readFileSync(jsPath, 'utf8');
assert(wxml.includes('active-countdown-card'), 'index.wxml 缺少 active-countdown-card');
assert(wxml.includes('timer-digits'), 'index.wxml 缺少 timer-digits 倒计时节点');
assert(wxss.includes('.active-countdown-card'), 'index.wxss 缺少 .active-countdown-card');
assert(wxss.includes('.status-active'), 'index.wxss 缺少 .status-active');
assert(wxss.includes('.status-expiring'), 'index.wxss 缺少 .status-expiring');
assert(wxss.includes('.status-inactive'), 'index.wxss 缺少 .status-inactive');
assert(js.includes('countdown'), 'index.js 缺少 countdown 响应式对象');
assert(js.includes('activeState'), 'index.js 缺少 activeState 响应式计算属性');
});
console.log(`\n--------------------------------------------------------`);
console.log(`测试结果统计: 通过 ${passCount} 项, 失败 ${failCount} 项`);
console.log(`========================================================\n`);
if (failCount > 0) {
process.exit(1);
} else {
process.exit(0);
}

查看文件

@@ -3,13 +3,60 @@
<s-layout navbar="inner" class="index-wrap" title="收益中心" :bgStyle="bgStyle" onShareAppMessage>
<!-- 分销商信息 -->
<commission-info />
<!-- 30 日活跃警示 -->
<view v-if="userInfo?.active_30d === false || agentInfo?.active_30d === false" class="active-warn">
<view class="warn-text">近 30 日暂无本人订单,业务奖励已暂停。</view>
<button class="ss-reset-button warn-btn" @tap="sheep.$router.go('/pages/index/index')">
去首页
</button>
<!-- 30 天收益保活与预警倒计时看板 -->
<view class="active-countdown-card" :class="'status-' + activeState.status">
<!-- 头部状态栏 -->
<view class="card-header ss-flex ss-row-between ss-col-center">
<view class="header-left ss-flex ss-col-center">
<view class="status-icon-box ss-flex ss-row-center ss-col-center">
<text v-if="activeState.status === 'active'" class="cicon-check icon-symbol text-green"></text>
<text v-else-if="activeState.status === 'expiring'" class="cicon-notice icon-symbol text-amber"></text>
<text v-else class="cicon-close icon-symbol text-red"></text>
</view>
<view class="header-title-box ss-m-l-12 ss-flex ss-col-center">
<text class="status-title-text">{{ activeState.title }}</text>
<text class="status-pill ss-m-l-10" :class="'pill-' + activeState.status">{{ activeState.badge }}</text>
</view>
</view>
<button class="ss-reset-button action-btn" :class="'btn-' + activeState.status" @tap="onActionTap">
{{ activeState.btnText }}
</button>
</view>
<!-- 倒计时与状态内容 -->
<view class="card-body ss-m-t-14">
<!-- 正常活跃 / 临期预警:展示秒级动态跳动倒计时 -->
<view v-if="activeState.status !== 'inactive'" class="countdown-row ss-flex ss-col-center ss-row-between">
<view class="countdown-label ss-flex ss-col-center">
<text class="label-prefix">保活剩余:</text>
<text class="label-days-highlight">{{ countdown.days }}</text>
<text class="label-days-text">天</text>
</view>
<view class="timer-digits ss-flex ss-col-center">
<view class="digit-box"><text class="num">{{ countdown.hours }}</text><text class="unit">时</text></view>
<text class="colon">:</text>
<view class="digit-box"><text class="num">{{ countdown.minutes }}</text><text class="unit">分</text></view>
<text class="colon">:</text>
<view class="digit-box sec-box"><text class="num">{{ countdown.seconds }}</text><text class="unit">秒</text></view>
</view>
</view>
<!-- 已暂停状态说明 -->
<view v-else class="inactive-row ss-flex ss-col-center">
<text class="inactive-desc">近 30 日内暂无本人自购订单,所有收益分成已暂缓沉淀。</text>
</view>
<!-- 规则小贴士与上次自购时间 -->
<view class="rule-footer ss-flex ss-row-between ss-col-center ss-m-t-10">
<view class="footer-left ss-flex ss-col-center ss-line-1 ss-flex-1">
<text class="footer-dot">●</text>
<text class="footer-tip-text ss-line-1">{{ activeState.tip }}</text>
</view>
<text v-if="activeState.lastOrderText" class="footer-right-time ss-m-l-10">{{ activeState.lastOrderText }}</text>
</view>
</view>
</view>
<!-- 账户信息 -->
<account-info />
<!-- 团队作战快捷看板 -->
@@ -21,7 +68,8 @@
<view class="team-quick-info ss-m-l-16">
<view class="team-title ss-flex ss-col-center">
<text class="title-bold">团队作战中心</text>
<text class="active-badge ss-m-l-10" v-if="userInfo?.active_30d !== false && agentInfo?.active_30d !== false">正常活跃</text>
<text class="active-badge ss-m-l-10" v-if="activeState.status === 'active'">正常活跃 (剩{{ countdown.days }}天)</text>
<text class="amber-badge ss-m-l-10" v-else-if="activeState.status === 'expiring'">临期预警 (仅剩{{ countdown.days }}天)</text>
<text class="warn-badge ss-m-l-10" v-else>奖励暂停</text>
</view>
<view class="team-sub ss-m-t-6">
@@ -48,8 +96,9 @@
<script setup>
import sheep from '@/sheep';
import { onShow } from '@dcloudio/uni-app';
import { onShow, onHide, onUnload } from '@dcloudio/uni-app';
import { computed, reactive } from 'vue';
import dayjs from 'dayjs';
import commissionInfo from './components/commission-info.vue';
import accountInfo from './components/account-info.vue';
import commissionLog from './components/commission-log.vue';
@@ -68,6 +117,130 @@
const agentInfo = computed(() => sheep.$store('user').agentInfo);
const userInfo = computed(() => sheep.$store('user').userInfo);
// 实时倒计时数据
const countdown = reactive({
days: '00',
hours: '00',
minutes: '00',
seconds: '00',
totalSeconds: 0,
});
let timer = null;
// 活跃状态与文案综合计算
const activeState = computed(() => {
const user = userInfo.value || {};
const agent = agentInfo.value || {};
const lastTime = Number(agent.last_self_order_time || user.last_self_order_time || 0);
const isExplicitInactive = user.active_30d === false || agent.active_30d === false;
// 1. 明确暂停或无自购
if (isExplicitInactive || (lastTime <= 0 && agent.active_30d === false)) {
return {
status: 'inactive',
title: '收益奖励已暂停',
badge: '暂缓计奖',
btnText: '去选购恢复',
tip: '次日完成任意一笔本人自购订单即可自动恢复保活',
lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '暂无自购记录',
};
}
// 2. 计算到期时间戳
const nowSec = Math.floor(Date.now() / 1000);
let expireTime = Number(agent.expire_time || 0);
if (!expireTime) {
expireTime = lastTime > 0 ? (lastTime + 30 * 86400) : (nowSec + 30 * 86400);
}
const remainingSeconds = Math.max(0, expireTime - nowSec);
const remainingDays = Math.ceil(remainingSeconds / 86400);
if (remainingSeconds <= 0) {
return {
status: 'inactive',
title: '收益奖励已暂停',
badge: '暂缓计奖',
btnText: '去选购恢复',
tip: '近30日暂无本人自购,次日下单后即可自动激活',
lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
};
}
if (remainingDays <= 5) {
return {
status: 'expiring',
title: '30天保活临期预警',
badge: `仅剩 ${remainingDays} 天`,
btnText: '立即自购保活',
tip: '即将暂停分润!请在倒计时结束前完成自购续期',
lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
};
}
return {
status: 'active',
title: '30天收益资格生效中',
badge: '正常活跃',
btnText: '去自购续期',
tip: '已满足30日保活,享有全部团队分红与奖项',
lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
};
});
function updateCountdown() {
const user = userInfo.value || {};
const agent = agentInfo.value || {};
const lastTime = Number(agent.last_self_order_time || user.last_self_order_time || 0);
if (user.active_30d === false || agent.active_30d === false) {
countdown.days = '00';
countdown.hours = '00';
countdown.minutes = '00';
countdown.seconds = '00';
countdown.totalSeconds = 0;
return;
}
const nowSec = Math.floor(Date.now() / 1000);
let expireTime = Number(agent.expire_time || 0);
if (!expireTime) {
expireTime = lastTime > 0 ? (lastTime + 30 * 86400) : (nowSec + 30 * 86400);
}
const remaining = Math.max(0, expireTime - nowSec);
countdown.totalSeconds = remaining;
const d = Math.floor(remaining / 86400);
const h = Math.floor((remaining % 86400) / 3600);
const m = Math.floor((remaining % 3600) / 60);
const s = remaining % 60;
countdown.days = d < 10 ? '0' + d : String(d);
countdown.hours = h < 10 ? '0' + h : String(h);
countdown.minutes = m < 10 ? '0' + m : String(m);
countdown.seconds = s < 10 ? '0' + s : String(s);
}
function startTimer() {
if (timer) clearInterval(timer);
updateCountdown();
timer = setInterval(updateCountdown, 1000);
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function onActionTap() {
sheep.$router.go('/pages/index/index');
}
async function getUserInfo() {
await sheep.$store('user').getInfo();
}
@@ -82,10 +255,21 @@
state.error = res?.code || 0;
state.errorData = res?.data || {};
}
updateCountdown();
}
onShow(() => {
getUserInfo();
getAgentInfo();
startTimer();
});
onHide(() => {
stopTimer();
});
onUnload(() => {
stopTimer();
});
</script>
@@ -94,35 +278,231 @@
background-size: 100% 100% !important;
}
.active-warn {
.active-countdown-card {
width: 694rpx;
margin: 0 auto 16rpx;
padding: 20rpx 24rpx;
margin: 16rpx auto 0;
padding: 24rpx 28rpx;
box-sizing: border-box;
background: #fff4e5;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 16rpx;
position: relative;
z-index: 3;
transition: all 0.3s ease;
.warn-text {
flex: 1;
font-size: 24rpx;
line-height: 34rpx;
color: #a17545;
padding-right: 16rpx;
// 正常活跃状态
&.status-active {
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%);
border: 1rpx solid #bbf7d0;
box-shadow: 0 4rpx 16rpx rgba(187, 247, 208, 0.35);
.status-icon-box {
background: #dcfce7;
}
.status-title-text {
color: #166534;
}
.pill-active {
background: #dcfce7;
color: #15803d;
}
.btn-active {
background: #059669;
color: #ffffff;
}
.digit-box {
background: #ecfdf5;
border: 1rpx solid #a7f3d0;
.num {
color: #047857;
}
.unit {
color: #065f46;
}
}
.colon {
color: #059669;
}
}
.warn-btn {
flex-shrink: 0;
height: 52rpx;
padding: 0 22rpx;
background: #ff6000;
border-radius: 26rpx;
font-size: 22rpx;
color: #ffffff;
// 临期预警状态
&.status-expiring {
background: linear-gradient(135deg, #fff7ed 0%, #fffbeb 100%);
border: 1rpx solid #fed7aa;
box-shadow: 0 4rpx 18rpx rgba(253, 186, 116, 0.45);
.status-icon-box {
background: #ffedd5;
}
.status-title-text {
color: #9a3412;
}
.pill-expiring {
background: #ffedd5;
color: #c2410c;
font-weight: bold;
}
.btn-expiring {
background: linear-gradient(90deg, #ea580c 0%, #f97316 100%);
color: #ffffff;
box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.3);
}
.digit-box {
background: #ffedd5;
border: 1rpx solid #fdba74;
.num {
color: #c2410c;
}
.unit {
color: #9a3412;
}
}
.colon {
color: #ea580c;
}
}
// 奖励暂停状态
&.status-inactive {
background: linear-gradient(135deg, #fef2f2 0%, #fff1f2 100%);
border: 1rpx solid #fecaca;
box-shadow: 0 4rpx 16rpx rgba(254, 202, 202, 0.35);
.status-icon-box {
background: #fee2e2;
}
.status-title-text {
color: #991b1b;
}
.pill-inactive {
background: #fee2e2;
color: #b91c1c;
}
.btn-inactive {
background: #dc2626;
color: #ffffff;
}
}
.card-header {
.status-icon-box {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
.icon-symbol {
font-size: 26rpx;
font-weight: bold;
}
.text-green { color: #059669; }
.text-amber { color: #d97706; }
.text-red { color: #dc2626; }
}
.status-title-text {
font-size: 28rpx;
font-weight: bold;
}
.status-pill {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
font-weight: 500;
}
.action-btn {
height: 52rpx;
padding: 0 24rpx;
border-radius: 26rpx;
font-size: 22rpx;
font-weight: 500;
}
}
.card-body {
.countdown-row {
background: rgba(255, 255, 255, 0.7);
padding: 12rpx 16rpx;
border-radius: 10rpx;
.countdown-label {
.label-prefix {
font-size: 24rpx;
color: #64748b;
}
.label-days-highlight {
font-size: 30rpx;
font-weight: bold;
color: #0f172a;
margin: 0 2rpx;
font-family: OPPOSANS;
}
.label-days-text {
font-size: 22rpx;
color: #475569;
}
}
.timer-digits {
.digit-box {
padding: 4rpx 8rpx;
border-radius: 6rpx;
display: flex;
align-items: baseline;
min-width: 52rpx;
justify-content: center;
.num {
font-family: OPPOSANS, DIN, monospace;
font-size: 24rpx;
font-weight: bold;
}
.unit {
font-size: 18rpx;
margin-left: 2rpx;
}
}
.colon {
font-weight: bold;
font-size: 24rpx;
margin: 0 4rpx;
}
}
}
.inactive-row {
background: rgba(254, 226, 226, 0.4);
padding: 14rpx 18rpx;
border-radius: 10rpx;
.inactive-desc {
font-size: 24rpx;
color: #991b1b;
line-height: 34rpx;
}
}
.rule-footer {
font-size: 20rpx;
color: #94a3b8;
.footer-dot {
font-size: 14rpx;
color: #cbd5e1;
margin-right: 6rpx;
}
.footer-tip-text {
font-size: 22rpx;
color: #64748b;
}
.footer-right-time {
font-size: 20rpx;
color: #94a3b8;
font-family: OPPOSANS;
flex-shrink: 0;
}
}
}
}
@@ -159,6 +539,14 @@
color: #059669;
font-weight: 500;
}
.amber-badge {
font-size: 20rpx;
padding: 2rpx 10rpx;
border-radius: 12rpx;
background: #ffedd5;
color: #c2410c;
font-weight: 500;
}
.warn-badge {
font-size: 20rpx;
padding: 2rpx 10rpx;