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>
<view class="collection-matrix">
<text class="section-title">藏品矩阵</text>
<image
class="chart-bg"
src="/static/dashboard/image-bj.png"
mode="scaleToFill"
/>
<!-- TOP 5 -->
<TopFiveAssets :items="topFive || []" />
<TopFiveAssets :items="topFive || []" class="top-five-assets" />
<!-- 等级分布 -->
<LevelDistribution :items="levels" />
@ -17,26 +21,43 @@
</template>
<script setup>
import TopFiveAssets from './TopFiveAssets.vue'
import LevelDistribution from './LevelDistribution.vue'
import UpcomingUpgrades from './UpcomingUpgrades.vue'
import RecentUpgrades from './RecentUpgrades.vue'
import TopFiveAssets from "./TopFiveAssets.vue";
import LevelDistribution from "./LevelDistribution.vue";
import UpcomingUpgrades from "./UpcomingUpgrades.vue";
import RecentUpgrades from "./RecentUpgrades.vue";
defineProps({
topFive: { type: Array, default: () => [] },
levels: { type: Array, default: () => [] },
upgrades: { type: Object, default: () => ({ upcoming: [], recent: [] }) },
})
});
</script>
<style lang="scss" scoped>
.collection-matrix {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(10px);
background:
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;
padding: 24rpx;
margin: 24rpx 0;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15);
padding: 12rpx;
margin: 12rpx 0;
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 {
@ -44,8 +65,22 @@ defineProps({
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 24rpx;
margin-bottom: 34rpx;
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 {

View File

@ -16,7 +16,7 @@
<text class="card-label">水晶余额</text>
<text class="card-value">{{ data.crystal_balance }}</text>
</view>
<view class="data-card card-today">
<view class="data-card card-crystal">
<text class="card-label">今日收益</text>
<text class="card-value">+ {{ data.today_income }}</text>
</view>
@ -29,13 +29,13 @@ defineProps({
data: { type: Object, default: null }, // { crystal_balance, today_income, week_rank? }
loading: { type: Boolean, default: false },
error: { type: String, default: null },
})
defineEmits(['retry'])
});
defineEmits(["retry"]);
</script>
<style lang="scss" scoped>
.crystal-overview {
margin: 24rpx 0;
margin:0 0 12rpx 0;
}
.card-row {
@ -44,41 +44,53 @@ defineEmits(['retry'])
}
.data-card {
flex: 1;
height: 200rpx;
border-radius: 22rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 88rpx;
border-top-left-radius: 19px;
border-top-right-radius: 10px;
border-bottom-right-radius: 7px;
border-bottom-left-radius: 7px;
opacity: 1;
position: relative;
overflow: hidden;
box-sizing: border-box;
}
.card-crystal {
background: linear-gradient(135deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%);
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25);
background: linear-gradient(
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 {
background: linear-gradient(137deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%);
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25);
}
// .card-today {
// background: linear-gradient(137deg, #ffdf77 0%, #8e95e2 40%, #f48cff 100%);
// box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25);
// }
.card-label {
position: absolute;
top: 10rpx;
left: 12rpx;
font-size: 24rpx;
color: #ffffff;
text-shadow: 0px 4px 4px rgba(164, 60, 60, 0.55);
margin-bottom: 12rpx;
}
.card-value {
font-size: 70rpx;
font-weight: 700;
color: #FFFABD;
position: absolute;
bottom: 0;
right: 12rpx;
font-size: 64rpx;
font-weight: 600;
color: #fffabd;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
font-family: 'Baloo Bhai', sans-serif;
line-height: 1;
font-family: "Baloo Bhai", sans-serif;
}
/* 骨架态 */
@ -91,14 +103,23 @@ defineEmits(['retry'])
flex: 1;
height: 200rpx;
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%;
animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
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-glow"></view>
<!-- <view class="header-glow"></view> -->
<!-- 状态栏占位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">
<!-- 毛绒怪头像占位纯色块 + emoji -->
<view class="mascot">
<text class="mascot-emoji">🐾</text>
</view>
<!-- 渐变标题 -->
<view class="title-wrap">
<text class="header-title">数据看板</text>
</view>
<!-- Tab 胶囊 -->
<view class="header-tabs">
<view
:class="['tab', activeTab === 'crystal' ? 'tab-active' : '']"
@tap="$emit('update:activeTab', 'crystal')"
>
水晶相关
<image
class="tab-icon"
src="/static/dashboard/crystal-bg.png"
mode="aspectFit"
/>
<text class="tab-title">水晶相关</text>
</view>
<view
:class="['tab', activeTab === 'season' ? 'tab-active' : '']"
@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>
@ -41,16 +46,16 @@
<script setup>
defineProps({
activeTab: { type: String, default: 'crystal' },
})
defineEmits(['update:activeTab'])
activeTab: { type: String, default: "crystal" },
});
defineEmits(["update:activeTab"]);
</script>
<style lang="scss" scoped>
.dashboard-header {
position: relative;
height: 360rpx;
overflow: hidden;
// overflow: hidden;
}
.status-bar-placeholder {
@ -58,14 +63,26 @@ defineEmits(['update:activeTab'])
}
.header-deco-bg {
background-image: url("/static/dashboard/header-bj.png");
background-size: 130%;
// background-repeat: no-repeat;
// background-position: center;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 700rpx;
background: linear-gradient(179deg, #FFE5E5 0%, #F3A0A1 50%, #FF9C9C 86%, #FF2024 100%);
filter: blur(4px);
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 {
@ -75,7 +92,11 @@ defineEmits(['update:activeTab'])
transform: translateX(-50%);
width: 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);
z-index: 1;
}
@ -84,16 +105,14 @@ defineEmits(['update:activeTab'])
position: relative;
z-index: 2;
padding: 0 32rpx 32rpx;
display: flex;
flex-direction: column;
align-items: center;
top: 192rpx;
}
.mascot {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FF6B6B 0%, #FFB199 100%);
background: linear-gradient(135deg, #ff6b6b 0%, #ffb199 100%);
display: flex;
align-items: center;
justify-content: center;
@ -101,10 +120,6 @@ defineEmits(['update:activeTab'])
margin-bottom: 16rpx;
}
.mascot-emoji {
font-size: 64rpx;
}
.title-wrap {
margin-bottom: 24rpx;
}
@ -112,7 +127,7 @@ defineEmits(['update:activeTab'])
.header-title {
font-size: 48rpx;
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;
background-clip: text;
color: transparent;
@ -121,27 +136,69 @@ defineEmits(['update:activeTab'])
.header-tabs {
display: flex;
background: rgba(0, 0, 0, 0.15);
border-radius: 22rpx;
// background: rgba(0, 0, 0, 0.15);
// border-radius: 22rpx;
padding: 6rpx;
width: 100%;
max-width: 500rpx;
height: 184rpx;
position: absolute;
right: 32rpx;
}
.tab {
flex: 1;
text-align: center;
padding: 14rpx 0;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.75);
width: 100%;
display: flex;
justify-content: center;
padding: 28rpx 0;
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 {
background: linear-gradient(231deg, #A8A6ED 0%, #88C8D8 64%, #F380EF 100%);
color: #ffffff;
font-weight: 600;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
background: linear-gradient(
90deg,
rgba(255, 222, 8, 0.4018) -17.54%,
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>

View File

@ -1,7 +1,11 @@
<template>
<view class="exhibition-center">
<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')">
<text class="error-text">加载失败点击重试</text>
@ -16,11 +20,13 @@
</view>
<!-- 正常态 -->
<view v-else>
<view v-else class="content">
<!-- 顶部 3 联统计 -->
<view class="stats-row">
<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>
</view>
<view class="stat-cell">
@ -36,7 +42,7 @@
<!-- 5 行表格 -->
<view class="table">
<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-earnings">七日收益</text>
<text class="th th-avg">平均收益</text>
@ -47,13 +53,19 @@
class="table-row"
>
<view class="td td-thumb">
<view class="thumb-placeholder" :class="`thumb-grad-${idx % 5}`">
<text class="thumb-emoji">🎨</text>
<view class="thumb-placeholder">
<image
v-if="item.asset_thumb"
class="thumb-image"
:src="item.asset_thumb"
mode="aspectFill"
/>
<text v-else class="thumb-emoji">🎨</text>
</view>
</view>
<text class="td td-duration">{{ item.duration_7d }}</text>
<text class="td td-earnings">{{ item.earnings_7d }}</text>
<text class="td td-avg">{{ item.avg_earnings }} / H</text>
<text class="td td-duration thd">{{ item.duration_7d }}</text>
<text class="td td-earnings thd">{{ item.earnings_7d }}</text>
<text class="td td-avg thd">{{ item.avg_earnings }} / H</text>
</view>
</view>
</view>
@ -65,18 +77,38 @@ defineProps({
data: { type: Object, default: null }, // ExhibitionIncomeSummary
loading: { type: Boolean, default: false },
error: { type: String, default: null },
})
defineEmits(['retry'])
});
defineEmits(["retry"]);
</script>
<style lang="scss" scoped>
.exhibition-center {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(10px);
background:
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;
padding: 24rpx;
margin: 24rpx 0;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15);
padding: 12rpx;
margin: 12rpx 0;
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 {
@ -86,22 +118,55 @@ defineEmits(['retry'])
color: #ffffff;
margin-bottom: 24rpx;
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 {
display: flex;
background: rgba(255, 255, 255, 0.08);
justify-content: space-between;
// background: rgba(255, 255, 255, 0.08);
border-radius: 17rpx;
padding: 24rpx 0;
margin-bottom: 24rpx;
}
.stat-cell {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
border-right: 1px solid rgba(255, 255, 255, 0.1);
min-width: 192rpx;
height: 80rpx;
position: relative;
box-sizing: border-box;
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 {
border-right: none;
@ -109,20 +174,28 @@ defineEmits(['retry'])
}
.stat-value {
position: absolute;
bottom: 4rpx;
right: 12rpx;
font-size: 32rpx;
font-weight: 700;
color: #FFFABD;
font-family: 'Baloo Bhai', sans-serif;
margin-bottom: 8rpx;
font-weight: 600;
color: #fffabd;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
font-family: "Baloo Bhai", sans-serif;
}
.stat-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
position: absolute;
top: 6rpx;
left: 12rpx;
font-size: 20rpx;
color: rgba(255, 255, 255, 1);
}
.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;
overflow: hidden;
}
@ -132,7 +205,7 @@ defineEmits(['retry'])
display: flex;
align-items: center;
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 {
@ -141,8 +214,10 @@ defineEmits(['retry'])
.th,
.td {
font-size: 24rpx;
font-size: 20rpx;
font-weight: 500;
color: #ffffff;
text-shadow: 0px 0px 4px rgba(164, 60, 60, 1);
text-align: center;
}
@ -165,35 +240,55 @@ defineEmits(['retry'])
flex-shrink: 0;
}
.th {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.6);
// .thd flex + 32rpx padding
// .td.thd .td-duration / .td-earnings / .td-avg flex/width
.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 {
width: 56rpx;
width: 40rpx;
height: 56rpx;
border-radius: 3rpx;
display: flex;
align-items: center;
justify-content: center;
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-grad-1 { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); }
.thumb-grad-2 { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); }
.thumb-grad-3 { background: linear-gradient(135deg, #FFE066 0%, #FFB199 100%); }
.thumb-grad-4 { background: linear-gradient(135deg, #C5C5C5 0%, #8C8C8C 100%); }
.thumb-emoji {
font-size: 32rpx;
.thumb-image {
width: 100%;
height: 100%;
display: block;
}
.td-earnings {
color: #FFFABD;
color: #fffabd;
font-weight: 600;
font-family: 'Baloo Bhai', sans-serif;
font-family: "Baloo Bhai", sans-serif;
}
/* 骨架 */
@ -206,7 +301,12 @@ defineEmits(['retry'])
.skeleton-stats {
height: 120rpx;
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%;
animation: shimmer 1.5s infinite;
}
@ -220,14 +320,23 @@ defineEmits(['retry'])
.skeleton-row {
height: 80rpx;
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%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.error-box {

View File

@ -1,14 +1,21 @@
<template>
<view class="income-curve-card">
<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')">
<text class="error-text">加载失败点击重试</text>
</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">
@ -19,129 +26,154 @@
:chartData="chartData"
:ontouch="true"
:onmovetip="true"
canvas2d
:in-scroll-view="true"
:tooltipShow="true"
:canvas2d="false"
canvasId="incomeCurveCanvas"
:canvasHeight="240"
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>
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from "vue";
// 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({
points: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
error: { type: String, default: null },
})
defineEmits(['retry'])
});
defineEmits(["retry"]);
// canvas2d opts.pix=1 canvas2d opts.pix=systemInfo.pixelRatio
// emit opts 1
const pixel = ref(1)
const chartOptsRef = ref(null)
const calPoints = ref([])
// tap
const currentIndex = ref(0);
watch(
() => props.points.length,
(len) => {
currentIndex.value = Math.max(0, len - 1);
},
{ immediate: true },
);
// @completeuCharts emit opts.chartData.calPoints canvas
function handleChartComplete(e) {
const opts = e?.opts
if (!opts) return
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 onChartTap = (e) => {
const idx = e?.index;
if (typeof idx === "number" && idx >= 0 && idx < props.points.length) {
currentIndex.value = idx;
}
};
const chartData = computed(() => {
const categories = props.points.map((p) => p.date.slice(5))
const lineData = props.points.map((p) => p.income)
const categories = props.points.map((p) => p.date.slice(5));
const lineData = props.points.map((p) => p.income);
return {
categories,
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 = {
color: ['#1BAFEE'],
color: ["#1BAFEE"],
padding: PADDING,
dataLabel: false,
legend: { show: 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: {
disabled: true,
showTitle: false,
data: [{ min: 0, disabled: true, axisLine: false }],
disableGrid: true,
fontColor: '#FFFFFF',
fontColor: "#FFFFFF",
fontSize: 9,
},
extra: {
tooltip: {
showBox: true,
showArrow: true,
showCategory: true,
bgColor: '#000000',
showCategory: false,
bgColor: "#000000",
bgOpacity: 0.6,
fontColor: '#FFFFFF',
fontColor: "#FFFFFF",
fontSize: 11,
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>
<style lang="scss" scoped>
.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;
padding: 24rpx;
margin: 24rpx 0;
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25);
min-height: 360rpx;
box-sizing: border-box; // padding
box-shadow: 0px 4px 4px 0px #BD323240;
position: relative;
height: 256rpx;
display: flex;
flex-direction: column;
overflow: hidden; //
}
.curve-title {
@ -149,80 +181,51 @@ const chartOpts = {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
margin-bottom: 4rpx; //
text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2; //
}
.chart-wrap {
background: rgba(255, 255, 255, 0.15);
position: relative;
flex: 1; //
min-height: 0; // flex
border-radius: 17rpx;
padding: 16rpx;
backdrop-filter: blur(10px);
overflow: hidden;
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 {
position: relative;
width: 100%;
height: 240rpx;
}
.latest-indicator {
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;
height: 100%; // .chart-wrap
// drop-shadow 沿 canvas alpha 线
// CSS: box-shadow: -1px -5px 4px 0px #CF232338 (#CF2323 + alpha 0x38 0.22)
filter: drop-shadow(-1px -5px 4px rgba(207, 35, 35, 0.22));
}
.skeleton-chart {
height: 360rpx;
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%;
animation: skeleton-shimmer 1.5s infinite;
}
@ -243,7 +246,11 @@ const chartOpts = {
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

View File

@ -5,71 +5,141 @@
<text class="empty-text">暂无数据</text>
</view>
<view v-else class="ring-row">
<view
v-for="item in items"
:key="item.level"
class="ring-cell"
>
<view
class="ring-outer"
:style="getRingStyle(item)"
>
<view class="ring-inner">
<view v-for="item in items" :key="item.level" class="ring-cell">
<view class="ring-chart">
<qiun-data-charts
type="arcbar"
:opts="getOpts(item)"
:chartData="getChartData(item)"
:canvasId="`arcbar-${item.level}`"
:canvas2d="false"
:ontouch="false"
: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 ring-text">{{ getPercent(item) }}%</text>
<image
class="ring-label-img"
:src="getGradeBadge(item.level)"
mode="aspectFit"
/>
</view>
</view>
<text class="ring-label">{{ item.level }}</text>
<text class="ring-pct">{{ getPercent(item) }}%</text>
</view>
</view>
</view>
</template>
<script setup>
import QiunDataCharts from "@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue";
const props = defineProps({
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) {
if (!item.total) return 0
return Math.round((item.count / item.total) * 100)
if (!item.total) return 0;
return Math.round((item.count / item.total) * 100);
}
function getRingStyle(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%
function getChartData(item) {
const pct = getPercent(item);
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>
<style lang="scss" scoped>
.level-distribution {
background: rgba(255, 255, 255, 0.1);
background: rgba(121, 120, 215, 0.31);
border-radius: 17rpx;
padding: 20rpx;
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 {
display: block;
font-size: 28rpx;
font-size: 24rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
@ -89,46 +159,50 @@ function getRingStyle(item) {
flex: 1;
}
.ring-outer {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
.ring-chart {
width: 112rpx;
height: 112rpx;
position: relative;
display: flex;
align-items: center;
justify-content: center;
position: relative;
box-shadow: 0 0 12px rgba(255, 255, 255, 0.15);
// conic-gradient canvas
filter: drop-shadow(0 0 12rpx rgba(255, 255, 255, 0.15));
}
.ring-inner {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.3);
// HTML text-shadowcanvas
.ring-center {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
z-index: 1;
flex-direction: column;
}
.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;
color: rgba(255, 255, 255, 0.6);
margin-top: 2rpx;
font-weight: 700;
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 {

View File

@ -1,7 +1,11 @@
<template>
<view class="like-income-board">
<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')">
<text class="error-text">加载失败点击重试</text>
@ -27,17 +31,28 @@
<!-- 右侧等级列表 -->
<view class="right-list">
<view
v-for="(item, idx) in levels"
:key="idx"
class="level-row"
>
<view class="level-header">
<text class="th th-thumb">藏品</text>
<text class="th th-name">等级</text>
<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="thumb-circle" :class="`level-${item.level}`">
<text class="thumb-letter">{{ item.level }}</text>
</view>
<image
v-if="item.thumb"
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>
<text class="level-name">{{ item.level }}</text>
<text class="level-income">{{ item.total_income }}</text>
</view>
</view>
@ -51,18 +66,73 @@ defineProps({
levels: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
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>
<style lang="scss" scoped>
.like-income-board {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(10px);
border-radius: 22rpx;
padding: 24rpx;
margin: 24rpx 0;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15);
background:
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(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 {
@ -72,6 +142,8 @@ defineEmits(['retry'])
color: #ffffff;
margin-bottom: 24rpx;
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 2;
}
.board-row {
@ -90,32 +162,84 @@ defineEmits(['retry'])
}
.stat-block {
display: flex;
flex-direction: column;
align-items: center;
min-width: 264rpx;
height: 98rpx;
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 {
font-size: 40rpx;
position: absolute;
bottom: 4rpx;
right: 16rpx;
font-size: 48rpx;
font-weight: 700;
color: #FFFABD;
font-family: 'Baloo Bhai', sans-serif;
color: #fffabd;
font-family: "Baloo Bhai", sans-serif;
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
margin-bottom: 6rpx;
}
.stat-text {
position: absolute;
top: 6rpx;
left: 16rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
color: rgba(255, 255, 255, 0.85);
}
.right-list {
flex: 1.5;
width: 320rpx;
display: flex;
flex-direction: column;
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 {
display: flex;
align-items: center;
@ -126,48 +250,57 @@ defineEmits(['retry'])
}
.level-thumb {
width: 56rpx;
width: 40rpx;
height: 56rpx;
flex-shrink: 0;
}
.thumb-circle {
width: 100%;
height: 100%;
border-radius: 50%;
border-radius: 6rpx;
overflow: hidden;
display: flex;
align-items: 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 {
font-size: 18rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
// .level-thumb
.thumb-asset-img {
width: 100%;
height: 100%;
display: block;
}
.level-UR { background: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%); }
.level-SSR { background: linear-gradient(135deg, #FF5E9C 0%, #FFB199 100%); }
.level-SR { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); }
.level-R { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); }
.level-N { background: linear-gradient(135deg, #C5C5C5 0%, #8C8C8C 100%); }
// emoji
.thumb-emoji {
font-size: 28rpx;
}
.level-name {
flex: 0 0 60rpx;
font-size: 22rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
flex: 0 0 96rpx; // .th-name
display: flex;
align-items: center;
justify-content: flex-end; //
}
//
.level-badge-img {
width: 64rpx;
height: 64rpx;
}
.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-weight: 700;
color: #FFFABD;
font-family: 'Baloo Bhai', sans-serif;
text-align: right;
border-radius: 7px;
color: #fffabd;
font-family: "Baloo Bhai", sans-serif;
text-align: center; //
}
/* 骨架/错误 */
@ -181,7 +314,12 @@ defineEmits(['retry'])
.skeleton-list {
height: 200rpx;
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%;
animation: shimmer 1.5s infinite;
}
@ -202,7 +340,11 @@ defineEmits(['retry'])
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>

View File

@ -1,27 +1,35 @@
<template>
<view class="recent-upgrades">
<text class="card-title">最近升级</text>
<text class="card-title">即将升级藏品</text>
<view v-if="!items || items.length === 0" class="empty-row">
<text class="empty-text">暂无数据</text>
</view>
<view v-else class="upgrades-list">
<view
v-for="item in items"
:key="item.asset_id"
class="upgrade-row"
>
<view v-for="item in items" :key="item.asset_id" class="upgrade-row">
<view class="upgrade-thumb">
<view class="thumb-circle" :style="{ background: 'linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%)' }">
<text class="thumb-letter">{{ item.asset_name[0] }}</text>
</view>
<image
v-if="item.asset_thumb"
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 class="upgrade-info">
<text class="upgrade-name">{{ item.asset_name }}</text>
<text class="upgrade-time">{{ formatTime(item.upgrade_time) }}</text>
<text class="lv-up">Lv <text class="lv-up lv-up-text">UP</text></text>
</view>
<view class="level-badge" :class="`level-${item.new_level}`">
<text class="level-letter">{{ item.new_level }}</text>
<text class="lv-up">Lv UP</text>
<view class="level-badge">
<image
class="level-badge-img"
:src="getGradeBadge(item.new_level)"
mode="aspectFit"
/>
</view>
</view>
</view>
@ -31,28 +39,52 @@
<script setup>
defineProps({
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) {
const now = Date.now()
const diff = now - ts
if (diff < 60 * 60 * 1000) return `${Math.floor(diff / 60000)} 分钟前`
if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / 3600000)} 小时前`
return `${Math.floor(diff / 86400000)} 天前`
const now = Date.now();
const diff = now - ts;
if (diff < 60 * 60 * 1000) return `${Math.floor(diff / 60000)} 分钟前`;
if (diff < 24 * 60 * 60 * 1000) return `${Math.floor(diff / 3600000)} 小时前`;
return `${Math.floor(diff / 86400000)} 天前`;
}
</script>
<style lang="scss" scoped>
.recent-upgrades {
background: rgba(255, 255, 255, 0.1);
border-radius: 17rpx;
padding: 20rpx;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15);
background: linear-gradient(
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%
);
border-radius: 14px;
padding: 16rpx;
box-shadow: 0px 4px 4px 0px rgba(189, 50, 50, 0.25);
}
.card-title {
display: block;
font-size: 28rpx;
font-size: 22rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
@ -75,32 +107,48 @@ function formatTime(ts) {
.upgrade-thumb {
width: 56rpx;
height: 56rpx;
height: 67rpx;
flex-shrink: 0;
}
.thumb-circle {
width: 100%;
height: 100%;
border-radius: 12rpx;
// overflow: hidden; // image
display: flex;
align-items: 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;
color: #ffffff;
}
.upgrade-info {
flex: 1;
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 4rpx;
min-width: 0;
margin-left: 24rpx;
}
.upgrade-name {
@ -121,28 +169,25 @@ function formatTime(ts) {
display: flex;
flex-direction: column;
align-items: center;
padding: 6rpx 12rpx;
border-radius: 8rpx;
padding: 4rpx 6rpx;
flex-shrink: 0;
}
.level-UR { background: linear-gradient(135deg, #FF8A65 0%, #FFD740 100%); }
.level-SSR { background: linear-gradient(135deg, #FF5E9C 0%, #FFB199 100%); }
.level-SR { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); }
.level-R { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); }
.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);
//
.level-badge-img {
width: 80rpx;
height: 80rpx;
}
.lv-up {
font-size: 14rpx;
font-size: 30rpx;
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 {

View File

@ -5,15 +5,17 @@
<text class="empty-text">暂无数据</text>
</view>
<view v-else class="top-five-row">
<view
v-for="item in items"
:key="item.asset_id"
class="top-cell"
>
<view class="top-thumb" :class="`top-grad-${item.rank}`">
<text class="top-emoji">🏆</text>
<view v-for="item in items" :key="item.asset_id" class="top-cell">
<view class="top-thumb">
<image
v-if="item.asset_thumb"
class="top-thumb-img"
:src="item.asset_thumb"
mode="aspectFill"
/>
<text v-else class="top-emoji">🏆</text>
</view>
<view class="top-badge" :class="`top-badge-${item.rank}`">
<view class="top-badge">
<text class="top-badge-text">TOP {{ item.rank }}</text>
</view>
</view>
@ -24,20 +26,32 @@
<script setup>
defineProps({
items: { type: Array, default: () => [] }, // TopAssetItem[]
})
});
</script>
<style lang="scss" scoped>
.top-five-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 17rpx;
background: linear-gradient(
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;
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 {
display: block;
font-size: 28rpx;
font-size: 24rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
@ -57,21 +71,24 @@ defineProps({
}
.top-thumb {
width: 100rpx;
width: 80rpx;
height: 100rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
box-shadow: 0 0 12px rgba(255, 200, 100, 0.3);
margin-bottom: 24rpx;
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-grad-2 { background: linear-gradient(135deg, #C0C0C0 0%, #808080 100%); }
.top-grad-3 { background: linear-gradient(135deg, #CD7F32 0%, #8B4513 100%); }
.top-grad-4 { background: linear-gradient(135deg, #B17BFF 0%, #FF8FE6 100%); }
.top-grad-5 { background: linear-gradient(135deg, #5EDFFF 0%, #6FA9FF 100%); }
// .top-thumb
.top-thumb-img {
width: 100%;
height: 100%;
display: block;
}
.top-emoji {
font-size: 48rpx;
@ -81,20 +98,26 @@ defineProps({
padding: 4rpx 12rpx;
border-radius: 8rpx;
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);
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 {
font-size: 18rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
font-family: C800;
font-weight: 600;
color: #fffabd;
text-shadow: -1px 1px 4px #ce0909d6;
}
.empty-row {

View File

@ -1,27 +1,57 @@
<template>
<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">
<text class="empty-text">暂无数据</text>
</view>
<view v-else class="upgrades-list">
<view
v-for="item in items"
:key="item.asset_id"
class="upgrade-row"
>
<view v-for="item in items" :key="item.asset_id" class="upgrade-row">
<view class="upgrade-thumb">
<view class="thumb-circle" :style="{ background: 'linear-gradient(135deg, #FF6B6B 0%, #FFB199 100%)' }">
<text class="thumb-letter">{{ item.asset_name[0] }}</text>
</view>
<image
v-if="item.asset_thumb"
class="upgrade-thumb-img"
:src="item.asset_thumb"
mode="aspectFill"
/>
</view>
<view class="upgrade-progress">
<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>
</view>
<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>
</view>
</view>
@ -33,25 +63,85 @@
<script setup>
defineProps({
items: { type: Array, default: () => [] }, // UpcomingLevelUpItem[]
})
});
</script>
<style lang="scss" scoped>
.upcoming-upgrades {
background: rgba(255, 255, 255, 0.1);
border-radius: 17rpx;
padding: 20rpx;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.15);
background: linear-gradient(
106.77deg,
rgba(255, 223, 119, 0.52) -9.76%,
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 {
display: block;
font-size: 28rpx;
font-size: 22rpx;
font-weight: 600;
color: #ffffff;
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 {
display: flex;
flex-direction: column;
@ -61,27 +151,35 @@ defineProps({
.upgrade-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 24rpx;
}
.upgrade-thumb {
width: 64rpx;
height: 64rpx;
width: 56rpx;
height: 67rpx;
flex-shrink: 0;
}
.thumb-circle {
width: 100%;
height: 100%;
border-radius: 12rpx;
// overflow: hidden; // image
display: flex;
align-items: 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 {
font-size: 24rpx;
// .upgrade-thumb
.upgrade-thumb-img {
width: 100%;
height: 100%;
display: block;
}
// emoji
.thumb-emoji {
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
}
@ -91,6 +189,7 @@ defineProps({
display: flex;
flex-direction: column;
gap: 6rpx;
position: relative;
}
.progress-bar {
@ -108,22 +207,23 @@ defineProps({
}
.progress-cyan .progress-fill {
background: linear-gradient(90deg, #5EDFFF 0%, #FFC8C8 100%);
background: linear-gradient(90deg, #5edfff 0%, #ffc8c8 100%);
}
.progress-pink .progress-fill {
background: linear-gradient(90deg, #FFF375 0%, #FF6B84 100%);
background: linear-gradient(90deg, #fff375 0%, #ff6b84 100%);
}
.progress-text {
position: absolute;
right: 6rpx;
// right: 6rpx;
top: 50%;
transform: translateY(-50%);
font-size: 16rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
left: 50%;
transform: translate(-50%, -50%);
font-size: 18rpx;
font-weight: 400;
color: rgba(255, 241, 163, 1);
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
}
.empty-row {

View File

@ -5,53 +5,62 @@
:refresher-enabled="true"
:refresher-triggered="loading.overall"
@refresherrefresh="handlePullDownRefresh"
:show-scrollbar="false"
>
<view class="dashboard-container">
<DashboardHeader
:active-tab="activeTab"
@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: 水晶相关 -->
<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 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>
<!-- 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>
@ -59,55 +68,58 @@
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import DashboardHeader from './components/DashboardHeader.vue'
import CrystalOverview from './components/CrystalOverview.vue'
import IncomeCurve from './components/IncomeCurve.vue'
import ExhibitionCenter from './components/ExhibitionCenter.vue'
import LikeIncomeBoard from './components/LikeIncomeBoard.vue'
import CollectionMatrix from './components/CollectionMatrix.vue'
import { useDashboardData } from '@/composables/useDashboardData'
import { ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import DashboardHeader from "./components/DashboardHeader.vue";
import CrystalOverview from "./components/CrystalOverview.vue";
import IncomeCurve from "./components/IncomeCurve.vue";
import ExhibitionCenter from "./components/ExhibitionCenter.vue";
import LikeIncomeBoard from "./components/LikeIncomeBoard.vue";
import CollectionMatrix from "./components/CollectionMatrix.vue";
import { useDashboardData } from "@/composables/useDashboardData";
const activeTab = ref('crystal')
const starId = ref(uni.getStorageSync('star_id') || null)
const isFirstShow = ref(true)
const activeTab = ref("crystal");
const starId = ref(uni.getStorageSync("star_id") || null);
const isFirstShow = ref(true);
const { loading, error, data, refresh } = useDashboardData({
starId: starId.value,
})
});
// Tab 30 Tab cache-aware
function handleTabChange(tab) {
activeTab.value = tab
if (tab === 'crystal') {
activeTab.value = tab;
if (tab === "crystal") {
// refresh() 30 refresh(null, true)
refresh()
refresh();
}
}
//
async function handlePullDownRefresh() {
await refresh(null, true) // force=true
uni.stopPullDownRefresh()
await refresh(null, true); // force=true
uni.stopPullDownRefresh();
}
// 30 spec §4.1
// onShow composable loadAll() 14
onShow(() => {
if (isFirstShow.value) {
isFirstShow.value = false
return
isFirstShow.value = false;
return;
}
refresh(null, true)
})
refresh(null, true);
});
</script>
<style lang="scss" scoped>
@import '@/uni.scss';
@import "@/uni.scss";
.dashboard-page-bg {
background: $d-page-bg;
min-height: 100vh;
// $d-page-bg bj.png
background:
$d-page-bg,
url("/static/dashboard/bj.png") center / cover no-repeat;
}
.dashboard-scroll {
height: 100vh;
@ -115,8 +127,26 @@ onShow(() => {
.dashboard-container {
min-height: 100vh;
}
.dashboard-box {
min-height: 100vh;
position: absolute;
left: 6px;
right: 6px;
z-index: 3;
}
.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 {
background: rgba(255, 255, 255, 0.15);
@ -127,7 +157,18 @@ onShow(() => {
flex-direction: column;
align-items: center;
}
.placeholder-icon { font-size: 96rpx; 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; }
.placeholder-icon {
font-size: 96rpx;
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>

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-bar-blue-yellow: linear-gradient(90deg, #1BAFEE 0%, #FFCC14 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 徽章) */
$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.setStrokeStyle(hexToRgb(eachSeries.color, areaOption.opacity));
if (areaOption.gradient) {
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);
// [patch] 支持 series.linearColor + linearDirection 自定义多色渐变
// linearColor: [[offset, cssColor], ...]
// linearDirection: 'horizontal' (默认) | 'vertical'
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 {
context.setFillStyle(hexToRgb(eachSeries.color, areaOption.opacity));
}
@ -5171,6 +5189,11 @@ function drawArcbarDataPoints(series, opts, config, context) {
gap: 2 ,
linearType: 'none',
customColor: [],
// [PATCH] 背景渐变支持:与 .linearType/.customColor 对称,但作用于 background 描边
// 用法backgroundLinearType:'custom' + backgroundLinearColor:[[pos,color],...] + backgroundLinearAngle(度, CSS 语义 0=下→上, 90=左→右)
backgroundLinearType: 'none',
backgroundLinearColor: [],
backgroundLinearAngle: 0,
}, opts.extra.arcbar);
series = getArcbarDataPoints(series, arcbarOption, process);
var centerPosition;
@ -5200,7 +5223,26 @@ function drawArcbarDataPoints(series, opts, config, context) {
let eachSeries = series[i];
//背景颜色
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.beginPath();
if (arcbarOption.type == 'default') {

View File

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