feat:设备台账引入sv-focus-no-keyboard

master
ck-chenkang 6 days ago
parent 6012a3d94a
commit dae5297e22

@ -4,35 +4,58 @@
<view class="filter-bar">
<view class="line-filter" @click="openLineCascader">
<text :class="['line-filter-text', selectedLineId === '' ? 'placeholder' : '']">{{ selectedLineLabel }}</text>
<text
:class="[
'line-filter-text',
selectedLineId === '' ? 'placeholder' : '',
]"
>{{ selectedLineLabel }}</text
>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
<view class="keyword-wrap">
<input
id="equipment-ledger-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('equipmentLedger.searchPlaceholder')"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@confirm="handleSearch"
/>
</view>
<picker :range="statusPickerLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<picker
:range="statusPickerLabels"
:value="statusPickerIndex"
@change="onStatusFilterChange"
>
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ selectedStatusLabel }}</text>
<text
:class="[
'status-filter-text',
selectedStatus === '' ? 'placeholder' : '',
]"
>{{ selectedStatusLabel }}</text
>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
<view class="reset-filter-btn" @click="resetFilters">{{ resetFilterText }}</view>
<!-- <view class="scan-btn" @click="handleScan">
<view class="reset-filter-btn" @click="resetFilters">{{
resetFilterText
}}</view>
<!-- <view class="scan-btn" @click="handleScan">
<uni-icons type="scan" size="24" color="#1f2937"></uni-icons>
</view> -->
</view>
<view class="list-scroll">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="ledger-card" @click="openDetail(item)">
<view
v-for="item in list"
:key="item.id"
class="ledger-card"
@click="openDetail(item)"
>
<view class="card-top">
<text class="device-code">{{ textValue(item.deviceCode) }}</text>
<picker
@ -43,21 +66,36 @@
@change="(e) => onItemStatusChange(item, e)"
>
<view class="status-chip">
<view :class="['status-dot', getStatusClass(item.deviceStatus)]"></view>
<text :class="['status-text', getStatusClass(item.deviceStatus)]">{{ getStatusText(item.deviceStatus) }}</text>
<view
:class="['status-dot', getStatusClass(item.deviceStatus)]"
></view>
<text
:class="['status-text', getStatusClass(item.deviceStatus)]"
>{{ getStatusText(item.deviceStatus) }}</text
>
</view>
</picker>
</view>
<view class="card-bottom">
<text class="device-name">{{ textValue(item.deviceName) }}</text>
<text class="date-text">{{ formatDateValue(item.createTime) }}</text>
<text class="date-text">{{
formatDateValue(item.createTime)
}}</text>
</view>
</view>
<view v-if="loading && pageNo === 1" class="loading-text">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="empty-text">{{ t('equipmentLedger.empty') }}</view>
<view v-else-if="loadingMore" class="loading-text">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="finished-text">{{ t('functionCommon.noMore') }}</view>
<view v-if="loading && pageNo === 1" class="loading-text">{{
t('functionCommon.loading')
}}</view>
<view v-else-if="!list.length" class="empty-text">{{
t('equipmentLedger.empty')
}}</view>
<view v-else-if="loadingMore" class="loading-text">{{
t('functionCommon.loadingMore')
}}</view>
<view v-else-if="finished" class="finished-text">{{
t('functionCommon.noMore')
}}</view>
</view>
</view>
@ -78,237 +116,290 @@
:auto-close="false"
@confirm="onLineCascaderConfirm"
/>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { onLoad, onPageScroll, onReachBottom, onShow } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerPage, updateDeviceLedger } from '@/api/mes/deviceLedger'
import { getDeviceLineTree } from '@/api/mes/deviceLine'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
import useDictStore from '@/store/modules/dict'
const { t } = useI18n()
const dictStore = useDictStore()
const resetFilterText = computed(() => t('functionCommon.reset'))
const searchKeyword = ref('')
const selectedStatus = ref('')
const selectedLineId = ref('')
const lineTree = ref([])
const lineCascaderShow = ref(false)
const lineCascaderValue = ref([])
const lineCascaderKey = ref(0)
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const showGoTop = ref(false)
const statusUpdatingMap = ref({})
const keywordFocus = ref(false)
import { ref, computed, nextTick } from 'vue';
import { onLoad, onPageScroll, onReachBottom, onReady, onShow } from '@dcloudio/uni-app';
import { useI18n } from 'vue-i18n';
import NavBar from '@/components/common/NavBar.vue';
import {
getDeviceLedgerPage,
updateDeviceLedger,
} from '@/api/mes/deviceLedger';
import { getDeviceLineTree } from '@/api/mes/deviceLine';
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict';
import useDictStore from '@/store/modules/dict';
const { t } = useI18n();
const dictStore = useDictStore();
const resetFilterText = computed(() => t('functionCommon.reset'));
const searchKeyword = ref('');
const selectedStatus = ref('');
const selectedLineId = ref('');
const lineTree = ref([]);
const lineCascaderShow = ref(false);
const lineCascaderValue = ref([]);
const lineCascaderKey = ref(0);
const list = ref([]);
const loading = ref(false);
const loadingMore = ref(false);
const finished = ref(false);
const pageNo = ref(1);
const pageSize = ref(10);
const total = ref(0);
const showGoTop = ref(false);
const statusUpdatingMap = ref({});
const focusNoKeyboardRef = ref(null);
const keywordInputSelector = '#equipment-ledger-keyword-input input, input#equipment-ledger-keyword-input';
const statusOptions = computed(() => {
const dicts = dictStore.getDict(DICT_TYPE.MES_TZ_STATUS) || []
const dicts = dictStore.getDict(DICT_TYPE.MES_TZ_STATUS) || [];
return dicts
.filter(item => item?.value !== null && item?.value !== undefined && String(item.value) !== '')
.map(item => ({ label: item.label, value: item.value }))
})
const statusPickerLabels = computed(() => statusOptions.value.map(item => item.label))
const statusChangeLabels = computed(() => statusOptions.value.map(item => item.label))
.filter(
(item) =>
item?.value !== null &&
item?.value !== undefined &&
String(item.value) !== '',
)
.map((item) => ({ label: item.label, value: item.value }));
});
const statusPickerLabels = computed(() =>
statusOptions.value.map((item) => item.label),
);
const statusChangeLabels = computed(() =>
statusOptions.value.map((item) => item.label),
);
const statusPickerIndex = computed(() => {
const idx = statusOptions.value.findIndex(item => String(item.value) === String(selectedStatus.value))
return idx >= 0 ? idx : 0
})
const idx = statusOptions.value.findIndex(
(item) => String(item.value) === String(selectedStatus.value),
);
return idx >= 0 ? idx : 0;
});
const selectedStatusLabel = computed(() => {
if (selectedStatus.value === '') return t('equipmentLedger.deviceStatus')
const found = statusOptions.value.find(item => String(item.value) === String(selectedStatus.value))
return found?.label || t('equipmentLedger.deviceStatus')
})
const lineOptions = computed(() => flattenLineTree(lineTree.value))
if (selectedStatus.value === '') return t('equipmentLedger.deviceStatus');
const found = statusOptions.value.find(
(item) => String(item.value) === String(selectedStatus.value),
);
return found?.label || t('equipmentLedger.deviceStatus');
});
const lineOptions = computed(() => flattenLineTree(lineTree.value));
const lineCascaderOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...normalizeLineTreeForCascader(lineTree.value)
])
...normalizeLineTreeForCascader(lineTree.value),
]);
const selectedLineLabel = computed(() => {
if (selectedLineId.value === '') return t('equipmentLedger.lineFilter')
const found = lineOptions.value.find(item => String(item.id) === String(selectedLineId.value))
return found?.name || t('equipmentLedger.lineFilter')
})
if (selectedLineId.value === '') return t('equipmentLedger.lineFilter');
const found = lineOptions.value.find(
(item) => String(item.id) === String(selectedLineId.value),
);
return found?.name || t('equipmentLedger.lineFilter');
});
onLoad(async () => {
activateKeywordFocus()
await initAllDict()
await fetchLineTree()
await fetchList(true)
})
activateKeywordFocus();
await initAllDict();
await fetchLineTree();
await fetchList(true);
});
onShow(() => {
activateKeywordFocus()
})
activateKeywordFocus();
});
onReady(() => {
focusKeywordNoKeyboard();
setTimeout(focusKeywordNoKeyboard, 300);
setTimeout(focusKeywordNoKeyboard, 800);
});
onReachBottom(() => {
loadMore()
})
loadMore();
});
onPageScroll((e) => {
showGoTop.value = (e?.scrollTop || 0) > 600
})
showGoTop.value = (e?.scrollTop || 0) > 600;
});
async function fetchLineTree() {
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)
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 (e) {
lineTree.value = []
lineTree.value = [];
}
}
function normalizeLineTree(nodes) {
return (Array.isArray(nodes) ? nodes : []).map((node) => {
const children = normalizeLineTree(node.children)
const children = normalizeLineTree(node.children);
const item = {
...node,
id: String(node.id ?? ''),
name: node.name || node.label || String(node.id || '')
}
name: node.name || node.label || String(node.id || ''),
};
if (children.length) {
item.children = children
item.children = children;
} else {
delete item.children
delete item.children;
}
return item
})
return item;
});
}
function flattenLineTree(nodes, level = 0) {
const result = []
;(Array.isArray(nodes) ? nodes : []).forEach((node) => {
const result = [];
(Array.isArray(nodes) ? nodes : []).forEach((node) => {
result.push({
id: node.id,
name: node.name || node.label || String(node.id || ''),
level
})
level,
});
if (Array.isArray(node.children) && node.children.length) {
result.push(...flattenLineTree(node.children, level + 1))
result.push(...flattenLineTree(node.children, level + 1));
}
})
return result
});
return result;
}
function normalizeLineTreeForCascader(nodes) {
return (Array.isArray(nodes) ? nodes : []).map((node) => {
const children = normalizeLineTreeForCascader(node.children)
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
})
value: String(node.id ?? ''),
};
if (children.length) item.children = children;
return item;
});
}
function findLinePath(nodes, id, parents = []) {
const target = String(id)
const target = String(id);
for (const node of Array.isArray(nodes) ? nodes : []) {
const currentPath = [...parents, String(node.id ?? '')]
if (String(node.id) === target) return currentPath
const childPath = findLinePath(node.children, target, currentPath)
if (childPath.length) return childPath
const currentPath = [...parents, String(node.id ?? '')];
if (String(node.id) === target) return currentPath;
const childPath = findLinePath(node.children, target, currentPath);
if (childPath.length) return childPath;
}
return []
return [];
}
function activateKeywordFocus() {
keywordFocus.value = false
focusKeywordNoKeyboard();
}
function focusKeywordNoKeyboard() {
nextTick(() => {
keywordFocus.value = true
})
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector);
}, 80);
});
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
finished.value = false
pageNo.value = 1;
finished.value = false;
}
if (pageNo.value === 1) {
loading.value = true
loading.value = true;
} else {
loadingMore.value = true
loadingMore.value = true;
}
try {
const keyword = searchKeyword.value.trim()
const keyword = searchKeyword.value.trim();
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
deviceCode: keyword || undefined,
deviceName: keyword || undefined,
deviceStatus: selectedStatus.value === '' ? undefined : selectedStatus.value,
deviceLine: selectedLineId.value === '' ? undefined : selectedLineId.value
}
const res = await getDeviceLedgerPage(params)
const page = normalizePageData(res)
total.value = page.total
list.value = reset ? page.list : [...list.value, ...page.list]
finished.value = list.value.length >= total.value || page.list.length < pageSize.value
deviceStatus:
selectedStatus.value === '' ? undefined : selectedStatus.value,
deviceLine:
selectedLineId.value === '' ? undefined : selectedLineId.value,
};
const res = await getDeviceLedgerPage(params);
const page = normalizePageData(res);
total.value = page.total;
list.value = reset ? page.list : [...list.value, ...page.list];
finished.value =
list.value.length >= total.value || 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' })
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
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) }
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 onStatusFilterChange(e) {
const idx = Number(e?.detail?.value || 0)
selectedStatus.value = statusOptions.value[idx]?.value ?? ''
fetchList(true)
const idx = Number(e?.detail?.value || 0);
selectedStatus.value = statusOptions.value[idx]?.value ?? '';
fetchList(true);
}
function openLineCascader() {
lineCascaderShow.value = true
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)
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 resetFilters() {
searchKeyword.value = ''
selectedStatus.value = ''
selectedLineId.value = ''
lineCascaderValue.value = []
lineCascaderShow.value = false
lineCascaderKey.value += 1
activateKeywordFocus()
await fetchList(true)
searchKeyword.value = '';
selectedStatus.value = '';
selectedLineId.value = '';
lineCascaderValue.value = [];
lineCascaderShow.value = false;
lineCascaderKey.value += 1;
activateKeywordFocus();
await fetchList(true);
}
async function resetLineFilter() {
await resetFilters()
await resetFilters();
}
async function handleSearch() {
await fetchList(true)
await fetchList(true);
}
function handleScan() {
@ -316,125 +407,138 @@ function handleScan() {
onlyFromCamera: true,
scanType: ['qrCode', 'barCode'],
success: async (res) => {
const scan = parseScanResult(res?.result)
const scan = parseScanResult(res?.result);
if (!scan.type || !scan.id) {
uni.showToast({ title: t('equipmentLedger.scanUnrecognized'), icon: 'none' })
return
uni.showToast({
title: t('equipmentLedger.scanUnrecognized'),
icon: 'none',
});
return;
}
if (scan.type === 'DEVICE_LINE') {
selectedLineId.value = scan.id
lineCascaderValue.value = findLinePath(lineTree.value, scan.id)
await fetchList(true)
return
selectedLineId.value = scan.id;
lineCascaderValue.value = findLinePath(lineTree.value, scan.id);
await fetchList(true);
return;
}
if (scan.type === 'EQUIPMENT') {
uni.navigateTo({
url: `/pages_function/pages/equipmentLedger/detail?id=${encodeURIComponent(String(scan.id))}`
})
return
url: `/pages_function/pages/equipmentLedger/detail?id=${encodeURIComponent(String(scan.id))}`,
});
return;
}
uni.showToast({ title: t('equipmentLedger.scanTypeMismatch'), icon: 'none' })
uni.showToast({
title: t('equipmentLedger.scanTypeMismatch'),
icon: 'none',
});
},
fail: (err) => {
const msg = String(err?.errMsg || '')
if (msg.includes('cancel')) return
uni.showToast({ title: t('equipmentLedger.scanFailed'), icon: 'none' })
}
})
const msg = String(err?.errMsg || '');
if (msg.includes('cancel')) return;
uni.showToast({ title: t('equipmentLedger.scanFailed'), icon: 'none' });
},
});
}
function parseScanResult(value) {
const text = String(value || '').trim()
const match = text.match(/^([A-Z_]+)-(\d+)$/i)
if (!match) return { type: '', id: '' }
const text = String(value || '').trim();
const match = text.match(/^([A-Z_]+)-(\d+)$/i);
if (!match) return { type: '', id: '' };
return {
type: match[1].toUpperCase(),
id: match[2]
}
id: match[2],
};
}
function goTop() {
uni.pageScrollTo({ scrollTop: 0, duration: 200 })
uni.pageScrollTo({ scrollTop: 0, duration: 200 });
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
if (loading.value || loadingMore.value || finished.value) return;
pageNo.value += 1;
await fetchList(false);
}
function getStatusText(status) {
return getDictLabel(DICT_TYPE.MES_TZ_STATUS, status, textValue(status))
return getDictLabel(DICT_TYPE.MES_TZ_STATUS, status, textValue(status));
}
function getStatusClass(status) {
const s = String(status)
if (s === '0') return 'status-enabled'
if (s === '1') return 'status-idle'
return 'status-danger'
const s = String(status);
if (s === '0') return 'status-enabled';
if (s === '1') return 'status-idle';
return 'status-danger';
}
function getStatusChangeIndex(status) {
const idx = statusOptions.value.findIndex(item => String(item.value) === String(status))
return idx >= 0 ? idx : 0
const idx = statusOptions.value.findIndex(
(item) => String(item.value) === String(status),
);
return idx >= 0 ? idx : 0;
}
async function onItemStatusChange(item, e) {
const id = item?.id
const id = item?.id;
if (id === undefined || id === null) {
uni.showToast({ title: t('equipmentLedger.noId'), icon: 'none' })
return
uni.showToast({ title: t('equipmentLedger.noId'), icon: 'none' });
return;
}
const nextOption = statusOptions.value[Number(e?.detail?.value || 0)]
if (!nextOption || String(nextOption.value) === String(item.deviceStatus)) return
const nextOption = statusOptions.value[Number(e?.detail?.value || 0)];
if (!nextOption || String(nextOption.value) === String(item.deviceStatus))
return;
const oldStatus = item.deviceStatus
item.deviceStatus = nextOption.value
statusUpdatingMap.value = { ...statusUpdatingMap.value, [id]: true }
const oldStatus = item.deviceStatus;
item.deviceStatus = nextOption.value;
statusUpdatingMap.value = { ...statusUpdatingMap.value, [id]: true };
try {
await updateDeviceLedger({ id, deviceStatus: nextOption.value })
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
await updateDeviceLedger({ id, deviceStatus: nextOption.value });
uni.showToast({
title: t('functionCommon.updateSuccess'),
icon: 'success',
});
} catch (err) {
item.deviceStatus = oldStatus
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
item.deviceStatus = oldStatus;
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' });
} finally {
statusUpdatingMap.value = { ...statusUpdatingMap.value, [id]: false }
statusUpdatingMap.value = { ...statusUpdatingMap.value, [id]: false };
}
}
function openDetail(item) {
const id = item?.id
if (!id && id !== 0) return
const id = item?.id;
if (!id && id !== 0) return;
uni.navigateTo({
url: `/pages_function/pages/equipmentLedger/detail?id=${encodeURIComponent(String(id))}`
})
url: `/pages_function/pages/equipmentLedger/detail?id=${encodeURIComponent(String(id))}`,
});
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
if (value === 0) return '0';
if (value === false) return t('functionCommon.no');
if (value === true) return t('functionCommon.yes');
if (value === null || value === undefined) return '-';
const text = String(value).trim();
return text || '-';
}
function formatDateValue(value) {
if (!value) return '-'
const pad = (n) => String(n).padStart(2, '0')
const formatDate = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
if (!value) return '-';
const pad = (n) => String(n).padStart(2, '0');
const formatDate = (date) =>
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d] = value
return `${y}-${pad(m)}-${pad(d)}`
const [y, m, d] = value;
return `${y}-${pad(m)}-${pad(d)}`;
}
const text = String(value).trim()
if (!text) return '-'
const text = String(value).trim();
if (!text) return '-';
if (/^\d{10,13}$/.test(text)) {
const timestamp = Number(text.length === 10 ? `${text}000` : text)
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) return formatDate(date)
const timestamp = Number(text.length === 10 ? `${text}000` : text);
const date = new Date(timestamp);
if (!Number.isNaN(date.getTime())) return formatDate(date);
}
return text.split(' ')[0]
return text.split(' ')[0];
}
</script>
@ -532,9 +636,9 @@ function formatDateValue(value) {
}
.reset-filter-btn {
min-width: 10rpx;
flex: 1;
width: 80rpx;
min-width: 10rpx;
flex: 1;
width: 80rpx;
height: 64rpx;
display: flex;
align-items: center;
@ -687,4 +791,3 @@ function formatDateValue(value) {
font-size: 26rpx;
}
</style>

Loading…
Cancel
Save