feat:入库审核、出库审核模块-更多筛选

master
黄伟杰 7 days ago
parent eeec5c2169
commit f47c203896

@ -1493,7 +1493,10 @@ export default {
inboundInfo: 'Inbound Info',
inboundTime: 'Inbound Time',
selectInboundTime: 'Select inbound time',
placeholderInboundStartTime: 'Start time',
placeholderInboundEndTime: 'End time',
operator: 'Operator',
creator: 'Creator',
selectOperator: 'Select operator',
inboundQuantity: 'Inbound Quantity',
reviewer: 'Reviewer',
@ -1730,7 +1733,10 @@ export default {
outboundInfo: 'Outbound Info',
outboundTime: 'Outbound Time',
selectOutboundTime: 'Select outbound time',
placeholderOutboundStartTime: 'Start time',
placeholderOutboundEndTime: 'End time',
operator: 'Operator',
creator: 'Creator',
selectOperator: 'Select operator',
outboundQuantity: 'Outbound Qty',
reviewer: 'Reviewer',

@ -1471,6 +1471,7 @@ export default {
inboundStatus: '入库状态',
reset: '重置',
clear: '清空',
moreFilter: '更多筛选',
loading: '加载中...',
loadingMore: '加载更多...',
noMoreData: '没有更多数据',
@ -1496,7 +1497,10 @@ export default {
inboundInfo: '入库信息',
inboundTime: '入库时间',
selectInboundTime: '请选择入库时间',
placeholderInboundStartTime: '开始时间',
placeholderInboundEndTime: '结束时间',
operator: '经办人',
creator: '创建人',
selectOperator: '请选择经办人',
inboundQuantity: '入库数量',
reviewer: '审核人',
@ -1710,6 +1714,7 @@ export default {
outboundStatus: '出库状态',
reset: '重置',
clear: '清空',
moreFilter: '更多筛选',
loading: '加载中...',
loadingMore: '加载更多...',
noMoreData: '没有更多数据',
@ -1731,7 +1736,10 @@ export default {
outboundInfo: '出库信息',
outboundTime: '出库时间',
selectOutboundTime: '请选择出库时间',
placeholderOutboundStartTime: '开始时间',
placeholderOutboundEndTime: '结束时间',
operator: '经办人',
creator: '创建人',
selectOperator: '请选择经办人',
outboundQuantity: '出库数量',
reviewer: '审核人',

@ -3,25 +3,42 @@
<NavBar :title="t('productInbound.moduleName')" />
<view class="filter-bar">
<view class="keyword-box">
<input
id="product-inbound-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('productInbound.searchDocumentPlaceholder')"
placeholder-class="placeholder"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@confirm="handleSearch"
/>
<view class="filter-row quick-row">
<picker class="quick-picker-host" mode="selector" :range="productPickerOptions" range-key="label" :value="productPickerIndex" @change="onProductFilterChange">
<view class="line-filter">
<text :class="['line-filter-text', selectedProductId === '' ? 'placeholder' : '']">{{ selectedProductLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
<picker class="quick-picker-host" :range="statusLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ selectedStatusLabel || t('productInbound.inboundStatus') }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
</view>
<view class="status-box" @click="openStatusPicker">
<text class="status-box-text">{{ selectedStatusLabel || t('productInbound.inboundStatus') }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af" />
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
id="product-inbound-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('productInbound.searchDocumentPlaceholder')"
placeholder-class="placeholder"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@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 class="reset-filter-btn" @click="resetFilters">{{ t('productInbound.reset') }}</view>
</view>
<scroll-view
@ -112,25 +129,64 @@
</view>
</view>
<uni-popup ref="statusPickerRef" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">{{ t('productInbound.selectInboundStatus') }}</text>
<view class="picker-clear" @click="resetStatus">
<text class="picker-clear-text">{{ t('productInbound.clear') }}</text>
</view>
<uni-popup
ref="filterPopupRef"
class="product-filter-popup"
type="right"
background-color="transparent"
:animation="false"
>
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">{{ t('productInbound.moreFilter') }}</text>
</view>
<scroll-view scroll-y class="picker-list">
<view
v-for="option in statusOptions"
:key="String(option.value)"
class="picker-item"
@click="selectStatus(option)"
>
<text class="picker-text">{{ option.label }}</text>
<uni-icons v-if="selectedStatus === option.value" class="picker-check" type="checkmarkempty" size="18" color="#1f61ff"></uni-icons>
<scroll-view scroll-y class="drawer-body">
<view class="drawer-section drawer-fields">
<view class="drawer-field">
<text class="drawer-label">{{ t('productInbound.inboundTime') }}</text>
<view class="drawer-date">
<uni-datetime-picker
v-model="inTimeFilter"
type="daterange"
:clear-icon="true"
:start-placeholder="t('productInbound.placeholderInboundStartTime')"
:end-placeholder="t('productInbound.placeholderInboundEndTime')"
/>
</view>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('productInbound.warehouse') }}</text>
<picker class="drawer-picker-host" mode="selector" :range="warehousePickerOptions" range-key="label" :value="warehousePickerIndex" @change="onWarehouseFilterChange">
<view class="drawer-picker">
<text :class="['drawer-picker-text', selectedWarehouseId === '' ? 'placeholder' : '']">{{ selectedWarehouseLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('productInbound.creator') }}</text>
<picker class="drawer-picker-host" mode="selector" :range="creatorPickerOptions" range-key="label" :value="creatorPickerIndex" @change="onCreatorFilterChange">
<view class="drawer-picker">
<text :class="['drawer-picker-text', selectedCreatorId === '' ? 'placeholder' : '']">{{ selectedCreatorLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('productInbound.remark') }}</text>
<input
v-model="remarkFilter"
class="drawer-input"
type="text"
:placeholder="t('productInbound.remarkPlaceholder')"
/>
</view>
</view>
</scroll-view>
<view class="drawer-actions">
<view class="drawer-action reset" @click="resetFilters">{{ t('productInbound.reset') }}</view>
<view class="drawer-action confirm" @click="confirmFilterDrawer">{{ t('productInbound.confirm') }}</view>
</view>
</view>
</uni-popup>
@ -144,20 +200,85 @@ import { onReady, onShow, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { auditProductInbound, getProductInboundPage, submitProductInbound } from '@/api/mes/productInbound'
import { getProductSimpleList } from '@/api/erp/productInfo'
import { getWarehouseSimpleList } from '@/api/mes/moldget'
import { getSimpleUserList } from '@/api/system/user'
const { t } = useI18n()
const selectedStatus = ref('')
const selectedProductId = ref('')
const selectedWarehouseId = ref('')
const selectedCreatorId = ref('')
const searchKeyword = ref('')
const statusPickerRef = ref(null)
const inTimeFilter = ref([])
const remarkFilter = ref('')
const filterPopupRef = ref(null)
const productList = ref([])
const warehouseList = ref([])
const creatorList = ref([])
const filterOptionsLoaded = ref(false)
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('productInbound.statusPending'), value: '0' },
{ label: t('productInbound.statusRejected'), value: '1' },
{ label: t('productInbound.statusAuditing'), value: '10' },
{ label: t('productInbound.statusStored'), value: '20' }
])
const statusLabels = computed(() => statusOptions.value.map((item) => item.label))
const statusPickerIndex = computed(() => {
const index = statusOptions.value.findIndex((item) => String(item.value) === String(selectedStatus.value))
return index >= 0 ? index : 0
})
const selectedStatusLabel = computed(() => {
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
if (selectedStatus.value === '') return ''
const current = statusOptions.value.find((item) => String(item.value) === String(selectedStatus.value))
return current ? current.label : ''
})
const productPickerOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...productList.value.map((item) => ({
label: item.name || item.productName || item.code || String(item.id || ''),
value: item.id
}))
])
const productPickerIndex = computed(() => {
const index = productPickerOptions.value.findIndex((item) => String(item.value) === String(selectedProductId.value))
return index >= 0 ? index : 0
})
const selectedProductLabel = computed(() => {
if (selectedProductId.value === '') return t('productInbound.product')
return productPickerOptions.value.find((item) => String(item.value) === String(selectedProductId.value))?.label || t('productInbound.product')
})
const warehousePickerOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...warehouseList.value.map((item) => ({
label: item.name || item.warehouseName || item.code || String(item.id || ''),
value: item.id
}))
])
const warehousePickerIndex = computed(() => {
const index = warehousePickerOptions.value.findIndex((item) => String(item.value) === String(selectedWarehouseId.value))
return index >= 0 ? index : 0
})
const selectedWarehouseLabel = computed(() => {
if (selectedWarehouseId.value === '') return t('functionCommon.all')
return warehousePickerOptions.value.find((item) => String(item.value) === String(selectedWarehouseId.value))?.label || t('functionCommon.all')
})
const creatorPickerOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...creatorList.value.map((item) => ({
label: item.nickname || item.userName || item.name || String(item.id || ''),
value: item.id
}))
])
const creatorPickerIndex = computed(() => {
const index = creatorPickerOptions.value.findIndex((item) => String(item.value) === String(selectedCreatorId.value))
return index >= 0 ? index : 0
})
const selectedCreatorLabel = computed(() => {
if (selectedCreatorId.value === '') return t('functionCommon.all')
return creatorPickerOptions.value.find((item) => String(item.value) === String(selectedCreatorId.value))?.label || t('functionCommon.all')
})
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
@ -220,6 +341,39 @@ function normalizePageData(res) {
}
}
function normalizeListResponse(res) {
const root = res && res.data !== undefined ? res.data : res
return Array.isArray(root) ? root : (Array.isArray(root?.list) ? root.list : [])
}
function formatQueryDate(value, isEnd) {
if (!value) return undefined
const text = String(value).trim()
if (!text) return undefined
if (text.includes(':')) return text
return text + ' ' + (isEnd ? '23:59:59' : '00:00:00')
}
async function ensureFilterOptions() {
if (filterOptionsLoaded.value) return
filterOptionsLoaded.value = true
try {
const [productRes, warehouseRes, creatorRes] = await Promise.all([
getProductSimpleList({ categoryType: 1 }),
getWarehouseSimpleList(),
getSimpleUserList()
])
productList.value = normalizeListResponse(productRes)
warehouseList.value = normalizeListResponse(warehouseRes)
creatorList.value = normalizeListResponse(creatorRes)
} catch (e) {
filterOptionsLoaded.value = false
productList.value = []
warehouseList.value = []
creatorList.value = []
}
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
@ -229,11 +383,19 @@ async function fetchList(reset) {
else loadingMore.value = true
try {
const keyword = searchKeyword.value.trim()
const remark = remarkFilter.value.trim()
const inTimeRange = Array.isArray(inTimeFilter.value) ? inTimeFilter.value : []
const res = await getProductInboundPage({
pageNo: pageNo.value,
pageSize: pageSize.value,
no: keyword || undefined,
status: selectedStatus.value || undefined
productId: selectedProductId.value === '' ? undefined : selectedProductId.value,
warehouseId: selectedWarehouseId.value === '' ? undefined : selectedWarehouseId.value,
creator: selectedCreatorId.value === '' ? undefined : selectedCreatorId.value,
statusList: selectedStatus.value === '' ? undefined : [Number(selectedStatus.value)],
remark: remark || undefined,
'inTime[0]': formatQueryDate(inTimeRange[0], false),
'inTime[1]': formatQueryDate(inTimeRange[1], true)
})
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
@ -246,7 +408,6 @@ async function fetchList(reset) {
loadingMore.value = false
}
}
async function handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
@ -269,22 +430,43 @@ async function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedStatus.value = ''
selectedProductId.value = ''
selectedWarehouseId.value = ''
selectedCreatorId.value = ''
inTimeFilter.value = []
remarkFilter.value = ''
closeFilterDrawer()
activateKeywordFocus()
await fetchList(true)
}
function openStatusPicker() {
statusPickerRef.value?.open()
function openFilterDrawer() {
filterPopupRef.value?.open()
}
async function selectStatus(option) {
selectedStatus.value = option.value
statusPickerRef.value?.close()
function closeFilterDrawer() {
filterPopupRef.value?.close()
}
async function confirmFilterDrawer() {
closeFilterDrawer()
await fetchList(true)
}
async function resetStatus() {
selectedStatus.value = ''
statusPickerRef.value?.close()
async function onProductFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedProductId.value = productPickerOptions.value[index]?.value === '' ? '' : String(productPickerOptions.value[index]?.value ?? '')
await fetchList(true)
}
async function onStatusFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedStatus.value = statusOptions.value[index]?.value ?? ''
await fetchList(true)
}
function onWarehouseFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedWarehouseId.value = warehousePickerOptions.value[index]?.value === '' ? '' : String(warehousePickerOptions.value[index]?.value ?? '')
}
function onCreatorFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedCreatorId.value = creatorPickerOptions.value[index]?.value === '' ? '' : String(creatorPickerOptions.value[index]?.value ?? '')
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
@ -408,27 +590,72 @@ onReady(() => {
focusKeywordNoKeyboard()
})
onShow(() => {
onShow(async () => {
consumeSelectedAuditor()
fetchList(true)
await ensureFilterOptions()
await fetchList(true)
})
onUnload(() => clearSearchTimer())
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f4f5f7; }
.filter-bar { display: grid; grid-template-columns: minmax(0, 1fr) 188rpx 96rpx; align-items: center; gap: 14rpx; padding: 18rpx 28rpx 20rpx; }
.keyword-box,
.status-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; }
.status-box { justify-content: space-between; padding: 0 18rpx; }
.status-box-text,
.filter-bar { padding: 18rpx 14rpx 20rpx; background: #f4f5f7; }
.filter-row { display: flex; align-items: center; gap: 18rpx; }
.search-row { margin-top: 18rpx; }
.quick-picker-host { min-width: 0; flex: 1; display: block; }
.keyword-wrap,
.status-filter,
.line-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; box-sizing: border-box; }
.status-filter,
.line-filter { min-width: 0; flex: 1; display: flex; align-items: center; justify-content: space-between; padding: 0 18rpx 0 26rpx; }
.icon-filter-btn { width: 66rpx; flex: 0 0 66rpx; display: flex; align-items: center; justify-content: center; border-color: transparent; background: transparent; }
.status-filter-text,
.line-filter-text,
.placeholder { font-size: 26rpx; }
.status-box-text { flex: 1; min-width: 0; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reset-filter-btn { justify-content: center; font-size: 24rpx; color: #4b5563; }
.list-scroll { height: calc(100vh - 194rpx); }
.status-filter-text,
.line-filter-text { min-width: 0; flex: 1; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-filter-text.placeholder,
.line-filter-text.placeholder,
.placeholder { color: #a8adb7; }
.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; }
:deep(.product-filter-popup.right .uni-popup__content-transition) { transform: none !important; }
.drawer-header { height: 104rpx; padding: 18rpx 34rpx 0; background: #ffffff; display: flex; align-items: center; 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: 8rpx 28rpx; border-radius: 24rpx; background: #ffffff; box-sizing: border-box; }
.drawer-fields { display: flex; flex-direction: column; }
.drawer-field { min-width: 0; min-height: 98rpx; display: flex; align-items: center; gap: 20rpx; border-bottom: 1rpx solid #eceff3; box-sizing: border-box; }
.drawer-field:last-child { border-bottom: 0; }
.drawer-label { width: 160rpx; flex: 0 0 160rpx; font-size: 24rpx; line-height: 1.3; color: #4b5563; font-weight: 500; }
.drawer-picker-host { min-width: 0; flex: 1; display: block; }
.drawer-input,
.drawer-picker,
.drawer-date { min-width: 0; flex: 1; 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: right; }
.drawer-picker { display: flex; align-items: center; justify-content: flex-end; padding: 0 18rpx; gap: 8rpx; }
.drawer-picker-text { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 26rpx; color: #111827; text-align: right; }
.drawer-picker-text.placeholder { color: #9ca3af; }
.drawer-date { display: flex; align-items: center; padding: 0 12rpx; }
.drawer-date :deep(.uni-date),
.drawer-date :deep(.uni-date-editor),
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) { width: 100%; }
.drawer-date :deep(.uni-date-editor),
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) { min-height: 74rpx; }
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) { border: 0; padding: 0; background: transparent; }
.drawer-date :deep(.uni-date-range) { display: flex; align-items: center; }
.drawer-date :deep(.uni-date__x-input) { text-align: center; font-size: 26rpx; color: #111827; }
.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; }
.list-scroll { height: calc(100vh - 280rpx); }
.list-wrap { padding: 0 24rpx 60rpx; }
.task-card { 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; }
@ -471,12 +698,4 @@ onUnload(() => clearSearchTimer())
.modal-btn { flex: 1; height: 80rpx; line-height: 80rpx; text-align: center; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; }
.cancel-btn { background: #f0f0f0; color: #6b7280; }
.confirm-btn { background: #1f4b79; color: #fff; }
.picker-content { padding: 24rpx 24rpx 36rpx; border-radius: 28rpx 28rpx 0 0; }
.picker-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
.picker-title { color: #1f2d3d; font-size: 30rpx; font-weight: 700; }
.picker-clear-text { color: #1f61ff; font-size: 26rpx; }
.picker-list { max-height: 480rpx; }
.picker-item { display: flex; align-items: center; justify-content: space-between; padding: 26rpx 6rpx; border-bottom: 1rpx solid #edf1f6; }
.picker-text { color: #243447; font-size: 28rpx; }
.picker-check { color: #1f61ff; font-size: 30rpx; }
</style>

@ -3,25 +3,42 @@
<NavBar :title="t('productOutbound.moduleName')" />
<view class="filter-bar">
<view class="keyword-box">
<input
id="product-outbound-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('productOutbound.searchDocumentPlaceholder')"
placeholder-class="placeholder"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@confirm="handleSearch"
/>
<view class="filter-row quick-row">
<picker class="quick-picker-host" mode="selector" :range="productPickerOptions" range-key="label" :value="productPickerIndex" @change="onProductFilterChange">
<view class="line-filter">
<text :class="['line-filter-text', selectedProductId === '' ? 'placeholder' : '']">{{ selectedProductLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
<picker class="quick-picker-host" :range="statusLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ selectedStatusLabel || t('productOutbound.outboundStatus') }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
</view>
<view class="status-box" @click="openStatusPicker">
<text class="status-box-text">{{ selectedStatusLabel || t('productOutbound.outboundStatus') }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af" />
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
id="product-outbound-keyword-input"
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('productOutbound.searchDocumentPlaceholder')"
placeholder-class="placeholder"
:focus="keywordFocus"
confirm-type="search"
@blur="keywordFocus = false"
@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 class="reset-filter-btn" @click="resetFilters">{{ t('productOutbound.reset') }}</view>
</view>
<scroll-view
@ -112,25 +129,64 @@
</view>
</view>
<uni-popup ref="statusPickerRef" type="bottom" background-color="#fff">
<view class="picker-content">
<view class="picker-header">
<text class="picker-title">{{ t('productOutbound.selectOutboundStatus') }}</text>
<view class="picker-clear" @click="resetStatus">
<text class="picker-clear-text">{{ t('productOutbound.clear') }}</text>
</view>
<uni-popup
ref="filterPopupRef"
class="product-filter-popup"
type="right"
background-color="transparent"
:animation="false"
>
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">{{ t('productOutbound.moreFilter') }}</text>
</view>
<scroll-view scroll-y class="picker-list">
<view
v-for="option in statusOptions"
:key="String(option.value)"
class="picker-item"
@click="selectStatus(option)"
>
<text class="picker-text">{{ option.label }}</text>
<uni-icons v-if="selectedStatus === option.value" class="picker-check" type="checkmarkempty" size="18" color="#1f61ff"></uni-icons>
<scroll-view scroll-y class="drawer-body">
<view class="drawer-section drawer-fields">
<view class="drawer-field">
<text class="drawer-label">{{ t('productOutbound.outboundTime') }}</text>
<view class="drawer-date">
<uni-datetime-picker
v-model="outTimeFilter"
type="daterange"
:clear-icon="true"
:start-placeholder="t('productOutbound.placeholderOutboundStartTime')"
:end-placeholder="t('productOutbound.placeholderOutboundEndTime')"
/>
</view>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('productOutbound.warehouse') }}</text>
<picker class="drawer-picker-host" mode="selector" :range="warehousePickerOptions" range-key="label" :value="warehousePickerIndex" @change="onWarehouseFilterChange">
<view class="drawer-picker">
<text :class="['drawer-picker-text', selectedWarehouseId === '' ? 'placeholder' : '']">{{ selectedWarehouseLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('productOutbound.creator') }}</text>
<picker class="drawer-picker-host" mode="selector" :range="creatorPickerOptions" range-key="label" :value="creatorPickerIndex" @change="onCreatorFilterChange">
<view class="drawer-picker">
<text :class="['drawer-picker-text', selectedCreatorId === '' ? 'placeholder' : '']">{{ selectedCreatorLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
</picker>
</view>
<view class="drawer-field">
<text class="drawer-label">{{ t('productOutbound.remark') }}</text>
<input
v-model="remarkFilter"
class="drawer-input"
type="text"
:placeholder="t('productOutbound.remarkPlaceholder')"
/>
</view>
</view>
</scroll-view>
<view class="drawer-actions">
<view class="drawer-action reset" @click="resetFilters">{{ t('productOutbound.reset') }}</view>
<view class="drawer-action confirm" @click="confirmFilterDrawer">{{ t('productOutbound.confirm') }}</view>
</view>
</view>
</uni-popup>
@ -144,21 +200,85 @@ import { onReady, onShow, onUnload } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { auditProductOutbound, getProductOutboundPage, submitProductOutbound } from '@/api/mes/productOutbound'
import { getProductSimpleList } from '@/api/erp/productInfo'
import { getWarehouseSimpleList } from '@/api/mes/moldget'
import { getSimpleUserList } from '@/api/system/user'
const { t } = useI18n()
const selectedStatus = ref('')
const selectedProductId = ref('')
const selectedWarehouseId = ref('')
const selectedCreatorId = ref('')
const searchKeyword = ref('')
const statusPickerRef = ref(null)
const outTimeFilter = ref([])
const remarkFilter = ref('')
const filterPopupRef = ref(null)
const productList = ref([])
const warehouseList = ref([])
const creatorList = ref([])
const filterOptionsLoaded = ref(false)
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('productOutbound.statusPending'), value: '0' },
{ label: t('productOutbound.statusRejected'), value: '1' },
{ label: t('productOutbound.statusAuditing'), value: '10' },
{ label: t('productOutbound.statusStored'), value: '20' },
{ label: t('productOutbound.statusRejected'), value: '1' }
{ label: t('productOutbound.statusStored'), value: '20' }
])
const statusLabels = computed(() => statusOptions.value.map((item) => item.label))
const statusPickerIndex = computed(() => {
const index = statusOptions.value.findIndex((item) => String(item.value) === String(selectedStatus.value))
return index >= 0 ? index : 0
})
const selectedStatusLabel = computed(() => {
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
if (selectedStatus.value === '') return ''
const current = statusOptions.value.find((item) => String(item.value) === String(selectedStatus.value))
return current ? current.label : ''
})
const productPickerOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...productList.value.map((item) => ({
label: item.name || item.productName || item.code || String(item.id || ''),
value: item.id
}))
])
const productPickerIndex = computed(() => {
const index = productPickerOptions.value.findIndex((item) => String(item.value) === String(selectedProductId.value))
return index >= 0 ? index : 0
})
const selectedProductLabel = computed(() => {
if (selectedProductId.value === '') return t('productOutbound.product')
return productPickerOptions.value.find((item) => String(item.value) === String(selectedProductId.value))?.label || t('productOutbound.product')
})
const warehousePickerOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...warehouseList.value.map((item) => ({
label: item.name || item.warehouseName || item.code || String(item.id || ''),
value: item.id
}))
])
const warehousePickerIndex = computed(() => {
const index = warehousePickerOptions.value.findIndex((item) => String(item.value) === String(selectedWarehouseId.value))
return index >= 0 ? index : 0
})
const selectedWarehouseLabel = computed(() => {
if (selectedWarehouseId.value === '') return t('functionCommon.all')
return warehousePickerOptions.value.find((item) => String(item.value) === String(selectedWarehouseId.value))?.label || t('functionCommon.all')
})
const creatorPickerOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
...creatorList.value.map((item) => ({
label: item.nickname || item.userName || item.name || String(item.id || ''),
value: item.id
}))
])
const creatorPickerIndex = computed(() => {
const index = creatorPickerOptions.value.findIndex((item) => String(item.value) === String(selectedCreatorId.value))
return index >= 0 ? index : 0
})
const selectedCreatorLabel = computed(() => {
if (selectedCreatorId.value === '') return t('functionCommon.all')
return creatorPickerOptions.value.find((item) => String(item.value) === String(selectedCreatorId.value))?.label || t('functionCommon.all')
})
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
@ -222,6 +342,39 @@ function normalizePageData(res) {
}
}
function normalizeListResponse(res) {
const root = res && res.data !== undefined ? res.data : res
return Array.isArray(root) ? root : (Array.isArray(root?.list) ? root.list : [])
}
function formatQueryDate(value, isEnd) {
if (!value) return undefined
const text = String(value).trim()
if (!text) return undefined
if (text.includes(':')) return text
return text + ' ' + (isEnd ? '23:59:59' : '00:00:00')
}
async function ensureFilterOptions() {
if (filterOptionsLoaded.value) return
filterOptionsLoaded.value = true
try {
const [productRes, warehouseRes, creatorRes] = await Promise.all([
getProductSimpleList({ categoryType: 1 }),
getWarehouseSimpleList(),
getSimpleUserList()
])
productList.value = normalizeListResponse(productRes)
warehouseList.value = normalizeListResponse(warehouseRes)
creatorList.value = normalizeListResponse(creatorRes)
} catch (e) {
filterOptionsLoaded.value = false
productList.value = []
warehouseList.value = []
creatorList.value = []
}
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
@ -231,11 +384,19 @@ async function fetchList(reset) {
else loadingMore.value = true
try {
const keyword = searchKeyword.value.trim()
const remark = remarkFilter.value.trim()
const outTimeRange = Array.isArray(outTimeFilter.value) ? outTimeFilter.value : []
const res = await getProductOutboundPage({
pageNo: pageNo.value,
pageSize: pageSize.value,
no: keyword || undefined,
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined
productId: selectedProductId.value === '' ? undefined : selectedProductId.value,
warehouseId: selectedWarehouseId.value === '' ? undefined : selectedWarehouseId.value,
creator: selectedCreatorId.value === '' ? undefined : selectedCreatorId.value,
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined,
remark: remark || undefined,
'outTime[0]': formatQueryDate(outTimeRange[0], false),
'outTime[1]': formatQueryDate(outTimeRange[1], true)
})
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
@ -248,7 +409,6 @@ async function fetchList(reset) {
loadingMore.value = false
}
}
async function handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
@ -271,22 +431,43 @@ async function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedStatus.value = ''
selectedProductId.value = ''
selectedWarehouseId.value = ''
selectedCreatorId.value = ''
outTimeFilter.value = []
remarkFilter.value = ''
closeFilterDrawer()
activateKeywordFocus()
await fetchList(true)
}
function openStatusPicker() {
statusPickerRef.value?.open()
function openFilterDrawer() {
filterPopupRef.value?.open()
}
async function selectStatus(option) {
selectedStatus.value = option.value
statusPickerRef.value?.close()
function closeFilterDrawer() {
filterPopupRef.value?.close()
}
async function confirmFilterDrawer() {
closeFilterDrawer()
await fetchList(true)
}
async function resetStatus() {
selectedStatus.value = ''
statusPickerRef.value?.close()
async function onProductFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedProductId.value = productPickerOptions.value[index]?.value === '' ? '' : String(productPickerOptions.value[index]?.value ?? '')
await fetchList(true)
}
async function onStatusFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedStatus.value = statusOptions.value[index]?.value ?? ''
await fetchList(true)
}
function onWarehouseFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedWarehouseId.value = warehousePickerOptions.value[index]?.value === '' ? '' : String(warehousePickerOptions.value[index]?.value ?? '')
}
function onCreatorFilterChange(event) {
const index = Number(event?.detail?.value || 0)
selectedCreatorId.value = creatorPickerOptions.value[index]?.value === '' ? '' : String(creatorPickerOptions.value[index]?.value ?? '')
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
@ -410,27 +591,72 @@ onReady(() => {
focusKeywordNoKeyboard()
})
onShow(() => {
onShow(async () => {
consumeSelectedAuditor()
fetchList(true)
await ensureFilterOptions()
await fetchList(true)
})
onUnload(() => clearSearchTimer())
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f4f5f7; }
.filter-bar { display: grid; grid-template-columns: minmax(0, 1fr) 188rpx 96rpx; align-items: center; gap: 14rpx; padding: 18rpx 28rpx 20rpx; }
.keyword-box,
.status-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; }
.status-box { justify-content: space-between; padding: 0 18rpx; }
.status-box-text,
.filter-bar { padding: 18rpx 14rpx 20rpx; background: #f4f5f7; }
.filter-row { display: flex; align-items: center; gap: 18rpx; }
.search-row { margin-top: 18rpx; }
.quick-picker-host { min-width: 0; flex: 1; display: block; }
.keyword-wrap,
.status-filter,
.line-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; box-sizing: border-box; }
.status-filter,
.line-filter { min-width: 0; flex: 1; display: flex; align-items: center; justify-content: space-between; padding: 0 18rpx 0 26rpx; }
.icon-filter-btn { width: 66rpx; flex: 0 0 66rpx; display: flex; align-items: center; justify-content: center; border-color: transparent; background: transparent; }
.status-filter-text,
.line-filter-text,
.placeholder { font-size: 26rpx; }
.status-box-text { flex: 1; min-width: 0; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reset-filter-btn { justify-content: center; font-size: 24rpx; color: #4b5563; }
.list-scroll { height: calc(100vh - 194rpx); }
.status-filter-text,
.line-filter-text { min-width: 0; flex: 1; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-filter-text.placeholder,
.line-filter-text.placeholder,
.placeholder { color: #a8adb7; }
.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; }
:deep(.product-filter-popup.right .uni-popup__content-transition) { transform: none !important; }
.drawer-header { height: 104rpx; padding: 18rpx 34rpx 0; background: #ffffff; display: flex; align-items: center; 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: 8rpx 28rpx; border-radius: 24rpx; background: #ffffff; box-sizing: border-box; }
.drawer-fields { display: flex; flex-direction: column; }
.drawer-field { min-width: 0; min-height: 98rpx; display: flex; align-items: center; gap: 20rpx; border-bottom: 1rpx solid #eceff3; box-sizing: border-box; }
.drawer-field:last-child { border-bottom: 0; }
.drawer-label { width: 160rpx; flex: 0 0 160rpx; font-size: 24rpx; line-height: 1.3; color: #4b5563; font-weight: 500; }
.drawer-picker-host { min-width: 0; flex: 1; display: block; }
.drawer-input,
.drawer-picker,
.drawer-date { min-width: 0; flex: 1; 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: right; }
.drawer-picker { display: flex; align-items: center; justify-content: flex-end; padding: 0 18rpx; gap: 8rpx; }
.drawer-picker-text { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 26rpx; color: #111827; text-align: right; }
.drawer-picker-text.placeholder { color: #9ca3af; }
.drawer-date { display: flex; align-items: center; padding: 0 12rpx; }
.drawer-date :deep(.uni-date),
.drawer-date :deep(.uni-date-editor),
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) { width: 100%; }
.drawer-date :deep(.uni-date-editor),
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) { min-height: 74rpx; }
.drawer-date :deep(.uni-date-editor--x),
.drawer-date :deep(.uni-date-x) { border: 0; padding: 0; background: transparent; }
.drawer-date :deep(.uni-date-range) { display: flex; align-items: center; }
.drawer-date :deep(.uni-date__x-input) { text-align: center; font-size: 26rpx; color: #111827; }
.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; }
.list-scroll { height: calc(100vh - 280rpx); }
.list-wrap { padding: 0 24rpx 60rpx; }
.task-card { 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; }
@ -473,12 +699,4 @@ onUnload(() => clearSearchTimer())
.modal-btn { flex: 1; height: 80rpx; line-height: 80rpx; text-align: center; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; }
.cancel-btn { background: #f0f0f0; color: #6b7280; }
.confirm-btn { background: #1f4b79; color: #fff; }
.picker-content { padding: 24rpx 24rpx 36rpx; border-radius: 28rpx 28rpx 0 0; }
.picker-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
.picker-title { color: #1f2d3d; font-size: 30rpx; font-weight: 700; }
.picker-clear-text { color: #1f61ff; font-size: 26rpx; }
.picker-list { max-height: 480rpx; }
.picker-item { display: flex; align-items: center; justify-content: space-between; padding: 26rpx 6rpx; border-bottom: 1rpx solid #edf1f6; }
.picker-text { color: #243447; font-size: 28rpx; }
.picker-check { color: #1f61ff; font-size: 30rpx; }
</style>

Loading…
Cancel
Save