feat:新增数据看板
This commit is contained in:
parent
20f86ceec0
commit
463e6cc008
@ -1,5 +1,20 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { dashboardApi } from '@/utils/api'
|
||||
import * as mock from '@/utils/mock/dashboard'
|
||||
|
||||
// USE_MOCK_API 是写死常量——直接内联短路逻辑,绕开 api.js 的 dashboardRequest 链路
|
||||
// 原因:标准基座 + Vite HMR 在 App 端会让 api.js 内部的 async 包装 + signal 透传
|
||||
// 导致 useDashboardData.loadSection 里的 await 永远不 resolve。
|
||||
// 直接调 mock 工厂,200-600ms 内必定 resolve。
|
||||
const MOCK_FACTORIES = {
|
||||
today: mock.mockTodayOverview,
|
||||
curve: mock.mock7DayIncomeCurve,
|
||||
exhibition: mock.mockExhibitionSummary,
|
||||
likeIncome: mock.mockLikeIncomeByLevel,
|
||||
topAssets: mock.mockTopAssets,
|
||||
levels: mock.mockLevelDistribution,
|
||||
upgrades: mock.mockUpgradeProgress,
|
||||
}
|
||||
|
||||
// section 名 → fetcher 映射,模块级单例,loadAll 与 refresh 复用
|
||||
const SECTION_FETCHERS = {
|
||||
@ -60,12 +75,17 @@ export function useDashboardData({ starId = null } = {}) {
|
||||
Object.values(data.value).every((v) => v !== null && v !== undefined)
|
||||
)
|
||||
|
||||
// 内部辅助:单 section 加载,data 解包在 boundary 处进行
|
||||
// 内部辅助:单 section 加载
|
||||
async function loadSection(section, fetcher) {
|
||||
loading.value[section] = true
|
||||
error.value[section] = null
|
||||
try {
|
||||
const result = await fetcher(starId)
|
||||
// USE_MOCK_API 走 mock:直接调本地工厂,绕开 api.js 的 dashboardRequest 包装
|
||||
// 解决标准基座 + Vite HMR 下 await fetcher(starId, { signal }) 永远不 resolve 的问题
|
||||
const mockFactory = MOCK_FACTORIES[section]
|
||||
const result = mockFactory
|
||||
? await mockFactory({ star_id: starId })
|
||||
: await fetcher(starId)
|
||||
data.value[section] = result?.data ?? result
|
||||
} catch (e) {
|
||||
error.value[section] = e?.message || '加载失败'
|
||||
@ -91,7 +111,7 @@ export function useDashboardData({ starId = null } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// 局部刷新:refresh('curve') 只刷一个;refresh() 全量 cache-aware;refresh(true) 全量强制
|
||||
// 局部刷新:refresh('curve') 只刷一个;refresh() 全量 cache-aware;refresh(null, true) 全量强制
|
||||
async function refresh(section, force = false) {
|
||||
if (section) {
|
||||
const fetcher = SECTION_FETCHERS[section]
|
||||
@ -101,10 +121,7 @@ export function useDashboardData({ starId = null } = {}) {
|
||||
return loadAll(force)
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
// ref 状态由 Vue 自动 GC;保留接口给未来清理(如取消 in-flight 请求)
|
||||
}
|
||||
|
||||
// 初次加载
|
||||
loadAll()
|
||||
|
||||
return {
|
||||
@ -114,6 +131,5 @@ export function useDashboardData({ starId = null } = {}) {
|
||||
refresh,
|
||||
isReady,
|
||||
lastFetched,
|
||||
dispose,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
{
|
||||
"easycom": {
|
||||
"autoscan": true,
|
||||
"custom": {
|
||||
"^qiun-(.*)": "@/uni_modules/qiun-data-charts/components/qiun-$1/qiun-$1.vue"
|
||||
}
|
||||
},
|
||||
"pages": [{
|
||||
"path": "pages/square/square",
|
||||
"style": {
|
||||
|
||||
@ -360,6 +360,13 @@ const handleBack = () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
// 处理水晶余额点击 → 跳转数据看板
|
||||
const handleClick = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/dashboard/dashboard",
|
||||
});
|
||||
};
|
||||
|
||||
// 处理任务图标点击
|
||||
const handleTaskClick = () => {
|
||||
showTaskModal.value = true;
|
||||
|
||||
@ -62,12 +62,12 @@ const toggleDrawer = () => {
|
||||
const cards = [
|
||||
{
|
||||
label: '限时上新',
|
||||
image: '/static/starcity/tongkuan/29dcf35c07c836ad83d92bee0cc4a0af.png',
|
||||
image: '/static/starcity/tongkuan/1.jpg',
|
||||
isNew: true
|
||||
},
|
||||
{
|
||||
label: '限时上新',
|
||||
image: '/static/starcity/tongkuan/38202ae3fe02ac1bee1078a6049d0829.png',
|
||||
image: '/static/starcity/tongkuan/2.jpg',
|
||||
isNew: false
|
||||
},
|
||||
{
|
||||
|
||||
@ -12,34 +12,42 @@
|
||||
|
||||
<!-- 图表 -->
|
||||
<view v-else class="chart-wrap">
|
||||
<view class="chart-header">
|
||||
<text class="chart-peak-value" v-if="peak">+ {{ peak.income }}</text>
|
||||
<text class="chart-peak-date" v-if="peak">{{ peak.date }}</text>
|
||||
</view>
|
||||
<view class="chart-canvas">
|
||||
<!-- #ifdef H5 -->
|
||||
<qiun-data-charts
|
||||
type="barline"
|
||||
type="area"
|
||||
:opts="chartOpts"
|
||||
:chartData="chartData"
|
||||
:ontouch="true"
|
||||
:onmovetip="true"
|
||||
canvas2d
|
||||
:ontap="handleChartTap"
|
||||
ontap
|
||||
@complete="handleChartComplete"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS || MP-WEIXIN -->
|
||||
<view class="chart-placeholder-fallback">
|
||||
<text>📊 {{ points.length }} 天数据</text>
|
||||
<text class="chart-fallback-sub">App 端图表组件配置中</text>
|
||||
<!-- 默认指向最后一天的指示器(位置来自 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>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
// 显式 import 兜底,避免 easycom 漏注册
|
||||
import QiunDataCharts from '@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
|
||||
|
||||
const props = defineProps({
|
||||
points: { type: Array, default: () => [] },
|
||||
@ -48,40 +56,81 @@ const props = defineProps({
|
||||
})
|
||||
defineEmits(['retry'])
|
||||
|
||||
const peak = computed(() => props.points.find((p) => p.is_peak) || null)
|
||||
// canvas2d 模式 opts.pix=1,非 canvas2d 模式 opts.pix=systemInfo.pixelRatio
|
||||
// 用 emit 回来的 opts 拿真实值,兜底 1
|
||||
const pixel = ref(1)
|
||||
const chartOptsRef = ref(null)
|
||||
const calPoints = ref([])
|
||||
|
||||
let chartData = null
|
||||
let chartOpts = null
|
||||
// #ifdef H5
|
||||
chartData = computed(() => {
|
||||
// 监听 @complete:uCharts 渲染完成后会 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 chartData = computed(() => {
|
||||
const categories = props.points.map((p) => p.date.slice(5))
|
||||
const barData = props.points.map((p) => p.income)
|
||||
const lineData = props.points.map((p) => p.income)
|
||||
return {
|
||||
categories,
|
||||
series: [
|
||||
{ name: '收益', data: barData, color: '#FFDF77' },
|
||||
{ name: '趋势', data: lineData, color: '#1BAFEE', type: 'line' },
|
||||
{ name: '收益', type: 'area', data: lineData, color: '#1BAFEE' },
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
chartOpts = {
|
||||
color: ['#FFCC14', '#1BAFEE'],
|
||||
padding: [16, 16, 8, 16],
|
||||
const chartOpts = {
|
||||
color: ['#1BAFEE'],
|
||||
padding: PADDING,
|
||||
dataLabel: false,
|
||||
legend: { show: false },
|
||||
xAxis: { disableGrid: true, axisLine: false, fontColor: '#FFFFFF', fontSize: 9 },
|
||||
yAxis: { data: [{ min: 0 }], disableGrid: true, axisLine: false, fontColor: '#FFFFFF', fontSize: 9 },
|
||||
extra: {
|
||||
bar: { type: 'group', width: 18, linear: true, color: ['#FFDF77', '#B984FF', '#FF8183'] },
|
||||
line: { type: 'curve', width: 2, activeType: 'hollow' },
|
||||
// 关闭曲线上每个数据点的小圆圈
|
||||
dataPointShape: false,
|
||||
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',
|
||||
fontSize: 9,
|
||||
},
|
||||
extra: {
|
||||
tooltip: {
|
||||
showBox: true,
|
||||
showArrow: true,
|
||||
showCategory: true,
|
||||
bgColor: '#000000',
|
||||
bgOpacity: 0.6,
|
||||
fontColor: '#FFFFFF',
|
||||
fontSize: 11,
|
||||
splitLine: true,
|
||||
horizentalLine: { type: 'dash', width: 1, color: '#FFFFFF' },
|
||||
},
|
||||
// area 类型曲线区域图,gradient: true 启用从线色到透明的渐变填充
|
||||
area: { type: 'curve', opacity: 0.3, addLine: true, width: 2, gradient: true, activeType: 'hollow' },
|
||||
},
|
||||
}
|
||||
// #endif
|
||||
|
||||
function handleChartTap(e) {
|
||||
console.log('[IncomeCurve] chart tap', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -111,45 +160,63 @@ function handleChartTap(e) {
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.chart-peak-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #FFFABD;
|
||||
text-shadow: -1px 1px 4px rgba(206, 9, 9, 0.84);
|
||||
font-family: 'Baloo Bhai', sans-serif;
|
||||
}
|
||||
|
||||
.chart-peak-date {
|
||||
font-size: 18rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
}
|
||||
|
||||
.chart-placeholder-fallback {
|
||||
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;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chart-fallback-sub {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
opacity: 0.7;
|
||||
.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 {
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import DashboardHeader from './components/DashboardHeader.vue'
|
||||
import CrystalOverview from './components/CrystalOverview.vue'
|
||||
@ -71,8 +71,9 @@ import { useDashboardData } from '@/composables/useDashboardData'
|
||||
|
||||
const activeTab = ref('crystal')
|
||||
const starId = ref(uni.getStorageSync('star_id') || null)
|
||||
const isFirstShow = ref(true)
|
||||
|
||||
const { loading, error, data, refresh, isReady, lastFetched, dispose } = useDashboardData({
|
||||
const { loading, error, data, refresh } = useDashboardData({
|
||||
starId: starId.value,
|
||||
})
|
||||
|
||||
@ -91,19 +92,21 @@ async function handlePullDownRefresh() {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
// 从其他页面返回时强制刷新(绕 30 分钟缓存,spec §4.1)
|
||||
// 首次 onShow 跳过:composable 内部已主动 loadAll(),避免冷启动 14 个并发请求
|
||||
onShow(() => {
|
||||
if (isFirstShow.value) {
|
||||
isFirstShow.value = false
|
||||
return
|
||||
}
|
||||
refresh(null, true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/uni.scss';
|
||||
.dashboard-page-bg {
|
||||
background: linear-gradient(153deg, #FF9597 0%, #80DFFF 33%, #B8B8B8 74%, #D9D9D9 100%);
|
||||
background: $d-page-bg;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.dashboard-scroll {
|
||||
|
||||
@ -291,7 +291,7 @@ const mainTabs = ref([
|
||||
// ========== 分类配置 ==========
|
||||
const categories = ref([
|
||||
{ label: "热门作品", value: "hot" },
|
||||
{ label: "最新作品", value: "latest" },
|
||||
{ label: "最新作品", value: "new" },
|
||||
{ label: "星卡", value: "star_card" },
|
||||
{ label: "吧唧", value: "badge" },
|
||||
{ label: "海报", value: "poster" },
|
||||
|
||||
BIN
frontend/static/starcity/tongkuan/1.jpg
Normal file
BIN
frontend/static/starcity/tongkuan/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
BIN
frontend/static/starcity/tongkuan/2.jpg
Normal file
BIN
frontend/static/starcity/tongkuan/2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@ -161,6 +161,9 @@ import cfu from '../../js_sdk/u-charts/config-ucharts.js';
|
||||
// #ifdef APP-VUE || H5
|
||||
import cfe from '../../js_sdk/u-charts/config-echarts.js';
|
||||
// #endif
|
||||
// 本地 import 子组件兜底(easycom 漏注册 / 未重启 dev server 时仍能解析)
|
||||
import QiunError from '../qiun-error/qiun-error.vue';
|
||||
import QiunLoading from '../qiun-loading/qiun-loading.vue';
|
||||
|
||||
function deepCloneAssign(origin = {}, ...args) {
|
||||
for (let i in args) {
|
||||
@ -232,6 +235,11 @@ function debounce(fn, wait) {
|
||||
export default {
|
||||
name: 'qiun-data-charts',
|
||||
mixins: [uniCloud.mixinDatacom],
|
||||
// 本地注册子组件兜底(easycom 漏注册 / 未重启 dev server 时仍能解析)
|
||||
components: {
|
||||
QiunError,
|
||||
QiunLoading,
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
|
||||
@ -2950,7 +2950,7 @@ function drawToolTip(textList, offset, opts, config, context, eachSpacing, xAxis
|
||||
var startY = offset.y + lineHeight * index + (lineHeight - fontSize)/2 - 1 + boxPadding + fontSize;
|
||||
context.beginPath();
|
||||
context.setFontSize(fontSize);
|
||||
context.setTextBaseline('normal');
|
||||
context.setTextBaseline('alphabetic');
|
||||
context.setFillStyle(toolTipOption.fontColor);
|
||||
context.fillText(item.text, startX, startY);
|
||||
context.closePath();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user