You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

377 lines
12 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<NavBar :title="t('equipmentInspectionTasks.moduleName')" />
<view class="filter-bar">
<view class="keyword-box">
<input
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('equipmentInspectionTasks.searchPlaceholder')"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
</view>
<picker mode="selector" :range="taskTypeLabels" :value="taskTypeIndex" @change="onTaskTypeChange">
<view class="status-box">
<text class="status-box-text">{{ currentTaskTypeLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</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="task-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-main">
<text class="task-name">{{ textValue(item.name) }}</text>
<text :class="['task-type-tag', Number(item.taskType) === 1 ? 'tag-inspect' : 'tag-maintain']">{{ taskTypeText(item.taskType) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('equipmentInspectionTasks.deviceList') }}</text>
<text class="value">{{ getDeviceNames(item.deviceList) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentInspectionTasks.dateRange') }}</text>
<text class="value">{{ formatDate(item.startDate) }} ~ {{ formatDate(item.endDate) }}</text>
</view>
</view>
<view class="card-actions">
<view :class="['action-btn', ticketLoadingId === item.id ? 'action-btn-disabled' : 'ticket-btn']" @click.stop="handleCreateTicket(item)">
<uni-icons type="paperplane" 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('equipmentInspectionTasks.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>
</view>
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import { onLoad, onReachBottom, onShow, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getTaskManagementPage, createTaskManagementTicket } from '@/api/mes/taskManagement'
import { getDeviceLedgerList } from '@/api/mes/deviceLedger'
const { t } = useI18n()
const searchKeyword = ref('')
const selectedTaskType = 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 ticketLoadingId = ref(null)
const deviceOptions = ref([])
const keywordFocus = ref(false)
let searchTimer = null
const taskTypeOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('equipmentInspectionTasks.taskTypeInspect'), value: '1' },
{ label: t('equipmentInspectionTasks.taskTypeMaintain'), value: '2' }
])
const taskTypeLabels = computed(() => taskTypeOptions.value.map((item) => item.label))
const taskTypeIndex = computed(() => {
const index = taskTypeOptions.value.findIndex((item) => item.value === selectedTaskType.value)
return index >= 0 ? index : 0
})
const currentTaskTypeLabel = computed(() => {
const current = taskTypeOptions.value.find((item) => item.value === selectedTaskType.value)
return current ? current.label : t('functionCommon.all')
})
onLoad(async () => {
activateKeywordFocus()
await Promise.all([ensureDeviceOptionsLoaded(), fetchList(true)])
})
onShow(() => {
activateKeywordFocus()
})
onUnload(() => {
clearSearchTimer()
})
onReachBottom(() => {
loadMore()
})
async function ensureDeviceOptionsLoaded() {
try {
const res = await getDeviceLedgerList()
const root = res && res.data !== undefined ? res.data : res
const data = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : root?.list || [])
deviceOptions.value = (Array.isArray(data) ? data : [])
.filter((item) => item && item.id !== undefined && item.id !== null)
.map((item) => ({
id: item.id,
label: `${item.deviceCode || ''} ${item.deviceName || ''}`.trim(),
deviceName: item.deviceName || '',
raw: item
}))
} catch (error) {
deviceOptions.value = []
}
}
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,
name: searchKeyword.value.trim() || undefined,
taskType: selectedTaskType.value || undefined
}
const res = await getTaskManagementPage(params)
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
finished.value = list.value.length >= page.total || page.list.length < pageSize.value
} catch (error) {
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 handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
fetchList(true)
}
function handleKeywordInput() {
clearSearchTimer()
searchTimer = setTimeout(() => {
fetchList(true)
}, 300)
}
async function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedTaskType.value = ''
activateKeywordFocus()
await nextTick()
await fetchList(true)
}
function onTaskTypeChange(event) {
const index = Number(event?.detail?.value || 0)
selectedTaskType.value = taskTypeOptions.value[index]?.value ?? ''
fetchList(true)
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function openDetail(item) {
if (!item?.id) {
uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' })
return
}
uni.setStorageSync('equipmentInspectionTasksDetail', JSON.stringify(item))
uni.navigateTo({
url: `/pages_function/pages/equipmentInspectionTasks/detail?id=${encodeURIComponent(String(item.id))}`
})
}
async function handleCreateTicket(item) {
if (!item?.id || ticketLoadingId.value) return
ticketLoadingId.value = item.id
try {
await createTaskManagementTicket(item.id)
uni.showToast({ title: t('equipmentInspectionTasks.createTicketSuccess'), icon: 'success' })
} catch (error) {
uni.showToast({ title: t('equipmentInspectionTasks.createTicketFail'), icon: 'none' })
} finally {
ticketLoadingId.value = null
}
}
function onScroll(event) {
showGoTop.value = (event?.detail?.scrollTop || 0) > 600
}
function goTop() {
scrollTop.value = 0
}
function activateKeywordFocus() {
keywordFocus.value = false
nextTick(() => {
keywordFocus.value = true
})
}
function clearSearchTimer() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
}
function parseIdsValue(value) {
if (!value) return []
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean)
return String(value).split(',').map((item) => item.trim()).filter(Boolean)
}
function getDeviceNames(value) {
const ids = parseIdsValue(value)
if (!ids.length) return '-'
const names = ids
.map((id) => {
const matched = deviceOptions.value.find((item) => String(item.id) === id)
return matched?.deviceName || matched?.label || id
})
.filter(Boolean)
return names.length ? names.join('') : '-'
}
function taskTypeText(value) {
const normalized = Number(value)
if (normalized === 1) return t('equipmentInspectionTasks.taskTypeInspect')
if (normalized === 2) return t('equipmentInspectionTasks.taskTypeMaintain')
return textValue(value)
}
function isEnabled(value) {
return value === true || value === 1 || value === '1' || value === 'true'
}
function enabledText(value) {
return isEnabled(value) ? t('functionCommon.yes') : t('functionCommon.no')
}
function textValue(value) {
if (value === 0) return '0'
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function formatDate(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [year, month, day] = value
const pad = (num) => String(num).padStart(2, '0')
return `${year}-${pad(month)}-${pad(day)}`
}
return String(value).trim().substring(0, 10) || '-'
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f4f5f7; }
.filter-bar {
display: grid;
grid-template-columns: minmax(0, 1fr) 150rpx 96rpx;
align-items: center;
gap: 14rpx;
padding: 18rpx 28rpx 20rpx;
}
.keyword-box,
.status-box,
.reset-filter-btn {
height: 66rpx;
background: #ffffff;
border: 1rpx solid #d9dde5;
box-sizing: border-box;
display: flex;
align-items: center;
}
.keyword-box {
padding: 0 20rpx;
}
.keyword-input {
width: 100%;
font-size: 26rpx;
color: #374151;
}
.status-box {
justify-content: space-between;
padding: 0 18rpx;
}
.status-box-text {
font-size: 26rpx;
color: #374151;
}
.reset-filter-btn {
justify-content: center;
font-size: 24rpx;
color: #4b5563;
}
.list-scroll { height: calc(100vh - 194rpx); }
.list-wrap { padding: 0 24rpx 32rpx; }
.task-card { position: relative; margin-top: 20rpx; padding: 28rpx; background: #fff; border-radius: 22rpx; box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.06); }
.card-header { margin-bottom: 18rpx; }
.header-main { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.task-name { flex: 1; font-size: 32rpx; font-weight: 700; color: #0f172a; }
.task-type-tag { padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; }
.tag-inspect { color: #1d4ed8; background: #dbeafe; }
.tag-maintain { color: #15803d; background: #dcfce7; }
.card-body .row { display: flex; justify-content: space-between; align-items: flex-start; gap: 20rpx; padding: 10rpx 0; }
.label { width: 180rpx; font-size: 25rpx; color: #94a3b8; }
.value { flex: 1; text-align: right; font-size: 27rpx; color: #334155; line-height: 1.5; }
.text-success { color: #16a34a; }
.text-danger { color: #dc2626; }
.card-actions { display: flex; justify-content: flex-end; margin-top: 18rpx; }
.action-btn { width: 72rpx; height: 72rpx; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; }
.ticket-btn { background: linear-gradient(135deg, #1f4b79, #2f6ea8); }
.action-btn-disabled { background: #94a3b8; }
.hint { padding: 36rpx 0; text-align: center; color: #94a3b8; font-size: 26rpx; }
.go-top-btn { position: fixed; right: 28rpx; bottom: calc(40rpx + env(safe-area-inset-bottom)); width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(255,255,255,0.96); box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.12); display: flex; align-items: center; justify-content: center; }
</style>