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.

529 lines
17 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="line-filter" @click="openLineCascader">
<text :class="['line-filter-text', selectedLineId === '' ? 'placeholder' : '']">{{ selectedLineLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
<view class="keyword-box">
<input
id="equipment-inspection-tasks-keyword-input"
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('equipmentInspectionTasks.searchPlaceholder')"
confirm-type="search"
@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>
<up-cascader
:key="lineCascaderKey"
v-model:show="lineCascaderShow"
v-model="lineCascaderValue"
:data="lineCascaderOptions"
value-key="value"
label-key="label"
children-key="children"
header-direction="row"
:options-cols="2"
:auto-close="false"
@confirm="onLineCascaderConfirm"
/>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
</view>
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import { onLoad, onReachBottom, onReady, 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 { getDeviceLineTree } from '@/api/mes/deviceLine'
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 focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#equipment-inspection-tasks-keyword-input input, input#equipment-inspection-tasks-keyword-input'
const selectedLineId = ref('')
const lineTree = ref([])
const lineCascaderShow = ref(false)
const lineCascaderValue = ref([])
const lineCascaderKey = ref(0)
const lineCascaderOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...normalizeLineTreeForCascader(lineTree.value)
])
const selectedLineLabel = computed(() => {
if (selectedLineId.value === '') return t('equipmentInspectionTasks.lineFilter')
const found = lineOptions.value.find(item => String(item.id) === String(selectedLineId.value))
return found?.name || t('equipmentInspectionTasks.lineFilter')
})
const lineOptions = computed(() => {
return flattenLineTree(lineTree.value, 1)
})
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([loadLineTree(), ensureDeviceOptionsLoaded(), fetchList(true)])
})
onShow(() => {
activateKeywordFocus()
})
onReady(() => {
focusKeywordNoKeyboard()
setTimeout(focusKeywordNoKeyboard, 300)
setTimeout(focusKeywordNoKeyboard, 800)
})
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 keyword = searchKeyword.value.trim()
const deviceId = parseDeviceIdKeyword(keyword)
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
name: deviceId ? undefined : keyword || undefined,
deviceIds: deviceId ? [deviceId] : undefined,
taskType: selectedTaskType.value || undefined,
deviceLineId: selectedLineId.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 parseDeviceIdKeyword(keyword) {
const match = String(keyword || '').trim().match(/-(\d+)$/)
return match ? Number(match[1]) : undefined
}
async function loadLineTree() {
try {
const res = await getDeviceLineTree()
const root = res && res.data !== undefined ? res.data : res
const treeData = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
lineTree.value = normalizeLineTree(treeData)
} catch (_) {
lineTree.value = []
}
}
function openLineCascader() {
lineCascaderShow.value = true
}
function onLineCascaderConfirm(values) {
const selectedValues = Array.isArray(values) ? values : []
const nextValue = selectedValues[selectedValues.length - 1] ?? ''
selectedLineId.value = nextValue === '' ? '' : String(nextValue)
lineCascaderValue.value = nextValue === '' ? [] : selectedValues.map(item => String(item))
fetchList(true)
}
async function resetLineFilter() {
selectedLineId.value = ''
lineCascaderValue.value = []
lineCascaderShow.value = false
lineCascaderKey.value += 1
}
function flattenLineTree(nodes, level) {
const result = []
;(Array.isArray(nodes) ? nodes : []).forEach((node) => {
result.push({
id: node.id,
name: node.name || node.label || String(node.id || ''),
level
})
if (Array.isArray(node.children) && node.children.length) {
result.push(...flattenLineTree(node.children, level + 1))
}
})
return result
}
function normalizeLineTreeForCascader(nodes) {
return (Array.isArray(nodes) ? nodes : []).map((node) => {
const children = normalizeLineTreeForCascader(node.children)
const item = {
label: node.name || node.label || String(node.id || ''),
value: String(node.id ?? '')
}
if (children.length) item.children = children
return item
})
}
function normalizeLineTree(nodes) {
return (Array.isArray(nodes) ? nodes : []).map((node) => {
const children = normalizeLineTree(node.children)
const item = {
...node,
id: String(node.id ?? ''),
children
}
if (!children.length) delete item.children
return item
})
}
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 = ''
resetLineFilter()
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() {
focusKeywordNoKeyboard()
}
function focusKeywordNoKeyboard() {
nextTick(() => {
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector)
}, 80)
})
}
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 4rpx 0rpx;
}
.line-filter,
.keyword-box,
.status-box,
.reset-filter-btn {
height: 66rpx;
background: #ffffff;
border: 1rpx solid #d9dde5;
box-sizing: border-box;
display: flex;
align-items: center;
}
.line-filter {
grid-column: 1 / -1;
justify-content: space-between;
padding: 0 28rpx;
border-radius: 8rpx;
}
.line-filter-text {
font-size: 26rpx;
color: #374151;
max-width: 85%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line-filter-text.placeholder {
color: #a8adb7;
}
.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 4rpx 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>