feat:新增模具管理-点检记录

master
黄伟杰 2 weeks ago
parent 929dea162b
commit 493fd0125f

@ -0,0 +1,33 @@
import request from '@/utils/request'
export function getTicketManagementPage(params = {}) {
return request({
url: '/admin-api/mes/mold-ticket-management/page',
method: 'get',
params
})
}
export function batchUpdateTicketStatus(data) {
return request({
url: '/admin-api/mes/mold-ticket-management/batchUpdateStatus',
method: 'put',
data
})
}
export function getTicketResultsPage(params = {}) {
return request({
url: '/admin-api/mes/mold-ticket-results/page',
method: 'get',
params
})
}
export function batchUpdateTicketResults(data) {
return request({
url: '/admin-api/mes/mold-ticket-results/batchUpdate',
method: 'put',
data
})
}

@ -408,6 +408,53 @@ export default {
planSelectTitle: 'Select Inspection Plans',
userSelectTitle: 'Select Operable Users'
},
moldWorkOrder: {
moduleName: 'Inspection Records',
subTitle: 'Mold management inspection records',
detailTitle: 'Inspection Record Detail',
basicInfo: 'Basic Info',
resultListTitle: 'Inspection Results',
planNo: 'Ticket No.',
moldName: 'Mold Name',
planType: 'Task Type',
planTypeInspect: 'Inspection',
planTypeMaintain: 'Maintenance',
configName: 'Task Config',
jobStatus: 'Job Status',
jobStatusPending: 'Pending',
jobStatusProcessing: 'Processing',
jobStatusCompleted: 'Completed',
jobStatusTimeout: 'Timeout',
jobStatusCancelled: 'Cancelled',
jobResult: 'Job Result',
jobResultOk: 'OK',
jobResultNg: 'NG',
operatorName: 'Operator',
taskTime: 'Task Time',
taskEndTime: 'End Time',
cancelReason: 'Cancel Reason',
createTime: 'Created At',
searchPlaceholder: 'Enter ticket no.',
empty: 'No inspection records',
cancelTask: 'Cancel Task',
cancelSuccess: 'Cancelled successfully',
cancelFail: 'Cancel failed',
placeholderCancelReason: 'Enter cancel reason',
validatorCancelReasonRequired: 'Cancel reason is required',
inspectionItemName: 'Inspection Item',
inspectionMethod: 'Inspection Method',
judgmentCriteria: 'Judgment Criteria',
valueType: 'Value Type',
inspectionResult: 'Inspection Result',
inspectionResultPending: 'Pending',
inspectionResultPass: 'Pass',
inspectionResultFail: 'Fail',
textInput: 'Input Value',
remark: 'Remark',
images: 'Images',
noResultData: 'No inspection result data',
loadMore: 'Load More'
},
mine: {
clickLogin: 'Tap to sign in',
username: 'Username: {name}',

@ -75,7 +75,9 @@ const literalMap = {
'点检模板': 'moldInspectionPlan.moduleName',
'点检模板详情': 'moldInspectionPlan.detailTitle',
'点检任务': 'moldTaskConfig.moduleName',
'点检任务详情': 'moldTaskConfig.detailTitle'
'点检任务详情': 'moldTaskConfig.detailTitle',
'点检记录': 'moldWorkOrder.moduleName',
'点检记录详情': 'moldWorkOrder.detailTitle'
}
function applyTabBarLanguage() {

@ -408,6 +408,53 @@ export default {
planSelectTitle: '选择点检模板',
userSelectTitle: '选择可操作用户'
},
moldWorkOrder: {
moduleName: '点检记录',
subTitle: '模具管理点检记录查询',
detailTitle: '点检记录详情',
basicInfo: '基础信息',
resultListTitle: '检验结果',
planNo: '工单编号',
moldName: '模具名称',
planType: '任务类型',
planTypeInspect: '点检',
planTypeMaintain: '保养',
configName: '任务配置',
jobStatus: '作业状态',
jobStatusPending: '待处理',
jobStatusProcessing: '处理中',
jobStatusCompleted: '已完成',
jobStatusTimeout: '已超时',
jobStatusCancelled: '已取消',
jobResult: '作业结果',
jobResultOk: 'OK',
jobResultNg: 'NG',
operatorName: '操作人',
taskTime: '任务时间',
taskEndTime: '结束时间',
cancelReason: '取消原因',
createTime: '创建时间',
searchPlaceholder: '请输入工单编号',
empty: '暂无点检记录数据',
cancelTask: '取消任务',
cancelSuccess: '取消成功',
cancelFail: '取消失败',
placeholderCancelReason: '请输入取消原因',
validatorCancelReasonRequired: '取消原因不能为空',
inspectionItemName: '检验项名称',
inspectionMethod: '检验方式',
judgmentCriteria: '判定基准',
valueType: '值类型',
inspectionResult: '检验结果',
inspectionResultPending: '待检',
inspectionResultPass: '合格',
inspectionResultFail: '不合格',
textInput: '输入值',
remark: '备注',
images: '图片',
noResultData: '暂无检验结果数据',
loadMore: '加载更多'
},
mine: {
clickLogin: '点击登录',
username: '用户名:{name}',

@ -533,6 +533,20 @@
"navigationStyle": "custom"
}
},
{
"path": "moldWorkOrderInquiry/index",
"style": {
"navigationBarTitleText": "点检记录",
"navigationStyle": "custom"
}
},
{
"path": "moldWorkOrderInquiry/detail",
"style": {
"navigationBarTitleText": "点检记录详情",
"navigationStyle": "custom"
}
},
{
"path": "planList/index",
"style": {

@ -279,9 +279,9 @@
</view>
<view class="function-item" @click="handleClick('点检记录')">
<view class="function-icon" style="background: rgba(0, 188, 212, 0.1);">
<text class="icon-inner">📜</text>
<uni-icons type="list" size="22" color="#00bcd4"></uni-icons>
</view>
<text class="function-name">点检记录</text>
<text class="function-name">{{ t('moldWorkOrder.moduleName') }}</text>
</view>
</view>
</view>
@ -383,7 +383,7 @@ function handleClick(name) {
'点检项库': '',
'点检模板': '/pages_function/pages/moldInspectionPlan/index',
'点检任务': '/pages_function/pages/moldTaskConfiguration/index',
'点检记录': '',
'点检记录': '/pages_function/pages/moldWorkOrderInquiry/index',
'维修项目': '',
'维修单': ''
};

@ -0,0 +1,306 @@
<template>
<view class="page-container">
<view class="fixed-header">
<AppTitleHeader :title="t('moldWorkOrder.detailTitle')" />
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="content-section">
<!-- 基础信息 -->
<view class="info-card">
<view class="card-title">{{ t('moldWorkOrder.basicInfo') }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.planNo') }}</text>
<text class="info-value">{{ textValue(detailData.planNo) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.moldName') }}</text>
<text class="info-value">{{ textValue(detailData.moldName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.planType') }}</text>
<text class="info-value">{{ planTypeText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.configName') }}</text>
<text class="info-value">{{ textValue(detailData.configName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.jobStatus') }}</text>
<text :class="['info-value', jobStatusClass]">{{ jobStatusText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.jobResult') }}</text>
<text :class="['info-value', jobResultClass]">{{ jobResultText }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.operatorName') }}</text>
<text class="info-value">{{ textValue(detailData.operatorName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.taskTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.taskTime) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.taskEndTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.taskEndTime) }}</text>
</view>
<view v-if="String(detailData.jobStatus) === '4'" class="info-row">
<text class="info-label">{{ t('moldWorkOrder.cancelReason') }}</text>
<text class="info-value">{{ textValue(detailData.cancelReason) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldWorkOrder.createTime') }}</text>
<text class="info-value">{{ formatDateTime(detailData.createTime) }}</text>
</view>
</view>
</view>
<!-- 检验结果列表 -->
<view class="info-card">
<view class="card-title">{{ t('moldWorkOrder.resultListTitle') }}</view>
<view v-if="resultLoading" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!resultList.length" class="hint">{{ t('moldWorkOrder.noResultData') }}</view>
<view v-else class="result-list">
<view v-for="(item, index) in resultList" :key="item.id || index" class="result-card">
<view class="result-index">{{ index + 1 }}</view>
<view class="result-body">
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.inspectionItemName') }}</text>
<text class="result-value">{{ textValue(item.inspectionItemName) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.inspectionMethod') }}</text>
<text class="result-value">{{ getInspectionMethodLabel(item.inspectionMethod) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.judgmentCriteria') }}</text>
<text class="result-value">{{ textValue(item.judgmentCriteria) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.valueType') }}</text>
<text class="result-value">{{ getValueTypeLabel(item.valueType) }}</text>
</view>
<view class="result-row">
<text class="result-label">{{ t('moldWorkOrder.inspectionResult') }}</text>
<text :class="['result-value', inspectionResultClass(item.inspectionResult)]">{{ inspectionResultText(item.inspectionResult) }}</text>
</view>
<view v-if="item.textInput" class="result-row">
<text class="result-label">{{ t('moldWorkOrder.textInput') }}</text>
<text class="result-value">{{ textValue(item.textInput) }}</text>
</view>
<view v-if="item.remark" class="result-row">
<text class="result-label">{{ t('moldWorkOrder.remark') }}</text>
<text class="result-value">{{ textValue(item.remark) }}</text>
</view>
<view v-if="item.images" class="result-row">
<text class="result-label">{{ t('moldWorkOrder.images') }}</text>
<view class="result-images">
<image v-for="(img, imgIdx) in parseImages(item.images)" :key="imgIdx" :src="img" class="result-img" mode="aspectFill" @click="previewImage(img, parseImages(item.images))" />
</view>
</view>
</view>
</view>
</view>
<view v-if="resultList.length < resultTotal" class="load-more-btn" @click="loadMoreResults">
<text class="load-more-text">{{ resultLoadingMore ? t('functionCommon.loadingMore') : t('moldWorkOrder.loadMore') }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import AppTitleHeader from '@/components/common/AppTitleHeader.vue'
import { getTicketResultsPage } from '@/api/mes/moldWorkOrderInquiry'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const detailData = reactive({})
const resultList = ref([])
const resultTotal = ref(0)
const resultLoading = ref(false)
const resultLoadingMore = ref(false)
const resultPageNo = ref(1)
const resultPageSize = ref(10)
const managementId = ref(null)
const planTypeText = computed(() => {
const v = detailData.planType
if (String(v) === '1') return t('moldWorkOrder.planTypeInspect')
if (String(v) === '2') return t('moldWorkOrder.planTypeMaintain')
return textValue(v)
})
const jobStatusText = computed(() => {
return getDictLabel(DICT_TYPE.JOB_STATUS, detailData.jobStatus, textValue(detailData.jobStatus))
})
const jobStatusClass = computed(() => {
const s = String(detailData.jobStatus)
if (s === '0') return 'text-warning'
if (s === '1') return 'text-primary'
if (s === '2') return 'text-success'
if (s === '3') return 'text-danger'
if (s === '4') return 'text-muted'
return ''
})
const jobResultText = computed(() => {
const v = detailData.jobResult
if (String(v) === '1') return t('moldWorkOrder.jobResultOk')
if (String(v) === '2') return t('moldWorkOrder.jobResultNg')
return textValue(v)
})
const jobResultClass = computed(() => {
if (String(detailData.jobResult) === '1') return 'text-success'
if (String(detailData.jobResult) === '2') return 'text-danger'
return ''
})
function getInspectionMethodLabel(value) {
return getDictLabel(DICT_TYPE.INSPECTION_METHOD, value, textValue(value))
}
function getValueTypeLabel(value) {
return getDictLabel(DICT_TYPE.VALUE_TYPES, value, textValue(value))
}
function inspectionResultText(v) {
if (String(v) === '0') return t('moldWorkOrder.inspectionResultPending')
if (String(v) === '1') return t('moldWorkOrder.inspectionResultPass')
if (String(v) === '2') return t('moldWorkOrder.inspectionResultFail')
return textValue(v)
}
function inspectionResultClass(v) {
if (String(v) === '0') return 'text-warning'
if (String(v) === '1') return 'text-success'
if (String(v) === '2') return 'text-danger'
return ''
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function parseImages(value) {
if (!value) return []
if (Array.isArray(value)) return value.map(String).filter(Boolean)
return String(value).split(',').map(v => v.trim()).filter(Boolean)
}
function previewImage(current, urls) {
uni.previewImage({ current, urls })
}
async function fetchResults(reset) {
if (!managementId.value) return
if (reset) {
resultPageNo.value = 1
}
if (resultPageNo.value === 1) {
resultLoading.value = true
} else {
resultLoadingMore.value = true
}
try {
const res = await getTicketResultsPage({
pageNo: resultPageNo.value,
pageSize: resultPageSize.value,
managementId: managementId.value
})
const root = res && res.data !== undefined ? res.data : res
const items = root?.list || root?.rows || root?.records || []
const total = root?.total ?? 0
if (reset) {
resultList.value = Array.isArray(items) ? items : []
} else {
resultList.value = [...resultList.value, ...(Array.isArray(items) ? items : [])]
}
resultTotal.value = Number(total)
} catch (e) {
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
resultLoading.value = false
resultLoadingMore.value = false
}
}
function loadMoreResults() {
if (resultLoadingMore.value) return
resultPageNo.value += 1
fetchResults(false)
}
onLoad(async (query) => {
const id = query?.id
if (!id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
await initAllDict()
managementId.value = id
try {
const cached = uni.getStorageSync('moldWorkOrderDetail')
if (cached) {
const data = JSON.parse(cached)
Object.assign(detailData, data)
uni.removeStorageSync('moldWorkOrderDetail')
}
} catch (e) {}
await fetchResults(true)
})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.fixed-header { position: sticky; top: 0; z-index: 20; }
.detail-scroll { height: calc(100vh - 88rpx); }
.content-section { padding: 0 24rpx 24rpx; }
.info-card { margin-top: 20rpx; background: #fff; border-radius: 20rpx; padding: 28rpx; box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05); }
.card-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; margin-bottom: 18rpx; }
.info-row { display: flex; justify-content: space-between; align-items: flex-start; padding: 18rpx 0; border-bottom: 1rpx solid #edf0f3; }
.info-label { font-size: 27rpx; color: #8a9099; width: 220rpx; }
.info-value { flex: 1; text-align: right; font-size: 28rpx; color: #303133; line-height: 1.45; }
.text-success { color: #18bc37; }
.text-danger { color: #e34d59; }
.text-warning { color: #f0883e; }
.text-primary { color: #1a3a5c; }
.text-muted { color: #909399; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; font-size: 26rpx; }
.result-list { display: flex; flex-direction: column; gap: 16rpx; }
.result-card { display: flex; background: #f7f9fc; border-radius: 14rpx; padding: 20rpx; gap: 16rpx; }
.result-index { min-width: 48rpx; height: 48rpx; border-radius: 10rpx; background: #1a3a5c; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 24rpx; font-weight: 700; }
.result-body { flex: 1; display: flex; flex-direction: column; gap: 8rpx; }
.result-row { display: flex; justify-content: space-between; align-items: flex-start; }
.result-label { font-size: 24rpx; color: #8a9099; min-width: 160rpx; }
.result-value { font-size: 26rpx; color: #30363d; flex: 1; text-align: right; }
.result-images { display: flex; flex-wrap: wrap; gap: 10rpx; flex: 1; justify-content: flex-end; }
.result-img { width: 100rpx; height: 100rpx; border-radius: 8rpx; }
.load-more-btn { text-align: center; padding: 20rpx 0 0; }
.load-more-text { font-size: 26rpx; color: #1a3a5c; }
</style>

@ -0,0 +1,406 @@
<template>
<view class="page-container">
<AppTitleHeader :title="t('moldWorkOrder.moduleName')" :subTitle="t('moldWorkOrder.subTitle')" :showSubTitle="true" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('moldWorkOrder.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</view>
<view class="filter-row">
<picker mode="selector" :range="planTypeFilterOptions" range-key="label" @change="onPlanTypeFilter">
<view :class="['filter-tag', filterPlanType ? 'filter-active' : '']">{{ filterPlanTypeLabel || t('moldWorkOrder.planType') }}</view>
</picker>
<picker mode="selector" :range="jobStatusFilterOptions" range-key="label" @change="onJobStatusFilter">
<view :class="['filter-tag', filterJobStatus ? 'filter-active' : '']">{{ filterJobStatusLabel || t('moldWorkOrder.jobStatus') }}</view>
</picker>
<picker mode="selector" :range="jobResultFilterOptions" range-key="label" @change="onJobResultFilter">
<view :class="['filter-tag', filterJobResult ? 'filter-active' : '']">{{ filterJobResultLabel || t('moldWorkOrder.jobResult') }}</view>
</picker>
</view>
</view>
<!-- 列表区域 -->
<scroll-view scroll-y class="list-scroll" :scroll-top="scrollTop" @scroll="onScroll" @scrolltolower="loadMore" :lower-threshold="80">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.planNo) }}</text>
<view class="tag-row">
<text :class="['type-code', String(item.planType) === '1' ? 'tag-inspect' : 'tag-maintain']">{{ planTypeLabel(item.planType) }}</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('moldWorkOrder.moldName') }}</text>
<text class="value">{{ textValue(item.moldName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.configName') }}</text>
<text class="value">{{ textValue(item.configName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.jobStatus') }}</text>
<text :class="['value', jobStatusClass(item.jobStatus)]">{{ jobStatusLabel(item.jobStatus) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.jobResult') }}</text>
<text :class="['value', jobResultClass(item.jobResult)]">{{ jobResultLabel(item.jobResult) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.operatorName') }}</text>
<text class="value">{{ textValue(item.operatorName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('moldWorkOrder.taskTime') }}</text>
<text class="value">{{ formatDateTime(item.taskTime) }}</text>
</view>
</view>
<view class="card-actions">
<view v-if="canCancel(item)" class="action-btn cancel-btn" @click.stop="handleCancel(item)">
<uni-icons type="closeempty" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="hint">{{ t('moldWorkOrder.empty') }}</view>
<view v-else-if="loadingMore" class="hint">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="hint">{{ t('functionCommon.noMoreData') }}</view>
</view>
</scroll-view>
<!-- 返回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<uni-icons type="arrow-up" size="20" color="#1a3a5c"></uni-icons>
</view>
<!-- 取消原因弹框 -->
<uni-popup ref="cancelPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ t('moldWorkOrder.cancelTask') }}</text>
<view class="popup-close" @click="closeCancelPopup">
<text class="close-icon">×</text>
</view>
</view>
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ t('moldWorkOrder.cancelReason') }} <text class="required-star">*</text></text>
<textarea v-model="cancelReason" class="form-textarea" :placeholder="t('moldWorkOrder.placeholderCancelReason')" />
</view>
</view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeCancelPopup"><text class="btn-text">{{ t('functionCommon.cancel') }}</text></view>
<view class="footer-btn confirm-btn" @click="submitCancel"><text class="btn-text">{{ t('functionCommon.save') }}</text></view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import AppTitleHeader from '@/components/common/AppTitleHeader.vue'
import { getTicketManagementPage, batchUpdateTicketStatus } from '@/api/mes/moldWorkOrderInquiry'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const cancelPopupRef = ref(null)
const searchKeyword = ref('')
const filterPlanType = ref('')
const filterJobStatus = ref('')
const filterJobResult = ref('')
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const scrollTop = ref(0)
const showGoTop = ref(false)
const cancelReason = ref('')
const cancelTargetId = ref(null)
const planTypeFilterOptions = computed(() => [
{ label: t('moldWorkOrder.planTypeInspect'), value: '1' },
{ label: t('moldWorkOrder.planTypeMaintain'), value: '2' }
])
const jobStatusFilterOptions = computed(() => {
const opts = [
{ label: t('moldWorkOrder.jobStatusPending'), value: '0' },
{ label: t('moldWorkOrder.jobStatusProcessing'), value: '1' },
{ label: t('moldWorkOrder.jobStatusCompleted'), value: '2' },
{ label: t('moldWorkOrder.jobStatusTimeout'), value: '3' },
{ label: t('moldWorkOrder.jobStatusCancelled'), value: '4' }
]
return opts
})
const jobResultFilterOptions = computed(() => [
{ label: t('moldWorkOrder.jobResultOk'), value: '1' },
{ label: t('moldWorkOrder.jobResultNg'), value: '2' }
])
const filterPlanTypeLabel = computed(() => {
if (!filterPlanType.value) return ''
const opt = planTypeFilterOptions.value.find(o => o.value === filterPlanType.value)
return opt ? opt.label : ''
})
const filterJobStatusLabel = computed(() => {
if (!filterJobStatus.value) return ''
return getDictLabel(DICT_TYPE.JOB_STATUS, filterJobStatus.value, '')
})
const filterJobResultLabel = computed(() => {
if (!filterJobResult.value) return ''
const opt = jobResultFilterOptions.value.find(o => o.value === filterJobResult.value)
return opt ? opt.label : ''
})
onLoad(async () => {
await initAllDict()
await fetchList(true)
})
async function handleSearch() {
await fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
finished.value = false
}
if (pageNo.value === 1) {
loading.value = true
} else {
loadingMore.value = true
}
try {
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
planNo: searchKeyword.value.trim() || undefined,
planType: filterPlanType.value || undefined,
jobStatus: filterJobStatus.value || undefined,
jobResult: filterJobResult.value || undefined
}
const res = await getTicketManagementPage(params)
const page = normalizePageData(res)
if (reset) {
list.value = page.list
} else {
list.value = [...list.value, ...page.list]
}
finished.value = list.value.length >= page.total || page.list.length < pageSize.value
} catch (e) {
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const candidateList = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || root?.data?.records || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return { list: Array.isArray(candidateList) ? candidateList : [], total: Number(candidateTotal || 0) }
}
function onScroll(e) {
showGoTop.value = (e?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function onPlanTypeFilter(e) {
const idx = e.detail.value
const opt = planTypeFilterOptions.value[idx]
filterPlanType.value = opt ? opt.value : ''
handleSearch()
}
function onJobStatusFilter(e) {
const idx = e.detail.value
const opt = jobStatusFilterOptions.value[idx]
filterJobStatus.value = opt ? opt.value : ''
handleSearch()
}
function onJobResultFilter(e) {
const idx = e.detail.value
const opt = jobResultFilterOptions.value[idx]
filterJobResult.value = opt ? opt.value : ''
handleSearch()
}
function planTypeLabel(v) {
if (String(v) === '1') return t('moldWorkOrder.planTypeInspect')
if (String(v) === '2') return t('moldWorkOrder.planTypeMaintain')
return textValue(v)
}
function jobStatusLabel(v) {
return getDictLabel(DICT_TYPE.JOB_STATUS, v, textValue(v))
}
function jobStatusClass(v) {
const s = String(v)
if (s === '0') return 'text-warning'
if (s === '1') return 'text-primary'
if (s === '2') return 'text-success'
if (s === '3') return 'text-danger'
if (s === '4') return 'text-muted'
return ''
}
function jobResultLabel(v) {
if (String(v) === '1') return t('moldWorkOrder.jobResultOk')
if (String(v) === '2') return t('moldWorkOrder.jobResultNg')
return textValue(v)
}
function jobResultClass(v) {
if (String(v) === '1') return 'text-success'
if (String(v) === '2') return 'text-danger'
return ''
}
function canCancel(item) {
const s = String(item.jobStatus)
return s === '0' || s === '1'
}
function textValue(v) {
if (v === 0) return '0'
if (v === null || v === undefined) return '-'
const s = String(v).trim()
return s || '-'
}
function formatDateTime(v) {
if (!v) return '-'
if (Array.isArray(v) && v.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = v
const p = (n) => String(n).padStart(2, '0')
return `${y}-${p(m)}-${p(d)} ${p(hh)}:${p(mm)}:${p(ss)}`
}
const d = new Date(Number(v))
if (Number.isNaN(d.getTime())) return textValue(v)
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.setStorageSync('moldWorkOrderDetail', JSON.stringify(item))
uni.navigateTo({
url: `/pages_function/pages/moldWorkOrderInquiry/detail?id=${encodeURIComponent(String(item.id))}`
})
}
function handleCancel(item) {
cancelTargetId.value = item.id
cancelReason.value = ''
cancelPopupRef.value?.open()
}
function closeCancelPopup() {
cancelPopupRef.value?.close()
}
async function submitCancel() {
if (!cancelReason.value.trim()) {
uni.showToast({ title: t('moldWorkOrder.validatorCancelReasonRequired'), icon: 'none' })
return
}
try {
await batchUpdateTicketStatus({
ids: String(cancelTargetId.value),
jobStatus: '4',
cancelReason: cancelReason.value.trim()
})
uni.showToast({ title: t('moldWorkOrder.cancelSuccess'), icon: 'success' })
closeCancelPopup()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('moldWorkOrder.cancelFail'), icon: 'none' })
}
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 16rpx; font-size: 34rpx; color: #666; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.filter-row { display: flex; gap: 16rpx; margin-top: 16rpx; flex-wrap: wrap; }
.filter-tag { font-size: 24rpx; padding: 8rpx 20rpx; border-radius: 24rpx; background: #f5f7fa; color: #8a9099; border: 1rpx solid #e4e7ed; }
.filter-active { background: rgba(26, 58, 92, 0.08); color: #1a3a5c; border-color: #1a3a5c; }
.list-scroll { height: calc(100vh - 340rpx); }
.list-wrap { padding: 0 24rpx 40rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; gap: 8rpx; flex: 1; }
.type-name { font-size: 32rpx; font-weight: 700; color: #1f2d3d; }
.tag-row { display: flex; align-items: center; }
.type-code { font-size: 24rpx; padding: 4rpx 14rpx; border-radius: 8rpx; }
.tag-inspect { color: #1a3a5c; background: rgba(26, 58, 92, 0.08); }
.tag-maintain { color: #18bc37; background: rgba(24, 188, 55, 0.08); }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.text-success { color: #18bc37; }
.text-danger { color: #e34d59; }
.text-warning { color: #f0883e; }
.text-primary { color: #1a3a5c; }
.text-muted { color: #909399; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.cancel-btn { background: #f0883e; }
.hint { text-align: center; color: #909399; padding: 24rpx 0; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.popup-content { width: 680rpx; max-height: 80vh; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; position: relative; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.popup-close { position: absolute; right: 24rpx; top: 20rpx; width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
.close-icon { font-size: 42rpx; color: #9aa0a6; line-height: 1; }
.form-content { padding: 22rpx 28rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; font-size: 25rpx; color: #5f6b7a; margin-bottom: 10rpx; }
.required-star { color: #e34d59; }
.form-textarea { min-height: 160rpx; background: #f7f9fc; border-radius: 12rpx; padding: 18rpx; font-size: 26rpx; color: #2f3a45; width: 100%; box-sizing: border-box; }
.form-footer { height: 88rpx; display: flex; border-top: 1rpx solid #edf0f3; }
.footer-btn { flex: 1; display: flex; align-items: center; justify-content: center; }
.btn-text { font-size: 28rpx; }
.confirm-btn { background: #1a3a5c; color: #fff; }
</style>

@ -10,6 +10,7 @@ export enum DICT_TYPE {
MES_TZ_STATUS = "mes_tz_status",
INSPECTION_METHOD = "Inspection_method",
VALUE_TYPES = "value_types",
JOB_STATUS = "job_status",
}
type DictItem = {

Loading…
Cancel
Save