style:产品库存查询模块
parent
16a0af8bd3
commit
1b6301a01a
@ -0,0 +1,12 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getProductInventoryPage(params = {}) {
|
||||
return request({
|
||||
url: '/admin-api/erp/stock/page',
|
||||
method: 'get',
|
||||
params: {
|
||||
categoryType: 1,
|
||||
...params
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<NavBar :title="t('productInventory.detailTitle')" />
|
||||
|
||||
<scroll-view scroll-y class="detail-scroll">
|
||||
<view v-if="detail" class="content-section">
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<view class="section-icon">
|
||||
<uni-icons type="info" size="22" color="#1f7cff" />
|
||||
</view>
|
||||
<text class="section-title">{{ t('productInventory.stockInfo') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-list">
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.barCode') }}</text>
|
||||
<text class="info-value strong">{{ textValue(detail.barCode || detail.productBarCode) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.productName') }}</text>
|
||||
<text class="info-value strong">{{ textValue(detail.name || detail.productName) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.category') }}</text>
|
||||
<text class="info-value">{{ textValue(detail.categoryName || detail.subCategoryName) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.packagingRule') }}</text>
|
||||
<text class="info-value">{{ textValue(detail.packagingRule) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.warehouse') }}</text>
|
||||
<text class="info-value">{{ textValue(detail.warehouseName) }}</text>
|
||||
</view>
|
||||
<view class="info-row block-row">
|
||||
<text class="info-label">{{ t('productInventory.stockDisplay') }}</text>
|
||||
<view class="tag-wrap">
|
||||
<text v-for="item in stockDisplayList" :key="item" class="stock-tag">{{ item }}</text>
|
||||
<text v-if="!stockDisplayList.length" class="info-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row block-row">
|
||||
<text class="info-label">{{ t('productInventory.areaStockDisplay') }}</text>
|
||||
<view class="tag-wrap">
|
||||
<text v-for="item in areaStockDisplayList" :key="item" class="stock-tag area-tag">{{ item }}</text>
|
||||
<text v-if="!areaStockDisplayList.length" class="info-value">-</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.totalPackageCount') }}</text>
|
||||
<text class="info-value highlight">{{ formatStockCount(detail.totalPackageCount) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.totalBaseCount') }}</text>
|
||||
<text class="info-value highlight">{{ formatStockCount(detail.totalBaseCount) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.unit') }}</text>
|
||||
<text class="info-value">{{ textValue(detail.unitName) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.latestInTime') }}</text>
|
||||
<text class="info-value">{{ formatDateTime(getLatestInTime(detail)) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">{{ t('productInventory.latestOutTime') }}</text>
|
||||
<text class="info-value">{{ formatDateTime(getLatestOutTime(detail)) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="loading" class="hint">{{ t('functionCommon.loading') }}</view>
|
||||
<view v-else class="hint">{{ t('functionCommon.loadFailed') }}</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import NavBar from '@/components/common/NavBar.vue'
|
||||
import { getProductInventoryPage } from '@/api/mes/productInventory'
|
||||
|
||||
const { t } = useI18n()
|
||||
const detail = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
const stockDisplayList = computed(() => formatStockDisplay(detail.value?.stockDisplay))
|
||||
const areaStockDisplayList = computed(() => getAreaStockDisplayList(detail.value?.areaStocks))
|
||||
|
||||
function textValue(value) {
|
||||
if (value === 0) return '0'
|
||||
if (value == null) return '-'
|
||||
const text = String(value).trim()
|
||||
return text || '-'
|
||||
}
|
||||
|
||||
function formatStockCount(value) {
|
||||
if (value === undefined || value === null || value === '') return '-'
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num.toLocaleString() : String(value)
|
||||
}
|
||||
|
||||
function formatStockDisplay(value) {
|
||||
if (!value) return []
|
||||
return String(value)
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function getAreaStockDisplayList(value) {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.map((item) => item?.stockDisplay?.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '-'
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [year, month, day, hour = 0, minute = 0, second = 0] = value
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
|
||||
}
|
||||
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 formatDate(date)
|
||||
}
|
||||
const date = new Date(text)
|
||||
return Number.isNaN(date.getTime()) ? text : formatDate(date)
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
function getLatestInTime(row) {
|
||||
return row?.latestInTime ?? row?.lastInTime ?? row?.recentInTime ?? row?.latestStockInTime ?? row?.lastStockInTime
|
||||
}
|
||||
|
||||
function getLatestOutTime(row) {
|
||||
return row?.latestOutTime ?? row?.lastOutTime ?? row?.recentOutTime ?? row?.latestStockOutTime ?? row?.lastStockOutTime
|
||||
}
|
||||
|
||||
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 || []
|
||||
return Array.isArray(candidateList) ? candidateList : []
|
||||
}
|
||||
|
||||
function applyStockItem(item) {
|
||||
detail.value = {
|
||||
name: item?.name || item?.productName,
|
||||
barCode: item?.barCode || item?.productBarCode,
|
||||
categoryName: item?.categoryName || item?.subCategoryName,
|
||||
...item,
|
||||
stockDisplay: item?.stockDisplay || String(item?.count || '')
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (options) => {
|
||||
const id = options?.id || options?.productId
|
||||
const warehouseId = options?.warehouseId
|
||||
const areaId = options?.areaId
|
||||
if (!id) {
|
||||
loading.value = false
|
||||
uni.showToast({ title: t('productInventory.noDetailId'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const cached = getApp().globalData?._productInventoryDetail
|
||||
if (cached && String(cached.productId || cached.id) === String(id)) {
|
||||
applyStockItem(cached)
|
||||
getApp().globalData._productInventoryDetail = null
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const pageRes = await getProductInventoryPage({
|
||||
pageNo: 1,
|
||||
pageSize: 1,
|
||||
productId: id,
|
||||
warehouseId: warehouseId || undefined,
|
||||
areaId: areaId || undefined
|
||||
})
|
||||
const rows = normalizePageData(pageRes)
|
||||
if (rows[0]) {
|
||||
applyStockItem(rows[0])
|
||||
} else {
|
||||
detail.value = { stockDisplay: '-' }
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container { min-height: 100vh; background: #f5f7fb; }
|
||||
.detail-scroll { height: calc(100vh - 120rpx); }
|
||||
.content-section { padding: 20rpx 24rpx 40rpx; }
|
||||
.section-card { padding: 24rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 20rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
|
||||
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
|
||||
.info-list { display: flex; flex-direction: column; }
|
||||
.info-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 20rpx; padding: 18rpx 0; border-bottom: 1rpx solid #f3f4f6; }
|
||||
.info-row:last-child { border-bottom: 0; }
|
||||
.info-row.block-row { flex-direction: column; gap: 12rpx; }
|
||||
.info-label { width: 190rpx; font-size: 26rpx; color: #94a3b8; flex-shrink: 0; }
|
||||
.info-value { flex: 1; text-align: right; font-size: 27rpx; color: #334155; line-height: 1.5; word-break: break-all; }
|
||||
.info-value.strong { color: #0f172a; font-weight: 700; }
|
||||
.info-value.highlight { color: #1f4b79; font-weight: 700; }
|
||||
.tag-wrap { width: 100%; display: flex; flex-wrap: wrap; gap: 12rpx; justify-content: flex-end; }
|
||||
.stock-tag { max-width: 100%; padding: 8rpx 16rpx; border-radius: 999rpx; color: #1d4ed8; background: #dbeafe; font-size: 24rpx; line-height: 1.35; word-break: break-all; }
|
||||
.area-tag { color: #0369a1; background: #e0f2fe; }
|
||||
.hint { padding: 160rpx 0; text-align: center; color: #94a3b8; font-size: 26rpx; }
|
||||
</style>
|
||||
@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<NavBar :title="t('productInventory.moduleName')" />
|
||||
|
||||
<view class="filter-bar">
|
||||
<view class="keyword-box">
|
||||
<input
|
||||
id="product-inventory-keyword-input"
|
||||
v-model="searchKeyword"
|
||||
class="keyword-input"
|
||||
type="text"
|
||||
:placeholder="t('productInventory.searchPlaceholder')"
|
||||
placeholder-class="placeholder"
|
||||
:focus="keywordFocus"
|
||||
confirm-type="search"
|
||||
@blur="keywordFocus = false"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
<view class="reset-filter-btn" @click="resetFilters">{{ t('functionCommon.reset') }}</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="list-scroll"
|
||||
:scroll-top="scrollTop"
|
||||
:lower-threshold="80"
|
||||
@scroll="onScroll"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view class="list-wrap">
|
||||
<view v-for="item in list" :key="itemKey(item)" class="stock-card" hover-class="stock-card-hover" @click="openDetail(item)">
|
||||
<view class="card-header">
|
||||
<text class="product-code">{{ textValue(item.barCode || item.productBarCode) }}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="row">
|
||||
<text class="label">{{ t('productInventory.productName') }}</text>
|
||||
<text class="value">{{ textValue(item.name || item.productName) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">{{ t('productInventory.stockDisplay') }}</text>
|
||||
<text class="value highlight">{{ textValue(item.stockDisplay) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">{{ t('productInventory.count') }}</text>
|
||||
<text class="value highlight">{{ formatStockCount(item.count) }}{{ textUnit(item.unitName) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">{{ t('productInventory.latestInTime') }}</text>
|
||||
<text class="value">{{ formatDateTime(getLatestInTime(item)) }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">{{ t('productInventory.latestOutTime') }}</text>
|
||||
<text class="value">{{ formatDateTime(getLatestOutTime(item)) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && pageNo === 1" class="hint">{{ t('functionCommon.loading') }}</view>
|
||||
<view v-else-if="!list.length" class="hint empty">{{ t('productInventory.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="#1f4b79" />
|
||||
</view>
|
||||
|
||||
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { onLoad, onReady, onUnload } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import NavBar from '@/components/common/NavBar.vue'
|
||||
import { getProductInventoryPage } from '@/api/mes/productInventory'
|
||||
|
||||
const { t } = useI18n()
|
||||
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 scrollTop = ref(0)
|
||||
const showGoTop = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const keywordFocus = ref(false)
|
||||
const focusNoKeyboardRef = ref(null)
|
||||
const keywordInputSelector = '#product-inventory-keyword-input input, input#product-inventory-keyword-input'
|
||||
let searchTimer = null
|
||||
|
||||
function focusKeywordNoKeyboard() {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
focusNoKeyboardRef.value?.focus(keywordInputSelector)
|
||||
}, 80)
|
||||
})
|
||||
}
|
||||
|
||||
function textValue(value) {
|
||||
if (value === 0) return '0'
|
||||
if (value == null) return '-'
|
||||
const text = String(value).trim()
|
||||
return text || '-'
|
||||
}
|
||||
|
||||
function textUnit(value) {
|
||||
if (value == null || value === '') return ''
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
function formatStockCount(value) {
|
||||
if (value === undefined || value === null || value === '') return '-'
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num.toLocaleString() : String(value)
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '-'
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [year, month, day, hour = 0, minute = 0, second = 0] = value
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${year}-${pad(month)}-${pad(day)} ${pad(hour)}:${pad(minute)}:${pad(second)}`
|
||||
}
|
||||
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 formatDate(date)
|
||||
}
|
||||
const date = new Date(text)
|
||||
return Number.isNaN(date.getTime()) ? text : formatDate(date)
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
function getLatestInTime(row) {
|
||||
return row?.latestInTime ?? row?.lastInTime ?? row?.recentInTime ?? row?.latestStockInTime ?? row?.lastStockInTime
|
||||
}
|
||||
|
||||
function getLatestOutTime(row) {
|
||||
return row?.latestOutTime ?? row?.lastOutTime ?? row?.recentOutTime ?? row?.latestStockOutTime ?? row?.lastStockOutTime
|
||||
}
|
||||
|
||||
function itemKey(item) {
|
||||
return [item?.id, item?.productId, item?.warehouseId, item?.areaId].filter((value) => value !== undefined && value !== null).join('_')
|
||||
}
|
||||
|
||||
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 buildQuery(pageNoValue, pageSizeValue, includeKeyword = true) {
|
||||
const keyword = searchKeyword.value.trim()
|
||||
const scanMatch = keyword.match(/(?:productmaterial|product)[-_](\d+)/i)
|
||||
return {
|
||||
pageNo: pageNoValue,
|
||||
pageSize: pageSizeValue,
|
||||
categoryType: 1,
|
||||
productId: scanMatch ? Number(scanMatch[1]) : undefined,
|
||||
keyword: includeKeyword && !scanMatch ? keyword || undefined : undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAllForSearch() {
|
||||
const all = []
|
||||
let currentPage = 1
|
||||
const maxPage = 10
|
||||
while (currentPage <= maxPage) {
|
||||
const res = await getProductInventoryPage(buildQuery(currentPage, 100, false))
|
||||
const page = normalizePageData(res)
|
||||
all.push(...page.list)
|
||||
if (page.list.length < 100) break
|
||||
currentPage += 1
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
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().toLowerCase()
|
||||
if (keyword && reset) {
|
||||
const allData = await fetchAllForSearch()
|
||||
const scanMatch = keyword.match(/(?:productmaterial|product)[-_](\d+)/i)
|
||||
const filtered = scanMatch
|
||||
? allData.filter((item) => Number(item.productId) === Number(scanMatch[1]))
|
||||
: allData.filter((item) => {
|
||||
const code = String(item.barCode || item.productBarCode || '').toLowerCase()
|
||||
const name = String(item.name || item.productName || '').toLowerCase()
|
||||
return code.includes(keyword) || name.includes(keyword)
|
||||
})
|
||||
list.value = filtered
|
||||
total.value = filtered.length
|
||||
finished.value = true
|
||||
} else {
|
||||
const res = await getProductInventoryPage(buildQuery(pageNo.value, pageSize.value))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
clearSearchTimer()
|
||||
uni.hideKeyboard()
|
||||
await fetchList(true)
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
clearSearchTimer()
|
||||
searchKeyword.value = ''
|
||||
keywordFocus.value = false
|
||||
nextTick(() => {
|
||||
keywordFocus.value = true
|
||||
})
|
||||
await fetchList(true)
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (searchKeyword.value.trim()) return
|
||||
if (loading.value || loadingMore.value || finished.value) return
|
||||
pageNo.value += 1
|
||||
await fetchList(false)
|
||||
}
|
||||
|
||||
function openDetail(item) {
|
||||
if (!item?.productId && !item?.id) {
|
||||
uni.showToast({ title: t('productInventory.noDetailId'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
getApp().globalData._productInventoryDetail = item
|
||||
const productId = item.productId || item.id
|
||||
uni.navigateTo({
|
||||
url: `/pages_function/pages/productInventory/detail?id=${encodeURIComponent(String(productId))}&warehouseId=${encodeURIComponent(String(item.warehouseId || ''))}&areaId=${encodeURIComponent(String(item.areaId || ''))}`
|
||||
})
|
||||
}
|
||||
|
||||
function onScroll(event) {
|
||||
showGoTop.value = (event?.detail?.scrollTop || 0) > 600
|
||||
}
|
||||
|
||||
function goTop() {
|
||||
scrollTop.value = 0
|
||||
}
|
||||
|
||||
function clearSearchTimer() {
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer)
|
||||
searchTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async () => {
|
||||
await fetchList(true)
|
||||
})
|
||||
|
||||
onReady(() => {
|
||||
focusKeywordNoKeyboard()
|
||||
})
|
||||
|
||||
onUnload(() => clearSearchTimer())
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container { min-height: 100vh; background: #f4f5f7; }
|
||||
.filter-bar { display: grid; grid-template-columns: minmax(0, 1fr) 96rpx; align-items: center; gap: 14rpx; padding: 18rpx 28rpx 20rpx; }
|
||||
.keyword-box,
|
||||
.reset-filter-btn { height: var(--app-form-control-height, 70rpx); 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; }
|
||||
.placeholder { font-size: 26rpx; }
|
||||
.reset-filter-btn { justify-content: center; font-size: 24rpx; color: #4b5563; }
|
||||
.list-scroll { height: calc(100vh - 194rpx); }
|
||||
.list-wrap { padding: 0 24rpx 60rpx; }
|
||||
.stock-card { margin-top: 20rpx; padding: 28rpx; background: #fff; border-radius: 22rpx; box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.06); }
|
||||
.stock-card-hover { opacity: 0.86; }
|
||||
.card-header { margin-bottom: 18rpx; }
|
||||
.product-code { display: block; min-width: 0; font-size: 32rpx; font-weight: 700; color: #0f172a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.card-body .row { display: flex; justify-content: space-between; align-items: flex-start; gap: 20rpx; margin-top: 12rpx; }
|
||||
.card-body .row:first-child { margin-top: 0; }
|
||||
.label { width: 150rpx; font-size: 25rpx; color: #94a3b8; flex-shrink: 0; }
|
||||
.value { flex: 1; text-align: right; font-size: 27rpx; color: #334155; line-height: 1.5; word-break: break-all; }
|
||||
.value.highlight { color: #1f4b79; font-weight: 700; }
|
||||
.hint { padding: 36rpx 0; text-align: center; color: #94a3b8; font-size: 26rpx; }
|
||||
.hint.empty { padding-top: 160rpx; }
|
||||
.go-top-btn { position: fixed; right: 28rpx; bottom: calc(56rpx + env(safe-area-inset-bottom)); width: 92rpx; height: 92rpx; border-radius: 46rpx; 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>
|
||||
Loading…
Reference in New Issue