feat:生产概括-查看更多添加筛选条件

master
黄伟杰 2 days ago
parent 7741a50eac
commit 1df0eda266

@ -67,27 +67,27 @@
<view v-else class="trend-content">
<scroll-view scroll-x enable-flex class="trend-stats-scroll">
<view class="trend-stats">
<view class="trend-stat-card">
<view class="trend-stat-card" @click="viewTaskList('')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.totalNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.totalTask') }}</text>
</view>
<view class="trend-stat-card">
<view class="trend-stat-card" @click="viewTaskList('2')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.issuedNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.issuedNum') }}</text>
</view>
<view class="trend-stat-card">
<view class="trend-stat-card" @click="viewTaskList('7')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.partialScheduledNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.partialScheduledNum') }}</text>
</view>
<view class="trend-stat-card">
<view class="trend-stat-card" @click="viewTaskList('8')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.waitingProductionNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.waitingProduction') }}</text>
</view>
<view class="trend-stat-card">
<view class="trend-stat-card" @click="viewTaskList('9')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.producingNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.producing') }}</text>
</view>
<view class="trend-stat-card">
<view class="trend-stat-card" @click="viewTaskList('10')">
<text class="trend-stat-value">{{ formatNumber(taskTrendData.completedNum) }}</text>
<text class="trend-stat-label">{{ t('dashboard.completed') }}</text>
</view>
@ -473,6 +473,13 @@ function viewMore() {
}
}
function viewTaskList(status) {
const url = status
? `/pages_function/pages/taskList/index?status=${status}`
: '/pages_function/pages/taskList/index'
uni.navigateTo({ url })
}
function loadData() {
if (currentFilter.value === 'task') {
loadTaskTrendData()
@ -623,6 +630,10 @@ defineExpose({ loadData })
&:last-child {
margin-right: 0;
}
&:active {
background: #e8ecf0;
}
}
.trend-stat-value {

@ -1,45 +1,61 @@
<template>
<view class="stats-section">
<view class="section-title">{{ t('dashboard.productionOverview') }}</view>
<view class="stats-grid">
<view v-for="(stat, index) in statsData" :key="index" class="stat-card" :class="'stat-' + stat.type">
<text class="stat-value">{{ formatNumber(stat.value) }}</text>
<text class="stat-label">{{ t(`dashboard.${stat.labelKey}`) }}</text>
<scroll-view scroll-x enable-flex class="stats-scroll">
<view class="stats-grid">
<view
v-for="(stat, index) in taskStats"
:key="stat.key"
class="stat-card"
:style="cardStyle(index)"
>
<text class="stat-value">{{ formatNumber(stat.value) }}</text>
<text class="stat-label">{{ stat.label }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { reactive, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { formatNumber } from '@/utils/format'
const { t } = useI18n()
const statsData = reactive([
{ labelKey: 'all', type: 'total', value: 0 },
{ labelKey: 'pending', type: 'pending', value: 0 },
{ labelKey: 'running', type: 'running', value: 0 },
{ labelKey: 'finished', type: 'finished', value: 0 }
])
const COLOR_PALETTE = [
{ border: '#1a3a5c', value: '#1a3a5c' },
{ border: '#ff8c00', value: '#ff8c00' },
{ border: '#18bc37', value: '#18bc37' },
{ border: '#4a90c2', value: '#4a90c2' },
{ border: '#e74c3c', value: '#e74c3c' },
{ border: '#8e44ad', value: '#8e44ad' },
{ border: '#16a085', value: '#16a085' },
{ border: '#d35400', value: '#d35400' },
{ border: '#2980b9', value: '#2980b9' },
{ border: '#c0392b', value: '#c0392b' }
]
const taskStats = ref([])
function cardStyle(index) {
const color = COLOR_PALETTE[index % COLOR_PALETTE.length]
return {
borderLeft: `6rpx solid ${color.border}`,
color: color.value
}
}
async function loadProductionStats() {
const res = await request({ url: '/admin-api/mes/dashboard/getProduction', method: 'get' })
const taskItems = (res?.data?.taskItems || []).map((i) => ({
const items = res?.data?.taskItems || []
taskStats.value = items.map((i) => ({
key: String(i.key),
label: i.label || '',
value: Number(i.value ?? 0)
}))
const byKey = taskItems.reduce((acc, cur) => {
acc[cur.key] = cur.value
return acc
}, {})
const keyOrder = ['11', '8', '9', '10']
statsData.forEach((stat, index) => {
const k = keyOrder[index]
stat.value = byKey[k] ?? 0
})
}
onMounted(() => {
@ -61,19 +77,27 @@ defineExpose({ loadProductionStats })
margin-bottom: 24rpx;
}
.stats-scroll {
width: 100%;
white-space: nowrap;
}
.stats-grid {
display: flex;
justify-content: space-between;
display: inline-flex;
flex-wrap: nowrap;
padding: 0 4rpx;
}
.stat-card {
flex: 1;
flex-shrink: 0;
width: 174rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 28rpx 16rpx;
margin: 0 8rpx;
padding: 24rpx 12rpx;
margin: 0 6rpx;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
white-space: normal;
&:first-child {
margin-left: 0;
@ -90,46 +114,15 @@ defineExpose({ loadProductionStats })
.stat-value {
display: block;
font-size: 48rpx;
font-size: 40rpx;
font-weight: bold;
margin-bottom: 8rpx;
margin-bottom: 6rpx;
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666666;
}
.stat-total {
border-left: 6rpx solid #1a3a5c;
.stat-value {
color: #1a3a5c;
}
}
.stat-pending {
border-left: 6rpx solid #ff8c00;
.stat-value {
color: #ff8c00;
}
}
.stat-running {
border-left: 6rpx solid #18bc37;
.stat-value {
color: #18bc37;
}
}
.stat-finished {
border-left: 6rpx solid #4a90c2;
.stat-value {
color: #4a90c2;
}
word-break: break-all;
}
</style>

@ -3,16 +3,16 @@
<template v-for="(item, index) in options">
<template v-if="values.includes(item.value)">
<span
v-if="item.elTagType == 'default' || item.elTagType == ''"
v-if="item.tagType == 'default' || item.tagType == ''"
:key="item.value"
:index="index"
:class="item.elTagClass"
>{{ item.label }}</span>
<uni-tag
<u-tag
v-else
:key="item.value + ''"
:index="index"
:type="elTagType(item.elTagType)"
:type="mapTagType(item.tagType)"
:class="item.elTagClass"
:text="item.label"
/>
@ -40,15 +40,16 @@ const values = computed(() => {
}
})
const elTagType = (tagType) =>{
tagType === 'danger' ? 'error' : tagType
tagType === 'info' ? 'default' : tagType
const mapTagType = (tagType) => {
if (tagType === 'danger') return 'error'
if (tagType === 'default') return 'info'
return tagType
}
</script>
<style scoped>
.uni-tag + .uni-tag {
.u-tag + .u-tag {
margin-left: 10px;
}
</style>

@ -117,9 +117,14 @@ export default {
selectDeviceHint: 'Please select a device to view trend'
},
taskList: {
filter: 'Filter',
code: 'Task Code',
status: 'Status',
taskType: 'Task Type',
orderDate: 'Order Date',
deliveryDate: 'Delivery Date',
remark: 'Remark',
createTime: 'Create Time',
totalNumber: 'Total',
planNumber: 'Planned',
unPlanNumber: 'Unplanned',
@ -162,6 +167,7 @@ export default {
uploadImageFailed: 'Image upload failed',
yes: 'Yes',
no: 'No',
all: 'All',
noMore: 'No more data',
confirmTitle: 'Confirm'
},

@ -117,9 +117,14 @@ export default {
selectDeviceHint: '请选择设备查看趋势'
},
taskList: {
filter: '筛选条件',
code: '任务单编码',
status: '状态',
taskType: '任务类型',
orderDate: '下单日期',
deliveryDate: '交付日期',
remark: '备注',
createTime: '创建时间',
totalNumber: '总数量',
planNumber: '已排产',
unPlanNumber: '未排产',
@ -162,6 +167,7 @@ export default {
uploadImageFailed: '图片上传失败',
yes: '是',
no: '否',
all: '全部',
noMore: '没有更多数据了',
confirmTitle: '提示'
},

@ -1,12 +1,58 @@
<template>
<view class="page-container">
<view class="filter-bar">
<view class="filter-toggle" @click="showFilter = !showFilter">
<text class="filter-toggle-text">{{ t('taskList.filter') }}</text>
<text class="filter-toggle-arrow">{{ showFilter ? '▲' : '▼' }}</text>
</view>
</view>
<view v-if="showFilter" class="filter-panel">
<view class="filter-row">
<text class="filter-label">{{ t('taskList.code') }}</text>
<input class="filter-input" v-model="filterParams.code" :placeholder="t('taskList.code')" />
</view>
<view class="filter-row">
<text class="filter-label">{{ t('taskList.status') }}</text>
<picker mode="selector" :range="statusOptions" range-key="label" :value="statusIndex" @change="onStatusChange">
<view class="filter-picker">
<text :class="{ 'filter-placeholder': !filterParams.status }">{{ statusLabel || t('taskList.status') }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="filter-row">
<text class="filter-label">{{ t('taskList.orderDate') }}</text>
<uni-datetime-picker v-model="filterParams.orderDate" type="daterange"
:placeholder="t('taskList.orderDate')" />
</view>
<view class="filter-row">
<text class="filter-label">{{ t('taskList.deliveryDate') }}</text>
<uni-datetime-picker v-model="filterParams.deliveryDate" type="daterange"
:placeholder="t('taskList.deliveryDate')" />
</view>
<view class="filter-row">
<text class="filter-label">{{ t('taskList.remark') }}</text>
<input class="filter-input" v-model="filterParams.remark" :placeholder="t('taskList.remark')" />
</view>
<view class="filter-row">
<text class="filter-label">{{ t('taskList.createTime') }}</text>
<uni-datetime-picker v-model="filterParams.createTime" type="daterange"
:placeholder="t('taskList.createTime')" />
</view>
<view class="filter-actions">
<view class="filter-btn reset-btn" @click="resetFilter">
<text>{{ t('functionCommon.cancel') }}</text>
</view>
<view class="filter-btn search-btn" @click="handleSearch">
<text>{{ t('functionCommon.search') }}</text>
</view>
</view>
</view>
<view class="task-list">
<view v-for="(task, index) in taskList" :key="index" class="task-card" @click="handleTaskClick(task)">
<view v-for="(task, index) in taskList" :key="index" class="task-card">
<view class="task-header">
<text class="task-code">{{ task.code }}</text>
<view class="task-status" :class="'status-' + task.statusType">
<text>{{ task.statusText }}</text>
</view>
<u-tag :text="task.statusText" :type="statusTagType(task.status)" size="mini" />
</view>
<view class="task-body">
<view class="task-row">
@ -56,13 +102,34 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { onReachBottom, onShow } from '@dcloudio/uni-app'
import { ref, reactive, computed, onMounted } from 'vue'
import { onReachBottom, onShow, onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import request from '@/utils/request'
import { setNavigationTitle } from '@/locales'
import { DICT_TYPE, getDictLabel, useDict } from '@/utils/dict'
const { t } = useI18n()
const { mes_task_status } = useDict(DICT_TYPE.MES_TASK_STATUS)
const statusOptions = computed(() => {
return [{ label: t('functionCommon.all'), value: '' }, ...(mes_task_status.value || [])]
})
const statusIndex = computed(() => {
return statusOptions.value.findIndex((item) => String(item.value) === String(filterParams.status))
})
const statusLabel = computed(() => {
const item = statusOptions.value.find((item) => String(item.value) === String(filterParams.status))
return item?.label || ''
})
function onStatusChange(e) {
const idx = e.detail.value
const item = statusOptions.value[idx]
filterParams.status = item ? item.value : ''
}
const taskList = reactive([])
const pageNo = ref(1)
@ -70,6 +137,16 @@ const pageSize = ref(10)
const total = ref(0)
const loading = ref(false)
const noMore = ref(false)
const showFilter = ref(false)
const filterParams = reactive({
code: '',
status: '',
orderDate: [],
deliveryDate: [],
remark: '',
createTime: []
})
function formatDate(dateStr) {
if (!dateStr) return '-'
@ -79,16 +156,46 @@ function formatDate(dateStr) {
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
}
function mapTaskStatus(status) {
const v = status === '' || status === null || status === undefined ? undefined : String(status)
if (v === '1') return { statusText: t('dashboard.pending'), statusType: 'pending' }
if (v === '2') return { statusText: t('dashboard.running'), statusType: 'running' }
if (v === '3') return { statusText: t('dashboard.finished'), statusType: 'finished' }
return { statusText: '-', statusType: 'pending' }
function statusTagType(status) {
const dict = mes_task_status.value || []
const item = dict.find((d) => String(d.value) === String(status))
const tagType = item?.tagType || 'primary'
if (tagType === 'danger') return 'error'
if (tagType === 'default') return 'info'
return tagType
}
function handleTaskClick(task) {
uni.showToast({ title: t('taskList.viewTask', { code: task.code }), icon: 'none' })
function buildFilterParams() {
const params = {}
if (filterParams.code) params.code = filterParams.code
if (filterParams.status) params.status = filterParams.status
if (filterParams.remark) params.remark = filterParams.remark
appendDateRange(params, 'orderDate', filterParams.orderDate)
appendDateRange(params, 'deliveryDate', filterParams.deliveryDate)
appendDateRange(params, 'createTime', filterParams.createTime)
return params
}
function appendDateRange(params, field, range) {
if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
params[`${field}[0]`] = range[0] + ' 00:00:00'
params[`${field}[1]`] = range[1] + ' 23:59:59'
}
}
function handleSearch() {
showFilter.value = false
loadTaskList(true)
}
function resetFilter() {
filterParams.code = ''
filterParams.status = ''
filterParams.orderDate = []
filterParams.deliveryDate = []
filterParams.remark = ''
filterParams.createTime = []
loadTaskList(true)
}
async function loadTaskList(reset = false) {
@ -101,15 +208,15 @@ async function loadTaskList(reset = false) {
if (noMore.value && !reset) return
loading.value = true
try {
const params = { pageNo: pageNo.value, pageSize: pageSize.value, ...buildFilterParams() }
const res = await request({
url: '/admin-api/mes/task/pagePlanTask',
method: 'get',
params: { pageNo: pageNo.value, pageSize: pageSize.value }
params
})
const list = res?.data?.list || []
total.value = res?.data?.total || 0
const mapped = list.map((item) => {
const statusInfo = mapTaskStatus(item?.status)
return {
id: item?.id,
code: item?.code ?? '-',
@ -119,8 +226,8 @@ async function loadTaskList(reset = false) {
isUrgent: item?.isUrgent,
isScheduled: item?.isScheduled,
status: item?.status,
statusText: statusInfo.statusText,
statusType: statusInfo.statusType,
statusText: getDictLabel(DICT_TYPE.MES_TASK_STATUS, item?.status),
statusType: statusTagType(item?.status),
totalNumber: item?.totalNumber ?? 0,
planNumber: item?.planNumber ?? 0,
unPlanNumber: item?.unPlanNumber ?? 0,
@ -139,10 +246,16 @@ async function loadTaskList(reset = false) {
}
}
onMounted(() => {
onLoad((query) => {
if (query?.status) {
filterParams.status = query.status
}
loadTaskList(true)
})
onMounted(() => {
})
onReachBottom(() => {
loadTaskList()
})
@ -159,6 +272,121 @@ onShow(() => {
padding: 24rpx;
}
.filter-bar {
margin-bottom: 20rpx;
}
.filter-toggle {
display: flex;
align-items: center;
justify-content: center;
padding: 16rpx 24rpx;
background: #ffffff;
border-radius: 12rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
&:active {
background: #f0f2f5;
}
}
.filter-toggle-text {
font-size: 28rpx;
color: #1a3a5c;
font-weight: 500;
margin-right: 8rpx;
}
.filter-toggle-arrow {
font-size: 20rpx;
color: #999999;
}
.filter-panel {
background: #ffffff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.filter-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-of-type {
margin-bottom: 0;
}
}
.filter-label {
width: 140rpx;
font-size: 26rpx;
color: #666666;
flex-shrink: 0;
}
.filter-input {
flex: 1;
height: 64rpx;
background: #f8fafc;
border-radius: 8rpx;
padding: 0 16rpx;
font-size: 26rpx;
border: 2rpx solid #e0e6ed;
}
.filter-picker {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
height: 64rpx;
background: #f8fafc;
border-radius: 8rpx;
padding: 0 16rpx;
font-size: 26rpx;
border: 2rpx solid #e0e6ed;
}
.filter-placeholder {
color: #999999;
}
.picker-arrow {
font-size: 20rpx;
color: #999999;
}
.filter-actions {
display: flex;
justify-content: flex-end;
margin-top: 24rpx;
gap: 16rpx;
}
.filter-btn {
padding: 14rpx 40rpx;
border-radius: 8rpx;
font-size: 26rpx;
font-weight: 500;
&:active {
opacity: 0.8;
}
}
.reset-btn {
background: #f0f2f5;
color: #666666;
}
.search-btn {
background: #1a3a5c;
color: #ffffff;
}
.task-list {
display: flex;
flex-direction: column;
@ -189,27 +417,6 @@ onShow(() => {
color: #1a3a5c;
}
.task-status {
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 22rpx;
}
.status-pending {
background: rgba(255, 140, 0, 0.15);
color: #ff8c00;
}
.status-running {
background: rgba(24, 188, 55, 0.15);
color: #18bc37;
}
.status-finished {
background: rgba(74, 144, 194, 0.15);
color: #4a90c2;
}
.task-body {
display: flex;
flex-direction: column;

@ -11,12 +11,13 @@ export enum DICT_TYPE {
INSPECTION_METHOD = "Inspection_method",
VALUE_TYPES = "value_types",
JOB_STATUS = "job_status",
MES_TASK_STATUS = "mes_task_status",
}
type DictItem = {
label: string;
value: string | number;
elTagType?: string;
tagType?: string;
elTagClass?: string;
};
@ -26,7 +27,7 @@ function normalizeDictItem(p: any): DictItem {
return {
label: String(label),
value: typeof value === "number" ? value : String(value),
elTagType: p?.colorType ?? p?.listClass ?? p?.elTagType,
tagType: p?.colorType ?? p?.listClass ?? p?.tagType,
elTagClass: p?.cssClass ?? p?.elTagClass,
};
}
@ -94,7 +95,7 @@ export function useDict(...args: any[]) {
res.value[dictType] = resp.data.map((p: any) => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
tagType: p.colorType || p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);

Loading…
Cancel
Save