fix: Rock Token 超时按满 2/3 天判定并统一显示口径
This commit is contained in:
@@ -108,6 +108,13 @@ type RockToken struct {
|
|||||||
DeletedAtText string `gorm:"-" json:"deleted_at,omitempty"` // 回收站展示用,仅管理端响应返回
|
DeletedAtText string `gorm:"-" json:"deleted_at,omitempty"` // 回收站展示用,仅管理端响应返回
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// 以下为列表派生字段,不落库:由后端用与筛选/统计完全相同的 now 计算。
|
||||||
|
BoundOverdue bool `gorm:"-" json:"bound_overdue"`
|
||||||
|
UsingOverdue bool `gorm:"-" json:"using_overdue"`
|
||||||
|
UsingDanger bool `gorm:"-" json:"using_danger"`
|
||||||
|
BoundDays float64 `gorm:"-" json:"bound_days"`
|
||||||
|
UsingDays float64 `gorm:"-" json:"using_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (RockToken) TableName() string {
|
func (RockToken) TableName() string {
|
||||||
|
|||||||
@@ -98,6 +98,80 @@ func TestListTokens_OverdueFilters(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestListTokens_OverdueBoundaryInclusive 锁定「满 2 天 / 满 3 天」的含边界口径:
|
||||||
|
// 刚好达到或超过阈值必须进入筛选并带超时标记,差 1 小时则不能进入。
|
||||||
|
func TestListTokens_OverdueBoundaryInclusive(t *testing.T) {
|
||||||
|
db := newRockTestDB(t)
|
||||||
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
exactBoundAt := now.AddDate(0, 0, -2).Add(-time.Second) // 2 天零 1 秒
|
||||||
|
almostBoundAt := now.AddDate(0, 0, -2).Add(time.Hour) // 1 天 23 小时
|
||||||
|
exactUsingAt := now.AddDate(0, 0, -3).Add(-time.Second) // 3 天零 1 秒
|
||||||
|
almostUsingAt := now.AddDate(0, 0, -3).Add(time.Hour) // 2 天 23 小时
|
||||||
|
|
||||||
|
tokens := []model.RockToken{
|
||||||
|
{UpstreamTokenID: "bound-exact", RedeemCode: "BOUND-1", Status: model.RockStatusBound, BoundAt: &exactBoundAt},
|
||||||
|
{UpstreamTokenID: "bound-almost", RedeemCode: "BOUND-2", Status: model.RockStatusBound, BoundAt: &almostBoundAt},
|
||||||
|
{UpstreamTokenID: "using-exact", RedeemCode: "USING-1", Status: model.RockStatusUsing, UsingAt: &exactUsingAt},
|
||||||
|
{UpstreamTokenID: "using-almost", RedeemCode: "USING-2", Status: model.RockStatusUsing, UsingAt: &almostUsingAt},
|
||||||
|
}
|
||||||
|
for i := range tokens {
|
||||||
|
if err := db.Create(&tokens[i]).Error; err != nil {
|
||||||
|
t.Fatalf("创建 Token 失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boundList, boundTotal, err := svc.ListTokens(ctx, 1, 20, rockTokenStatusBoundOverdue, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("查询绑定超时失败: %v", err)
|
||||||
|
}
|
||||||
|
if boundTotal != 1 || len(boundList) != 1 || boundList[0].ID != tokens[0].ID {
|
||||||
|
t.Fatalf("绑定超时边界错误: total=%d list=%+v", boundTotal, boundList)
|
||||||
|
}
|
||||||
|
if !boundList[0].BoundOverdue || boundList[0].BoundDays < 2 {
|
||||||
|
t.Fatalf("绑定超时标记/天数错误: overdue=%v days=%.4f", boundList[0].BoundOverdue, boundList[0].BoundDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
usingList, usingTotal, err := svc.ListTokens(ctx, 1, 20, rockTokenStatusUsingOverdue, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("查询使用超时失败: %v", err)
|
||||||
|
}
|
||||||
|
if usingTotal != 1 || len(usingList) != 1 || usingList[0].ID != tokens[2].ID {
|
||||||
|
t.Fatalf("使用超时边界错误: total=%d list=%+v", usingTotal, usingList)
|
||||||
|
}
|
||||||
|
if !usingList[0].UsingOverdue || usingList[0].UsingDays < 3 {
|
||||||
|
t.Fatalf("使用超时标记/天数错误: overdue=%v days=%.4f", usingList[0].UsingOverdue, usingList[0].UsingDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
all, _, err := svc.ListTokens(ctx, 1, 20, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("查询全部 Token 失败: %v", err)
|
||||||
|
}
|
||||||
|
byID := make(map[uint]model.RockToken, len(all))
|
||||||
|
for _, token := range all {
|
||||||
|
byID[token.ID] = token
|
||||||
|
}
|
||||||
|
if byID[tokens[1].ID].BoundOverdue {
|
||||||
|
t.Fatal("差 1 小时满 2 天的 Token 不应标记绑定超时")
|
||||||
|
}
|
||||||
|
if byID[tokens[3].ID].UsingOverdue {
|
||||||
|
t.Fatal("差 1 小时满 3 天的 Token 不应标记使用超时")
|
||||||
|
}
|
||||||
|
if !byID[tokens[0].ID].BoundOverdue || !byID[tokens[2].ID].UsingOverdue {
|
||||||
|
t.Fatal("刚好达到阈值的 Token 必须标记超时")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := svc.GetStats(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("获取统计失败: %v", err)
|
||||||
|
}
|
||||||
|
if stats.BoundOverdueStock != 1 || stats.UsingOverdueStock != 1 {
|
||||||
|
t.Fatalf("超时统计不符: bound=%d using=%d", stats.BoundOverdueStock, stats.UsingOverdueStock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRefreshStaleTokens_SkipsWhenAutoRefreshDisabled(t *testing.T) {
|
func TestRefreshStaleTokens_SkipsWhenAutoRefreshDisabled(t *testing.T) {
|
||||||
db := newRockTestDB(t)
|
db := newRockTestDB(t)
|
||||||
var calls int32
|
var calls int32
|
||||||
|
|||||||
@@ -572,8 +572,9 @@ func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
warnDays, _, boundWarnDays := rockOverdueThresholds(cfg)
|
warnDays, dangerDays, boundWarnDays := rockOverdueThresholds(cfg)
|
||||||
db := applyRockTokenListFilters(s.db.WithContext(ctx).Model(&model.RockToken{}), status, search, time.Now(), warnDays, boundWarnDays)
|
now := time.Now()
|
||||||
|
db := applyRockTokenListFilters(s.db.WithContext(ctx).Model(&model.RockToken{}), status, search, now, warnDays, boundWarnDays)
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := db.Count(&total).Error; err != nil {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
@@ -589,6 +590,7 @@ func (s *RockService) ListTokens(ctx context.Context, page, pageSize int, status
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
applyRockTokenOverdueFlags(tokens, now, warnDays, dangerDays, boundWarnDays)
|
||||||
s.prepareRockTokenList(ctx, tokens)
|
s.prepareRockTokenList(ctx, tokens)
|
||||||
|
|
||||||
return tokens, total, nil
|
return tokens, total, nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"affiliate_dash/internal/model"
|
"affiliate_dash/internal/model"
|
||||||
)
|
)
|
||||||
@@ -27,6 +28,13 @@ func (s *RockService) ListSupportBoundTokens(ctx context.Context, page, pageSize
|
|||||||
pageSize = 100
|
pageSize = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := s.GetConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
warnDays, dangerDays, boundWarnDays := rockOverdueThresholds(cfg)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
db := s.db.WithContext(ctx).Model(&model.RockToken{}).Where("status = ?", model.RockStatusBound)
|
db := s.db.WithContext(ctx).Model(&model.RockToken{}).Where("status = ?", model.RockStatusBound)
|
||||||
term := "%" + search + "%"
|
term := "%" + search + "%"
|
||||||
db = db.Where(
|
db = db.Where(
|
||||||
@@ -45,6 +53,7 @@ func (s *RockService) ListSupportBoundTokens(ctx context.Context, page, pageSize
|
|||||||
Find(&tokens).Error; err != nil {
|
Find(&tokens).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
applyRockTokenOverdueFlags(tokens, now, warnDays, dangerDays, boundWarnDays)
|
||||||
s.prepareRockTokenList(ctx, tokens)
|
s.prepareRockTokenList(ctx, tokens)
|
||||||
return tokens, total, nil
|
return tokens, total, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,39 @@ func rockOverdueCutoff(now time.Time, days int) time.Time {
|
|||||||
return now.AddDate(0, 0, -days)
|
return now.AddDate(0, 0, -days)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyRockTokenOverdueFlags 用与筛选、统计完全相同的 now 和阈值,
|
||||||
|
// 为列表补充超时标记与实际天数,避免前端用本地时间算出「显示 2.0 天但未超时」。
|
||||||
|
func applyRockTokenOverdueFlags(tokens []model.RockToken, now time.Time, warnDays, dangerDays, boundWarnDays int) {
|
||||||
|
usingCutoff := rockOverdueCutoff(now, warnDays)
|
||||||
|
dangerCutoff := rockOverdueCutoff(now, dangerDays)
|
||||||
|
boundCutoff := rockOverdueCutoff(now, boundWarnDays)
|
||||||
|
for i := range tokens {
|
||||||
|
switch tokens[i].Status {
|
||||||
|
case model.RockStatusUsing:
|
||||||
|
if tokens[i].UsingAt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
days := now.Sub(*tokens[i].UsingAt).Hours() / 24
|
||||||
|
if days < 0 {
|
||||||
|
days = 0
|
||||||
|
}
|
||||||
|
tokens[i].UsingDays = days
|
||||||
|
tokens[i].UsingOverdue = !tokens[i].UsingAt.After(usingCutoff)
|
||||||
|
tokens[i].UsingDanger = !tokens[i].UsingAt.After(dangerCutoff)
|
||||||
|
case model.RockStatusBound:
|
||||||
|
if tokens[i].BoundAt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
days := now.Sub(*tokens[i].BoundAt).Hours() / 24
|
||||||
|
if days < 0 {
|
||||||
|
days = 0
|
||||||
|
}
|
||||||
|
tokens[i].BoundDays = days
|
||||||
|
tokens[i].BoundOverdue = !tokens[i].BoundAt.After(boundCutoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// applyRockTokenListFilters 与 Token 列表使用同一套筛选条件,避免“看到多少、删多少”不一致。
|
// applyRockTokenListFilters 与 Token 列表使用同一套筛选条件,避免“看到多少、删多少”不一致。
|
||||||
// 列表、删除预览、全部删除、回收站共用;now 由调用方传入以便同一请求内阈值一致。
|
// 列表、删除预览、全部删除、回收站共用;now 由调用方传入以便同一请求内阈值一致。
|
||||||
func applyRockTokenListFilters(db *gorm.DB, status, search string, now time.Time, warnDays, boundWarnDays int) *gorm.DB {
|
func applyRockTokenListFilters(db *gorm.DB, status, search string, now time.Time, warnDays, boundWarnDays int) *gorm.DB {
|
||||||
|
|||||||
@@ -55,10 +55,11 @@ func (s *RockService) ListDeletedRockTokens(ctx context.Context, page, pageSize
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
warnDays, _, boundWarnDays := rockOverdueThresholds(cfg)
|
warnDays, dangerDays, boundWarnDays := rockOverdueThresholds(cfg)
|
||||||
|
now := time.Now()
|
||||||
db := applyRockTokenListFilters(
|
db := applyRockTokenListFilters(
|
||||||
s.db.WithContext(ctx).Unscoped().Model(&model.RockToken{}).Where("deleted_at IS NOT NULL"),
|
s.db.WithContext(ctx).Unscoped().Model(&model.RockToken{}).Where("deleted_at IS NOT NULL"),
|
||||||
status, search, time.Now(), warnDays, boundWarnDays,
|
status, search, now, warnDays, boundWarnDays,
|
||||||
)
|
)
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
@@ -79,6 +80,7 @@ func (s *RockService) ListDeletedRockTokens(ctx context.Context, page, pageSize
|
|||||||
tokens[i].DeletedAtText = tokens[i].DeletedAt.Time.Format(time.RFC3339)
|
tokens[i].DeletedAtText = tokens[i].DeletedAt.Time.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
applyRockTokenOverdueFlags(tokens, now, warnDays, dangerDays, boundWarnDays)
|
||||||
return tokens, total, nil
|
return tokens, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -675,6 +675,12 @@ export interface RockTokenItem {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
deleted_at?: string | null;
|
deleted_at?: string | null;
|
||||||
|
// 后端按与筛选/统计一致的阈值计算,前端标签直接使用,避免本地时间造成边界不一致。
|
||||||
|
bound_overdue?: boolean;
|
||||||
|
using_overdue?: boolean;
|
||||||
|
using_danger?: boolean;
|
||||||
|
bound_days?: number;
|
||||||
|
using_days?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RockPurchaseResult {
|
export interface RockPurchaseResult {
|
||||||
|
|||||||
@@ -103,6 +103,12 @@ export default function PlatformRock() {
|
|||||||
}
|
}
|
||||||
}, [form])
|
}, [form])
|
||||||
|
|
||||||
|
// 只刷新统计,不触碰配置表单,供定时刷新和列表刷新共用。
|
||||||
|
const loadStats = useCallback(async () => {
|
||||||
|
const st = await rockApi.getStats()
|
||||||
|
setStats(st)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const loadTokens = useCallback(async () => {
|
const loadTokens = useCallback(async () => {
|
||||||
setLoadingTokens(true)
|
setLoadingTokens(true)
|
||||||
try {
|
try {
|
||||||
@@ -129,6 +135,20 @@ export default function PlatformRock() {
|
|||||||
loadTokens()
|
loadTokens()
|
||||||
}, [loadTokens])
|
}, [loadTokens])
|
||||||
|
|
||||||
|
// 定时刷新列表与统计,保证跨过 2 天/3 天阈值后标签和徽标自动更新。
|
||||||
|
const refreshTokensAndStats = useCallback(() => {
|
||||||
|
void loadStats().catch(() => {})
|
||||||
|
void loadTokens()
|
||||||
|
}, [loadStats, loadTokens])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
if (document.visibilityState !== 'visible') return
|
||||||
|
refreshTokensAndStats()
|
||||||
|
}, 60000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [refreshTokensAndStats])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTokenIds([])
|
setSelectedTokenIds([])
|
||||||
setSelectedTokens([])
|
setSelectedTokens([])
|
||||||
@@ -480,7 +500,7 @@ export default function PlatformRock() {
|
|||||||
togglingAutoRefresh={togglingAutoRefresh}
|
togglingAutoRefresh={togglingAutoRefresh}
|
||||||
onSearchKeywordChange={setSearchKeyword}
|
onSearchKeywordChange={setSearchKeyword}
|
||||||
onFilterStatusChange={handleFilterStatusChange}
|
onFilterStatusChange={handleFilterStatusChange}
|
||||||
onRefresh={loadTokens}
|
onRefresh={refreshTokensAndStats}
|
||||||
onRefreshStale={handleRefreshStale}
|
onRefreshStale={handleRefreshStale}
|
||||||
onToggleAutoRefreshMismatch={handleToggleAutoRefreshMismatch}
|
onToggleAutoRefreshMismatch={handleToggleAutoRefreshMismatch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,9 +29,15 @@ function elapsedDaysSince(value?: string): number | null {
|
|||||||
return diff > 0 ? diff / MS_PER_DAY : 0
|
return diff > 0 ? diff / MS_PER_DAY : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用中超时起点:using_at。
|
// 展示用天数:向下取整到 1 位,避免 1.99 天被四舍五入成 2.0 天而误以为已超时。
|
||||||
function usingElapsedDays(usingAt?: string): number | null {
|
function floorDays(days: number): number {
|
||||||
return elapsedDaysSince(usingAt)
|
return Math.floor(days * 10) / 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用后端返回的服务端天数,旧接口没有该字段时回退到本地计算。
|
||||||
|
function serverElapsedDays(serverDays: number | undefined, at?: string): number | null {
|
||||||
|
if (typeof serverDays === 'number') return serverDays
|
||||||
|
return elapsedDaysSince(at)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token 主信息:编号/兑换码 与 包ID/提取码 上下两行展示,
|
// Token 主信息:编号/兑换码 与 包ID/提取码 上下两行展示,
|
||||||
@@ -155,21 +161,24 @@ export function buildTokenColumns({
|
|||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
case 'using': {
|
case 'using': {
|
||||||
const days = usingElapsedDays(record.using_at)
|
const days = serverElapsedDays(record.using_days, record.using_at)
|
||||||
if (days !== null && days >= usingDangerDays) {
|
const usingOverdue = record.using_overdue ?? (days !== null && days >= usingWarnDays)
|
||||||
|
const usingDanger = record.using_danger ?? (days !== null && days >= usingDangerDays)
|
||||||
|
const displayDays = days === null ? 0 : floorDays(days)
|
||||||
|
if (usingDanger) {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
color="error"
|
color="error"
|
||||||
style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff4d4f' }}
|
style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff4d4f' }}
|
||||||
>
|
>
|
||||||
⚠️ 使用中超时 {days.toFixed(1)} 天
|
⚠️ 使用中超时 {displayDays.toFixed(1)} 天
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (days !== null && days >= usingWarnDays) {
|
if (usingOverdue) {
|
||||||
return (
|
return (
|
||||||
<Tag color="warning" style={{ fontWeight: 600, padding: '2px 8px' }}>
|
<Tag color="warning" style={{ fontWeight: 600, padding: '2px 8px' }}>
|
||||||
● 扫码使用中 · {days.toFixed(1)} 天
|
● 扫码使用中 · {displayDays.toFixed(1)} 天
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -180,14 +189,16 @@ export function buildTokenColumns({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
case 'bound': {
|
case 'bound': {
|
||||||
const boundDays = elapsedDaysSince(record.bound_at)
|
const boundDays = serverElapsedDays(record.bound_days, record.bound_at)
|
||||||
if (boundDays !== null && boundDays >= boundWarnDays) {
|
const boundOverdue = record.bound_overdue ?? (boundDays !== null && boundDays >= boundWarnDays)
|
||||||
|
if (boundOverdue) {
|
||||||
|
const displayDays = boundDays === null ? 0 : floorDays(boundDays)
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
color="volcano"
|
color="volcano"
|
||||||
style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff7a45' }}
|
style={{ fontWeight: 600, padding: '2px 8px', border: '1px solid #ff7a45' }}
|
||||||
>
|
>
|
||||||
⏱ 绑定超时 {boundDays.toFixed(1)} 天
|
⏱ 绑定超时 {displayDays.toFixed(1)} 天
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -233,14 +244,17 @@ export function buildTokenColumns({
|
|||||||
title: '流转时间',
|
title: '流转时间',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const usingDays = record.status === 'using' ? usingElapsedDays(record.using_at) : null
|
const usingDays = record.status === 'using' ? serverElapsedDays(record.using_days, record.using_at) : null
|
||||||
const boundDays = record.status === 'bound' ? elapsedDaysSince(record.bound_at) : null
|
const boundDays = record.status === 'bound' ? serverElapsedDays(record.bound_days, record.bound_at) : null
|
||||||
|
const usingOverdue = record.using_overdue ?? (usingDays !== null && usingDays >= usingWarnDays)
|
||||||
|
const usingDanger = record.using_danger ?? (usingDays !== null && usingDays >= usingDangerDays)
|
||||||
|
const boundOverdue = record.bound_overdue ?? (boundDays !== null && boundDays >= boundWarnDays)
|
||||||
const usingColor =
|
const usingColor =
|
||||||
usingDays === null
|
usingDays === null
|
||||||
? '#64748b'
|
? '#64748b'
|
||||||
: usingDays >= usingDangerDays
|
: usingDanger
|
||||||
? '#dc2626'
|
? '#dc2626'
|
||||||
: usingDays >= usingWarnDays
|
: usingOverdue
|
||||||
? '#d97706'
|
? '#d97706'
|
||||||
: '#0891b2'
|
: '#0891b2'
|
||||||
return (
|
return (
|
||||||
@@ -253,7 +267,7 @@ export function buildTokenColumns({
|
|||||||
<div style={{ fontSize: 11, color: usingColor }}>
|
<div style={{ fontSize: 11, color: usingColor }}>
|
||||||
<span>⏳ 使用中: </span>
|
<span>⏳ 使用中: </span>
|
||||||
{formatDateTime(record.using_at)}
|
{formatDateTime(record.using_at)}
|
||||||
{usingDays !== null ? `(${usingDays.toFixed(1)} 天)` : ''}
|
{usingDays !== null ? `(${floorDays(usingDays).toFixed(1)} 天)` : ''}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{record.mismatch_at && (
|
{record.mismatch_at && (
|
||||||
@@ -269,10 +283,10 @@ export function buildTokenColumns({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!record.redeemed_at && record.bound_at && (
|
{!record.redeemed_at && record.bound_at && (
|
||||||
<div style={{ fontSize: 11, color: boundDays !== null && boundDays >= boundWarnDays ? '#ea580c' : '#0284c7' }}>
|
<div style={{ fontSize: 11, color: boundOverdue ? '#ea580c' : '#0284c7' }}>
|
||||||
<span>🔗 绑定: </span>
|
<span>🔗 绑定: </span>
|
||||||
{formatDateTime(record.bound_at)}
|
{formatDateTime(record.bound_at)}
|
||||||
{boundDays !== null ? `(${boundDays.toFixed(1)} 天)` : ''}
|
{boundDays !== null ? `(${floorDays(boundDays).toFixed(1)} 天)` : ''}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!record.bound_at && !record.mismatch_at && record.claimed_at && (
|
{!record.bound_at && !record.mismatch_at && record.claimed_at && (
|
||||||
|
|||||||
Reference in New Issue
Block a user