优化记录页趋势分析展示
This commit is contained in:
@@ -15,7 +15,8 @@ const { getBarPercent } = require('../../utils/util')
|
|||||||
*/
|
*/
|
||||||
Component({
|
Component({
|
||||||
properties: {
|
properties: {
|
||||||
chartData: { type: Array, value: [] }
|
chartData: { type: Array, value: [] },
|
||||||
|
variant: { type: String, value: 'total' }
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
bars: []
|
bars: []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<view class="trend-chart">
|
<view class="trend-chart {{variant === 'peak' ? 'peak' : ''}}">
|
||||||
<view class="trend-bars">
|
<view class="trend-bars">
|
||||||
<view class="trend-bar-col" wx:for="{{bars}}" wx:key="label">
|
<view class="trend-bar-col" wx:for="{{bars}}" wx:key="label">
|
||||||
<view class="trend-bar {{item.value === 0 ? 'zero' : ''}} {{item.highlight ? 'highlight' : ''}}" style="height: {{item.percent}}%;">
|
<view class="trend-bar {{item.value === 0 ? 'zero' : ''}} {{item.highlight ? 'highlight' : ''}}" style="height: {{item.percent}}%;">
|
||||||
|
|||||||
@@ -36,6 +36,32 @@
|
|||||||
box-shadow: 0 0 14rpx rgba(var(--primary-rgb), 0.5);
|
box-shadow: 0 0 14rpx rgba(var(--primary-rgb), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trend-chart.peak .trend-bars {
|
||||||
|
height: 156rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-chart.peak .trend-bar {
|
||||||
|
width: 12rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 8rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-chart.peak .trend-bar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -8rpx;
|
||||||
|
left: -4rpx;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-chart.peak .trend-bar.zero::after {
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
/* value caption floats above the bar so it never eats into bar height */
|
/* value caption floats above the bar so it never eats into bar height */
|
||||||
.trend-bar-value {
|
.trend-bar-value {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -29,8 +29,12 @@ Page({
|
|||||||
monthRecords: [],
|
monthRecords: [],
|
||||||
historyList: [],
|
historyList: [],
|
||||||
listMode: 'month',
|
listMode: 'month',
|
||||||
|
contentMode: 'records',
|
||||||
trendMode: 'week',
|
trendMode: 'week',
|
||||||
trendData: [],
|
trendData: [],
|
||||||
|
trendSummary: {},
|
||||||
|
bestTrendData: [],
|
||||||
|
bestTrendSummary: {},
|
||||||
showDayDetail: false,
|
showDayDetail: false,
|
||||||
dayDetail: {},
|
dayDetail: {},
|
||||||
icons: iconsMod.build()
|
icons: iconsMod.build()
|
||||||
@@ -61,7 +65,7 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
storage.validateStreak()
|
const streak = storage.validateStreak()
|
||||||
const stats = storage.getTotalStats()
|
const stats = storage.getTotalStats()
|
||||||
const monthKey = `${this.data.currentYear}-${String(this.data.currentMonth).padStart(2, '0')}`
|
const monthKey = `${this.data.currentYear}-${String(this.data.currentMonth).padStart(2, '0')}`
|
||||||
const records = (storage.getRecordsByMonth(monthKey) || []).map(r => ({ ...r, durationText: util.formatTime(r.duration) }))
|
const records = (storage.getRecordsByMonth(monthKey) || []).map(r => ({ ...r, durationText: util.formatTime(r.duration) }))
|
||||||
@@ -103,6 +107,9 @@ Page({
|
|||||||
: `还差 ${badgeDefs[nextIdx].days - totalDays} 天解锁「${badgeDefs[nextIdx].name}」`
|
: `还差 ${badgeDefs[nextIdx].days - totalDays} 天解锁「${badgeDefs[nextIdx].name}」`
|
||||||
|
|
||||||
const theme = themeMod.getCurrentTheme()
|
const theme = themeMod.getCurrentTheme()
|
||||||
|
const trendMode = this.data.trendMode || 'week'
|
||||||
|
const trendData = this._computeTrend(trendMode)
|
||||||
|
const bestTrendData = this._computeBestTrend(trendMode)
|
||||||
this.setData({
|
this.setData({
|
||||||
theme,
|
theme,
|
||||||
icons: iconsMod.build(theme),
|
icons: iconsMod.build(theme),
|
||||||
@@ -119,7 +126,10 @@ Page({
|
|||||||
historyList,
|
historyList,
|
||||||
listMode: this.data.listMode || 'month',
|
listMode: this.data.listMode || 'month',
|
||||||
displayList: (this.data.listMode || 'month') === 'month' ? records : historyList,
|
displayList: (this.data.listMode || 'month') === 'month' ? records : historyList,
|
||||||
trendData: this._computeTrend(this.data.trendMode || 'week'),
|
trendData,
|
||||||
|
trendSummary: { ...util.getTrendSummary(trendData, trendMode), streakCount: Number(streak.count) || 0 },
|
||||||
|
bestTrendData,
|
||||||
|
bestTrendSummary: util.getTrendSummary(bestTrendData, trendMode),
|
||||||
loading: false
|
loading: false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -155,10 +165,24 @@ Page({
|
|||||||
this.refresh()
|
this.refresh()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onToggleContentMode(e) {
|
||||||
|
const mode = e.currentTarget.dataset.mode
|
||||||
|
if (!['records', 'trend'].includes(mode) || mode === this.data.contentMode) return
|
||||||
|
this.setData({ contentMode: mode })
|
||||||
|
},
|
||||||
|
|
||||||
onToggleTrendMode(e) {
|
onToggleTrendMode(e) {
|
||||||
const mode = e.currentTarget.dataset.mode
|
const mode = e.currentTarget.dataset.mode
|
||||||
if (!mode || mode === this.data.trendMode) return
|
if (!mode || mode === this.data.trendMode) return
|
||||||
this.setData({ trendMode: mode, trendData: this._computeTrend(mode) })
|
const trendData = this._computeTrend(mode)
|
||||||
|
const bestTrendData = this._computeBestTrend(mode)
|
||||||
|
this.setData({
|
||||||
|
trendMode: mode,
|
||||||
|
trendData,
|
||||||
|
trendSummary: { ...util.getTrendSummary(trendData, mode), streakCount: Number(storage.getStreak().count) || 0 },
|
||||||
|
bestTrendData,
|
||||||
|
bestTrendSummary: util.getTrendSummary(bestTrendData, mode)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,6 +195,10 @@ Page({
|
|||||||
return util.getTrendData(storage.getRecords(), mode)
|
return util.getTrendData(storage.getRecords(), mode)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_computeBestTrend(mode) {
|
||||||
|
return util.getBestTrendData(storage.getRecords(), mode)
|
||||||
|
},
|
||||||
|
|
||||||
onDayTap(e) {
|
onDayTap(e) {
|
||||||
const { year, month, day } = e.detail
|
const { year, month, day } = e.detail
|
||||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||||
|
|||||||
@@ -80,7 +80,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|
||||||
|
<!-- 内容分区:日历与记录共享月份状态,趋势按需展示,减少默认页长度。 -->
|
||||||
|
<view class="records-content-tabs" hidden="{{totalSessions === 0}}">
|
||||||
|
<view class="records-content-tab {{contentMode === 'records' ? 'active' : ''}}" data-mode="records" bindtap="onToggleContentMode">记录日历</view>
|
||||||
|
<view class="records-content-tab {{contentMode === 'trend' ? 'active' : ''}}" data-mode="trend" bindtap="onToggleContentMode">趋势</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 训练趋势 -->
|
<!-- 训练趋势 -->
|
||||||
|
<block wx:if="{{contentMode === 'trend'}}">
|
||||||
<ui-card variant="in" hidden="{{totalSessions === 0}}">
|
<ui-card variant="in" hidden="{{totalSessions === 0}}">
|
||||||
<view class="trend-section">
|
<view class="trend-section">
|
||||||
<view class="trend-header">
|
<view class="trend-header">
|
||||||
@@ -93,11 +100,40 @@
|
|||||||
<view class="rec-seg-item {{trendMode === 'month' ? 'active' : ''}}" data-mode="month" bindtap="onToggleTrendMode">近6月</view>
|
<view class="rec-seg-item {{trendMode === 'month' ? 'active' : ''}}" data-mode="month" bindtap="onToggleTrendMode">近6月</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="trend-summary">
|
||||||
|
<view class="trend-summary-item">
|
||||||
|
<text class="trend-summary-value">{{trendSummary.totalText}}</text>
|
||||||
|
<text class="trend-summary-label">累计时长</text>
|
||||||
|
</view>
|
||||||
|
<view class="trend-summary-item">
|
||||||
|
<text class="trend-summary-value">{{trendSummary.activeCount}}</text>
|
||||||
|
<text class="trend-summary-label">{{trendSummary.activeLabel}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="trend-summary-item">
|
||||||
|
<text class="trend-summary-value">{{trendSummary.streakCount}}</text>
|
||||||
|
<text class="trend-summary-label">连续打卡</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<trend-chart chart-data="{{trendData}}"></trend-chart>
|
<trend-chart chart-data="{{trendData}}"></trend-chart>
|
||||||
|
<view class="trend-highlight">
|
||||||
|
<image class="trend-highlight-icon" src="{{icons.sparkleFill}}" mode="aspectFit"></image>
|
||||||
|
<text class="trend-highlight-title">{{trendSummary.peakTitle}}</text>
|
||||||
|
<text class="trend-highlight-text">{{trendSummary.peakText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="best-trend-header">
|
||||||
|
<view class="trend-title-wrap">
|
||||||
|
<image class="history-title-icon" src="{{icons.crownFill}}" mode="aspectFit"></image>
|
||||||
|
<text class="best-trend-title">个人最佳趋势</text>
|
||||||
|
</view>
|
||||||
|
<text class="best-trend-best">{{bestTrendSummary.peakText}}</text>
|
||||||
|
</view>
|
||||||
|
<trend-chart chart-data="{{bestTrendData}}" variant="peak"></trend-chart>
|
||||||
</view>
|
</view>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
</block>
|
||||||
|
|
||||||
<!-- 日历热力图 -->
|
<!-- 日历热力图 -->
|
||||||
|
<block wx:if="{{contentMode === 'records'}}">
|
||||||
<ui-card variant="in" hidden="{{totalSessions === 0}}">
|
<ui-card variant="in" hidden="{{totalSessions === 0}}">
|
||||||
<view class="calendar-section">
|
<view class="calendar-section">
|
||||||
<view class="month-picker">
|
<view class="month-picker">
|
||||||
@@ -167,6 +203,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
</block>
|
||||||
</view>
|
</view>
|
||||||
<!-- /hidden wrapper -->
|
<!-- /hidden wrapper -->
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,32 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- records / trend top-level switch ---- */
|
||||||
|
.records-content-tabs {
|
||||||
|
display: flex;
|
||||||
|
margin: 8rpx 0 20rpx;
|
||||||
|
padding: 6rpx;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-content-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14rpx 0;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 26rpx;
|
||||||
|
text-align: center;
|
||||||
|
transition: background 0.25s ease, color 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-content-tab.active {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- calendar ---- */
|
/* ---- calendar ---- */
|
||||||
.calendar-section {
|
.calendar-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -267,6 +293,79 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trend-summary {
|
||||||
|
display: flex;
|
||||||
|
margin: 16rpx 0 4rpx;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
background: rgba(var(--primary-rgb), 0.06);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-summary-item {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-summary-value {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-summary-label {
|
||||||
|
margin-top: 4rpx;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-highlight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 14rpx 16rpx;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border-radius: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-highlight-icon {
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-highlight-title {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-highlight-text {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.best-trend-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.best-trend-title {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.best-trend-best {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- history ---- */
|
/* ---- history ---- */
|
||||||
.history-section {
|
.history-section {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const test = require('node:test')
|
const test = require('node:test')
|
||||||
const assert = require('node:assert/strict')
|
const assert = require('node:assert/strict')
|
||||||
|
|
||||||
const { getTrendData, getBarPercent } = require('../utils/util')
|
const { getTrendData, getBestTrendData, getTrendSummary, getBarPercent } = require('../utils/util')
|
||||||
|
|
||||||
test('weekly trend combines multiple sessions from the same day', () => {
|
test('weekly trend combines multiple sessions from the same day', () => {
|
||||||
const records = {
|
const records = {
|
||||||
@@ -45,3 +45,34 @@ test('bar height remains proportional for very short sessions', () => {
|
|||||||
assert.ok(getBarPercent(1, 120) < 1)
|
assert.ok(getBarPercent(1, 120) < 1)
|
||||||
assert.equal(getBarPercent(120, 120), 100)
|
assert.equal(getBarPercent(120, 120), 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('trend summary reports total, active periods, and the highest period', () => {
|
||||||
|
const summary = getTrendSummary([
|
||||||
|
{ label: '一', value: 60 },
|
||||||
|
{ label: '二', value: 0 },
|
||||||
|
{ label: '今天', value: 75 }
|
||||||
|
], 'week')
|
||||||
|
|
||||||
|
assert.deepEqual(summary, {
|
||||||
|
totalText: '02:15',
|
||||||
|
activeCount: 2,
|
||||||
|
activeLabel: '训练天数',
|
||||||
|
peakTitle: '最高训练日',
|
||||||
|
peakText: '今天 · 01:15'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('personal best trend keeps the longest single session in each period', () => {
|
||||||
|
const records = {
|
||||||
|
'2026-07': [
|
||||||
|
{ date: '2026-07-29 08:00:00', duration: 30 },
|
||||||
|
{ date: '2026-07-29 20:00:00', duration: 45 },
|
||||||
|
{ date: '2026-07-23 08:00:00', duration: 60 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = getBestTrendData(records, 'week', new Date(2026, 6, 29))
|
||||||
|
assert.equal(data[0].value, 60)
|
||||||
|
assert.equal(data[6].value, 45)
|
||||||
|
assert.equal(data[6].valueText, '00:45')
|
||||||
|
})
|
||||||
|
|||||||
@@ -115,12 +115,65 @@ const getTrendData = (recordGroups, mode, now = new Date()) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBestTrendData = (recordGroups, mode, now = new Date()) => {
|
||||||
|
const peaks = {}
|
||||||
|
const groups = recordGroups && typeof recordGroups === 'object' ? recordGroups : {}
|
||||||
|
Object.values(groups).forEach((records) => {
|
||||||
|
if (!Array.isArray(records)) return
|
||||||
|
records.forEach((record) => {
|
||||||
|
const date = _dateOnly(record && record.date)
|
||||||
|
if (!date) return
|
||||||
|
const key = mode === 'month' ? date.substring(0, 7) : date
|
||||||
|
peaks[key] = Math.max(peaks[key] || 0, _normalizeDuration(record.duration))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const today = new Date(now)
|
||||||
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
const count = mode === 'month' ? 6 : 7
|
||||||
|
return Array.from({ length: count }, (_, index) => {
|
||||||
|
const offset = count - 1 - index
|
||||||
|
const date = mode === 'month'
|
||||||
|
? new Date(today.getFullYear(), today.getMonth() - offset, 1)
|
||||||
|
: new Date(today.getFullYear(), today.getMonth(), today.getDate() - offset)
|
||||||
|
const key = mode === 'month'
|
||||||
|
? `${date.getFullYear()}-${pad(date.getMonth() + 1)}`
|
||||||
|
: `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
|
const value = peaks[key] || 0
|
||||||
|
return {
|
||||||
|
label: mode === 'month' ? `${date.getMonth() + 1}月` : offset === 0 ? '今天' : weekdays[date.getDay()],
|
||||||
|
value,
|
||||||
|
valueText: value > 0 ? _formatTrendDuration(value) : '',
|
||||||
|
highlight: offset === 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getBarPercent = (value, max) => {
|
const getBarPercent = (value, max) => {
|
||||||
const amount = _normalizeDuration(value)
|
const amount = _normalizeDuration(value)
|
||||||
const maximum = _normalizeDuration(max)
|
const maximum = _normalizeDuration(max)
|
||||||
return maximum > 0 ? amount / maximum * 100 : 0
|
return maximum > 0 ? amount / maximum * 100 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTrendSummary = (trendData, mode) => {
|
||||||
|
const items = Array.isArray(trendData) ? trendData : []
|
||||||
|
const total = items.reduce((sum, item) => sum + _normalizeDuration(item && item.value), 0)
|
||||||
|
const activeItems = items.filter(item => _normalizeDuration(item && item.value) > 0)
|
||||||
|
const peak = activeItems.reduce((current, item) => {
|
||||||
|
return !current || _normalizeDuration(item.value) > _normalizeDuration(current.value) ? item : current
|
||||||
|
}, null)
|
||||||
|
const isMonth = mode === 'month'
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalText: _formatTrendDuration(total),
|
||||||
|
activeCount: activeItems.length,
|
||||||
|
activeLabel: isMonth ? '活跃月份' : '训练天数',
|
||||||
|
peakTitle: isMonth ? '最高训练月' : '最高训练日',
|
||||||
|
peakText: peak ? `${peak.label} · ${_formatTrendDuration(peak.value)}` : '暂无训练数据'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a local file path (typically `wxfile://...` from chooseAvatar) to a
|
* Convert a local file path (typically `wxfile://...` from chooseAvatar) to a
|
||||||
* base64 data URI. Used to make temporary file URLs survive the WeChat
|
* base64 data URI. Used to make temporary file URLs survive the WeChat
|
||||||
@@ -169,6 +222,8 @@ module.exports = {
|
|||||||
formatDate,
|
formatDate,
|
||||||
dateOnly: _dateOnly,
|
dateOnly: _dateOnly,
|
||||||
getTrendData,
|
getTrendData,
|
||||||
|
getBestTrendData,
|
||||||
|
getTrendSummary,
|
||||||
getBarPercent,
|
getBarPercent,
|
||||||
fileToDataURI
|
fileToDataURI
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user