feat: 修改数据看板

This commit is contained in:
zheng020 2026-06-03 20:25:08 +08:00
parent 76c4e2cdf8
commit 75e1222c55
29 changed files with 1295 additions and 598 deletions

View File

@ -1,9 +1,13 @@
<template> <template>
<view class="collection-matrix"> <view class="collection-matrix">
<text class="section-title">藏品矩阵</text> <text class="section-title">藏品矩阵</text>
<image
class="chart-bg"
src="/static/dashboard/image-bj.png"
mode="scaleToFill"
/>
<!-- TOP 5 --> <!-- TOP 5 -->
<TopFiveAssets :items="topFive || []" /> <TopFiveAssets :items="topFive || []" class="top-five-assets" />
<!-- 等级分布 --> <!-- 等级分布 -->
<LevelDistribution :items="levels" /> <LevelDistribution :items="levels" />
@ -17,26 +21,43 @@
</template> </template>
<script setup> <script setup>
import TopFiveAssets from './TopFiveAssets.vue' import TopFiveAssets from "./TopFiveAssets.vue";
import LevelDistribution from './LevelDistribution.vue' import LevelDistribution from "./LevelDistribution.vue";
import UpcomingUpgrades from './UpcomingUpgrades.vue' import UpcomingUpgrades from "./UpcomingUpgrades.vue";
import RecentUpgrades from './RecentUpgrades.vue' import RecentUpgrades from "./RecentUpgrades.vue";
defineProps({ defineProps({
topFive: { type: Array, default: () => [] }, topFive: { type: Array, default: () => [] },
levels: { type: Array, default: () => [] }, levels: { type: Array, default: () => [] },
upgrades: { type: Object, default: () => ({ upcoming: [], recent: [] }) }, upgrades: { type: Object, default: () => ({ upcoming: [], recent: [] }) },
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.collection-matrix { .collection-matrix {
background: rgba(255, 255, 255, 0.12); background:
backdrop-filter: blur(10px); linear-gradient(
106.77deg,
rgba(255, 223, 119, 0.13) -9.76%,
rgba(132, 255, 210, 0.13) 44.65%,
rgba(255, 129, 131, 0.13) 117.82%
),
linear-gradient(0deg, rgba(255, 159, 16, 0.2), rgba(255, 159, 16, 0.2));
border-radius: 22rpx; border-radius: 22rpx;
padding: 24rpx; padding: 12rpx;
margin: 24rpx 0; margin: 12rpx 0;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
inset: 0;
background: url("/static/dashboard/bj.png") center / cover no-repeat;
opacity: 0.2; // 0=1=
pointer-events: none;
z-index: 0;
}
} }
.section-title { .section-title {
@ -44,8 +65,22 @@ defineProps({
font-size: 36rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
color: #ffffff; color: #ffffff;
margin-bottom: 24rpx; margin-bottom: 34rpx;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2; // ::before
}
.chart-bg {
position: absolute;
top: -64rpx;
left: -56rpx;
width: 312rpx;
height: 312rpx;
z-index: 1; // ::before(z:0) (z:2) (z:1)
pointer-events: none; // tap穿 canvas
opacity: 0.5;
transform: rotate(-15deg);
} }
.upgrades-two-col { .upgrades-two-col {

View File

@ -16,7 +16,7 @@
<text class="card-label">水晶余额</text> <text class="card-label">水晶余额</text>
<text class="card-value">{{ data.crystal_balance }}</text> <text class="card-value">{{ data.crystal_balance }}</text>
</view> </view>
<view class="data-card card-today"> <view class="data-card card-crystal">
<text class="card-label">今日收益</text> <text class="card-label">今日收益</text>
<text class="card-value">+ {{ data.today_income }}</text> <text class="card-value">+ {{ data.today_income }}</text>
</view> </view>
@ -29,13 +29,13 @@ defineProps({
data: { type: Object, default: null }, // { crystal_balance, today_income, week_rank? } data: { type: Object, default: null }, // { crystal_balance, today_income, week_rank? }
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
error: { type: String, default: null }, error: { type: String, default: null },
}) });
defineEmits(['retry']) defineEmits(["retry"]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.crystal-overview { .crystal-overview {
margin: 24rpx 0; margin:0 0 12rpx 0;
} }
.card-row { .card-row {
@ -44,41 +44,53 @@ defineEmits(['retry'])
} }
.data-card { .data-card {
flex: 1; width: 100%;
height: 200rpx; height: 88rpx;
border-radius: 22rpx; border-top-left-radius: 19px;
display: flex; border-top-right-radius: 10px;
flex-direction: column; border-bottom-right-radius: 7px;
align-items: center; border-bottom-left-radius: 7px;
justify-content: center; opacity: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-sizing: border-box;
} }
.card-crystal { .card-crystal {
background: linear-gradient(135deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%); background: linear-gradient(
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25); 274.19deg,
rgba(168, 166, 237, 0.49) -9.28%,
rgba(136, 200, 216, 0.49) 61.89%,
rgba(243, 128, 239, 0.49) 106.57%
);
box-shadow: 0px 4px 4px 0px #00000040;
} }
.card-today { // .card-today {
background: linear-gradient(137deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%); // background: linear-gradient(137deg, #ffdf77 0%, #8e95e2 40%, #f48cff 100%);
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25); // box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25);
} // }
.card-label { .card-label {
position: absolute;
top: 10rpx;
left: 12rpx;
font-size: 24rpx; font-size: 24rpx;
color: #ffffff; color: #ffffff;
text-shadow: 0px 4px 4px rgba(164, 60, 60, 0.55); text-shadow: 0px 4px 4px rgba(164, 60, 60, 0.55);
margin-bottom: 12rpx;
} }
.card-value { .card-value {
font-size: 70rpx; position: absolute;
font-weight: 700; bottom: 0;
color: #FFFABD; right: 12rpx;
font-size: 64rpx;
font-weight: 600;
color: #fffabd;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84); text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
font-family: 'Baloo Bhai', sans-serif; font-family: "Baloo Bhai", sans-serif;
line-height: 1;
} }
/* 骨架态 */ /* 骨架态 */
@ -91,14 +103,23 @@ defineEmits(['retry'])
flex: 1; flex: 1;
height: 200rpx; height: 200rpx;
border-radius: 22rpx; border-radius: 22rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%); background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.1) 25%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.1) 75%
);
background-size: 200% 100%; background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite; animation: skeleton-shimmer 1.5s infinite;
} }
@keyframes skeleton-shimmer { @keyframes skeleton-shimmer {
0% { background-position: 200% 0; } 0% {
100% { background-position: -200% 0; } background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} }
/* 错误态 */ /* 错误态 */

View File

@ -4,35 +4,40 @@
<view class="header-deco-bg"></view> <view class="header-deco-bg"></view>
<!-- 装饰光晕红粉色 --> <!-- 装饰光晕红粉色 -->
<view class="header-glow"></view> <!-- <view class="header-glow"></view> -->
<!-- 状态栏占位iPhone 44px --> <!-- 状态栏占位iPhone 44px -->
<view class="status-bar-placeholder"></view> <!-- <view class="status-bar-placeholder"></view> -->
<!-- 渐变标题 -->
<view class="title-wrap">
<text class="header-title">数据看板</text>
</view>
<view class="header-content"> <view class="header-content">
<!-- 毛绒怪头像占位纯色块 + emoji -->
<view class="mascot">
<text class="mascot-emoji">🐾</text>
</view>
<!-- 渐变标题 -->
<view class="title-wrap">
<text class="header-title">数据看板</text>
</view>
<!-- Tab 胶囊 --> <!-- Tab 胶囊 -->
<view class="header-tabs"> <view class="header-tabs">
<view <view
:class="['tab', activeTab === 'crystal' ? 'tab-active' : '']" :class="['tab', activeTab === 'crystal' ? 'tab-active' : '']"
@tap="$emit('update:activeTab', 'crystal')" @tap="$emit('update:activeTab', 'crystal')"
> >
水晶相关 <image
class="tab-icon"
src="/static/dashboard/crystal-bg.png"
mode="aspectFit"
/>
<text class="tab-title">水晶相关</text>
</view> </view>
<view <view
:class="['tab', activeTab === 'season' ? 'tab-active' : '']" :class="['tab', activeTab === 'season' ? 'tab-active' : '']"
@tap="$emit('update:activeTab', 'season')" @tap="$emit('update:activeTab', 'season')"
> >
赛季总览 <image
class="tab-icon"
src="/static/dashboard/season-bg.png"
mode="aspectFit"
/>
<text class="tab-title">赛季总览</text>
</view> </view>
</view> </view>
</view> </view>
@ -41,16 +46,16 @@
<script setup> <script setup>
defineProps({ defineProps({
activeTab: { type: String, default: 'crystal' }, activeTab: { type: String, default: "crystal" },
}) });
defineEmits(['update:activeTab']) defineEmits(["update:activeTab"]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dashboard-header { .dashboard-header {
position: relative; position: relative;
height: 360rpx; height: 360rpx;
overflow: hidden; // overflow: hidden;
} }
.status-bar-placeholder { .status-bar-placeholder {
@ -58,14 +63,26 @@ defineEmits(['update:activeTab'])
} }
.header-deco-bg { .header-deco-bg {
background-image: url("/static/dashboard/header-bj.png");
background-size: 130%;
// background-repeat: no-repeat;
// background-position: center;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 700rpx; height: 700rpx;
background: linear-gradient(179deg, #FFE5E5 0%, #F3A0A1 50%, #FF9C9C 86%, #FF2024 100%);
filter: blur(4px);
z-index: 0; z-index: 0;
// header bg
// bj.png +
// 700rpx 0-30%(0-210rpx) 30-50%(210-360rpx)
mask-image: linear-gradient(to bottom, #000 0%, #000 60%, transparent 80%);
-webkit-mask-image: linear-gradient(
to bottom,
#000 0%,
#000 60%,
transparent 80%
);
} }
.header-glow { .header-glow {
@ -75,7 +92,11 @@ defineEmits(['update:activeTab'])
transform: translateX(-50%); transform: translateX(-50%);
width: 400rpx; width: 400rpx;
height: 400rpx; height: 400rpx;
background: radial-gradient(circle, rgba(255, 200, 100, 0.5) 0%, transparent 70%); background: radial-gradient(
circle,
rgba(255, 200, 100, 0.5) 0%,
transparent 70%
);
filter: blur(30px); filter: blur(30px);
z-index: 1; z-index: 1;
} }
@ -84,16 +105,14 @@ defineEmits(['update:activeTab'])
position: relative; position: relative;
z-index: 2; z-index: 2;
padding: 0 32rpx 32rpx; padding: 0 32rpx 32rpx;
display: flex; top: 192rpx;
flex-direction: column;
align-items: center;
} }
.mascot { .mascot {
width: 104rpx; width: 104rpx;
height: 104rpx; height: 104rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #FF6B6B 0%, #FFB199 100%); background: linear-gradient(135deg, #ff6b6b 0%, #ffb199 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -101,10 +120,6 @@ defineEmits(['update:activeTab'])
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
.mascot-emoji {
font-size: 64rpx;
}
.title-wrap { .title-wrap {
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
@ -112,7 +127,7 @@ defineEmits(['update:activeTab'])
.header-title { .header-title {
font-size: 48rpx; font-size: 48rpx;
font-weight: 700; font-weight: 700;
background: linear-gradient(90deg, #FFE5B4 0%, #FFB199 50%, #FF8A95 100%); background: linear-gradient(90deg, #ffe5b4 0%, #ffb199 50%, #ff8a95 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
@ -121,27 +136,69 @@ defineEmits(['update:activeTab'])
.header-tabs { .header-tabs {
display: flex; display: flex;
background: rgba(0, 0, 0, 0.15); // background: rgba(0, 0, 0, 0.15);
border-radius: 22rpx; // border-radius: 22rpx;
padding: 6rpx; padding: 6rpx;
width: 100%; width: 100%;
max-width: 500rpx; max-width: 500rpx;
height: 184rpx;
position: absolute;
right: 32rpx;
} }
.tab { .tab {
flex: 1; width: 100%;
text-align: center; display: flex;
padding: 14rpx 0; justify-content: center;
font-size: 28rpx; padding: 28rpx 0;
color: rgba(255, 255, 255, 0.75);
border-radius: 22rpx; border-radius: 22rpx;
transition: background 0.25s ease, color 0.25s ease; margin-left: 8px;
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.1519) -17.54%,
rgba(252, 100, 102, 0.31) 64.4%,
rgba(244, 88, 104, 0.31) 116.67%
);
box-shadow: 2px 2px 4px 0px #f2151578;
backdrop-filter: blur(29.299999237060547px);
//
transition:
transform 0.15s ease,
box-shadow 0.15s ease,
opacity 0.15s ease;
}
.tab-icon {
width: 160rpx;
height: 160rpx;
position: fixed;
left: 0;
top: 0;
z-index: 0;
opacity: 0.7;
}
.tab-title {
color: #ffffff;
font-size: 30rpx;
font-weight: 700;
text-shadow: 1px 4px 4px #00000054;
position: absolute;
} }
.tab-active { .tab-active {
background: linear-gradient(231deg, #A8A6ED 0%, #88C8D8 64%, #F380EF 100%); background: linear-gradient(
color: #ffffff; 90deg,
font-weight: 600; rgba(255, 222, 8, 0.4018) -17.54%,
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); rgba(252, 100, 102, 0.82) 64.4%,
rgba(244, 88, 104, 0.82) 116.67%
);
box-shadow: 2px 2px 4px 0px #f2151578;
backdrop-filter: blur(29.799999237060547px);
opacity: 0.79;
transform: translateY(-22rpx);
position: relative;
z-index: 2;
} }
</style> </style>

View File

@ -1,7 +1,11 @@
<template> <template>
<view class="exhibition-center"> <view class="exhibition-center">
<text class="section-title">展出收益中心</text> <text class="section-title">展出收益中心</text>
<image
class="chart-bg"
src="/static/dashboard/exhibition-bj.png"
mode="scaleToFill"
/>
<!-- 错误态 --> <!-- 错误态 -->
<view v-if="error" class="error-box" @tap="$emit('retry')"> <view v-if="error" class="error-box" @tap="$emit('retry')">
<text class="error-text">加载失败点击重试</text> <text class="error-text">加载失败点击重试</text>
@ -16,11 +20,13 @@
</view> </view>
<!-- 正常态 --> <!-- 正常态 -->
<view v-else> <view v-else class="content">
<!-- 顶部 3 联统计 --> <!-- 顶部 3 联统计 -->
<view class="stats-row"> <view class="stats-row">
<view class="stat-cell"> <view class="stat-cell">
<text class="stat-value">{{ data.exhibiting_count }} / {{ data.starbook_count }}</text> <text class="stat-value"
>{{ data.exhibiting_count }} / {{ data.starbook_count }}</text
>
<text class="stat-label">展出中 / 星册中</text> <text class="stat-label">展出中 / 星册中</text>
</view> </view>
<view class="stat-cell"> <view class="stat-cell">
@ -36,7 +42,7 @@
<!-- 5 行表格 --> <!-- 5 行表格 -->
<view class="table"> <view class="table">
<view class="table-header"> <view class="table-header">
<text class="th th-thumb"></text> <text class="th th-thumb">藏品</text>
<text class="th th-duration">七日展出时长</text> <text class="th th-duration">七日展出时长</text>
<text class="th th-earnings">七日收益</text> <text class="th th-earnings">七日收益</text>
<text class="th th-avg">平均收益</text> <text class="th th-avg">平均收益</text>
@ -47,13 +53,19 @@
class="table-row" class="table-row"
> >
<view class="td td-thumb"> <view class="td td-thumb">
<view class="thumb-placeholder" :class="`thumb-grad-${idx % 5}`"> <view class="thumb-placeholder">
<text class="thumb-emoji">🎨</text> <image
v-if="item.asset_thumb"
class="thumb-image"
:src="item.asset_thumb"
mode="aspectFill"
/>
<text v-else class="thumb-emoji">🎨</text>
</view> </view>
</view> </view>
<text class="td td-duration">{{ item.duration_7d }}</text> <text class="td td-duration thd">{{ item.duration_7d }}</text>
<text class="td td-earnings">{{ item.earnings_7d }}</text> <text class="td td-earnings thd">{{ item.earnings_7d }}</text>
<text class="td td-avg">{{ item.avg_earnings }} / H</text> <text class="td td-avg thd">{{ item.avg_earnings }} / H</text>
</view> </view>
</view> </view>
</view> </view>
@ -65,18 +77,38 @@ defineProps({
data: { type: Object, default: null }, // ExhibitionIncomeSummary data: { type: Object, default: null }, // ExhibitionIncomeSummary
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
error: { type: String, default: null }, error: { type: String, default: null },
}) });
defineEmits(['retry']) defineEmits(["retry"]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.exhibition-center { .exhibition-center {
background: rgba(255, 255, 255, 0.12); background:
backdrop-filter: blur(10px); linear-gradient(
106.77deg,
rgba(255, 223, 119, 0.33) -9.76%,
rgba(132, 255, 210, 0.33) 44.65%,
rgba(255, 129, 131, 0.33) 117.82%
),
linear-gradient(0deg, rgba(0, 75, 238, 0.2), rgba(0, 75, 238, 0.2));
// backdrop-filter: blur(10px);
border-radius: 22rpx; border-radius: 22rpx;
padding: 24rpx; padding: 12rpx;
margin: 24rpx 0; margin: 12rpx 0;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
position: relative;
overflow: hidden;
// [3] bj.png opacity
&::before {
content: "";
position: absolute;
inset: 0;
background: url("/static/dashboard/bj.png") center / cover no-repeat;
opacity: 0.2; // 0=1=
pointer-events: none;
z-index: 0;
}
} }
.section-title { .section-title {
@ -86,22 +118,55 @@ defineEmits(['retry'])
color: #ffffff; color: #ffffff;
margin-bottom: 24rpx; margin-bottom: 24rpx;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2;
}
.chart-bg {
position: absolute;
top: -24rpx;
left: -16rpx;
width: 224rpx;
height: 224rpx;
z-index: 0; // (z:2)(z:1)
pointer-events: none; // tap穿 canvas
opacity: 0.5;
transform: rotate(60deg);
}
.content {
position: relative;
z-index: 2;
} }
.stats-row { .stats-row {
display: flex; display: flex;
background: rgba(255, 255, 255, 0.08); justify-content: space-between;
// background: rgba(255, 255, 255, 0.08);
border-radius: 17rpx; border-radius: 17rpx;
padding: 24rpx 0; padding: 24rpx 0;
margin-bottom: 24rpx; margin-bottom: 24rpx;
} }
.stat-cell { .stat-cell {
flex: 1; min-width: 192rpx;
display: flex; height: 80rpx;
flex-direction: column; position: relative;
align-items: center; box-sizing: border-box;
border-right: 1px solid rgba(255, 255, 255, 0.1); padding: 8rpx 12rpx;
border-top-left-radius: 14px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
opacity: 1;
background: linear-gradient(
106.77deg,
rgba(255, 223, 119, 0.5) -9.76%,
rgba(185, 132, 255, 0.5) 44.65%,
rgba(255, 129, 131, 0.5) 117.82%
);
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
&:last-child { &:last-child {
border-right: none; border-right: none;
@ -109,20 +174,28 @@ defineEmits(['retry'])
} }
.stat-value { .stat-value {
position: absolute;
bottom: 4rpx;
right: 12rpx;
font-size: 32rpx; font-size: 32rpx;
font-weight: 700; font-weight: 600;
color: #FFFABD; color: #fffabd;
font-family: 'Baloo Bhai', sans-serif; text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
margin-bottom: 8rpx;
font-family: "Baloo Bhai", sans-serif;
} }
.stat-label { .stat-label {
font-size: 22rpx; position: absolute;
color: rgba(255, 255, 255, 0.7); top: 6rpx;
left: 12rpx;
font-size: 20rpx;
color: rgba(255, 255, 255, 1);
} }
.table { .table {
background: rgba(255, 255, 255, 0.05); background: rgba(121, 120, 215, 0.23);
box-shadow: 0px 4px 4px 0px rgba(96, 13, 13, 0.25);
border-radius: 14rpx; border-radius: 14rpx;
overflow: hidden; overflow: hidden;
} }
@ -132,7 +205,7 @@ defineEmits(['retry'])
display: flex; display: flex;
align-items: center; align-items: center;
padding: 16rpx 12rpx; padding: 16rpx 12rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.06); // border-bottom: 1px solid rgba(255, 255, 255, 0.06);
} }
.table-row:last-child { .table-row:last-child {
@ -141,8 +214,10 @@ defineEmits(['retry'])
.th, .th,
.td { .td {
font-size: 24rpx; font-size: 20rpx;
font-weight: 500;
color: #ffffff; color: #ffffff;
text-shadow: 0px 0px 4px rgba(164, 60, 60, 1);
text-align: center; text-align: center;
} }
@ -165,35 +240,55 @@ defineEmits(['retry'])
flex-shrink: 0; flex-shrink: 0;
} }
.th { // .thd flex + 32rpx padding
font-size: 20rpx; // .td.thd .td-duration / .td-earnings / .td-avg flex/width
color: rgba(255, 255, 255, 0.6); .td.thd {
flex: 0 0 auto;
width: auto;
padding: 8rpx 32rpx;
background: linear-gradient(
90deg,
rgba(27, 175, 238, 0.15) 0%,
rgba(255, 204, 20, 0.15) 100%
);
border-radius: 7px;
} }
// thumb + 3 chip space-between chip
// header flex
.table-row {
justify-content: space-between;
}
// .th {
// font-size: 20rpx;
// color: rgba(255, 255, 255, 0.6);
// }
.thumb-placeholder { .thumb-placeholder {
width: 56rpx; width: 40rpx;
height: 56rpx; height: 56rpx;
border-radius: 3rpx; border-radius: 3rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0 auto; margin: 0 auto;
overflow: hidden; // image
transform: rotate(-10deg);
box-shadow: 2px 2px 4.5px 2px rgba(230, 46, 46, 0.46);
// backdrop-filter: blur(0px);
} }
.thumb-grad-0 { background: linear-gradient(135deg, #FF6B6B 0%, #FFB199 100%); } .thumb-image {
.thumb-grad-1 { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); } width: 100%;
.thumb-grad-2 { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); } height: 100%;
.thumb-grad-3 { background: linear-gradient(135deg, #FFE066 0%, #FFB199 100%); } display: block;
.thumb-grad-4 { background: linear-gradient(135deg, #C5C5C5 0%, #8C8C8C 100%); }
.thumb-emoji {
font-size: 32rpx;
} }
.td-earnings { .td-earnings {
color: #FFFABD; color: #fffabd;
font-weight: 600; font-weight: 600;
font-family: 'Baloo Bhai', sans-serif; font-family: "Baloo Bhai", sans-serif;
} }
/* 骨架 */ /* 骨架 */
@ -206,7 +301,12 @@ defineEmits(['retry'])
.skeleton-stats { .skeleton-stats {
height: 120rpx; height: 120rpx;
border-radius: 17rpx; border-radius: 17rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 25%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.05) 75%); background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.05) 25%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.05) 75%
);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
@ -220,14 +320,23 @@ defineEmits(['retry'])
.skeleton-row { .skeleton-row {
height: 80rpx; height: 80rpx;
border-radius: 14rpx; border-radius: 14rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 25%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.05) 75%); background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.05) 25%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.05) 75%
);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
@keyframes shimmer { @keyframes shimmer {
0% { background-position: 200% 0; } 0% {
100% { background-position: -200% 0; } background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} }
.error-box { .error-box {

View File

@ -1,14 +1,21 @@
<template> <template>
<view class="income-curve-card"> <view class="income-curve-card">
<text class="curve-title">七日收益曲线</text> <text class="curve-title">七日收益曲线</text>
<image
class="chart-bg"
src="/static/dashboard/ucharts-bj.png"
mode="scaleToFill"
/>
<!-- 错误态 --> <!-- 错误态 -->
<view v-if="error" class="error-box" @tap="$emit('retry')"> <view v-if="error" class="error-box" @tap="$emit('retry')">
<text class="error-text">加载失败点击重试</text> <text class="error-text">加载失败点击重试</text>
</view> </view>
<!-- 骨架态 --> <!-- 骨架态 -->
<view v-else-if="loading || !points || points.length === 0" class="skeleton-chart"></view> <view
v-else-if="loading || !points || points.length === 0"
class="skeleton-chart"
></view>
<!-- 图表 --> <!-- 图表 -->
<view v-else class="chart-wrap"> <view v-else class="chart-wrap">
@ -19,129 +26,154 @@
:chartData="chartData" :chartData="chartData"
:ontouch="true" :ontouch="true"
:onmovetip="true" :onmovetip="true"
canvas2d :in-scroll-view="true"
:tooltipShow="true"
:canvas2d="false"
canvasId="incomeCurveCanvas"
:canvasHeight="240"
ontap ontap
@complete="handleChartComplete" @tap="onChartTap"
/> />
<!-- 默认指向最后一天的指示器位置来自 uCharts calPoints跟图表完全对齐 -->
<view
v-if="latestPoint && latestCalPoint"
class="latest-indicator"
:style="{
left: latestCalPoint.x / pixel + 'px',
top: latestCalPoint.y / pixel + 'px',
}"
>
<view class="latest-line"></view>
<view class="latest-dot"></view>
<view class="latest-tooltip">
<text class="latest-value">+{{ latestPoint.income }}</text>
<text class="latest-date">{{ latestPoint.date.slice(5) }}</text>
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref, watch } from "vue";
// import easycom // import easycom
import QiunDataCharts from '@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue' import QiunDataCharts from "@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue";
const props = defineProps({ const props = defineProps({
points: { type: Array, default: () => [] }, points: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
error: { type: String, default: null }, error: { type: String, default: null },
}) });
defineEmits(['retry']) defineEmits(["retry"]);
// canvas2d opts.pix=1 canvas2d opts.pix=systemInfo.pixelRatio // tap
// emit opts 1 const currentIndex = ref(0);
const pixel = ref(1) watch(
const chartOptsRef = ref(null) () => props.points.length,
const calPoints = ref([]) (len) => {
currentIndex.value = Math.max(0, len - 1);
},
{ immediate: true },
);
// @completeuCharts emit opts.chartData.calPoints canvas const onChartTap = (e) => {
function handleChartComplete(e) { const idx = e?.index;
const opts = e?.opts if (typeof idx === "number" && idx >= 0 && idx < props.points.length) {
if (!opts) return currentIndex.value = idx;
chartOptsRef.value = opts }
pixel.value = opts.pix || 1 };
calPoints.value = opts.chartData?.calPoints || []
}
// chartOpts.padding / chart-canvas rpx
const CHART_HEIGHT = 240
const PADDING = [16, 16, 8, 16] // top, right, bottom, left
const peak = computed(() => props.points.find((p) => p.is_peak) || null)
const latestPoint = computed(() =>
props.points.length > 0 ? props.points[props.points.length - 1] : null
)
// canvas uCharts @complete calPoints
// calPoints calPoints[seriesIndex][dataIndex] = { x, y }y canvas
const latestCalPoint = computed(() => {
if (!latestPoint.value || calPoints.value.length === 0) return null
const seriesPoints = calPoints.value[0]
if (!seriesPoints || seriesPoints.length === 0) return null
return seriesPoints[seriesPoints.length - 1] || null
})
const chartData = computed(() => { const chartData = computed(() => {
const categories = props.points.map((p) => p.date.slice(5)) const categories = props.points.map((p) => p.date.slice(5));
const lineData = props.points.map((p) => p.income) const lineData = props.points.map((p) => p.income);
return { return {
categories, categories,
series: [ series: [
{ name: '收益', type: 'area', data: lineData, color: '#1BAFEE' }, {
name: "收益",
type: "area",
data: lineData,
color: "#1BAFEE",
// [] u-charts.js patchseries.linearColor
// CSS: linear-gradient(90deg, #D1EFFFBC 0%, #E7E8A9D8 49.52%, #FF9CE5FA 100%)
linearColor: [
[0, "rgba(209, 239, 255, 0.735)"],
[0.4952, "rgba(231, 232, 169, 0.846)"],
[1, "rgba(255, 156, 229, 0.98)"],
],
linearDirection: "horizontal", // 'horizontal'() | 'vertical'
},
], ],
} };
}) });
const PADDING = [16, -16, 8, -16]; // top, right, bottom, left
const chartOpts = { const chartOpts = {
color: ['#1BAFEE'], color: ["#1BAFEE"],
padding: PADDING, padding: PADDING,
dataLabel: false, dataLabel: false,
legend: { show: false }, legend: { show: false },
// 线 // 线
dataPointShape: false, dataPointShape: false,
xAxis: { disabled: true, disableGrid: true, axisLine: false, fontColor: '#FFFFFF', fontSize: 9 }, xAxis: {
disabled: true,
disableGrid: true,
axisLine: false,
fontColor: "#FFFFFF",
fontSize: 9,
},
yAxis: { yAxis: {
disabled: true, disabled: true,
showTitle: false, showTitle: false,
data: [{ min: 0, disabled: true, axisLine: false }], data: [{ min: 0, disabled: true, axisLine: false }],
disableGrid: true, disableGrid: true,
fontColor: '#FFFFFF', fontColor: "#FFFFFF",
fontSize: 9, fontSize: 9,
}, },
extra: { extra: {
tooltip: { tooltip: {
showBox: true, showBox: true,
showArrow: true, showArrow: true,
showCategory: true, showCategory: false,
bgColor: '#000000', bgColor: "#000000",
bgOpacity: 0.6, bgOpacity: 0.6,
fontColor: '#FFFFFF', fontColor: "#FFFFFF",
fontSize: 11, fontSize: 11,
splitLine: true, splitLine: true,
horizentalLine: { type: 'dash', width: 1, color: '#FFFFFF' }, horizentalLine: { type: "dash", width: 1, color: "#FFFFFF" },
// uCharts tooltip index
// index < 0 tooltip
tooltipCustom: (_opts, _categories, index) => {
if (typeof index !== "number" || index < 0) {
return { textList: [] };
}
const point = props.points[index];
if (!point) return { textList: [] };
return {
textList: [
{ text: `+${point.income}`, color: "#1BAFEE" },
{ text: point.date.slice(5), color: "#999999" },
],
};
},
},
// area 线 series.linearType=custom + linearColor
// addLine:false 线线
area: {
type: "curve",
opacity: 1,
addLine: false,
width: 2,
gradient: true,
activeType: "hollow",
}, },
// area 线gradient: true 线
area: { type: 'curve', opacity: 0.3, addLine: true, width: 2, gradient: true, activeType: 'hollow' },
}, },
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.income-curve-card { .income-curve-card {
background: linear-gradient(135deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%); background: linear-gradient(
107.96deg,
rgba(255, 223, 119, 0.59) -28.33%,
rgba(142, 149, 226, 0.59) 44.4%,
rgba(244, 140, 255, 0.59) 117.77%
);
border-radius: 22rpx; border-radius: 22rpx;
padding: 24rpx; padding: 24rpx;
margin: 24rpx 0; box-sizing: border-box; // padding
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25); box-shadow: 0px 4px 4px 0px #BD323240;
min-height: 360rpx; position: relative;
height: 256rpx;
display: flex;
flex-direction: column;
overflow: hidden; //
} }
.curve-title { .curve-title {
@ -149,80 +181,51 @@ const chartOpts = {
font-size: 28rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
margin-bottom: 16rpx; margin-bottom: 4rpx; //
text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2); text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2; //
} }
.chart-wrap { .chart-wrap {
background: rgba(255, 255, 255, 0.15); position: relative;
flex: 1; //
min-height: 0; // flex
border-radius: 17rpx; border-radius: 17rpx;
padding: 16rpx; overflow: hidden;
backdrop-filter: blur(10px); z-index: 1;
// backdrop-filter: blur(10px);
}
.chart-bg {
position: absolute;
bottom: 0;
left: 0;
width: 224rpx;
height: 100%;
z-index: 0; // (z:2)(z:1)
pointer-events: none; // tap穿 canvas
opacity: 0.4;
} }
.chart-canvas { .chart-canvas {
position: relative; position: relative;
width: 100%; width: 100%;
height: 240rpx; height: 100%; // .chart-wrap
} // drop-shadow 沿 canvas alpha 线
// CSS: box-shadow: -1px -5px 4px 0px #CF232338 (#CF2323 + alpha 0x38 0.22)
.latest-indicator { filter: drop-shadow(-1px -5px 4px rgba(207, 35, 35, 0.22));
position: absolute;
z-index: 2;
// left/top translate(-50%, -100%) tooltip indicator
transform: translate(-50%, -100%);
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.latest-line {
width: 1rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.7);
margin-bottom: -2rpx;
align-self: center;
}
.latest-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #1BAFEE;
border: 2rpx solid #ffffff;
align-self: center;
margin-bottom: 6rpx;
}
.latest-tooltip {
background: rgba(0, 0, 0, 0.6);
border-radius: 8rpx;
padding: 6rpx 12rpx;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 4rpx;
}
.latest-value {
font-size: 24rpx;
font-weight: 700;
color: #FFFABD;
line-height: 1.2;
}
.latest-date {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.85);
line-height: 1.2;
margin-top: 2rpx;
} }
.skeleton-chart { .skeleton-chart {
height: 360rpx; height: 360rpx;
border-radius: 17rpx; border-radius: 17rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%); background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.1) 25%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.1) 75%
);
background-size: 200% 100%; background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite; animation: skeleton-shimmer 1.5s infinite;
} }
@ -243,7 +246,11 @@ const chartOpts = {
} }
@keyframes skeleton-shimmer { @keyframes skeleton-shimmer {
0% { background-position: 200% 0; } 0% {
100% { background-position: -200% 0; } background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} }
</style> </style>

View File

@ -5,71 +5,141 @@
<text class="empty-text">暂无数据</text> <text class="empty-text">暂无数据</text>
</view> </view>
<view v-else class="ring-row"> <view v-else class="ring-row">
<view <view v-for="item in items" :key="item.level" class="ring-cell">
v-for="item in items" <view class="ring-chart">
:key="item.level" <qiun-data-charts
class="ring-cell" type="arcbar"
> :opts="getOpts(item)"
<view :chartData="getChartData(item)"
class="ring-outer" :canvasId="`arcbar-${item.level}`"
:style="getRingStyle(item)" :canvas2d="false"
> :ontouch="false"
<view class="ring-inner"> :in-scroll-view="true"
:tooltipShow="false"
/>
<!--
中心数字用 HTML 覆盖层而非 ucharts title
ucharts canvas 文本fillText不支持 text-shadow
覆盖层可以走 CSS text-shadow
-->
<view class="ring-center" pointer-events="none">
<text class="ring-count">{{ item.count }}</text> <text class="ring-count">{{ item.count }}</text>
<text class="ring-count ring-text">{{ getPercent(item) }}%</text>
<image
class="ring-label-img"
:src="getGradeBadge(item.level)"
mode="aspectFit"
/>
</view> </view>
</view> </view>
<text class="ring-label">{{ item.level }}</text>
<text class="ring-pct">{{ getPercent(item) }}%</text>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import QiunDataCharts from "@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue";
const props = defineProps({ const props = defineProps({
items: { type: Array, default: () => [] }, // AssetLevelItem[] items: { type: Array, default: () => [] }, // AssetLevelItem[]
}) });
// conic-gradient
const COLOR_MAP = {
UR: { start: "#FF9C84", end: "#88F4EB" },
SSR: { start: "#FF6640", end: "#B5F488" },
SR: { start: "#DFFF5E", end: "#F48896" },
R: { start: "#8FA9FF", end: "#4FFFF0" },
N: { start: "#C5C5C5", end: "#8C8C8C" },
};
// item.level ('UR'/'SSR'/'SR'/'R'/'N')
// UR URengji.png d
const GRADE_BADGE_MAP = {
N: "/static/starbookcontent/grade/Ndengji.png",
R: "/static/starbookcontent/grade/Rdengji.png",
SR: "/static/starbookcontent/grade/SRdengji.png",
SSR: "/static/starbookcontent/grade/SSRdengji.png",
UR: "/static/starbookcontent/grade/URengji.png",
};
const getGradeBadge = (level) => GRADE_BADGE_MAP[level] || GRADE_BADGE_MAP.N;
function getPercent(item) { function getPercent(item) {
if (!item.total) return 0 if (!item.total) return 0;
return Math.round((item.count / item.total) * 100) return Math.round((item.count / item.total) * 100);
} }
function getRingStyle(item) { function getChartData(item) {
const pct = getPercent(item) const pct = getPercent(item);
const colorMap = {
UR: '#FF8A65, #FFD740',
SSR: '#FF5E9C, #FFB199',
SR: '#B17BFF, #FF8FE6',
R: '#5EDFFF, #6FA9FF',
N: '#C5C5C5, #8C8C8C',
}
const colors = colorMap[item.level] || '#999, #ccc'
// 0
if (pct === 0) {
return {
background: 'conic-gradient(rgba(255,255,255,0.1) 0deg, rgba(255,255,255,0.1) 360deg)',
}
}
// pct%
return { return {
background: `conic-gradient(${colors} 0deg ${pct * 3.6}deg, rgba(255,255,255,0.1) ${pct * 3.6}deg 360deg)`, series: [
} {
name: item.level,
color: COLOR_MAP[item.level]?.start || "#999",
data: pct / 100,
},
],
};
}
function getOpts(item) {
const colors = COLOR_MAP[item.level] || { start: "#999", end: "#ccc" };
return {
color: [colors.start],
padding: [0, 0, 0, 0],
dataLabel: false,
legend: { show: false },
title: {
// .ring-center text-shadow
name: "",
fontSize: 0,
color: "transparent",
},
subtitle: {
name: "",
fontSize: 0,
color: "transparent",
},
extra: {
arcbar: {
// 'default' u-charts.js if startAngleendAngle
// 90° ""
type: "default",
width: 5,
// [PATCH] u-charts.js backgroundLinearType
// backgroundColor
backgroundColor: "",
// CSS linear-gradient(36.72deg, rgba(199,244,255,.45) 13.45%, rgba(233,193,255,.45) 79.47%)
backgroundLinearType: "custom",
backgroundLinearAngle: 36.72,
backgroundLinearColor: [
[0.1345, "rgba(199, 244, 255, 0.45)"],
[0.7947, "rgba(233, 193, 255, 0.45)"],
],
startAngle: 0.75, // 12
endAngle: 0.25, // 6 3/4
gap: 0,
// fillColor = linear-gradient(left=color, right=customColor[0])
linearType: "custom",
customColor: [colors.end],
},
},
};
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.level-distribution { .level-distribution {
background: rgba(255, 255, 255, 0.1); background: rgba(121, 120, 215, 0.31);
border-radius: 17rpx; border-radius: 17rpx;
padding: 20rpx; padding: 20rpx;
margin-top: 16rpx; margin-top: 16rpx;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); box-shadow: 0px 4px 4px 0px rgba(96, 13, 13, 0.25);
} }
.card-title { .card-title {
display: block; display: block;
font-size: 28rpx; font-size: 24rpx;
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
margin-bottom: 16rpx; margin-bottom: 16rpx;
@ -89,46 +159,50 @@ function getRingStyle(item) {
flex: 1; flex: 1;
} }
.ring-outer { .ring-chart {
width: 80rpx; width: 112rpx;
height: 80rpx; height: 112rpx;
border-radius: 50%; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; // conic-gradient canvas
box-shadow: 0 0 12px rgba(255, 255, 255, 0.15); filter: drop-shadow(0 0 12rpx rgba(255, 255, 255, 0.15));
} }
.ring-inner { // HTML text-shadowcanvas
width: 60rpx; .ring-center {
height: 60rpx; position: absolute;
border-radius: 50%; top: 0;
background: rgba(0, 0, 0, 0.3); left: 0;
right: 0;
bottom: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
backdrop-filter: blur(4px); z-index: 1;
flex-direction: column;
} }
.ring-count { .ring-count {
font-size: 24rpx;
font-weight: 700;
color: #FFFABD;
font-family: 'Baloo Bhai', sans-serif;
}
.ring-label {
margin-top: 8rpx;
font-size: 20rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.ring-pct {
font-size: 18rpx; font-size: 18rpx;
color: rgba(255, 255, 255, 0.6); font-weight: 700;
margin-top: 2rpx; font-family: "Baloo Bhai", sans-serif;
line-height: 1;
color: rgba(255, 241, 163, 1);
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
}
.ring-text{
margin: 8rpx 0;
}
.ring-label-img {
width: 40rpx;
height: 40rpx;
margin-top: 4rpx;
position: absolute;
bottom: -8rpx;
} }
.empty-row { .empty-row {

View File

@ -1,7 +1,11 @@
<template> <template>
<view class="like-income-board"> <view class="like-income-board">
<text class="section-title">点赞收益看板</text> <text class="section-title">点赞收益看板</text>
<image
class="chart-bg"
src="/static/dashboard/liked-bj.png"
mode="scaleToFill"
/>
<!-- 错误/骨架态 --> <!-- 错误/骨架态 -->
<view v-if="error" class="error-box" @tap="$emit('retry')"> <view v-if="error" class="error-box" @tap="$emit('retry')">
<text class="error-text">加载失败点击重试</text> <text class="error-text">加载失败点击重试</text>
@ -27,17 +31,28 @@
<!-- 右侧等级列表 --> <!-- 右侧等级列表 -->
<view class="right-list"> <view class="right-list">
<view <view class="level-header">
v-for="(item, idx) in levels" <text class="th th-thumb">藏品</text>
:key="idx" <text class="th th-name">等级</text>
class="level-row" <text class="th th-income">累计收益</text>
> </view>
<view v-for="(item, idx) in levels" :key="idx" class="level-row">
<view class="level-thumb"> <view class="level-thumb">
<view class="thumb-circle" :class="`level-${item.level}`"> <image
<text class="thumb-letter">{{ item.level }}</text> v-if="item.thumb"
</view> class="thumb-asset-img"
:src="item.thumb"
mode="aspectFill"
/>
<text v-else class="thumb-emoji">🎨</text>
</view>
<view class="level-name">
<image
class="level-badge-img"
:src="getGradeBadge(item.level)"
mode="aspectFit"
/>
</view> </view>
<text class="level-name">{{ item.level }}</text>
<text class="level-income">{{ item.total_income }}</text> <text class="level-income">{{ item.total_income }}</text>
</view> </view>
</view> </view>
@ -51,18 +66,73 @@ defineProps({
levels: { type: Array, default: () => [] }, levels: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
error: { type: String, default: null }, error: { type: String, default: null },
}) });
defineEmits(['retry']) defineEmits(["retry"]);
// item.level ('UR'/'SSR'/'SR'/'R'/'N')
// UR URengji.png d
const GRADE_BADGE_MAP = {
N: "/static/starbookcontent/grade/Ndengji.png",
R: "/static/starbookcontent/grade/Rdengji.png",
SR: "/static/starbookcontent/grade/SRdengji.png",
SSR: "/static/starbookcontent/grade/SSRdengji.png",
UR: "/static/starbookcontent/grade/URengji.png",
};
const getGradeBadge = (level) => GRADE_BADGE_MAP[level] || GRADE_BADGE_MAP.N;
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.like-income-board { .like-income-board {
background: rgba(255, 255, 255, 0.12); background:
backdrop-filter: blur(10px); linear-gradient(
border-radius: 22rpx; 106.77deg,
padding: 24rpx; rgba(255, 223, 119, 0.13) -9.76%,
margin: 24rpx 0; rgba(132, 255, 210, 0.13) 44.65%,
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); rgba(255, 129, 131, 0.13) 117.82%
),
linear-gradient(0deg, rgba(249, 69, 69, 0.5), rgba(249, 69, 69, 0.5));
// bj.png ::before opacity
border-top-left-radius: 17px;
border-top-right-radius: 14px;
border-bottom-right-radius: 14px;
border-bottom-left-radius: 14px;
opacity: 1;
position: relative;
padding: 12rpx;
margin: 12rpx 0;
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
overflow: hidden;
// [3] bj.png opacity
&::before {
content: "";
position: absolute;
inset: 0;
background: url("/static/dashboard/bj.png") center / cover no-repeat;
opacity: 0.2; // 0=1=
pointer-events: none;
z-index: 0;
}
}
// ::before
.board-row,
.error-box,
.skeleton-board {
position: relative;
z-index: 1;
}
.chart-bg {
position: absolute;
top: -24rpx;
left: -16rpx;
width: 224rpx;
height: 224rpx;
z-index: 1; // ::before(z:0) (z:2) (z:1)
pointer-events: none; // tap穿 canvas
opacity: 0.44;
// transform: rotate(60deg);
} }
.section-title { .section-title {
@ -72,6 +142,8 @@ defineEmits(['retry'])
color: #ffffff; color: #ffffff;
margin-bottom: 24rpx; margin-bottom: 24rpx;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2;
} }
.board-row { .board-row {
@ -90,32 +162,84 @@ defineEmits(['retry'])
} }
.stat-block { .stat-block {
display: flex; min-width: 264rpx;
flex-direction: column; height: 98rpx;
align-items: center; width: 147;
border-top-left-radius: 14px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-bottom-left-radius: 12px;
position: relative;
box-sizing: border-box;
padding: 8rpx 16rpx;
background: linear-gradient(
106.77deg,
rgba(255, 223, 119, 0.43) -9.76%,
rgba(185, 132, 255, 0.43) 44.65%,
rgba(255, 129, 131, 0.43) 117.82%
);
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
} }
.stat-num { .stat-num {
font-size: 40rpx; position: absolute;
bottom: 4rpx;
right: 16rpx;
font-size: 48rpx;
font-weight: 700; font-weight: 700;
color: #FFFABD; color: #fffabd;
font-family: 'Baloo Bhai', sans-serif; font-family: "Baloo Bhai", sans-serif;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84); text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
margin-bottom: 6rpx;
} }
.stat-text { .stat-text {
position: absolute;
top: 6rpx;
left: 16rpx;
font-size: 22rpx; font-size: 22rpx;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.85);
} }
.right-list { .right-list {
flex: 1.5; width: 320rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12rpx; gap: 12rpx;
} }
// / gap / padding .level-row
.level-header {
display: flex;
align-items: center;
padding: 4rpx 12rpx;
gap: 16rpx;
}
.th {
font-size: 20rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
text-shadow: 0px 0px 4px rgba(164, 60, 60, 1);
display: flex;
justify-content: center;
}
.th-thumb {
width: 56rpx;
flex-shrink: 0; // .level-thumb
text-align: center;
}
.th-name {
flex: 0 0 100rpx; // .level-name
text-align: center;
}
.th-income {
flex: 1; // .level-income
text-align: center; //
}
.level-row { .level-row {
display: flex; display: flex;
align-items: center; align-items: center;
@ -126,48 +250,57 @@ defineEmits(['retry'])
} }
.level-thumb { .level-thumb {
width: 56rpx; width: 40rpx;
height: 56rpx; height: 56rpx;
flex-shrink: 0; flex-shrink: 0;
} border-radius: 6rpx;
overflow: hidden;
.thumb-circle {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.2); box-shadow: 2px 2px 4.5px 2px rgba(230, 46, 46, 0.46);
transform: rotate(-10deg);
// backdrop-filter: blur(0px);
} }
.thumb-letter { // .level-thumb
font-size: 18rpx; .thumb-asset-img {
font-weight: 700; width: 100%;
color: #ffffff; height: 100%;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); display: block;
} }
.level-UR { background: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%); } // emoji
.level-SSR { background: linear-gradient(135deg, #FF5E9C 0%, #FFB199 100%); } .thumb-emoji {
.level-SR { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); } font-size: 28rpx;
.level-R { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); } }
.level-N { background: linear-gradient(135deg, #C5C5C5 0%, #8C8C8C 100%); }
.level-name { .level-name {
flex: 0 0 60rpx; flex: 0 0 96rpx; // .th-name
font-size: 22rpx; display: flex;
font-weight: 600; align-items: center;
color: rgba(255, 255, 255, 0.9); justify-content: flex-end; //
}
//
.level-badge-img {
width: 64rpx;
height: 64rpx;
} }
.level-income { .level-income {
flex: 1; flex: 0 0 120rpx;
background: linear-gradient(
90deg,
rgba(27, 175, 238, 0.19) 0%,
rgba(255, 204, 20, 0.19) 100%
);
font-size: 28rpx; font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #FFFABD; border-radius: 7px;
font-family: 'Baloo Bhai', sans-serif; color: #fffabd;
text-align: right; font-family: "Baloo Bhai", sans-serif;
text-align: center; //
} }
/* 骨架/错误 */ /* 骨架/错误 */
@ -181,7 +314,12 @@ defineEmits(['retry'])
.skeleton-list { .skeleton-list {
height: 200rpx; height: 200rpx;
border-radius: 17rpx; border-radius: 17rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 25%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.05) 75%); background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.05) 25%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.05) 75%
);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
@ -202,7 +340,11 @@ defineEmits(['retry'])
} }
@keyframes shimmer { @keyframes shimmer {
0% { background-position: 200% 0; } 0% {
100% { background-position: -200% 0; } background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} }
</style> </style>

View File

@ -1,27 +1,35 @@
<template> <template>
<view class="recent-upgrades"> <view class="recent-upgrades">
<text class="card-title">最近升级</text> <text class="card-title">即将升级藏品</text>
<view v-if="!items || items.length === 0" class="empty-row"> <view v-if="!items || items.length === 0" class="empty-row">
<text class="empty-text">暂无数据</text> <text class="empty-text">暂无数据</text>
</view> </view>
<view v-else class="upgrades-list"> <view v-else class="upgrades-list">
<view <view v-for="item in items" :key="item.asset_id" class="upgrade-row">
v-for="item in items"
:key="item.asset_id"
class="upgrade-row"
>
<view class="upgrade-thumb"> <view class="upgrade-thumb">
<view class="thumb-circle" :style="{ background: 'linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%)' }"> <image
<text class="thumb-letter">{{ item.asset_name[0] }}</text> v-if="item.asset_thumb"
</view> class="upgrade-thumb-img"
:src="item.asset_thumb"
mode="aspectFill"
/>
<text v-else class="thumb-emoji">{{ item.asset_name[0] }}</text>
<!-- 等级徽章叠加在左上角显示升级前的等级 -->
<image
class="upgrade-thumb-badge"
:src="getGradeBadge(getPreviousLevel(item.new_level))"
mode="aspectFit"
/>
</view> </view>
<view class="upgrade-info"> <view class="upgrade-info">
<text class="upgrade-name">{{ item.asset_name }}</text> <text class="lv-up">Lv <text class="lv-up lv-up-text">UP</text></text>
<text class="upgrade-time">{{ formatTime(item.upgrade_time) }}</text>
</view> </view>
<view class="level-badge" :class="`level-${item.new_level}`"> <view class="level-badge">
<text class="level-letter">{{ item.new_level }}</text> <image
<text class="lv-up">Lv UP</text> class="level-badge-img"
:src="getGradeBadge(item.new_level)"
mode="aspectFit"
/>
</view> </view>
</view> </view>
</view> </view>
@ -31,28 +39,52 @@
<script setup> <script setup>
defineProps({ defineProps({
items: { type: Array, default: () => [] }, // RecentLevelUpItem[] items: { type: Array, default: () => [] }, // RecentLevelUpItem[]
}) });
// item.new_level ('UR'/'SSR'/'SR'/'R'/'N')
// UR URengji.png d
const GRADE_BADGE_MAP = {
N: "/static/starbookcontent/grade/Ndengji.png",
R: "/static/starbookcontent/grade/Rdengji.png",
SR: "/static/starbookcontent/grade/SRdengji.png",
SSR: "/static/starbookcontent/grade/SSRdengji.png",
UR: "/static/starbookcontent/grade/URengji.png",
};
const getGradeBadge = (level) => GRADE_BADGE_MAP[level] || GRADE_BADGE_MAP.N;
// N < R < SR < SSR < UR level ""
const LEVEL_ORDER = ["N", "R", "SR", "SSR", "UR"];
const getPreviousLevel = (level) => {
const idx = LEVEL_ORDER.indexOf(level);
return idx > 0 ? LEVEL_ORDER[idx - 1] : LEVEL_ORDER[0];
};
function formatTime(ts) { function formatTime(ts) {
const now = Date.now() const now = Date.now();
const diff = now - ts const diff = now - ts;
if (diff < 60 * 60 * 1000) return `${Math.floor(diff / 60000)} 分钟前` if (diff < 60 * 60 * 1000) return `${Math.floor(diff / 60000)} 分钟前`;
if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / 3600000)} 小时前` if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / 3600000)} 小时前`;
return `${Math.floor(diff / 86400000)} 天前` return `${Math.floor(diff / 86400000)} 天前`;
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.recent-upgrades { .recent-upgrades {
background: rgba(255, 255, 255, 0.1); background: linear-gradient(
border-radius: 17rpx; 106.77deg,
padding: 20rpx; rgba(255, 223, 119, 0.24) -9.76%,
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); rgba(185, 132, 255, 0.24) 44.65%,
rgba(255, 129, 131, 0.24) 117.82%
);
border-radius: 14px;
padding: 16rpx;
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
} }
.card-title { .card-title {
display: block; display: block;
font-size: 28rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
margin-bottom: 16rpx; margin-bottom: 16rpx;
@ -75,32 +107,48 @@ function formatTime(ts) {
.upgrade-thumb { .upgrade-thumb {
width: 56rpx; width: 56rpx;
height: 56rpx; height: 67rpx;
flex-shrink: 0; flex-shrink: 0;
}
.thumb-circle {
width: 100%;
height: 100%;
border-radius: 12rpx; border-radius: 12rpx;
// overflow: hidden; // image
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.2); position: relative; // .upgrade-thumb-badge
transform: rotate(-10deg);
box-shadow: 0px 4px 4px 0px rgba(214, 53, 53, 0.43);
} }
.thumb-letter { //
font-size: 22rpx; .upgrade-thumb-badge {
position: absolute;
top: -8rpx;
left: -8rpx;
width: 28rpx;
height: 28rpx;
z-index: 1;
filter: drop-shadow(0 0 2rpx rgba(0, 0, 0, 0.5)); //
}
// .upgrade-thumb
.upgrade-thumb-img {
width: 100%;
height: 100%;
display: block;
}
// emoji
.thumb-emoji {
font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #ffffff; color: #ffffff;
} }
.upgrade-info { .upgrade-info {
flex: 1; flex: 0 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4rpx; margin-left: 24rpx;
min-width: 0;
} }
.upgrade-name { .upgrade-name {
@ -121,28 +169,25 @@ function formatTime(ts) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 6rpx 12rpx; padding: 4rpx 6rpx;
border-radius: 8rpx;
flex-shrink: 0; flex-shrink: 0;
} }
.level-UR { background: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%); } //
.level-SSR { background: linear-gradient(135deg, #FF5E9C 0%, #FFB199 100%); } .level-badge-img {
.level-SR { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); } width: 80rpx;
.level-R { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); } height: 80rpx;
.level-N { background: linear-gradient(135deg, #C5C5C5 0%, #8C8C8C 100%); }
.level-letter {
font-size: 20rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
} }
.lv-up { .lv-up {
font-size: 14rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 241, 163, 1);
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
}
.lv-up-text {
font-size: 40rpx;
} }
.empty-row { .empty-row {

View File

@ -5,15 +5,17 @@
<text class="empty-text">暂无数据</text> <text class="empty-text">暂无数据</text>
</view> </view>
<view v-else class="top-five-row"> <view v-else class="top-five-row">
<view <view v-for="item in items" :key="item.asset_id" class="top-cell">
v-for="item in items" <view class="top-thumb">
:key="item.asset_id" <image
class="top-cell" v-if="item.asset_thumb"
> class="top-thumb-img"
<view class="top-thumb" :class="`top-grad-${item.rank}`"> :src="item.asset_thumb"
<text class="top-emoji">🏆</text> mode="aspectFill"
/>
<text v-else class="top-emoji">🏆</text>
</view> </view>
<view class="top-badge" :class="`top-badge-${item.rank}`"> <view class="top-badge">
<text class="top-badge-text">TOP {{ item.rank }}</text> <text class="top-badge-text">TOP {{ item.rank }}</text>
</view> </view>
</view> </view>
@ -24,20 +26,32 @@
<script setup> <script setup>
defineProps({ defineProps({
items: { type: Array, default: () => [] }, // TopAssetItem[] items: { type: Array, default: () => [] }, // TopAssetItem[]
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.top-five-card { .top-five-card {
background: rgba(255, 255, 255, 0.1); background: linear-gradient(
border-radius: 17rpx; 106.77deg,
rgba(255, 223, 119, 0.24) -9.76%,
rgba(185, 132, 255, 0.24) 44.65%,
rgba(255, 129, 131, 0.24) 117.82%
);
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
border-radius: 14px;
padding: 20rpx; padding: 20rpx;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15);
position: relative;
z-index: 2;
backdrop-filter: blur(3.5px);
// backdrop-filter: blur(1px);
} }
.card-title { .card-title {
display: block; display: block;
font-size: 28rpx; font-size: 24rpx;
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
margin-bottom: 16rpx; margin-bottom: 16rpx;
@ -57,21 +71,24 @@ defineProps({
} }
.top-thumb { .top-thumb {
width: 100rpx; width: 80rpx;
height: 100rpx; height: 100rpx;
border-radius: 12rpx; border-radius: 12rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 8rpx; margin-bottom: 24rpx;
box-shadow: 0 0 12px rgba(255, 200, 100, 0.3); box-shadow: 0px 4px 5.8px 0px rgba(226, 51, 51, 0.53);
transform: rotate(-10deg);
overflow: hidden; // image
} }
.top-grad-1 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); } // .top-thumb
.top-grad-2 { background: linear-gradient(135deg, #C0C0C0 0%, #808080 100%); } .top-thumb-img {
.top-grad-3 { background: linear-gradient(135deg, #CD7F32 0%, #8B4513 100%); } width: 100%;
.top-grad-4 { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); } height: 100%;
.top-grad-5 { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); } display: block;
}
.top-emoji { .top-emoji {
font-size: 48rpx; font-size: 48rpx;
@ -81,20 +98,26 @@ defineProps({
padding: 4rpx 12rpx; padding: 4rpx 12rpx;
border-radius: 8rpx; border-radius: 8rpx;
opacity: 0.85; opacity: 0.85;
background: linear-gradient(
93.1deg,
rgba(224, 180, 247, 0.71) -12.06%,
rgba(178, 246, 204, 0.71) 52.09%,
rgba(98, 178, 244, 0.71) 163.5%
);
backdrop-filter: blur(11.699999809265137px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
} }
.top-badge-1 { background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%); }
.top-badge-2 { background: linear-gradient(90deg, #C0C0C0 0%, #808080 100%); }
.top-badge-3 { background: linear-gradient(90deg, #CD7F32 0%, #8B4513 100%); }
.top-badge-4 { background: linear-gradient(90deg, #B17BFF 0%, #FF8FE6 100%); }
.top-badge-5 { background: linear-gradient(90deg, #5EDFFF 0%, #6FA9FF 100%); }
.top-badge-text { .top-badge-text {
font-size: 18rpx; font-size: 18rpx;
font-weight: 700; font-family: C800;
color: #ffffff; font-weight: 600;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); color: #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
} }
.empty-row { .empty-row {

View File

@ -1,27 +1,57 @@
<template> <template>
<view class="upcoming-upgrades"> <view class="upcoming-upgrades">
<text class="card-title">即将升级</text> <view class="header-row">
<text class="card-title">即将升级藏品</text>
<!-- 右上角图例标识两条进度条 -->
<view class="legend">
<view class="legend-item" style="margin-bottom: 14rpx">
<image
class="legend-tube-img"
src="/static/icon/like-after.png"
mode="heightFix"
/>
<view class="legend-dot legend-dot-cyan">
<text class="legend-text">获赞数</text>
</view>
</view>
<view class="legend-item">
<image
class="legend-tube-img"
src="/static/assetDetail/time.png"
mode="heightFix"
/>
<view class="legend-dot legend-dot-pink">
<text class="legend-text">展出时长</text>
</view>
</view>
</view>
</view>
<view v-if="!items || items.length === 0" class="empty-row"> <view v-if="!items || items.length === 0" class="empty-row">
<text class="empty-text">暂无数据</text> <text class="empty-text">暂无数据</text>
</view> </view>
<view v-else class="upgrades-list"> <view v-else class="upgrades-list">
<view <view v-for="item in items" :key="item.asset_id" class="upgrade-row">
v-for="item in items"
:key="item.asset_id"
class="upgrade-row"
>
<view class="upgrade-thumb"> <view class="upgrade-thumb">
<view class="thumb-circle" :style="{ background: 'linear-gradient(135deg, #FF6B6B 0%, #FFB199 100%)' }"> <image
<text class="thumb-letter">{{ item.asset_name[0] }}</text> v-if="item.asset_thumb"
</view> class="upgrade-thumb-img"
:src="item.asset_thumb"
mode="aspectFill"
/>
</view> </view>
<view class="upgrade-progress"> <view class="upgrade-progress">
<view class="progress-bar progress-cyan"> <view class="progress-bar progress-cyan">
<view class="progress-fill" :style="{ width: item.like_progress + '%' }"></view> <view
class="progress-fill"
:style="{ width: item.like_progress + '%' }"
></view>
<text class="progress-text">{{ item.like_progress }}%</text> <text class="progress-text">{{ item.like_progress }}%</text>
</view> </view>
<view class="progress-bar progress-pink"> <view class="progress-bar progress-pink">
<view class="progress-fill" :style="{ width: item.duration_progress + '%' }"></view> <view
class="progress-fill"
:style="{ width: item.duration_progress + '%' }"
></view>
<text class="progress-text">{{ item.duration_progress }}%</text> <text class="progress-text">{{ item.duration_progress }}%</text>
</view> </view>
</view> </view>
@ -33,25 +63,85 @@
<script setup> <script setup>
defineProps({ defineProps({
items: { type: Array, default: () => [] }, // UpcomingLevelUpItem[] items: { type: Array, default: () => [] }, // UpcomingLevelUpItem[]
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.upcoming-upgrades { .upcoming-upgrades {
background: rgba(255, 255, 255, 0.1); background: linear-gradient(
border-radius: 17rpx; 106.77deg,
padding: 20rpx; rgba(255, 223, 119, 0.52) -9.76%,
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15); rgba(185, 132, 255, 0.52) 44.65%,
rgba(255, 129, 131, 0.52) 117.82%
);
border-radius: 14px;
padding: 16rpx;
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
} }
.card-title { .card-title {
display: block; display: block;
font-size: 28rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
color: #ffffff; color: #ffffff;
margin-bottom: 16rpx; margin-bottom: 16rpx;
} }
//
.header-row {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 16rpx;
}
//
.legend {
display: flex;
align-items: center;
flex-direction: column;
}
.legend-item {
display: flex;
align-items: center;
position: relative;
}
.legend-tube-img {
width: 22rpx;
height: 22rpx;
position: absolute;
top: -4rpx;
left: -16rpx;
}
.legend-dot {
width: 64rpx;
height: 14rpx;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.legend-dot-cyan {
background: #5edfff;
box-shadow: 0 0 4rpx rgba(94, 223, 255, 0.6);
}
.legend-dot-pink {
background: #ff6b84;
box-shadow: 0 0 4rpx rgba(255, 107, 132, 0.6);
}
.legend-text {
font-size: 8rpx;
color: rgba(255, 241, 163, 1);
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
}
.upgrades-list { .upgrades-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -61,27 +151,35 @@ defineProps({
.upgrade-row { .upgrade-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; margin-top: 24rpx;
} }
.upgrade-thumb { .upgrade-thumb {
width: 64rpx; width: 56rpx;
height: 64rpx; height: 67rpx;
flex-shrink: 0; flex-shrink: 0;
}
.thumb-circle {
width: 100%;
height: 100%;
border-radius: 12rpx; border-radius: 12rpx;
// overflow: hidden; // image
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.2); position: relative; // .upgrade-thumb-badge
left: 8rpx;
z-index: 2;
transform: rotate(-10deg);
box-shadow: 0px 4px 4px 0px rgba(214, 53, 53, 0.43);
} }
.thumb-letter { // .upgrade-thumb
font-size: 24rpx; .upgrade-thumb-img {
width: 100%;
height: 100%;
display: block;
}
// emoji
.thumb-emoji {
font-size: 28rpx;
font-weight: 700; font-weight: 700;
color: #ffffff; color: #ffffff;
} }
@ -91,6 +189,7 @@ defineProps({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6rpx; gap: 6rpx;
position: relative;
} }
.progress-bar { .progress-bar {
@ -108,22 +207,23 @@ defineProps({
} }
.progress-cyan .progress-fill { .progress-cyan .progress-fill {
background: linear-gradient(90deg, #5EDFFF 0%, #FFC8C8 100%); background: linear-gradient(90deg, #5edfff 0%, #ffc8c8 100%);
} }
.progress-pink .progress-fill { .progress-pink .progress-fill {
background: linear-gradient(90deg, #FFF375 0%, #FF6B84 100%); background: linear-gradient(90deg, #fff375 0%, #ff6b84 100%);
} }
.progress-text { .progress-text {
position: absolute; position: absolute;
right: 6rpx; // right: 6rpx;
top: 50%; top: 50%;
transform: translateY(-50%); left: 50%;
font-size: 16rpx; transform: translate(-50%, -50%);
font-weight: 700; font-size: 18rpx;
color: #ffffff; font-weight: 400;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); color: rgba(255, 241, 163, 1);
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
} }
.empty-row { .empty-row {

View File

@ -5,53 +5,62 @@
:refresher-enabled="true" :refresher-enabled="true"
:refresher-triggered="loading.overall" :refresher-triggered="loading.overall"
@refresherrefresh="handlePullDownRefresh" @refresherrefresh="handlePullDownRefresh"
:show-scrollbar="false"
> >
<view class="dashboard-container"> <view class="dashboard-container">
<DashboardHeader <DashboardHeader
:active-tab="activeTab" :active-tab="activeTab"
@update:active-tab="handleTabChange" @update:active-tab="handleTabChange"
/> />
<view class="dashboard-box">
<!-- Tab 1: 水晶相关 -->
<view v-if="activeTab === 'crystal'" class="dashboard-content">
<CrystalOverview
:data="data.today"
:loading="loading.today"
:error="error.today"
@retry="refresh('today')"
/>
<IncomeCurve
:points="data.curve?.points || []"
:loading="loading.curve"
:error="error.curve"
@retry="refresh('curve')"
/>
<ExhibitionCenter
:data="data.exhibition"
:loading="loading.exhibition"
:error="error.exhibition"
@retry="refresh('exhibition')"
/>
<LikeIncomeBoard
:stats="
data.likeIncome
? {
total_like_count: data.likeIncome.total_like_count,
total_income: data.likeIncome.total_income,
}
: null
"
:levels="data.likeIncome?.levels || []"
:loading="loading.likeIncome"
:error="error.likeIncome"
@retry="refresh('likeIncome')"
/>
<CollectionMatrix
:top-five="data.topAssets?.items || []"
:levels="data.levels?.items || []"
:upgrades="data.upgrades || { upcoming: [], recent: [] }"
/>
</view>
<!-- Tab 1: 水晶相关 --> <!-- Tab 2: 赛季总览占位 -->
<view v-if="activeTab === 'crystal'" class="dashboard-content"> <view v-else class="dashboard-content">
<CrystalOverview <view class="season-placeholder">
:data="data.today" <text class="placeholder-icon">🏆</text>
:loading="loading.today" <text class="placeholder-title">赛季总览 · 即将上线</text>
:error="error.today" <text class="placeholder-sub">历史赛季数据正在筹备中</text>
@retry="refresh('today')" </view>
/>
<IncomeCurve
:points="data.curve?.points || []"
:loading="loading.curve"
:error="error.curve"
@retry="refresh('curve')"
/>
<ExhibitionCenter
:data="data.exhibition"
:loading="loading.exhibition"
:error="error.exhibition"
@retry="refresh('exhibition')"
/>
<LikeIncomeBoard
:stats="data.likeIncome ? { total_like_count: data.likeIncome.total_like_count, total_income: data.likeIncome.total_income } : null"
:levels="data.likeIncome?.levels || []"
:loading="loading.likeIncome"
:error="error.likeIncome"
@retry="refresh('likeIncome')"
/>
<CollectionMatrix
:top-five="data.topAssets?.items || []"
:levels="data.levels?.items || []"
:upgrades="data.upgrades || { upcoming: [], recent: [] }"
/>
</view>
<!-- Tab 2: 赛季总览占位 -->
<view v-else class="dashboard-content">
<view class="season-placeholder">
<text class="placeholder-icon">🏆</text>
<text class="placeholder-title">赛季总览 · 即将上线</text>
<text class="placeholder-sub">历史赛季数据正在筹备中</text>
</view> </view>
</view> </view>
</view> </view>
@ -59,55 +68,58 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from "vue";
import { onShow } from '@dcloudio/uni-app' import { onShow } from "@dcloudio/uni-app";
import DashboardHeader from './components/DashboardHeader.vue' import DashboardHeader from "./components/DashboardHeader.vue";
import CrystalOverview from './components/CrystalOverview.vue' import CrystalOverview from "./components/CrystalOverview.vue";
import IncomeCurve from './components/IncomeCurve.vue' import IncomeCurve from "./components/IncomeCurve.vue";
import ExhibitionCenter from './components/ExhibitionCenter.vue' import ExhibitionCenter from "./components/ExhibitionCenter.vue";
import LikeIncomeBoard from './components/LikeIncomeBoard.vue' import LikeIncomeBoard from "./components/LikeIncomeBoard.vue";
import CollectionMatrix from './components/CollectionMatrix.vue' import CollectionMatrix from "./components/CollectionMatrix.vue";
import { useDashboardData } from '@/composables/useDashboardData' import { useDashboardData } from "@/composables/useDashboardData";
const activeTab = ref('crystal') const activeTab = ref("crystal");
const starId = ref(uni.getStorageSync('star_id') || null) const starId = ref(uni.getStorageSync("star_id") || null);
const isFirstShow = ref(true) const isFirstShow = ref(true);
const { loading, error, data, refresh } = useDashboardData({ const { loading, error, data, refresh } = useDashboardData({
starId: starId.value, starId: starId.value,
}) });
// Tab 30 Tab cache-aware // Tab 30 Tab cache-aware
function handleTabChange(tab) { function handleTabChange(tab) {
activeTab.value = tab activeTab.value = tab;
if (tab === 'crystal') { if (tab === "crystal") {
// refresh() 30 refresh(null, true) // refresh() 30 refresh(null, true)
refresh() refresh();
} }
} }
// //
async function handlePullDownRefresh() { async function handlePullDownRefresh() {
await refresh(null, true) // force=true await refresh(null, true); // force=true
uni.stopPullDownRefresh() uni.stopPullDownRefresh();
} }
// 30 spec §4.1 // 30 spec §4.1
// onShow composable loadAll() 14 // onShow composable loadAll() 14
onShow(() => { onShow(() => {
if (isFirstShow.value) { if (isFirstShow.value) {
isFirstShow.value = false isFirstShow.value = false;
return return;
} }
refresh(null, true) refresh(null, true);
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/uni.scss'; @import "@/uni.scss";
.dashboard-page-bg { .dashboard-page-bg {
background: $d-page-bg;
min-height: 100vh; min-height: 100vh;
// $d-page-bg bj.png
background:
$d-page-bg,
url("/static/dashboard/bj.png") center / cover no-repeat;
} }
.dashboard-scroll { .dashboard-scroll {
height: 100vh; height: 100vh;
@ -115,8 +127,26 @@ onShow(() => {
.dashboard-container { .dashboard-container {
min-height: 100vh; min-height: 100vh;
} }
.dashboard-box {
min-height: 100vh;
position: absolute;
left: 6px;
right: 6px;
z-index: 3;
}
.dashboard-content { .dashboard-content {
padding: 24rpx 32rpx 80rpx; padding: 8px 9px 0;
background: linear-gradient(
174.13deg,
rgba(255, 149, 151, 0.57) -6.18%,
rgba(128, 223, 255, 0.57) 34.5%,
rgba(184, 184, 184, 0.57) 73.48%,
rgba(217, 217, 217, 0.57) 111.34%
);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 22px;
} }
.season-placeholder { .season-placeholder {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
@ -127,7 +157,18 @@ onShow(() => {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.placeholder-icon { font-size: 96rpx; margin-bottom: 24rpx; } .placeholder-icon {
.placeholder-title { color: #ffffff; font-size: 36rpx; font-weight: 700; margin-bottom: 16rpx; } font-size: 96rpx;
.placeholder-sub { color: rgba(255, 255, 255, 0.7); font-size: 26rpx; } margin-bottom: 24rpx;
}
.placeholder-title {
color: #ffffff;
font-size: 36rpx;
font-weight: 700;
margin-bottom: 16rpx;
}
.placeholder-sub {
color: rgba(255, 255, 255, 0.7);
font-size: 26rpx;
}
</style> </style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -87,7 +87,8 @@ $d-progress-cyan: linear-gradient(90deg, #5EDFFF 0%, #FFC8C8 100%);
$d-progress-pink: linear-gradient(90deg, #FFF375 0%, #FF6B84 100%); $d-progress-pink: linear-gradient(90deg, #FFF375 0%, #FF6B84 100%);
$d-bar-blue-yellow: linear-gradient(90deg, #1BAFEE 0%, #FFCC14 100%); $d-bar-blue-yellow: linear-gradient(90deg, #1BAFEE 0%, #FFCC14 100%);
$d-bar-fill: linear-gradient(135deg, #FFDF77 0%, #B984FF 60%, #FF8183 100%); $d-bar-fill: linear-gradient(135deg, #FFDF77 0%, #B984FF 60%, #FF8183 100%);
$d-page-bg: linear-gradient(153deg, #FF9597 0%, #80DFFF 33%, #B8B8B8 74%, #D9D9D9 100%); $d-page-bg: linear-gradient(174.13deg, rgba(255, 149, 151, 0.17) -6.18%, rgba(128, 223, 255, 0.17) 34.5%, rgba(184, 184, 184, 0.17) 73.48%, rgba(217, 217, 217, 0.17) 111.34%);
/* 5 个等级专属渐变环图、徽章、TOP 徽章) */ /* 5 个等级专属渐变环图、徽章、TOP 徽章) */
$d-level-ur: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%); $d-level-ur: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%);

View File

@ -3745,10 +3745,28 @@ function drawAreaDataPoints(series, opts, config, context) {
context.beginPath(); context.beginPath();
context.setStrokeStyle(hexToRgb(eachSeries.color, areaOption.opacity)); context.setStrokeStyle(hexToRgb(eachSeries.color, areaOption.opacity));
if (areaOption.gradient) { if (areaOption.gradient) {
let gradient = context.createLinearGradient(0, opts.area[0], 0, opts.height - opts.area[2]); // [patch] 支持 series.linearColor + linearDirection 自定义多色渐变
gradient.addColorStop('0', hexToRgb(eachSeries.color, areaOption.opacity)); // linearColor: [[offset, cssColor], ...]
gradient.addColorStop('1.0', hexToRgb("#FFFFFF", 0.1)); // linearDirection: 'horizontal' (默认) | 'vertical'
context.setFillStyle(gradient); if (eachSeries.linearColor && Array.isArray(eachSeries.linearColor) && eachSeries.linearColor.length > 0) {
let gradient;
let xs = opts.chartData.xAxisData && opts.chartData.xAxisData.startX;
let xe = opts.chartData.xAxisData && opts.chartData.xAxisData.endX;
if (eachSeries.linearDirection === 'vertical' || xs == null || xe == null) {
gradient = context.createLinearGradient(0, opts.area[0], 0, opts.height - opts.area[2]);
} else {
gradient = context.createLinearGradient(xs, 0, xe, 0);
}
eachSeries.linearColor.forEach(function(stop) {
gradient.addColorStop(String(stop[0]), stop[1]);
});
context.setFillStyle(gradient);
} else {
let gradient = context.createLinearGradient(0, opts.area[0], 0, opts.height - opts.area[2]);
gradient.addColorStop('0', hexToRgb(eachSeries.color, areaOption.opacity));
gradient.addColorStop('1.0', hexToRgb("#FFFFFF", 0.1));
context.setFillStyle(gradient);
}
} else { } else {
context.setFillStyle(hexToRgb(eachSeries.color, areaOption.opacity)); context.setFillStyle(hexToRgb(eachSeries.color, areaOption.opacity));
} }
@ -5171,6 +5189,11 @@ function drawArcbarDataPoints(series, opts, config, context) {
gap: 2 , gap: 2 ,
linearType: 'none', linearType: 'none',
customColor: [], customColor: [],
// [PATCH] 背景渐变支持:与 .linearType/.customColor 对称,但作用于 background 描边
// 用法backgroundLinearType:'custom' + backgroundLinearColor:[[pos,color],...] + backgroundLinearAngle(度, CSS 语义 0=下→上, 90=左→右)
backgroundLinearType: 'none',
backgroundLinearColor: [],
backgroundLinearAngle: 0,
}, opts.extra.arcbar); }, opts.extra.arcbar);
series = getArcbarDataPoints(series, arcbarOption, process); series = getArcbarDataPoints(series, arcbarOption, process);
var centerPosition; var centerPosition;
@ -5200,7 +5223,26 @@ function drawArcbarDataPoints(series, opts, config, context) {
let eachSeries = series[i]; let eachSeries = series[i];
//背景颜色 //背景颜色
context.setLineWidth(arcbarOption.width * opts.pix); context.setLineWidth(arcbarOption.width * opts.pix);
context.setStrokeStyle(arcbarOption.backgroundColor || '#E9E9E9'); // [PATCH] 背景渐变:若启用 backgroundLinearType=='custom',按 CSS 角度 + 颜色 stop 列表
// 在 canvas 上生成 createLinearGradient覆盖 backgroundColor 的纯色描边
var bgStroke = arcbarOption.backgroundColor || '#E9E9E9';
if (arcbarOption.backgroundLinearType == 'custom' && Array.isArray(arcbarOption.backgroundLinearColor) && arcbarOption.backgroundLinearColor.length >= 2) {
var bgAngle = (arcbarOption.backgroundLinearAngle || 0) * Math.PI / 180;
// CSS α0%=α+180° 处100%=α 处(顺时针从正上)
// 半径取 ring 外缘 (radius + width/2),让渐变覆盖整条环
var bgHalf = radius + (arcbarOption.width * opts.pix) / 2;
var bgX1 = centerPosition.x - bgHalf * Math.sin(bgAngle);
var bgY1 = centerPosition.y + bgHalf * Math.cos(bgAngle);
var bgX2 = centerPosition.x + bgHalf * Math.sin(bgAngle);
var bgY2 = centerPosition.y - bgHalf * Math.cos(bgAngle);
var bgGrd = context.createLinearGradient(bgX1, bgY1, bgX2, bgY2);
for (var bs = 0; bs < arcbarOption.backgroundLinearColor.length; bs++) {
var bsStop = arcbarOption.backgroundLinearColor[bs];
bgGrd.addColorStop(bsStop[0], bsStop[1]);
}
bgStroke = bgGrd;
}
context.setStrokeStyle(bgStroke);
context.setLineCap(arcbarOption.lineCap); context.setLineCap(arcbarOption.lineCap);
context.beginPath(); context.beginPath();
if (arcbarOption.type == 'default') { if (arcbarOption.type == 'default') {

View File

@ -55,11 +55,11 @@ export async function mockExhibitionSummary({ star_id }) {
total_duration: '712:13:56', total_duration: '712:13:56',
total_earnings: 39721, total_earnings: 39721,
top5: [ top5: [
{ asset_id: 1, asset_name: '璀璨星河', asset_thumb: '', duration_7d: '144:13:56', earnings_7d: 2173, avg_earnings: 15 }, { asset_id: 1, asset_name: '璀璨星河', asset_thumb: '/static/sucai/image-07.png', duration_7d: '144:13:56', earnings_7d: 2173, avg_earnings: 15 },
{ asset_id: 2, asset_name: '夏日微风', asset_thumb: '', duration_7d: '77:13:56', earnings_7d: 1332, avg_earnings: 15 }, { asset_id: 2, asset_name: '夏日微风', asset_thumb: '/static/sucai/image-19.png', duration_7d: '77:13:56', earnings_7d: 1332, avg_earnings: 15 },
{ asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '', duration_7d: '64:15:37', earnings_7d: 1201, avg_earnings: 12 }, { asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '/static/sucai/image-23.png', duration_7d: '64:15:37', earnings_7d: 1201, avg_earnings: 12 },
{ asset_id: 4, asset_name: '黎明序曲', asset_thumb: '', duration_7d: '51:22:12', earnings_7d: 783, avg_earnings: 12 }, { asset_id: 4, asset_name: '黎明序曲', asset_thumb: '/static/sucai/image-38.png', duration_7d: '51:22:12', earnings_7d: 783, avg_earnings: 12 },
{ asset_id: 5, asset_name: '深海回响', asset_thumb: '', duration_7d: '51:22:12', earnings_7d: 783, avg_earnings: 12 }, { asset_id: 5, asset_name: '深海回响', asset_thumb: '/static/sucai/image-46.png', duration_7d: '51:22:12', earnings_7d: 783, avg_earnings: 12 },
], ],
}, },
} }
@ -74,11 +74,11 @@ export async function mockLikeIncomeByLevel({ star_id }) {
total_like_count: 231, total_like_count: 231,
total_income: 12719, total_income: 12719,
levels: [ levels: [
{ level: 'UR', asset_count: 1, total_income: 723, thumb: '' }, { level: 'UR', asset_count: 1, total_income: 723, thumb: '/static/sucai/image-03.png' },
{ level: 'SSR', asset_count: 2, total_income: 381, thumb: '' }, { level: 'SSR', asset_count: 2, total_income: 381, thumb: '/static/sucai/image-14.png' },
{ level: 'SR', asset_count: 5, total_income: 233, thumb: '' }, { level: 'SR', asset_count: 5, total_income: 233, thumb: '/static/sucai/image-27.png' },
{ level: 'SR', asset_count: 4, total_income: 169, thumb: '' }, { level: 'SR', asset_count: 4, total_income: 169, thumb: '/static/sucai/image-35.png' },
{ level: 'R', asset_count: 6, total_income: 57, thumb: '' }, { level: 'R', asset_count: 6, total_income: 57, thumb: '/static/sucai/image-49.png' },
], ],
}, },
} }
@ -91,11 +91,11 @@ export async function mockTopAssets({ star_id }) {
code: 200, code: 200,
data: { data: {
items: [ items: [
{ asset_id: 1, asset_name: '璀璨星河', asset_thumb: '', total_earnings: 8420, rank: 1 }, { asset_id: 1, asset_name: '璀璨星河', asset_thumb: '/static/sucai/image-04.png', total_earnings: 8420, rank: 1 },
{ asset_id: 2, asset_name: '夏日微风', asset_thumb: '', total_earnings: 6230, rank: 2 }, { asset_id: 2, asset_name: '夏日微风', asset_thumb: '/static/sucai/image-11.png', total_earnings: 6230, rank: 2 },
{ asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '', total_earnings: 5180, rank: 3 }, { asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '/static/sucai/image-26.png', total_earnings: 5180, rank: 3 },
{ asset_id: 4, asset_name: '黎明序曲', asset_thumb: '', total_earnings: 4320, rank: 4 }, { asset_id: 4, asset_name: '黎明序曲', asset_thumb: '/static/sucai/image-39.png', total_earnings: 4320, rank: 4 },
{ asset_id: 5, asset_name: '深海回响', asset_thumb: '', total_earnings: 3980, rank: 5 }, { asset_id: 5, asset_name: '深海回响', asset_thumb: '/static/sucai/image-50.png', total_earnings: 3980, rank: 5 },
], ],
}, },
} }
@ -126,14 +126,14 @@ export async function mockUpgradeProgress({ star_id }) {
code: 200, code: 200,
data: { data: {
upcoming: [ upcoming: [
{ asset_id: 1, asset_name: '璀璨星河', asset_thumb: '', like_progress: 73, duration_progress: 92 }, { asset_id: 1, asset_name: '璀璨星河', asset_thumb: '/static/sucai/image-02.png', like_progress: 73, duration_progress: 92 },
{ asset_id: 2, asset_name: '夏日微风', asset_thumb: '', like_progress: 75, duration_progress: 96 }, { asset_id: 2, asset_name: '夏日微风', asset_thumb: '/static/sucai/image-16.png', like_progress: 75, duration_progress: 96 },
{ asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '', like_progress: 97, duration_progress: 71 }, { asset_id: 3, asset_name: '夜色霓虹', asset_thumb: '/static/sucai/image-28.png', like_progress: 97, duration_progress: 71 },
], ],
recent: [ recent: [
{ asset_id: 4, asset_name: '黎明序曲', asset_thumb: '', new_level: 'SSR', upgrade_time: Date.now() - 3600000 }, { asset_id: 4, asset_name: '黎明序曲', asset_thumb: '/static/sucai/image-02.png', new_level: 'SSR', upgrade_time: Date.now() - 3600000 },
{ asset_id: 5, asset_name: '深海回响', asset_thumb: '', new_level: 'SR', upgrade_time: Date.now() - 86400000 }, { asset_id: 5, asset_name: '深海回响', asset_thumb: '/static/sucai/image-16.png', new_level: 'SR', upgrade_time: Date.now() - 86400000 },
{ asset_id: 6, asset_name: '晨曦微光', asset_thumb: '', new_level: 'SR', upgrade_time: Date.now() - 172800000 }, { asset_id: 6, asset_name: '晨曦微光', asset_thumb: '/static/sucai/image-28.png', new_level: 'SR', upgrade_time: Date.now() - 172800000 },
], ],
}, },
} }