chore: initialize mini program frontend

This commit is contained in:
souplearn
2026-06-24 15:19:14 +08:00
commit fa73a3c184
96 changed files with 20561 additions and 0 deletions
+101
View File
@@ -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;
}