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.

874 lines
22 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('moldLedger.moduleName')" />
<view class="filter-bar">
<view class="filter-row quick-row">
<picker :range="statusPickerLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ selectedStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
</view>
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
id="mold-ledger-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('moldLedger.searchPlaceholder')"
confirm-type="search"
@confirm="handleSearch"
/>
</view>
<view class="icon-filter-btn" @click="resetFilters">
<uni-icons type="refresh" size="24" color="#7b8491"></uni-icons>
</view>
<view class="icon-filter-btn" @click="openFilterDrawer">
<uni-icons type="settings" size="24" color="#7b8491"></uni-icons>
</view>
</view>
</view>
<scroll-view
scroll-y
class="content-scroll"
:scroll-top="scrollTop"
@scroll="handleScroll"
@scrolltolower="loadMore"
:lower-threshold="80"
>
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="ledger-card" @click="openDetail(item)">
<view class="card-main">
<view class="card-top">
<text class="ledger-name">{{ textValue(item.code) }}</text>
<view :class="['status-chip', statusClass(item.status)]">{{ moldStatusText(item.status) }}</view>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldLedger.name') }}</text>
<text class="info-value">{{ textValue(item.name) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldLedger.product') }}</text>
<text class="info-value">{{ textValue(item.productName) }}</text>
</view>
<view class="info-row">
<text class="info-label">{{ t('moldLedger.createTime') }}</text>
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view class="card-actions">
<view class="card-action delete" @click.stop="confirmDelete(item)">
<u-icon name="trash" size="22" color="#dc2626"></u-icon>
</view>
</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('moldLedger.empty') }}</view>
<view v-else-if="loadingMore" class="loading-text">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="finished-text">{{ t('functionCommon.noMoreData') }}</view>
</view>
</scroll-view>
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon">↑</text>
</view>
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
<uni-popup ref="filterPopupRef" class="mold-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">{{ t('moldLedger.moreFilter') }}</text>
</view>
<scroll-view scroll-y class="drawer-body">
<view class="drawer-section">
<view class="drawer-section-head">
<text class="drawer-section-title">{{ t('moldLedger.basicInfo') }}</text>
</view>
<view class="drawer-grid">
<view class="drawer-field">
<text class="drawer-label">{{ t('moldLedger.productModel') }}</text>
<view class="drawer-picker" @click="toggleProductPanel">
<text :class="['drawer-picker-text', selectedProductIds.length === 0 ? 'placeholder' : '']">{{ selectedProductLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
<scroll-view v-if="productPanelOpen" scroll-y class="drawer-option-panel">
<view
v-for="item in productOptions"
:key="item.id"
:class="['drawer-option-item', selectedProductIds.includes(item.id) ? 'active' : '']"
@click="selectProduct(item)"
>
<text class="drawer-option-text">{{ item.name }}</text>
<text v-if="selectedProductIds.includes(item.id)" class="drawer-option-check">✓</text>
</view>
</scroll-view>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('moldLedger.currentDevice') }}</text>
<view class="drawer-picker" @click="toggleDevicePanel">
<text :class="['drawer-picker-text', !selectedCurrentDevice ? 'placeholder' : '']">{{ selectedDeviceLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
<scroll-view v-if="devicePanelOpen" scroll-y class="drawer-option-panel">
<view
v-for="item in deviceOptions"
:key="item.id"
:class="['drawer-option-item', selectedCurrentDevice === item.deviceName ? 'active' : '']"
@click="selectDevice(item)"
>
<text class="drawer-option-text">{{ item.deviceName }}{{ item.deviceCode }}</text>
<text v-if="selectedCurrentDevice === item.deviceName" class="drawer-option-check">✓</text>
</view>
</scroll-view>
</view>
</view>
</view>
</scroll-view>
<view class="drawer-actions">
<view class="drawer-action reset" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
<view class="drawer-action confirm" @click="confirmFilterDrawer">{{ t('functionCommon.confirm') }}</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { onLoad, onReady } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { deleteMoldBrand, getMoldBrandPage } from '@/api/mes/mold'
import { getMesProductSimpleList } from '@/api/mes/product'
import { getDeviceLedgerList } from '@/api/mes/deviceLedger'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const filterPopupRef = ref(null)
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 searchKeyword = ref('')
const selectedStatus = ref('')
const productOptions = ref([])
const deviceOptions = ref([])
const selectedProductIds = ref([])
const selectedCurrentDevice = ref('')
// 面板开关
const productPanelOpen = ref(false)
const devicePanelOpen = ref(false)
const scrollTop = ref(0)
const showGoTop = ref(false)
const focusNoKeyboardRef = ref(null)
const keywordInputSelector = '#mold-ledger-keyword-input input, input#mold-ledger-keyword-input'
const statusOptions = computed(() => {
const options = []
for (let i = 0; i <= 9; i += 1) {
const label = getDictLabel(DICT_TYPE.ERP_MOLD_STATUS, i, '')
if (label && label !== String(i)) {
options.push({ label, value: i })
}
}
return options
})
const statusPickerLabels = 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 selectedStatusLabel = computed(() => {
if (selectedStatus.value === '') return t('moldLedger.allStatus')
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
return current ? current.label : t('moldLedger.allStatus')
})
const selectedProductLabel = computed(() => {
if (selectedProductIds.value.length === 0) return t('moldLedger.productModel')
const names = productOptions.value
.filter((p) => selectedProductIds.value.includes(p.id))
.map((p) => p.name)
return names.join(', ') || t('moldLedger.productModel')
})
const selectedDeviceLabel = computed(() => {
if (!selectedCurrentDevice.value) return t('moldLedger.currentDevice')
const found = deviceOptions.value.find((d) => d.deviceName === selectedCurrentDevice.value)
return found ? `${found.deviceName}${found.deviceCode}` : selectedCurrentDevice.value
})
onLoad(async () => {
await initAllDict()
await Promise.all([fetchProductOptions(), fetchDeviceOptions()])
await fetchList(true)
})
onReady(() => {
nextTick(() => {
setTimeout(() => {
focusNoKeyboardRef.value?.focus(keywordInputSelector)
}, 200)
})
})
async function fetchProductOptions() {
try {
const res = await getMesProductSimpleList()
const root = res && res.data !== undefined ? res.data : res
productOptions.value = Array.isArray(root) ? root : (root?.data || [])
} catch { productOptions.value = [] }
}
async function fetchDeviceOptions() {
try {
const res = await getDeviceLedgerList()
const root = res && res.data !== undefined ? res.data : res
deviceOptions.value = Array.isArray(root) ? root : (root?.data || [])
} catch { deviceOptions.value = [] }
}
function onStatusFilterChange(event) {
const index = Number(event?.detail?.value || 0)
const current = statusOptions.value[index]
selectedStatus.value = current ? current.value : ''
fetchList(true)
}
function openFilterDrawer() {
filterPopupRef.value?.open()
}
function toggleProductPanel() {
productPanelOpen.value = !productPanelOpen.value
devicePanelOpen.value = false
}
function toggleDevicePanel() {
devicePanelOpen.value = !devicePanelOpen.value
productPanelOpen.value = false
}
function selectProduct(item) {
const idx = selectedProductIds.value.indexOf(item.id)
if (idx >= 0) {
selectedProductIds.value.splice(idx, 1)
} else {
selectedProductIds.value.push(item.id)
}
}
function selectDevice(item) {
selectedCurrentDevice.value = item.deviceName
devicePanelOpen.value = false
}
function confirmFilterDrawer() {
filterPopupRef.value?.close()
fetchList(true)
}
async function resetFilters() {
searchKeyword.value = ''
selectedStatus.value = ''
selectedProductIds.value = []
selectedCurrentDevice.value = ''
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 keyword = searchKeyword.value.trim()
const res = await getMoldBrandPage({
pageNo: pageNo.value,
pageSize: pageSize.value,
code: keyword || undefined,
name: keyword || undefined,
status: selectedStatus.value,
productIds: selectedProductIds.value.length ? selectedProductIds.value : undefined,
productName: selectedProductIds.value.length
? productOptions.value.filter((p) => selectedProductIds.value.includes(p.id)).map((p) => p.name).join(',')
: undefined,
currentDevice: selectedCurrentDevice.value || undefined
})
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' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const pageResult = root?.pageResult || root?.data?.pageResult || root?.data || root || {}
const candidateList = pageResult?.list || pageResult?.rows || pageResult?.records || []
const candidateTotal = pageResult?.total ?? root?.total ?? root?.data?.total ?? candidateList.length
return {
list: Array.isArray(candidateList) ? candidateList : [],
total: Number(candidateTotal || 0)
}
}
function handleScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
function openDetail(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: t('moldLedger.noIdView'), icon: 'none' })
return
}
uni.navigateTo({
url: `/pages_function/pages/moldLedger/detail?id=${encodeURIComponent(String(id))}`
})
}
function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: t('moldLedger.noIdDelete'), icon: 'none' })
return
}
uni.showModal({
title: t('functionCommon.confirmDelete'),
content: t('moldLedger.confirmDeleteContent', { name: textValue(item?.name) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteMoldBrand(id)
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function moldStatusText(status) {
return getDictLabel(DICT_TYPE.ERP_MOLD_STATUS, status, textValue(status))
}
function statusClass(status) {
const label = moldStatusText(status)
if (label.includes('正常') || label.includes('使用') || label.includes('待用') || label.toUpperCase() === 'OK') {
return 'status-normal'
}
if (label.includes('修') || label.includes('报废') || label.includes('停') || label.toUpperCase() === 'NG') {
return 'status-danger'
}
return 'status-warning'
}
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 || '-'
}
function formatDateTime(value) {
if (!value) return '-'
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d, hh = 0, mm = 0, ss = 0] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
}
const text = String(value).trim()
if (!text) return '-'
const numeric = Number(text)
if (Number.isFinite(numeric)) {
const timestamp = text.length === 10 ? numeric * 1000 : numeric
const date = new Date(timestamp)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
}
const date = new Date(text)
if (!Number.isNaN(date.getTime())) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
}
return text
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f3f4f6;
}
.filter-bar {
padding: 18rpx 14rpx 20rpx;
background: #f3f4f6;
}
.filter-row {
display: flex;
align-items: center;
gap: 18rpx;
}
.search-row {
margin-top: 18rpx;
}
.quick-row > picker {
min-width: 0;
flex: 1;
}
.keyword-wrap,
.status-filter,
.icon-filter-btn {
height: 66rpx;
border: 1rpx solid #d9dde5;
background: #ffffff;
box-sizing: border-box;
}
.keyword-wrap {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
}
.keyword-input {
width: 100%;
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #374151;
}
.status-filter {
min-width: 0;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18rpx 0 26rpx;
}
.status-filter-text {
min-width: 0rpx;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 26rpx;
color: #374151;
}
.status-filter-text.placeholder {
color: #a8adb7;
}
.icon-filter-btn {
width: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: center;
border-color: transparent;
background: transparent;
}
.content-scroll {
width: 100%;
}
.list-wrap {
padding: 0 28rpx 140rpx;
}
.ledger-card {
margin-bottom: 18rpx;
border-radius: 12rpx;
background: #ffffff;
box-shadow: none;
overflow: hidden;
}
.card-main {
padding: 24rpx;
}
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.ledger-name {
max-width: 420rpx;
font-size: 36rpx;
line-height: 44rpx;
color: #24456b;
font-weight: 700;
}
.status-chip {
flex-shrink: 0;
padding: 8rpx 18rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 600;
line-height: 1.2;
}
.status-normal {
color: #15803d;
background: #dcfce7;
}
.status-warning {
color: #c2410c;
background: #ffedd5;
}
.status-danger {
color: #dc2626;
background: #fee2e2;
}
.code-row {
display: flex;
align-items: center;
gap: 10rpx;
margin-top: 14rpx;
}
.ledger-code {
color: #9ca3af;
font-size: 26rpx;
}
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-top: 14rpx;
}
.info-label {
font-size: 28rpx;
color: #9ca3af;
font-weight: 600;
flex-shrink: 0;
}
.info-value {
flex: 1;
text-align: right;
font-size: 30rpx;
color: #9ca3af;
word-break: break-all;
}
.card-actions {
padding: 18rpx 24rpx;
border-top: 1rpx solid #f1f5f9;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 28rpx;
}
.card-action {
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
height: 56rpx;
}
.loading-text,
.empty-text,
.finished-text {
padding: 28rpx 0;
text-align: center;
color: #9ca3af;
font-size: 26rpx;
}
.empty-text {
padding-top: 160rpx;
}
.go-top-btn {
position: fixed;
right: 32rpx;
bottom: calc(56rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: center;
width: 92rpx;
height: 92rpx;
border-radius: 46rpx;
background: rgba(31, 75, 121, 0.84);
box-shadow: 0 14rpx 30rpx rgba(24, 63, 108, 0.24);
}
.go-top-icon {
color: #ffffff;
font-size: 32rpx;
}
// 筛选抽屉
::deep(.mold-filter-popup.right .uni-popup__content-transition) {
transform: none !important;
}
.filter-drawer {
width: 630rpx;
height: calc(100vh - var(--status-bar-height));
margin-top: var(--status-bar-height);
background: #f5f5f7;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 28rpx 0 0 28rpx;
}
.drawer-header {
height: 104rpx;
padding: 18rpx 34rpx 0;
background: #ffffff;
display: flex;
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
}
.drawer-title {
color: #1f2937;
font-size: 34rpx;
line-height: 1.3;
font-weight: 700;
}
.drawer-body {
flex: 1;
min-height: 0;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.drawer-section {
margin-bottom: 18rpx;
padding: 28rpx 28rpx 30rpx;
border-radius: 24rpx;
background: #ffffff;
box-sizing: border-box;
}
.drawer-section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.drawer-section-title {
font-size: 32rpx;
line-height: 1.3;
color: #1f2937;
font-weight: 700;
}
.drawer-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 22rpx 20rpx;
}
.drawer-field {
min-width: 0;
}
.drawer-field-wide {
grid-column: 1 / -1;
}
.drawer-label {
display: block;
margin-bottom: 12rpx;
font-size: 24rpx;
line-height: 1.3;
color: #4b5563;
font-weight: 500;
}
.drawer-input,
.drawer-picker {
width: 100%;
min-height: 74rpx;
border: 0;
border-radius: 8rpx;
background: #f7f8fb;
box-sizing: border-box;
}
.drawer-input {
height: 74rpx;
padding: 0 18rpx;
font-size: 26rpx;
color: #111827;
text-align: center;
}
.drawer-picker {
display: flex;
align-items: center;
justify-content: center;
padding: 0 18rpx;
gap: 8rpx;
}
.drawer-picker-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 26rpx;
color: #111827;
text-align: center;
}
.drawer-picker-text.placeholder {
color: #9ca3af;
}
.drawer-option-panel {
max-height: 360rpx;
margin-top: 12rpx;
border-radius: 12rpx;
background: #f7f8fb;
overflow: hidden;
}
.drawer-option-item {
min-height: 72rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #eceff3;
box-sizing: border-box;
}
.drawer-option-item:last-child {
border-bottom: 0;
}
.drawer-option-item.active {
background: rgba(23, 75, 120, 0.1);
}
.drawer-option-item.active .drawer-option-text {
color: #174b78;
font-weight: 600;
}
.drawer-option-text {
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 26rpx;
color: #1f2937;
}
.drawer-option-check {
flex-shrink: 0;
font-size: 28rpx;
color: #174b78;
margin-left: 16rpx;
}
.drawer-actions {
height: 126rpx;
padding: 18rpx 28rpx 24rpx;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 0;
background: #ffffff;
box-shadow: 0 -8rpx 24rpx rgba(17, 24, 39, 0.06);
}
.drawer-action {
flex: 1;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
border: 2rpx solid #174b78;
box-sizing: border-box;
}
.drawer-action.reset {
border-radius: 12rpx 0 0 12rpx;
background: #ffffff;
color: #174b78;
}
.drawer-action.confirm {
border-radius: 0 12rpx 12rpx 0;
background: #174b78;
color: #ffffff;
}
</style>