style: 备件出入库-物料出入库搜索框样式优化

master
zhongwenkai 1 day ago
parent a457da530c
commit eea217ccf8

@ -212,7 +212,8 @@ export default {
all: 'All',
noMore: 'No more data',
confirmTitle: 'Confirm',
clear: 'Clear'
clear: 'Clear',
moreFilter: 'More Filters'
},
moldGet: {
moduleName: 'Mold Stock-out',
@ -1162,6 +1163,7 @@ export default {
allStatus: 'All',
selectMoldStatus: 'Select Mold Status',
clear: 'Clear',
moreFilter: 'More Filters',
name: 'Name',
createTime: 'Created At',
confirmDeleteContent: 'Confirm delete mold brand "{name}"?',
@ -1464,6 +1466,7 @@ export default {
inboundStatus: 'Inbound Status',
reset: 'Reset',
clear: 'Clear',
moreFilter: 'More Filters',
loading: 'Loading...',
loadingMore: 'Loading more...',
noMoreData: 'No more data',
@ -1600,6 +1603,7 @@ export default {
all: 'All',
reset: 'Reset',
clear: 'Clear',
moreFilter: 'More Filters',
loading: 'Loading...',
loadingMore: 'Loading more...',
noMoreData: 'No more data',
@ -1702,6 +1706,7 @@ export default {
outboundStatus: 'Outbound Status',
reset: 'Reset',
clear: 'Clear',
moreFilter: 'More Filters',
loading: 'Loading...',
loadingMore: 'Loading more...',
noMoreData: 'No more data',

@ -212,7 +212,8 @@ export default {
all: '全部',
noMore: '没有更多数据了',
confirmTitle: '提示',
clear: '清除'
clear: '清除',
moreFilter: '更多筛选'
},
moldGet: {
moduleName: '模具出库',
@ -1802,6 +1803,12 @@ export default {
tabPending: '待入库',
tabAuditing: '待审核',
searchPlaceholder: '搜索入库单号',
allStatus: '备件状态',
creator: '创建人',
inboundTime: '入库时间',
startTime: '开始时间',
endTime: '结束时间',
to: '至',
sparepartInfo: '备件信息',
inboundTime: '入库时间',
creator: '创建人',
@ -1842,6 +1849,7 @@ export default {
tabPending: '待出库',
tabAuditing: '待审核',
searchPlaceholder: '搜索出库单号',
allStatus: '出库状态',
sparepartInfo: '备件信息',
outboundTime: '出库时间',
creator: '创建人',
@ -1855,6 +1863,8 @@ export default {
rejectSuccess: '已驳回',
deleteSuccess: '删除成功',
empty: '暂无出库单据',
startTime: '开始时间',
endTime: '结束时间',
createTitle: '新增备件出库'
},
materialOutbound: {
@ -1862,9 +1872,12 @@ export default {
tabPending: '待出库',
tabAuditing: '待审核',
searchPlaceholder: '搜索出库单号',
allStatus: '出库状态',
sparepartInfo: '物料信息',
materialInfo: '物料信息',
outboundTime: '出库时间',
startTime: '开始时间',
endTime: '结束时间',
creator: '创建人',
quantity: '数量',
reviewer: '审核人',

@ -4,37 +4,83 @@
<!-- 搜索栏 -->
<view class="filter-bar">
<view class="keyword-box">
<input
v-model="searchKeyword"
class="keyword-input"
placeholder="搜索单号"
confirm-type="search"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
<view class="filter-row quick-row">
<picker :range="statusPickerLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ currentStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
</view>
<view class="status-box-wrapper">
<view class="status-box" @click="toggleStatusDropdown">
<text class="status-box-text">{{ currentStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
v-model="searchKeyword"
class="keyword-input"
type="text"
placeholder="搜索单号"
confirm-type="search"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
</view>
<view v-if="showStatusDropdown" class="status-dropdown-panel">
<view
v-for="(item, idx) in statusOptions"
:key="idx"
class="status-dropdown-item"
:class="{ active: selectedStatus === item.value }"
@click.stop="selectStatus(item, idx)"
>
<text class="status-dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedStatus === item.value" class="status-dropdown-check"></text>
</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"></view>
</view>
<!-- 筛选抽屉 -->
<uni-popup ref="filterPopupRef" class="material-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">更多筛选</text>
</view>
<scroll-view scroll-y class="drawer-body">
<view class="drawer-section">
<view class="drawer-section-head">
<text class="drawer-section-title">物料信息</text>
</view>
<view class="drawer-grid">
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">创建人</text>
<view class="drawer-picker" @click="toggleCreatorPanel">
<text :class="['drawer-picker-text', !selectedCreator ? 'placeholder' : '']">{{ selectedCreatorLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
<scroll-view v-if="creatorPanelOpen" scroll-y class="drawer-option-panel">
<view
v-for="item in creatorOptions"
:key="item.value"
:class="['drawer-option-item', selectedCreator === item.value ? 'active' : '']"
@click="selectCreator(item)"
>
<text class="drawer-option-text">{{ item.label }}</text>
<text v-if="selectedCreator === item.value" class="drawer-option-check"></text>
</view>
</scroll-view>
</view>
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">入库时间</text>
<view class="drawer-date">
<uni-datetime-picker v-model="inTimeFilter" type="daterange" :clear-icon="true"
start-placeholder="开始时间"
end-placeholder="结束时间" />
</view>
</view>
</view>
</view>
</scroll-view>
<view class="drawer-actions">
<view class="drawer-action reset" @click="resetFilters"></view>
<view class="drawer-action confirm" @click="confirmFilterDrawer"></view>
</view>
</view>
</uni-popup>
<!-- 列表 -->
<scroll-view
scroll-y
@ -163,20 +209,39 @@ import { getSimpleUserList } from '@/api/mes/moldget'
const selectedStatus = ref('')
const searchKeyword = ref('')
const showStatusDropdown = ref(false)
const filterPopupRef = ref(null)
const selectedCreator = ref(null)
const inTimeFilter = ref([])
const creatorOptions = ref([])
const creatorPanelOpen = ref(false)
const statusOptions = computed(() => [
{ label: '全部', value: '' },
{ label: '待入库', value: '0' },
{ label: '待审核', value: '10' },
{ label: '已入库', value: '20' },
{ label: '已驳回', value: '1' }
])
const statusPickerLabels = computed(() => {
return ['物料状态', ...statusOptions.value.map((item) => item.label)]
})
const statusPickerIndex = computed(() => {
if (selectedStatus.value === '') return 0
const idx = statusOptions.value.findIndex((item) => item.value === selectedStatus.value)
return idx >= 0 ? idx + 1 : 0
})
const currentStatusLabel = computed(() => {
if (selectedStatus.value === '') return '物料状态'
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
return current ? current.label : '全部'
return current ? current.label : '物料状态'
})
const selectedCreatorLabel = computed(() => {
if (!selectedCreator.value) return '创建人'
const found = creatorOptions.value.find((u) => u.value === selectedCreator.value)
return found ? found.label : '创建人'
})
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
@ -243,11 +308,16 @@ async function fetchList(reset) {
loadingMore.value = true
}
try {
const inTimeRange = Array.isArray(inTimeFilter.value) ? inTimeFilter.value : []
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
no: searchKeyword.value.trim() || undefined,
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined,
creator: selectedCreator.value || undefined,
inTime: inTimeRange.length === 2
? [inTimeRange[0] + ' 00:00:00', inTimeRange[1] + ' 23:59:59']
: undefined
}
const res = await getMaterialInboundPage(params)
const page = normalizePageData(res)
@ -262,16 +332,50 @@ async function fetchList(reset) {
}
}
function toggleStatusDropdown() {
showStatusDropdown.value = !showStatusDropdown.value
function onStatusFilterChange(event) {
const index = Number(event?.detail?.value || 0)
if (index === 0) {
selectedStatus.value = ''
} else {
const item = statusOptions.value[index - 1]
selectedStatus.value = item ? item.value : ''
}
fetchList(true)
}
function selectStatus(item, _idx) {
selectedStatus.value = item.value
showStatusDropdown.value = false
function openFilterDrawer() {
loadCreatorOptions()
filterPopupRef.value?.open()
}
function confirmFilterDrawer() {
filterPopupRef.value?.close()
fetchList(true)
}
function toggleCreatorPanel() {
creatorPanelOpen.value = !creatorPanelOpen.value
}
function selectCreator(item) {
selectedCreator.value = selectedCreator.value === item.value ? null : item.value
creatorPanelOpen.value = false
}
async function loadCreatorOptions() {
if (creatorOptions.value.length) return
try {
const res = await getSimpleUserList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
creatorOptions.value = data.map((u) => ({
value: u.id || u.userId,
label: u.nickname || u.userName || u.name || String(u.id || '')
}))
} catch (e) {
console.error('loadCreatorOptions error', e)
}
}
function handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
@ -289,6 +393,8 @@ function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedStatus.value = ''
selectedCreator.value = null
inTimeFilter.value = []
fetchList(true)
}
@ -447,9 +553,7 @@ onUnload(() => {
clearSearchTimer()
})
onHide(() => {
showStatusDropdown.value = false
})
onHide(() => {})
</script>
<style lang="scss" scoped>
@ -460,107 +564,319 @@ onHide(() => {
/* ====== 搜索栏 ====== */
.filter-bar {
padding: 18rpx 14rpx 20rpx;
background: #f3f4f6;
}
.filter-row {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 24rpx;
background: #fff;
gap: 18rpx;
}
.keyword-box {
flex: 1;
.search-row {
margin-top: 18rpx;
}
.quick-row > picker {
min-width: 0;
flex: 1;
}
.keyword-wrap,
.status-filter,
.icon-filter-btn {
height: 66rpx;
padding: 0 28rpx;
background: #f4f5f7;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
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%;
font-size: 24rpx;
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #374151;
}
.status-box-wrapper {
position: relative;
flex-shrink: 0;
.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-box {
.status-filter-text.placeholder {
color: #a8adb7;
}
.icon-filter-btn {
width: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: space-between;
height: 66rpx;
padding: 0 28rpx;
min-width: 160rpx;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
justify-content: center;
border-color: transparent;
background: transparent;
}
.status-dropdown-panel {
position: absolute;
top: 74rpx;
left: 0;
right: 0;
z-index: 200;
background: #fff;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.12);
/* ====== 筛选抽屉 ====== */
::deep(.material-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;
}
.status-dropdown-item {
.drawer-header {
height: 104rpx;
padding: 18rpx 34rpx 0;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
justify-content: flex-start;
box-sizing: border-box;
}
&:last-child {
border-bottom: 0;
}
.drawer-title {
color: #1f2937;
font-size: 34rpx;
line-height: 1.3;
font-weight: 700;
}
&.active {
background: #f0f5ff;
}
.drawer-body {
flex: 1;
min-height: 0;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.status-dropdown-item-text {
font-size: 26rpx;
color: #374151;
.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);
}
.status-dropdown-check {
font-size: 26rpx;
color: #2563eb;
.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;
}
.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;
}
.status-box-text {
.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;
color: #374151;
line-height: 1.3;
color: #4b5563;
font-weight: 500;
}
.drawer-picker {
width: 100%;
min-height: 74rpx;
border: 0;
border-radius: 8rpx;
background: #f7f8fb;
box-sizing: border-box;
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;
}
.reset-filter-btn {
height: 66rpx;
line-height: 66rpx;
padding: 0 28rpx;
font-size: 24rpx;
color: #4b5563;
background: #fff;
border: 1rpx solid #e5e7eb;
.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-date {
width: 100%;
min-height: 74rpx;
border: 0;
border-radius: 8rpx;
background: #f7f8fb;
box-sizing: border-box;
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;
}
/* ====== 列表 ====== */

@ -3,21 +3,56 @@
<NavBar :title="t('materialOutbound.moduleName')" />
<view class="filter-bar">
<view class="keyword-box">
<input v-model="searchKeyword" class="keyword-input" :placeholder="t('materialOutbound.searchPlaceholder')" confirm-type="search" @input="handleKeywordInput" @confirm="handleSearch" />
</view>
<view class="status-box-wrapper">
<view class="status-box" @click="toggleStatusDropdown"><text class="status-box-text">{{ currentStatusLabel }}</text><uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons></view>
<view v-if="showStatusDropdown" class="status-dropdown-panel">
<view v-for="(item, idx) in statusOptions" :key="idx" class="status-dropdown-item" :class="{ active: selectedStatus === item.value }" @click.stop="selectStatus(item, idx)">
<text class="status-dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedStatus === item.value" class="status-dropdown-check"></text>
<view class="filter-row quick-row">
<picker :range="statusPickerLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ currentStatusLabel }}</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 v-model="searchKeyword" class="keyword-input" type="text" :placeholder="t('materialOutbound.searchPlaceholder')" confirm-type="search" @input="handleKeywordInput" @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('functionCommon.reset') }}</view>
</view>
<uni-popup ref="filterPopupRef" class="material-outbound-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header"><text class="drawer-title">{{ t('functionCommon.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('materialOutbound.materialInfo') }}</text></view>
<view class="drawer-grid">
<view class="drawer-field drawer-field-wide"><text class="drawer-label">{{ t('materialOutbound.creator') }}</text>
<view class="drawer-picker" @click="toggleCreatorPanel">
<text :class="['drawer-picker-text', !selectedCreator ? 'placeholder' : '']">{{ selectedCreatorLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
<scroll-view v-if="creatorPanelOpen" scroll-y class="drawer-option-panel">
<view v-for="item in creatorOptions" :key="item.value" :class="['drawer-option-item', selectedCreator === item.value ? 'active' : '']" @click="selectCreator(item)">
<text class="drawer-option-text">{{ item.label }}</text>
<text v-if="selectedCreator === item.value" class="drawer-option-check"></text>
</view>
</scroll-view>
</view>
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">{{ t('materialOutbound.outboundTime') }}</text>
<view class="drawer-date"><uni-datetime-picker v-model="inTimeFilter" type="daterange" :clear-icon="true" :start-placeholder="t('materialOutbound.startTime')" :end-placeholder="t('materialOutbound.endTime')" /></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>
<scroll-view scroll-y class="list-scroll" :scroll-top="scrollTop" @scroll="onScroll" @scrolltolower="loadMore" :lower-threshold="80">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="task-card" @click="openDetail(item)">
@ -100,15 +135,22 @@ const { t } = useI18n()
const selectedStatus = ref('')
const searchKeyword = ref('')
const showStatusDropdown = ref(false)
const filterPopupRef = ref(null)
const selectedCreator = ref(null)
const inTimeFilter = ref([])
const creatorOptions = ref([])
const creatorPanelOpen = ref(false)
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('materialOutbound.tabPending'), value: '0' },
{ label: t('materialOutbound.tabAuditing'), value: '10' },
{ label: t('materialOutbound.approve'), value: '20' },
{ label: '已驳回', value: '1' }
])
const currentStatusLabel = computed(() => { const cur = statusOptions.value.find(i => i.value === selectedStatus.value); return cur ? cur.label : t('functionCommon.all') })
const statusPickerLabels = computed(() => [t('materialOutbound.allStatus'), ...statusOptions.value.map(i => i.label)])
const statusPickerIndex = computed(() => { if (selectedStatus.value === '') return 0; const idx = statusOptions.value.findIndex(i => i.value === selectedStatus.value); return idx >= 0 ? idx + 1 : 0 })
const currentStatusLabel = computed(() => { if (selectedStatus.value === '') return t('materialOutbound.allStatus'); const cur = statusOptions.value.find(i => i.value === selectedStatus.value); return cur ? cur.label : t('materialOutbound.allStatus') })
const selectedCreatorLabel = computed(() => { if (!selectedCreator.value) return t('materialOutbound.creator'); const found = creatorOptions.value.find(u => u.value === selectedCreator.value); return found ? found.label : t('materialOutbound.creator') })
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
@ -149,7 +191,8 @@ 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 params = { pageNo: pageNo.value, pageSize: pageSize.value, no: searchKeyword.value.trim() || undefined, statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined }
const inTimeRange = Array.isArray(inTimeFilter.value) ? inTimeFilter.value : []
const params = { pageNo: pageNo.value, pageSize: pageSize.value, no: searchKeyword.value.trim() || undefined, statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined, creator: selectedCreator.value || undefined, outTime: inTimeRange.length === 2 ? [inTimeRange[0] + ' 00:00:00', inTimeRange[1] + ' 23:59:59'] : undefined }
const res = await getMaterialOutboundPage(params)
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
@ -158,11 +201,15 @@ async function fetchList(reset) {
finally { loading.value = false; loadingMore.value = false }
}
function toggleStatusDropdown() { showStatusDropdown.value = !showStatusDropdown.value }
function selectStatus(item, _idx) { selectedStatus.value = item.value; showStatusDropdown.value = false; fetchList(true) }
function onStatusFilterChange(event) { const index = Number(event?.detail?.value || 0); if (index === 0) { selectedStatus.value = '' } else { const item = statusOptions.value[index - 1]; selectedStatus.value = item ? item.value : '' }; fetchList(true) }
function handleSearch() { clearSearchTimer(); uni.hideKeyboard(); fetchList(true) }
function handleKeywordInput() { clearSearchTimer(); searchTimer = setTimeout(() => fetchList(true), 300) }
function resetFilters() { clearSearchTimer(); searchKeyword.value = ''; selectedStatus.value = ''; fetchList(true) }
function resetFilters() { clearSearchTimer(); searchKeyword.value = ''; selectedStatus.value = ''; selectedCreator.value = null; inTimeFilter.value = []; fetchList(true) }
function openFilterDrawer() { loadCreatorOptions(); filterPopupRef.value?.open() }
function confirmFilterDrawer() { filterPopupRef.value?.close(); fetchList(true) }
function toggleCreatorPanel() { creatorPanelOpen.value = !creatorPanelOpen.value }
function selectCreator(item) { selectedCreator.value = selectedCreator.value === item.value ? null : item.value; creatorPanelOpen.value = false }
async function loadCreatorOptions() { if (creatorOptions.value.length) return; try { const res = await getSimpleUserList(); const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : []); creatorOptions.value = data.map(u => ({ value: u.id || u.userId, label: u.nickname || u.userName || u.name || String(u.id || '') })) } catch (e) {} }
async function loadMore() { if (loading.value || loadingMore.value || finished.value) return; pageNo.value += 1; await fetchList(false) }
function openDetail(item) { if (!item?.id) { uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' }); return }; uni.navigateTo({ url: `/pages_function/pages/materialOutbound/detail?id=${encodeURIComponent(String(item.id))}` }) }
@ -206,22 +253,56 @@ function clearSearchTimer() { if (searchTimer) { clearTimeout(searchTimer); sear
onShow(() => { fetchList(true) })
onUnload(() => { clearSearchTimer() })
onHide(() => { showStatusDropdown.value = false })
onHide(() => {})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f4f5f7; }
.filter-bar { display: flex; align-items: center; gap: 12rpx; padding: 18rpx 24rpx; background: #fff; }
.keyword-box { flex: 1; min-width: 0; height: 66rpx; padding: 0 28rpx; background: #f4f5f7; border: 1rpx solid #e5e7eb; border-radius: 12rpx; display: flex; align-items: center; }
.keyword-input { width: 100%; font-size: 24rpx; color: #374151; }
.status-box-wrapper { position: relative; flex-shrink: 0; }
.status-box { display: flex; align-items: center; justify-content: space-between; height: 66rpx; padding: 0 28rpx; min-width: 160rpx; background: #fff; border: 1rpx solid #e5e7eb; border-radius: 12rpx; }
.status-dropdown-panel { position: absolute; top: 74rpx; left: 0; right: 0; z-index: 200; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.12); overflow: hidden; }
.status-dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 18rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: 0; } &.active { background: #f0f5ff; } }
.status-dropdown-item-text { font-size: 26rpx; color: #374151; }
.status-dropdown-check { font-size: 26rpx; color: #2563eb; font-weight: 700; }
.status-box-text { font-size: 24rpx; color: #374151; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reset-filter-btn { height: 66rpx; line-height: 66rpx; padding: 0 28rpx; font-size: 24rpx; color: #4b5563; background: #fff; border: 1rpx solid #e5e7eb; border-radius: 12rpx; flex-shrink: 0; }
.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; }
::deep(.material-outbound-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-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; }
.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-picker { width: 100%; min-height: 74rpx; border: 0; border-radius: 8rpx; background: #f7f8fb; box-sizing: border-box; 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-date { width: 100%; min-height: 74rpx; border: 0; border-radius: 8rpx; background: #f7f8fb; box-sizing: border-box; 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; }
.list-scroll { height: calc(100vh - 194rpx); }
.list-wrap { padding: 0 24rpx 60rpx; }

@ -4,31 +4,83 @@
<!-- 搜索栏 -->
<view class="filter-bar">
<view class="keyword-box">
<input
v-model="searchKeyword"
class="keyword-input"
:placeholder="t('sparepartInbound.searchPlaceholder')"
confirm-type="search"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
<view class="filter-row quick-row">
<picker :range="statusPickerLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ currentStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
</view>
</picker>
</view>
<view class="status-box-wrapper">
<view class="status-box" @click="toggleStatusDropdown">
<text class="status-box-text">{{ currentStatusLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
<view class="filter-row search-row">
<view class="keyword-wrap">
<input
v-model="searchKeyword"
class="keyword-input"
type="text"
:placeholder="t('sparepartInbound.searchPlaceholder')"
confirm-type="search"
@input="handleKeywordInput"
@confirm="handleSearch"
/>
</view>
<view v-if="showStatusDropdown" class="status-dropdown-panel">
<view v-for="(item, idx) in statusOptions" :key="idx" class="status-dropdown-item" :class="{ active: selectedStatus === item.value }" @click.stop="selectStatus(item, idx)">
<text class="status-dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedStatus === item.value" class="status-dropdown-check"></text>
</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('functionCommon.reset') }}</view>
</view>
<!-- 筛选抽屉 -->
<uni-popup ref="filterPopupRef" class="sparepart-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header">
<text class="drawer-title">{{ t('functionCommon.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('sparepartInbound.sparepartInfo') }}</text>
</view>
<view class="drawer-grid">
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">{{ t('sparepartInbound.creator') }}</text>
<view class="drawer-picker" @click="toggleCreatorPanel">
<text :class="['drawer-picker-text', !selectedCreator ? 'placeholder' : '']">{{ selectedCreatorLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
<scroll-view v-if="creatorPanelOpen" scroll-y class="drawer-option-panel">
<view
v-for="item in creatorOptions"
:key="item.value"
:class="['drawer-option-item', selectedCreator === item.value ? 'active' : '']"
@click="selectCreator(item)"
>
<text class="drawer-option-text">{{ item.label }}</text>
<text v-if="selectedCreator === item.value" class="drawer-option-check"></text>
</view>
</scroll-view>
</view>
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">{{ t('sparepartInbound.inboundTime') }}</text>
<view class="drawer-date">
<uni-datetime-picker v-model="inTimeFilter" type="daterange" :clear-icon="true"
:start-placeholder="t('sparepartInbound.startTime')"
:end-placeholder="t('sparepartInbound.endTime')" />
</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>
<!-- 列表 -->
<scroll-view
scroll-y
@ -154,26 +206,45 @@ import { onShow, onUnload, onHide } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getSparepartInboundPage, auditSparepartInbound, submitSparepartInbound } from '@/api/mes/sparepartInbound'
import { getSimpleUserList } from '@/api/mes/moldget'
import { getSimpleUserList } from '@/api/system/user'
const { t } = useI18n()
const selectedStatus = ref('')
const searchKeyword = ref('')
const showStatusDropdown = ref(false)
const filterPopupRef = ref(null)
const selectedCreator = ref(null)
const inTimeFilter = ref([])
const creatorOptions = ref([])
const creatorPanelOpen = ref(false)
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('sparepartInbound.tabPending'), value: '0' },
{ label: t('sparepartInbound.tabAuditing'), value: '10' },
{ label: t('sparepartInbound.approve'), value: '20' },
{ label: '已驳回', value: '1' }
])
const statusPickerLabels = computed(() => {
return [t('functionCommon.all'), ...statusOptions.value.map((item) => item.label)]
})
const statusPickerIndex = computed(() => {
if (selectedStatus.value === '') return 0
const idx = statusOptions.value.findIndex((item) => item.value === selectedStatus.value)
return idx >= 0 ? idx + 1 : 0
})
const currentStatusLabel = computed(() => {
if (selectedStatus.value === '') return t('sparepartInbound.allStatus')
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
return current ? current.label : t('functionCommon.all')
return current ? current.label : t('sparepartInbound.allStatus')
})
const selectedCreatorLabel = computed(() => {
if (!selectedCreator.value) return t('sparepartInbound.creator')
const found = creatorOptions.value.find((u) => u.value === selectedCreator.value)
return found ? found.label : t('sparepartInbound.creator')
})
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
@ -240,11 +311,16 @@ async function fetchList(reset) {
loadingMore.value = true
}
try {
const inTimeRange = Array.isArray(inTimeFilter.value) ? inTimeFilter.value : []
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
no: searchKeyword.value.trim() || undefined,
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined,
creator: selectedCreator.value || undefined,
inTime: inTimeRange.length === 2
? [inTimeRange[0] + ' 00:00:00', inTimeRange[1] + ' 23:59:59']
: undefined
}
const res = await getSparepartInboundPage(params)
const page = normalizePageData(res)
@ -259,16 +335,50 @@ async function fetchList(reset) {
}
}
function toggleStatusDropdown() {
showStatusDropdown.value = !showStatusDropdown.value
function onStatusFilterChange(event) {
const index = Number(event?.detail?.value || 0)
if (index === 0) {
selectedStatus.value = ''
} else {
const item = statusOptions.value[index - 1]
selectedStatus.value = item ? item.value : ''
}
fetchList(true)
}
function selectStatus(item, _idx) {
selectedStatus.value = item.value
showStatusDropdown.value = false
function openFilterDrawer() {
loadCreatorOptions()
filterPopupRef.value?.open()
}
function confirmFilterDrawer() {
filterPopupRef.value?.close()
fetchList(true)
}
function toggleCreatorPanel() {
creatorPanelOpen.value = !creatorPanelOpen.value
}
function selectCreator(item) {
selectedCreator.value = selectedCreator.value === item.value ? null : item.value
creatorPanelOpen.value = false
}
async function loadCreatorOptions() {
if (creatorOptions.value.length) return
try {
const res = await getSimpleUserList()
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
creatorOptions.value = data.map((u) => ({
value: u.id || u.userId,
label: u.nickname || u.userName || u.name || String(u.id || '')
}))
} catch (e) {
console.error('loadCreatorOptions error', e)
}
}
function handleSearch() {
clearSearchTimer()
uni.hideKeyboard()
@ -286,6 +396,8 @@ function resetFilters() {
clearSearchTimer()
searchKeyword.value = ''
selectedStatus.value = ''
selectedCreator.value = null
inTimeFilter.value = []
fetchList(true)
}
@ -443,9 +555,7 @@ onUnload(() => {
clearSearchTimer()
})
onHide(() => {
showStatusDropdown.value = false
})
onHide(() => {})
</script>
<style lang="scss" scoped>
@ -456,107 +566,319 @@ onHide(() => {
/* ====== 搜索栏 ====== */
.filter-bar {
padding: 18rpx 14rpx 20rpx;
background: #f3f4f6;
}
.filter-row {
display: flex;
align-items: center;
gap: 12rpx;
padding: 18rpx 24rpx;
background: #fff;
gap: 18rpx;
}
.keyword-box {
flex: 1;
.search-row {
margin-top: 18rpx;
}
.quick-row > picker {
min-width: 0;
flex: 1;
}
.keyword-wrap,
.status-filter,
.icon-filter-btn {
height: 66rpx;
padding: 0 28rpx;
background: #f4f5f7;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
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%;
font-size: 24rpx;
height: 64rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #374151;
}
.status-box-wrapper {
position: relative;
flex-shrink: 0;
.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-box {
.status-filter-text.placeholder {
color: #a8adb7;
}
.icon-filter-btn {
width: 66rpx;
flex: 0 0 66rpx;
display: flex;
align-items: center;
justify-content: space-between;
height: 66rpx;
padding: 0 28rpx;
min-width: 160rpx;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
justify-content: center;
border-color: transparent;
background: transparent;
}
.status-dropdown-panel {
position: absolute;
top: 74rpx;
left: 0;
right: 0;
z-index: 200;
background: #fff;
border: 1rpx solid #e0e0e0;
border-radius: 12rpx;
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.12);
/* ====== 筛选抽屉 ====== */
::deep(.sparepart-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;
}
.status-dropdown-item {
.drawer-header {
height: 104rpx;
padding: 18rpx 34rpx 0;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
justify-content: flex-start;
box-sizing: border-box;
}
&:last-child {
border-bottom: 0;
}
.drawer-title {
color: #1f2937;
font-size: 34rpx;
line-height: 1.3;
font-weight: 700;
}
&.active {
background: #f0f5ff;
}
.drawer-body {
flex: 1;
min-height: 0;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.status-dropdown-item-text {
font-size: 26rpx;
color: #374151;
.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);
}
.status-dropdown-check {
font-size: 26rpx;
color: #2563eb;
.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;
}
.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;
}
.status-box-text {
.drawer-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 22rpx 20rpx;
}
.drawer-field {
min-width: 0;
}
.drawer-label {
display: block;
margin-bottom: 12rpx;
font-size: 24rpx;
color: #374151;
line-height: 1.3;
color: #4b5563;
font-weight: 500;
}
.drawer-picker {
width: 100%;
min-height: 74rpx;
border: 0;
border-radius: 8rpx;
background: #f7f8fb;
box-sizing: border-box;
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;
}
.reset-filter-btn {
height: 66rpx;
line-height: 66rpx;
padding: 0 28rpx;
font-size: 24rpx;
color: #4b5563;
background: #fff;
border: 1rpx solid #e5e7eb;
.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-field-wide {
grid-column: 1 / -1;
}
.drawer-date {
width: 100%;
min-height: 74rpx;
border: 0;
border-radius: 8rpx;
background: #f7f8fb;
box-sizing: border-box;
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-option-check {
flex-shrink: 0;
font-size: 28rpx;
color: #174b78;
margin-left: 16rpx;
}
/* ====== 列表 ====== */

@ -3,21 +3,56 @@
<NavBar :title="t('sparepartOutbound.moduleName')" />
<view class="filter-bar">
<view class="keyword-box">
<input v-model="searchKeyword" class="keyword-input" :placeholder="t('sparepartOutbound.searchPlaceholder')" confirm-type="search" @input="handleKeywordInput" @confirm="handleSearch" />
</view>
<view class="status-box-wrapper">
<view class="status-box" @click="toggleStatusDropdown"><text class="status-box-text">{{ currentStatusLabel }}</text><uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons></view>
<view v-if="showStatusDropdown" class="status-dropdown-panel">
<view v-for="(item, idx) in statusOptions" :key="idx" class="status-dropdown-item" :class="{ active: selectedStatus === item.value }" @click.stop="selectStatus(item, idx)">
<text class="status-dropdown-item-text">{{ item.label }}</text>
<text v-if="selectedStatus === item.value" class="status-dropdown-check"></text>
<view class="filter-row quick-row">
<picker :range="statusPickerLabels" :value="statusPickerIndex" @change="onStatusFilterChange">
<view class="status-filter">
<text :class="['status-filter-text', selectedStatus === '' ? 'placeholder' : '']">{{ currentStatusLabel }}</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 v-model="searchKeyword" class="keyword-input" type="text" :placeholder="t('sparepartOutbound.searchPlaceholder')" confirm-type="search" @input="handleKeywordInput" @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('functionCommon.reset') }}</view>
</view>
<uni-popup ref="filterPopupRef" class="outbound-filter-popup" type="right" background-color="transparent" :animation="false">
<view class="filter-drawer">
<view class="drawer-header"><text class="drawer-title">{{ t('functionCommon.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('sparepartOutbound.sparepartInfo') }}</text></view>
<view class="drawer-grid">
<view class="drawer-field drawer-field-wide"><text class="drawer-label">{{ t('sparepartOutbound.creator') }}</text>
<view class="drawer-picker" @click="toggleCreatorPanel">
<text :class="['drawer-picker-text', !selectedCreator ? 'placeholder' : '']">{{ selectedCreatorLabel }}</text>
<uni-icons type="bottom" size="14" color="#9ca3af"></uni-icons>
</view>
<scroll-view v-if="creatorPanelOpen" scroll-y class="drawer-option-panel">
<view v-for="item in creatorOptions" :key="item.value" :class="['drawer-option-item', selectedCreator === item.value ? 'active' : '']" @click="selectCreator(item)">
<text class="drawer-option-text">{{ item.label }}</text>
<text v-if="selectedCreator === item.value" class="drawer-option-check"></text>
</view>
</scroll-view>
</view>
<view class="drawer-field drawer-field-wide">
<text class="drawer-label">{{ t('sparepartOutbound.outboundTime') }}</text>
<view class="drawer-date"><uni-datetime-picker v-model="inTimeFilter" type="daterange" :clear-icon="true" :start-placeholder="t('sparepartOutbound.startTime')" :end-placeholder="t('sparepartOutbound.endTime')" /></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>
<scroll-view scroll-y class="list-scroll" :scroll-top="scrollTop" @scroll="onScroll" @scrolltolower="loadMore" :lower-threshold="80">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="task-card" @click="openDetail(item)">
@ -100,15 +135,22 @@ const { t } = useI18n()
const selectedStatus = ref('')
const searchKeyword = ref('')
const showStatusDropdown = ref(false)
const filterPopupRef = ref(null)
const selectedCreator = ref(null)
const inTimeFilter = ref([])
const creatorOptions = ref([])
const creatorPanelOpen = ref(false)
const statusOptions = computed(() => [
{ label: t('functionCommon.all'), value: '' },
{ label: t('sparepartOutbound.tabPending'), value: '0' },
{ label: t('sparepartOutbound.tabAuditing'), value: '10' },
{ label: t('sparepartOutbound.approve'), value: '20' },
{ label: '已驳回', value: '1' }
])
const currentStatusLabel = computed(() => { const cur = statusOptions.value.find(i => i.value === selectedStatus.value); return cur ? cur.label : t('functionCommon.all') })
const statusPickerLabels = computed(() => [t('sparepartOutbound.allStatus'), ...statusOptions.value.map(i => i.label)])
const statusPickerIndex = computed(() => { if (selectedStatus.value === '') return 0; const idx = statusOptions.value.findIndex(i => i.value === selectedStatus.value); return idx >= 0 ? idx + 1 : 0 })
const currentStatusLabel = computed(() => { if (selectedStatus.value === '') return t('sparepartOutbound.allStatus'); const cur = statusOptions.value.find(i => i.value === selectedStatus.value); return cur ? cur.label : t('sparepartOutbound.allStatus') })
const selectedCreatorLabel = computed(() => { if (!selectedCreator.value) return t('sparepartOutbound.creator'); const found = creatorOptions.value.find(u => u.value === selectedCreator.value); return found ? found.label : t('sparepartOutbound.creator') })
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
@ -149,7 +191,8 @@ 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 params = { pageNo: pageNo.value, pageSize: pageSize.value, no: searchKeyword.value.trim() || undefined, statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined }
const inTimeRange = Array.isArray(inTimeFilter.value) ? inTimeFilter.value : []
const params = { pageNo: pageNo.value, pageSize: pageSize.value, no: searchKeyword.value.trim() || undefined, statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined, creator: selectedCreator.value || undefined, outTime: inTimeRange.length === 2 ? [inTimeRange[0] + ' 00:00:00', inTimeRange[1] + ' 23:59:59'] : undefined }
const res = await getSparepartOutboundPage(params)
const page = normalizePageData(res)
list.value = reset ? page.list : [...list.value, ...page.list]
@ -158,11 +201,15 @@ async function fetchList(reset) {
finally { loading.value = false; loadingMore.value = false }
}
function toggleStatusDropdown() { showStatusDropdown.value = !showStatusDropdown.value }
function selectStatus(item, _idx) { selectedStatus.value = item.value; showStatusDropdown.value = false; fetchList(true) }
function onStatusFilterChange(event) { const index = Number(event?.detail?.value || 0); if (index === 0) { selectedStatus.value = '' } else { const item = statusOptions.value[index - 1]; selectedStatus.value = item ? item.value : '' }; fetchList(true) }
function handleSearch() { clearSearchTimer(); uni.hideKeyboard(); fetchList(true) }
function handleKeywordInput() { clearSearchTimer(); searchTimer = setTimeout(() => fetchList(true), 300) }
function resetFilters() { clearSearchTimer(); searchKeyword.value = ''; selectedStatus.value = ''; fetchList(true) }
function resetFilters() { clearSearchTimer(); searchKeyword.value = ''; selectedStatus.value = ''; selectedCreator.value = null; inTimeFilter.value = []; fetchList(true) }
function openFilterDrawer() { loadCreatorOptions(); filterPopupRef.value?.open() }
function confirmFilterDrawer() { filterPopupRef.value?.close(); fetchList(true) }
function toggleCreatorPanel() { creatorPanelOpen.value = !creatorPanelOpen.value }
function selectCreator(item) { selectedCreator.value = selectedCreator.value === item.value ? null : item.value; creatorPanelOpen.value = false }
async function loadCreatorOptions() { if (creatorOptions.value.length) return; try { const res = await getSimpleUserList(); const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : []); creatorOptions.value = data.map(u => ({ value: u.id || u.userId, label: u.nickname || u.userName || u.name || String(u.id || '') })) } catch (e) {} }
async function loadMore() { if (loading.value || loadingMore.value || finished.value) return; pageNo.value += 1; await fetchList(false) }
function openDetail(item) { if (!item?.id) { uni.showToast({ title: t('functionCommon.noIdView'), icon: 'none' }); return }; uni.navigateTo({ url: `/pages_function/pages/sparepartOutbound/detail?id=${encodeURIComponent(String(item.id))}` }) }
@ -206,22 +253,56 @@ function clearSearchTimer() { if (searchTimer) { clearTimeout(searchTimer); sear
onShow(() => { fetchList(true) })
onUnload(() => { clearSearchTimer() })
onHide(() => { showStatusDropdown.value = false })
onHide(() => {})
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background: #f4f5f7; }
.filter-bar { display: flex; align-items: center; gap: 12rpx; padding: 18rpx 24rpx; background: #fff; }
.keyword-box { flex: 1; min-width: 0; height: 66rpx; padding: 0 28rpx; background: #f4f5f7; border: 1rpx solid #e5e7eb; border-radius: 12rpx; display: flex; align-items: center; }
.keyword-input { width: 100%; font-size: 24rpx; color: #374151; }
.status-box-wrapper { position: relative; flex-shrink: 0; }
.status-box { display: flex; align-items: center; justify-content: space-between; height: 66rpx; padding: 0 28rpx; min-width: 160rpx; background: #fff; border: 1rpx solid #e5e7eb; border-radius: 12rpx; }
.status-dropdown-panel { position: absolute; top: 74rpx; left: 0; right: 0; z-index: 200; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.12); overflow: hidden; }
.status-dropdown-item { display: flex; align-items: center; justify-content: space-between; padding: 18rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: 0; } &.active { background: #f0f5ff; } }
.status-dropdown-item-text { font-size: 26rpx; color: #374151; }
.status-dropdown-check { font-size: 26rpx; color: #2563eb; font-weight: 700; }
.status-box-text { font-size: 24rpx; color: #374151; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reset-filter-btn { height: 66rpx; line-height: 66rpx; padding: 0 28rpx; font-size: 24rpx; color: #4b5563; background: #fff; border: 1rpx solid #e5e7eb; border-radius: 12rpx; flex-shrink: 0; }
.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; }
::deep(.outbound-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-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; }
.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-picker { width: 100%; min-height: 74rpx; border: 0; border-radius: 8rpx; background: #f7f8fb; box-sizing: border-box; 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-date { width: 100%; min-height: 74rpx; border: 0; border-radius: 8rpx; background: #f7f8fb; box-sizing: border-box; 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; }
.list-scroll { height: calc(100vh - 194rpx); }
.list-wrap { padding: 0 24rpx 60rpx; }

Loading…
Cancel
Save