chore: initialize mini program frontend
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
const api = require('../../utils/api')
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
let usageStatsLoadSeq = 0
|
||||
let keywordSearchTimer = null
|
||||
|
||||
const pad2 = value => String(value).padStart(2, '0')
|
||||
|
||||
const formatDate = date => `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
|
||||
|
||||
const defaultDateRange = () => {
|
||||
const today = new Date()
|
||||
return {
|
||||
startDate: `${today.getFullYear()}-${pad2(today.getMonth() + 1)}-01`,
|
||||
endDate: formatDate(today),
|
||||
}
|
||||
}
|
||||
|
||||
const metricLabel = metricKind => (metricKind === 'quantity' ? '数量' : '时长')
|
||||
|
||||
const valueLabel = row => {
|
||||
if (row.metricKind === 'quantity') {
|
||||
return `${Number(row.value || 0)} 件`
|
||||
}
|
||||
return api.formatDurationAllUnits(row.value)
|
||||
}
|
||||
|
||||
const openDocumentFile = filePath => new Promise((resolve, reject) => {
|
||||
wx.openDocument({
|
||||
filePath,
|
||||
fileType: 'xlsx',
|
||||
showMenu: true,
|
||||
success: resolve,
|
||||
fail: () => reject(new Error('打开文件失败')),
|
||||
})
|
||||
})
|
||||
|
||||
Page({
|
||||
data: {
|
||||
...defaultDateRange(),
|
||||
category: 'device',
|
||||
sortBy: 'value',
|
||||
attendancePoints: [],
|
||||
attendancePointLabels: ['全部考勤点'],
|
||||
attendancePointIndex: 0,
|
||||
attendancePointName: '',
|
||||
keyword: '',
|
||||
rows: [],
|
||||
page: 1,
|
||||
pageInput: '1',
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
exporting: false,
|
||||
refreshing: false,
|
||||
scrollTop: 0,
|
||||
},
|
||||
onLoad() {
|
||||
this.init()
|
||||
},
|
||||
onUnload() {
|
||||
if (keywordSearchTimer) {
|
||||
clearTimeout(keywordSearchTimer)
|
||||
keywordSearchTimer = null
|
||||
}
|
||||
},
|
||||
async init() {
|
||||
await this.loadAttendancePoints()
|
||||
await this.load(1)
|
||||
},
|
||||
async loadAttendancePoints() {
|
||||
try {
|
||||
const points = await api.listAccessibleAttendancePoints()
|
||||
const attendancePoints = points || []
|
||||
this.setData({
|
||||
attendancePoints,
|
||||
attendancePointLabels: ['全部考勤点'].concat(attendancePoints.map(item => item.name)),
|
||||
})
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || '加载考勤点失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async load(page = this.data.page) {
|
||||
const loadSeq = usageStatsLoadSeq + 1
|
||||
usageStatsLoadSeq = loadSeq
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const result = await api.listUsageStats({
|
||||
category: this.data.category,
|
||||
sortBy: this.data.sortBy,
|
||||
startDate: this.data.startDate,
|
||||
endDate: this.data.endDate,
|
||||
attendancePointName: this.data.attendancePointName,
|
||||
keyword: this.data.keyword,
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
if (loadSeq !== usageStatsLoadSeq) {
|
||||
return
|
||||
}
|
||||
const rows = result.rows || []
|
||||
const maxValue = Math.max(1, ...rows.map(item => Number(item.value || 0)))
|
||||
this.setData({
|
||||
rows: rows.map(item => ({
|
||||
...item,
|
||||
metricLabel: metricLabel(item.metricKind),
|
||||
valueText: valueLabel(item),
|
||||
barPercent: Math.max(4, Math.round(Number(item.value || 0) / maxValue * 100)),
|
||||
uniqueKey: `${item.category || ''}-${item.id || ''}-${item.metricKind || ''}-${item.name || ''}`,
|
||||
})),
|
||||
page: result.page || 1,
|
||||
pageInput: String(result.page || 1),
|
||||
totalPages: result.totalPages || 1,
|
||||
scrollTop: 0,
|
||||
})
|
||||
} catch (error) {
|
||||
if (loadSeq === usageStatsLoadSeq) {
|
||||
wx.showToast({ title: error.message || '加载统计失败', icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
if (loadSeq === usageStatsLoadSeq) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
}
|
||||
},
|
||||
async onPullDownRefresh() {
|
||||
this.setData({ refreshing: true })
|
||||
try {
|
||||
await this.load(1)
|
||||
} finally {
|
||||
this.setData({ refreshing: false })
|
||||
if (wx.stopPullDownRefresh) {
|
||||
wx.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
},
|
||||
chooseCategory(e) {
|
||||
const category = e.currentTarget.dataset.category
|
||||
if (category === this.data.category) {
|
||||
return
|
||||
}
|
||||
this.setData({ category, page: 1 })
|
||||
this.load(1)
|
||||
},
|
||||
chooseSort(e) {
|
||||
const sortBy = e.currentTarget.dataset.sort
|
||||
if (sortBy === this.data.sortBy) {
|
||||
return
|
||||
}
|
||||
this.setData({ sortBy, page: 1 })
|
||||
this.load(1)
|
||||
},
|
||||
onStartChange(e) {
|
||||
this.setData({ startDate: e.detail.value, page: 1 })
|
||||
this.load(1)
|
||||
},
|
||||
onEndChange(e) {
|
||||
this.setData({ endDate: e.detail.value, page: 1 })
|
||||
this.load(1)
|
||||
},
|
||||
onAttendancePointChange(e) {
|
||||
const attendancePointIndex = Number(e.detail.value)
|
||||
const point = attendancePointIndex > 0 ? this.data.attendancePoints[attendancePointIndex - 1] : null
|
||||
this.setData({
|
||||
attendancePointIndex,
|
||||
attendancePointName: point ? point.name : '',
|
||||
page: 1,
|
||||
})
|
||||
this.load(1)
|
||||
},
|
||||
onKeywordInput(e) {
|
||||
this.setData({ keyword: e.detail.value, page: 1 })
|
||||
if (keywordSearchTimer) {
|
||||
clearTimeout(keywordSearchTimer)
|
||||
}
|
||||
keywordSearchTimer = setTimeout(() => {
|
||||
keywordSearchTimer = null
|
||||
this.load(1)
|
||||
}, 350)
|
||||
},
|
||||
searchKeyword() {
|
||||
if (keywordSearchTimer) {
|
||||
clearTimeout(keywordSearchTimer)
|
||||
keywordSearchTimer = null
|
||||
}
|
||||
this.setData({ page: 1 })
|
||||
this.load(1)
|
||||
},
|
||||
openDetail(e) {
|
||||
const row = this.data.rows[Number(e.currentTarget.dataset.index)]
|
||||
if (!row) {
|
||||
return
|
||||
}
|
||||
const attendancePointName = this.data.attendancePointName || row.attendancePointName || ''
|
||||
const params = [
|
||||
['category', row.category || this.data.category],
|
||||
['id', row.id || ''],
|
||||
['metricKind', row.metricKind || ''],
|
||||
['startDate', this.data.startDate],
|
||||
['endDate', this.data.endDate],
|
||||
['attendancePointName', attendancePointName],
|
||||
['name', row.name || ''],
|
||||
['productName', row.productName || ''],
|
||||
['processName', row.processName || ''],
|
||||
['stampingMethod', row.stampingMethod || ''],
|
||||
].map(pair => `${pair[0]}=${encodeURIComponent(pair[1])}`).join('&')
|
||||
wx.navigateTo({ url: `/pages/usageStatsDetail/usageStatsDetail?${params}` })
|
||||
},
|
||||
async exportFile() {
|
||||
if (this.data.exporting) {
|
||||
return
|
||||
}
|
||||
this.setData({ exporting: true })
|
||||
wx.showLoading({ title: '导出中' })
|
||||
let filePath = ''
|
||||
try {
|
||||
filePath = await api.exportUsageStats({
|
||||
startDate: this.data.startDate,
|
||||
endDate: this.data.endDate,
|
||||
attendancePointName: this.data.attendancePointName,
|
||||
keyword: this.data.keyword,
|
||||
})
|
||||
await openDocumentFile(filePath)
|
||||
} catch (error) {
|
||||
wx.showToast({ title: error.message || (filePath ? '打开文件失败' : '导出失败'), icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
this.setData({ exporting: false })
|
||||
}
|
||||
},
|
||||
onPageInput(e) {
|
||||
this.setData({ pageInput: e.detail.value })
|
||||
},
|
||||
jumpToPage(e) {
|
||||
wx.hideKeyboard()
|
||||
const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.pageInput
|
||||
const target = Math.max(1, Math.min(Number(inputValue) || this.data.page, this.data.totalPages))
|
||||
if (target === this.data.page) {
|
||||
this.setData({ pageInput: String(this.data.page || 1) })
|
||||
return
|
||||
}
|
||||
this.load(target)
|
||||
},
|
||||
prevPage() {
|
||||
if (this.data.page > 1) {
|
||||
this.load(this.data.page - 1)
|
||||
}
|
||||
},
|
||||
nextPage() {
|
||||
if (this.data.page < this.data.totalPages) {
|
||||
this.load(this.data.page + 1)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "设备模具使用统计"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<scroll-view
|
||||
class="page safe-bottom"
|
||||
scroll-y
|
||||
type="list"
|
||||
scroll-top="{{scrollTop}}"
|
||||
scroll-with-animation
|
||||
refresher-enabled
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onPullDownRefresh"
|
||||
>
|
||||
<view class="header">
|
||||
<text class="title">设备模具使用统计</text>
|
||||
<text class="subtitle">默认统计审核通过且未作废数据</text>
|
||||
</view>
|
||||
|
||||
<view class="card filter-card">
|
||||
<view class="date-filter-row">
|
||||
<view class="date-filter-item">
|
||||
<text class="label">开始日期</text>
|
||||
<picker mode="date" value="{{startDate}}" bindchange="onStartChange">
|
||||
<view class="picker">{{startDate || '开始日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="date-filter-item">
|
||||
<text class="label">结束日期</text>
|
||||
<picker mode="date" value="{{endDate}}" bindchange="onEndChange">
|
||||
<view class="picker">{{endDate || '结束日期'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-section">
|
||||
<text class="label">考勤点</text>
|
||||
<picker range="{{attendancePointLabels}}" value="{{attendancePointIndex}}" bindchange="onAttendancePointChange">
|
||||
<view class="picker ellipsis">{{attendancePointLabels[attendancePointIndex] || '全部考勤点'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="segmented-row">
|
||||
<button class="segment-btn {{category === 'device' ? 'active' : ''}}" data-category="device" bindtap="chooseCategory">设备</button>
|
||||
<button class="segment-btn {{category === 'mold' ? 'active' : ''}}" data-category="mold" bindtap="chooseCategory">模具</button>
|
||||
</view>
|
||||
|
||||
<view class="segmented-row">
|
||||
<button class="segment-btn {{sortBy === 'value' ? 'active' : ''}}" data-sort="value" bindtap="chooseSort">按时长/数量</button>
|
||||
<button class="segment-btn {{sortBy === 'count' ? 'active' : ''}}" data-sort="count" bindtap="chooseSort">按报工次数</button>
|
||||
</view>
|
||||
|
||||
<button class="btn primary full export-btn" loading="{{exporting}}" bindtap="exportFile">导出统计</button>
|
||||
</view>
|
||||
|
||||
<input
|
||||
class="input search"
|
||||
value="{{keyword}}"
|
||||
placeholder="搜索考勤点、设备、模具、产品、工序、冲压方式"
|
||||
confirm-type="search"
|
||||
bindinput="onKeywordInput"
|
||||
bindconfirm="searchKeyword"
|
||||
/>
|
||||
|
||||
<view wx:if="{{loading && !rows.length}}" class="list-empty">加载中</view>
|
||||
<view wx:elif="{{!rows.length}}" class="list-empty">暂无统计数据</view>
|
||||
|
||||
<view
|
||||
wx:for="{{rows}}"
|
||||
wx:key="uniqueKey"
|
||||
wx:for-index="index"
|
||||
class="card usage-card"
|
||||
data-index="{{index}}"
|
||||
bindtap="openDetail"
|
||||
>
|
||||
<view class="row usage-head">
|
||||
<view class="row-left">
|
||||
<text class="value usage-name">{{item.name || '-'}}</text>
|
||||
<text class="subtitle ellipsis">考勤点 {{item.attendancePointName || '-'}}</text>
|
||||
<text class="subtitle ellipsis">{{item.objectType || '-'}} · 报工 {{item.reportCount || 0}} 次</text>
|
||||
</view>
|
||||
<text class="pill primary">{{item.metricLabel}}</text>
|
||||
</view>
|
||||
|
||||
<view class="usage-bar-track">
|
||||
<view class="usage-bar-fill {{item.metricKind === 'quantity' ? 'quantity' : 'duration'}}" style="width: {{item.barPercent}}%;"></view>
|
||||
</view>
|
||||
|
||||
<view class="row usage-value-row">
|
||||
<text class="label">{{item.metricKind === 'quantity' ? '统计数量' : '使用时长'}}</text>
|
||||
<text class="value usage-value">{{item.valueText}}</text>
|
||||
</view>
|
||||
<view wx:if="{{item.productName || item.processName || item.stampingMethod}}" class="usage-tags">
|
||||
<text wx:if="{{item.productName}}" class="usage-tag">{{item.productName}}</text>
|
||||
<text wx:if="{{item.processName}}" class="usage-tag">{{item.processName}}</text>
|
||||
<text wx:if="{{item.stampingMethod}}" class="usage-tag">{{item.stampingMethod}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="button-row pager-row usage-pager" wx:if="{{rows.length}}">
|
||||
<button class="btn secondary pager-nav" bindtap="prevPage" disabled="{{page <= 1}}">上一页</button>
|
||||
<view class="page-jump-control usage-page-jump">
|
||||
<input class="page-jump-input usage-page-input" type="number" confirm-type="done" value="{{pageInput}}" bindinput="onPageInput" bindconfirm="jumpToPage" />
|
||||
<text class="page-total-label usage-total-label">/ {{totalPages}}</text>
|
||||
<text class="page-jump-action" bindtap="jumpToPage">跳</text>
|
||||
</view>
|
||||
<button class="btn secondary pager-nav" bindtap="nextPage" disabled="{{page >= totalPages}}">下一页</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,150 @@
|
||||
.filter-card {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.date-filter-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.date-filter-item {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.segmented-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.segment-btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
padding: 0 10rpx;
|
||||
border: 1rpx solid #cbd7e6;
|
||||
border-radius: 14rpx;
|
||||
background: #f7faff;
|
||||
color: #475467;
|
||||
font-size: 25rpx;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.segment-btn.active {
|
||||
border-color: #1463ff;
|
||||
background: #eaf2ff;
|
||||
color: #1456c2;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.usage-card {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.usage-card:active {
|
||||
transform: translateY(3rpx);
|
||||
}
|
||||
|
||||
.usage-head {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.usage-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage-bar-track {
|
||||
overflow: hidden;
|
||||
height: 18rpx;
|
||||
margin-top: 22rpx;
|
||||
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%);
|
||||
}
|
||||
|
||||
.usage-value-row {
|
||||
align-items: flex-start;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.usage-value {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
overflow: visible;
|
||||
line-height: 1.45;
|
||||
text-align: right;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.usage-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.usage-pager {
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.usage-page-jump {
|
||||
flex-basis: 236rpx;
|
||||
}
|
||||
|
||||
.usage-page-input {
|
||||
flex-basis: 72rpx;
|
||||
}
|
||||
|
||||
.usage-total-label {
|
||||
min-width: 86rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user