chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
const decodeOption = value => {
|
||||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
return decodeURIComponent(String(value))
|
||||
} catch (error) {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
const valueText = (value, metricKind) => {
|
||||
const numericValue = Number(value || 0)
|
||||
if (metricKind === 'quantity') {
|
||||
return `${numericValue} 件`
|
||||
}
|
||||
return api.formatDurationAllUnits(numericValue)
|
||||
}
|
||||
|
||||
const metricTitle = metricKind => (metricKind === 'quantity' ? '清洗数量' : '总使用时长')
|
||||
|
||||
const normalizeFilters = options => ({
|
||||
category: decodeOption(options.category),
|
||||
id: decodeOption(options.id),
|
||||
metricKind: decodeOption(options.metricKind),
|
||||
startDate: decodeOption(options.startDate),
|
||||
endDate: decodeOption(options.endDate),
|
||||
attendancePointName: decodeOption(options.attendancePointName),
|
||||
name: decodeOption(options.name),
|
||||
productName: decodeOption(options.productName),
|
||||
processName: decodeOption(options.processName),
|
||||
stampingMethod: decodeOption(options.stampingMethod),
|
||||
})
|
||||
|
||||
const decorateDetail = detail => {
|
||||
const metricKind = detail.metricKind || ''
|
||||
const dailyRows = detail.dailyRows || []
|
||||
const maxDailyValue = Math.max(1, ...dailyRows.map(item => Number(item.value || 0)))
|
||||
|
||||
return {
|
||||
...detail,
|
||||
metricTitle: metricTitle(metricKind),
|
||||
valueText: valueText(detail.value, metricKind),
|
||||
dailyRows: dailyRows.map(item => ({
|
||||
...item,
|
||||
valueText: valueText(item.value, metricKind),
|
||||
barPercent: Math.max(4, Math.round(Number(item.value || 0) / maxDailyValue * 100)),
|
||||
})),
|
||||
reportRows: (detail.reportRows || []).map(item => ({
|
||||
...item,
|
||||
valueText: valueText(item.value, metricKind),
|
||||
employeeText: item.employeeName || item.employeePhone || '-',
|
||||
})),
|
||||
tags: Array.isArray(detail.tags) ? detail.tags : [],
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
filters: {},
|
||||
detail: null,
|
||||
loading: false,
|
||||
loadError: false,
|
||||
errorText: '',
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const filters = normalizeFilters(options || {})
|
||||
this.setData({ filters })
|
||||
this.load(filters)
|
||||
},
|
||||
|
||||
async load(filters = this.data.filters) {
|
||||
const loadSeq = (this.loadSeq || 0) + 1
|
||||
this.loadSeq = loadSeq
|
||||
this.setData({ loading: true, loadError: false, errorText: '' })
|
||||
try {
|
||||
const detail = await api.getUsageStatsDetail(filters)
|
||||
if (loadSeq !== this.loadSeq) {
|
||||
return
|
||||
}
|
||||
this.setData({ detail: decorateDetail(detail || {}), loadError: false, errorText: '' })
|
||||
} catch (error) {
|
||||
if (loadSeq === this.loadSeq) {
|
||||
const errorText = error.message || '加载详情失败'
|
||||
this.setData({ detail: null, loadError: true, errorText })
|
||||
wx.showToast({ title: errorText, icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
if (loadSeq === this.loadSeq) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reload() {
|
||||
this.load()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "统计详情"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<scroll-view class="page safe-bottom" scroll-y type="list">
|
||||
<view class="header">
|
||||
<text class="title ellipsis">{{detail.name || filters.name || '-'}}</text>
|
||||
<text class="subtitle ellipsis">考勤点 {{detail.attendancePointName || filters.attendancePointName || '-'}}</text>
|
||||
<text class="subtitle ellipsis">{{detail.objectType || '-'}}</text>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading && !detail && !loadError}}" class="list-empty">加载中</view>
|
||||
|
||||
<view wx:elif="{{loadError}}" class="card error-card">
|
||||
<text class="value">加载失败</text>
|
||||
<text class="subtitle error-text">{{errorText || '加载详情失败'}}</text>
|
||||
<button class="btn secondary reload-btn" loading="{{loading}}" bindtap="reload">重新加载</button>
|
||||
</view>
|
||||
|
||||
<block wx:elif="{{detail}}">
|
||||
<view class="card metric-card">
|
||||
<view class="metric-grid">
|
||||
<view class="metric-item">
|
||||
<text class="label">{{detail.metricTitle}}</text>
|
||||
<text class="value metric-value">{{detail.valueText}}</text>
|
||||
</view>
|
||||
<view class="metric-item">
|
||||
<text class="label">报工次数</text>
|
||||
<text class="value metric-value">{{detail.reportCount || 0}} 次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{detail.productName || detail.processName || detail.stampingMethod || detail.tags.length}}" class="tag-list">
|
||||
<text wx:if="{{detail.productName}}" class="usage-tag">{{detail.productName}}</text>
|
||||
<text wx:if="{{detail.processName}}" class="usage-tag">{{detail.processName}}</text>
|
||||
<text wx:if="{{detail.stampingMethod}}" class="usage-tag">{{detail.stampingMethod}}</text>
|
||||
<text wx:for="{{detail.tags}}" wx:key="*this" class="usage-tag">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="section-title">每日趋势</text>
|
||||
<view wx:if="{{!detail.dailyRows.length}}" class="list-empty">暂无每日趋势</view>
|
||||
<view wx:for="{{detail.dailyRows}}" wx:key="reportDate" class="card trend-card">
|
||||
<view class="row trend-row">
|
||||
<view class="row-left">
|
||||
<text class="value">{{item.reportDate || '-'}}</text>
|
||||
<text class="subtitle">报工 {{item.reportCount || 0}} 次</text>
|
||||
</view>
|
||||
<text class="value trend-value">{{item.valueText}}</text>
|
||||
</view>
|
||||
<view class="usage-bar-track">
|
||||
<view class="usage-bar-fill {{detail.metricKind === 'quantity' ? 'quantity' : ''}}" style="width: {{item.barPercent}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="section-title">关联报工</text>
|
||||
<view wx:if="{{!detail.reportRows.length}}" class="list-empty">暂无关联报工</view>
|
||||
<view wx:for="{{detail.reportRows}}" wx:key="reportId" class="card report-card">
|
||||
<view class="row report-head">
|
||||
<view class="row-left">
|
||||
<text class="value ellipsis">{{item.employeeText}}</text>
|
||||
<text class="subtitle">{{item.reportDate || '-'}}</text>
|
||||
</view>
|
||||
<text class="value report-value">{{item.valueText}}</text>
|
||||
</view>
|
||||
<view class="report-meta">
|
||||
<text class="subtitle ellipsis">{{item.displayName || '-'}}</text>
|
||||
<text class="subtitle report-count">报工 {{item.reportCount || 0}} 次</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view wx:else class="list-empty">暂无统计详情</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,125 @@
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
min-width: 0;
|
||||
padding: 18rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f5f8fc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
overflow: visible;
|
||||
line-height: 1.45;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.usage-tag {
|
||||
max-width: 100%;
|
||||
padding: 7rpx 12rpx;
|
||||
border-radius: 10rpx;
|
||||
background: #eef2f7;
|
||||
color: #667085;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.reload-btn {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.trend-card,
|
||||
.report-card {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.trend-row,
|
||||
.report-head {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.trend-value,
|
||||
.report-value {
|
||||
flex: 1 1 260rpx;
|
||||
min-width: 240rpx;
|
||||
max-width: 380rpx;
|
||||
overflow: visible;
|
||||
line-height: 1.45;
|
||||
text-align: right;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.usage-bar-track {
|
||||
overflow: hidden;
|
||||
height: 18rpx;
|
||||
margin-top: 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #e8eef6;
|
||||
box-shadow: inset 0 2rpx 5rpx rgba(22, 34, 50, 0.1);
|
||||
}
|
||||
|
||||
.usage-bar-fill {
|
||||
height: 100%;
|
||||
min-width: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #1463ff 0%, #5b8def 100%);
|
||||
}
|
||||
|
||||
.usage-bar-fill.quantity {
|
||||
background: linear-gradient(90deg, #087443 0%, #4ab37c 100%);
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.report-meta .subtitle {
|
||||
min-width: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.report-count {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
Reference in New Issue
Block a user