feat:添加产品出库模块
parent
89d1d13f6c
commit
8fdf20f4af
@ -0,0 +1,51 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
const PRODUCT_OUT_TYPE = '产品出库'
|
||||||
|
|
||||||
|
export function createProductOutbound(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin-api/erp/stock-out/create',
|
||||||
|
method: 'post',
|
||||||
|
data: { ...data, outType: PRODUCT_OUT_TYPE }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProductOutboundPage(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/admin-api/erp/stock-out/page',
|
||||||
|
method: 'get',
|
||||||
|
params: { ...params, outType: PRODUCT_OUT_TYPE }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProductOutboundDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: '/admin-api/erp/stock-out/get',
|
||||||
|
method: 'get',
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function submitProductOutbound(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin-api/erp/stock-out/submit',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function auditProductOutbound(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin-api/erp/stock-out/audit',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPalletPage(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/admin-api/erp/pallet/page',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,372 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<NavBar :title="t('productOutbound.createTitle')" />
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="detail-scroll">
|
||||||
|
<view class="content-section">
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-header">
|
||||||
|
<view class="section-icon">
|
||||||
|
<uni-icons type="compose" size="24" color="#1f7cff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="section-title">{{ t('productOutbound.outboundInfo') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.outboundTime') }}<text class="required-star">*</text></text>
|
||||||
|
<picker mode="date" :value="outboundDate" @change="handleDateChange">
|
||||||
|
<view class="select-field">
|
||||||
|
<text :class="['select-text', outboundDate ? '' : 'placeholder']">{{ outboundDate || t('productOutbound.selectOutboundTime') }}</text>
|
||||||
|
<uni-icons type="calendar" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.operator') }}<text class="required-star">*</text></text>
|
||||||
|
<view class="select-field" @click="goSelectOperator">
|
||||||
|
<text :class="['select-text', selectedOperatorName ? '' : 'placeholder']">{{ selectedOperatorName || t('productOutbound.selectOperator') }}</text>
|
||||||
|
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.remark') }}</text>
|
||||||
|
<textarea v-model="remark" class="form-textarea" :placeholder="t('productOutbound.remarkPlaceholder')" placeholder-class="placeholder-text" maxlength="500" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.attachment') }}</text>
|
||||||
|
<view class="attachment-upload" @click="handleAddAttachment">
|
||||||
|
<uni-icons type="paperclip" size="20" color="#1f7cff"></uni-icons>
|
||||||
|
<text>{{ t('productOutbound.chooseFile') }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="attachmentList.length" class="attachment-list">
|
||||||
|
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-item">
|
||||||
|
<view class="attachment-main">
|
||||||
|
<uni-icons type="paperclip" size="16" color="#64748b"></uni-icons>
|
||||||
|
<text class="attachment-name">{{ file.name }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="attachment-delete" @click="removeAttachment(idx)">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#ef4444"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-header list-header">
|
||||||
|
<view class="section-title-wrap">
|
||||||
|
<view class="section-icon">
|
||||||
|
<uni-icons type="list" size="24" color="#1f7cff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="section-title">{{ t('productOutbound.itemList') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="add-product-btn" @click="handleAddProduct">
|
||||||
|
<uni-icons type="plusempty" size="16" color="#1f7cff"></uni-icons>
|
||||||
|
<text>{{ t('productOutbound.addProduct') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="itemList.length" class="summary-strip">
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ itemList.length }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.product') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ totalPalletCount }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.pallet') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ totalPackageCount }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.packageCount') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ totalPieceCount }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.pieceCount') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="itemList.length" class="item-list">
|
||||||
|
<view v-for="(item, idx) in itemList" :key="idx" class="item-card">
|
||||||
|
<view class="image-box">
|
||||||
|
<image v-if="getItemImage(item)" :src="getItemImage(item)" class="item-image" mode="aspectFill" />
|
||||||
|
<uni-icons v-else type="image" size="34" color="#cbd5e1"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<view class="item-info">
|
||||||
|
<view class="item-header">
|
||||||
|
<text class="item-name">{{ textValue(item.productName) }}</text>
|
||||||
|
<view class="delete-btn" @click="removeItem(idx)">
|
||||||
|
<uni-icons type="trash" size="18" color="#ef4444"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.outboundPackageCount') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.packageCount) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.outboundPieceCount') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.count) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.palletQuantity') }}</text>
|
||||||
|
<text class="info-value">{{ getPalletCount(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.outMode') }}</text>
|
||||||
|
<text class="info-value">{{ outModeText(item.outMode) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card" @click="handleAddProduct">
|
||||||
|
<uni-icons type="plusempty" size="30" color="#94a3b8"></uni-icons>
|
||||||
|
<text>{{ t('productOutbound.emptyAddProduct') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="action-bar">
|
||||||
|
<view class="action-btn back-btn" @click="handleCancel">{{ t('productOutbound.cancel') }}</view>
|
||||||
|
<view class="action-btn submit-btn" @click="handleSubmit">{{ t('productOutbound.confirmOutbound') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import NavBar from '@/components/common/NavBar.vue'
|
||||||
|
import { createProductOutbound } from '@/api/mes/productOutbound'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const itemList = ref([])
|
||||||
|
const outboundDate = ref(formatDate(new Date()))
|
||||||
|
const selectedOperatorId = ref(null)
|
||||||
|
const selectedOperatorName = ref('')
|
||||||
|
const remark = ref('')
|
||||||
|
const attachmentList = ref([])
|
||||||
|
|
||||||
|
const totalPalletCount = computed(() => itemList.value.reduce((sum, item) => sum + getPalletCount(item), 0))
|
||||||
|
const totalPackageCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(item.packageCount) || 0), 0))
|
||||||
|
const totalPieceCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(item.count) || 0), 0))
|
||||||
|
|
||||||
|
function formatDate(date) {
|
||||||
|
const y = date.getFullYear()
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const d = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${y}-${m}-${d}`
|
||||||
|
}
|
||||||
|
function textValue(v) {
|
||||||
|
if (v === 0) return '0'
|
||||||
|
if (v == null) return '-'
|
||||||
|
const s = String(v).trim()
|
||||||
|
return s || '-'
|
||||||
|
}
|
||||||
|
function handleDateChange(e) {
|
||||||
|
outboundDate.value = e.detail.value
|
||||||
|
}
|
||||||
|
function handleAddProduct() {
|
||||||
|
uni.navigateTo({ url: '/pages_function/pages/productOutbound/productConfirm' })
|
||||||
|
}
|
||||||
|
function goSelectOperator() {
|
||||||
|
getApp().globalData._productOutboundUserFrom = 'outbound'
|
||||||
|
uni.navigateTo({ url: '/pages_function/pages/moldRepair/userSelect?field=operator&from=productOutbound' })
|
||||||
|
}
|
||||||
|
function outModeText(value) {
|
||||||
|
if (Number(value) === 1) return t('productOutbound.outModeWholePallet')
|
||||||
|
if (Number(value) === 2) return t('productOutbound.outModeSplitPallet')
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
function getItemImage(item) {
|
||||||
|
const images = item.images || item._product?.images
|
||||||
|
if (!images) return ''
|
||||||
|
if (Array.isArray(images)) return String(images[0] || '')
|
||||||
|
return String(images).split(',')[0]?.trim() || ''
|
||||||
|
}
|
||||||
|
function getPalletCount(item) {
|
||||||
|
return Array.isArray(item.pallets) ? item.pallets.length : 0
|
||||||
|
}
|
||||||
|
function removeItem(idx) {
|
||||||
|
itemList.value.splice(idx, 1)
|
||||||
|
getApp().globalData._productOutboundItems = [...itemList.value]
|
||||||
|
}
|
||||||
|
function handleCancel() {
|
||||||
|
getApp().globalData._productOutboundItems = []
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
function handleAddAttachment() {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
plus.gallery.pick(
|
||||||
|
(res) => {
|
||||||
|
const files = (res?.files || []).map((f) => ({
|
||||||
|
name: f.name || 'unknown',
|
||||||
|
size: f.size || 0,
|
||||||
|
path: f
|
||||||
|
}))
|
||||||
|
attachmentList.value.push(...files)
|
||||||
|
},
|
||||||
|
() => {},
|
||||||
|
{ filter: 'all', multiple: true, maximum: 9 - attachmentList.value.length }
|
||||||
|
)
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 9 - attachmentList.value.length,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
res.tempFilePaths.forEach((path) => {
|
||||||
|
const parts = path.split('/')
|
||||||
|
attachmentList.value.push({ name: parts[parts.length - 1] || 'image.jpg', size: 0, path })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
function removeAttachment(idx) {
|
||||||
|
attachmentList.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
function buildOutTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const [y, m, d] = outboundDate.value.split('-').map(Number)
|
||||||
|
return new Date(y, m - 1, d, now.getHours(), now.getMinutes(), now.getSeconds()).getTime()
|
||||||
|
}
|
||||||
|
function normalizeItemsForSubmit() {
|
||||||
|
return itemList.value.map((item) => ({
|
||||||
|
warehouseId: item.warehouseId,
|
||||||
|
areaId: item.areaId,
|
||||||
|
productId: item.productId,
|
||||||
|
productName: item.productName,
|
||||||
|
productBarCode: item.productBarCode,
|
||||||
|
productUnitName: item.productUnitName,
|
||||||
|
inputUnitType: item.inputUnitType || '个',
|
||||||
|
inputCount: item.inputCount,
|
||||||
|
count: item.count,
|
||||||
|
outMode: item.outMode,
|
||||||
|
palletCount: item.palletCount,
|
||||||
|
packageCount: item.packageCount,
|
||||||
|
pieceCount: item.pieceCount,
|
||||||
|
packagingSchemeRelationId: item.packagingSchemeRelationId,
|
||||||
|
packagingSchemeId: item.packagingSchemeId,
|
||||||
|
packagingSchemeName: item.packagingSchemeName,
|
||||||
|
packageQuantity: item.packageQuantity,
|
||||||
|
palletPackageQuantity: item.palletPackageQuantity,
|
||||||
|
palletTotalQuantity: item.palletTotalQuantity,
|
||||||
|
palletCode: item.palletCode,
|
||||||
|
remark: item.remark,
|
||||||
|
pallets: Array.isArray(item.pallets) ? item.pallets : []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!itemList.value.length) {
|
||||||
|
uni.showToast({ title: t('productOutbound.addProductFirst'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!outboundDate.value) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectOutboundTime'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedOperatorId.value) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectOperator'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const invalid = itemList.value.find((item) => !item.productId || !Array.isArray(item.pallets) || item.pallets.length === 0 || !Number(item.count))
|
||||||
|
if (invalid) {
|
||||||
|
uni.showToast({ title: t('productOutbound.completeProductPalletInfo'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const items = normalizeItemsForSubmit()
|
||||||
|
const totalCount = items.reduce((sum, item) => sum + (Number(item.count) || 0), 0)
|
||||||
|
const submitData = {
|
||||||
|
isCode: true,
|
||||||
|
outTime: buildOutTime(),
|
||||||
|
outType: '产品出库',
|
||||||
|
stockUserId: String(selectedOperatorId.value),
|
||||||
|
status: 0,
|
||||||
|
totalCount,
|
||||||
|
totalPrice: 0,
|
||||||
|
remark: remark.value,
|
||||||
|
items
|
||||||
|
}
|
||||||
|
if (attachmentList.value.length) {
|
||||||
|
submitData.fileUrl = attachmentList.value.map((f) => f.path || f).join(',')
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: t('productOutbound.submitting'), mask: true })
|
||||||
|
try {
|
||||||
|
await createProductOutbound(submitData)
|
||||||
|
uni.hideLoading()
|
||||||
|
getApp().globalData._productOutboundItems = []
|
||||||
|
uni.showToast({ title: t('productOutbound.outboundSuccess'), icon: 'success' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 1200)
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
const msg = e?.message || e?.data?.msg || e?.response?.data?.msg || t('productOutbound.saveFailed')
|
||||||
|
uni.showToast({ title: String(msg).substring(0, 50), icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
const items = getApp().globalData?._productOutboundItems
|
||||||
|
if (Array.isArray(items)) itemList.value = [...items]
|
||||||
|
const userResult = getApp().globalData?._productOutboundUserSelectResult
|
||||||
|
if (userResult) {
|
||||||
|
selectedOperatorId.value = userResult.user.id
|
||||||
|
selectedOperatorName.value = userResult.user.nickname || userResult.user.userName || userResult.user.name || ''
|
||||||
|
getApp().globalData._productOutboundUserSelectResult = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container { min-height: 100vh; background: #f5f7fb; }
|
||||||
|
.detail-scroll { height: calc(100vh - 172rpx); }
|
||||||
|
.content-section { padding: 20rpx 24rpx 28rpx; }
|
||||||
|
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||||
|
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
|
||||||
|
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
|
||||||
|
.section-title-wrap { display: flex; align-items: center; gap: 12rpx; min-width: 0; }
|
||||||
|
.list-header { justify-content: space-between; gap: 16rpx; }
|
||||||
|
.form-field { display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
|
.form-field + .form-field { margin-top: 24rpx; }
|
||||||
|
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
|
||||||
|
.required-star { color: #ef4444; font-size: 28rpx; margin-left: 4rpx; }
|
||||||
|
.select-field { display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
|
||||||
|
.select-text { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.placeholder, .placeholder-text { color: #9ca3af; }
|
||||||
|
.form-textarea { width: 100%; min-height: 120rpx; background: #f8fafc; border-radius: 14rpx; padding: 18rpx 24rpx; font-size: 28rpx; color: #374151; box-sizing: border-box; }
|
||||||
|
.attachment-upload { height: 76rpx; border-radius: 14rpx; border: 1rpx dashed #bfdbfe; background: #eff6ff; color: #1f7cff; display: flex; align-items: center; justify-content: center; gap: 10rpx; font-size: 26rpx; font-weight: 600; }
|
||||||
|
.attachment-list { margin-top: 12rpx; display: flex; flex-direction: column; gap: 10rpx; }
|
||||||
|
.attachment-item { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; padding: 16rpx 18rpx; background: #f8fafc; border-radius: 12rpx; }
|
||||||
|
.attachment-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10rpx; }
|
||||||
|
.attachment-name { flex: 1; min-width: 0; font-size: 24rpx; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.attachment-delete { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.add-product-btn { height: 60rpx; padding: 0 18rpx; border-radius: 999rpx; border: 1rpx solid #bfdbfe; background: #eff6ff; color: #1f7cff; font-size: 24rpx; font-weight: 600; display: flex; align-items: center; gap: 8rpx; flex-shrink: 0; }
|
||||||
|
.summary-strip { display: grid; grid-template-columns: repeat(4, 1fr); background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 16rpx; overflow: hidden; margin-bottom: 18rpx; }
|
||||||
|
.summary-item { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 6rpx; padding: 16rpx 8rpx; border-right: 1rpx solid #eef2f7; }
|
||||||
|
.summary-item:last-child { border-right: 0; }
|
||||||
|
.summary-value { max-width: 100%; font-size: 30rpx; font-weight: 700; color: #1f4b79; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.summary-label { font-size: 22rpx; color: #8a94a6; }
|
||||||
|
.item-list { display: flex; flex-direction: column; gap: 18rpx; }
|
||||||
|
.item-card { display: flex; gap: 18rpx; padding: 20rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 18rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||||
|
.image-box { width: 128rpx; height: 128rpx; border-radius: 16rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; }
|
||||||
|
.item-image { width: 100%; height: 100%; }
|
||||||
|
.item-info { flex: 1; min-width: 0; }
|
||||||
|
.item-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 14rpx; }
|
||||||
|
.item-name { flex: 1; min-width: 0; font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.delete-btn { width: 48rpx; height: 48rpx; border-radius: 24rpx; background: #fef2f2; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
|
||||||
|
.info-cell { min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
|
||||||
|
.info-label { font-size: 22rpx; color: #9ca3af; }
|
||||||
|
.info-value { font-size: 26rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.empty-card { min-height: 220rpx; border: 2rpx dashed #d7dde8; border-radius: 18rpx; background: #f8fafc; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14rpx; color: #94a3b8; font-size: 27rpx; }
|
||||||
|
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99; }
|
||||||
|
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
|
||||||
|
.back-btn { background: #eef2f7; color: #475569; }
|
||||||
|
.submit-btn { background: #1f4b79; color: #ffffff; }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<NavBar :title="t('productOutbound.detailTitle')" />
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="detail-scroll">
|
||||||
|
<view class="content-section">
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-header">
|
||||||
|
<view class="section-icon">
|
||||||
|
<uni-icons type="compose" size="24" color="#1f7cff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="section-title">{{ t('productOutbound.outboundInfo') }}</text>
|
||||||
|
<text :class="['status-tag', statusClass(detail.status)]">{{ statusText(detail.status) }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="loading-card">{{ t('productOutbound.loading') }}</view>
|
||||||
|
<template v-else>
|
||||||
|
<view class="readonly-grid">
|
||||||
|
<view class="readonly-item">
|
||||||
|
<text class="readonly-label">{{ t('productOutbound.documentNo') }}</text>
|
||||||
|
<text class="readonly-value">{{ textValue(detail.no) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="readonly-item">
|
||||||
|
<text class="readonly-label">{{ t('productOutbound.outboundType') }}</text>
|
||||||
|
<text class="readonly-value">{{ textValue(detail.inType) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="readonly-item">
|
||||||
|
<text class="readonly-label">{{ t('productOutbound.outboundTime') }}</text>
|
||||||
|
<text class="readonly-value">{{ formatDateTime(detail.outTime || detail.createTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="readonly-item">
|
||||||
|
<text class="readonly-label">{{ t('productOutbound.operator') }}</text>
|
||||||
|
<text class="readonly-value">{{ textValue(getStockUserName(detail)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="readonly-item">
|
||||||
|
<text class="readonly-label">{{ t('productOutbound.outboundQuantity') }}</text>
|
||||||
|
<text class="readonly-value highlight">{{ textValue(detail.totalCount) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="readonly-item">
|
||||||
|
<text class="readonly-label">{{ t('productOutbound.reviewer') }}</text>
|
||||||
|
<text class="readonly-value">{{ textValue(detail.auditUserName) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.remark') }}</text>
|
||||||
|
<view class="readonly-box">{{ textValue(detail.remark) }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="attachmentList.length" class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.attachment') }}</text>
|
||||||
|
<view class="attachment-list">
|
||||||
|
<view v-for="(file, idx) in attachmentList" :key="idx" class="attachment-item">
|
||||||
|
<uni-icons type="paperclip" size="16" color="#64748b"></uni-icons>
|
||||||
|
<text class="attachment-name">{{ textValue(getAttachmentName(file, idx)) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-header list-header">
|
||||||
|
<view class="section-title-wrap">
|
||||||
|
<view class="section-icon">
|
||||||
|
<uni-icons type="list" size="24" color="#1f7cff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="section-title">{{ t('productOutbound.itemList') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="itemList.length" class="summary-strip">
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ itemList.length }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.product') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ totalPalletCount }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.pallet') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ totalInputCount }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.packageCount') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-item">
|
||||||
|
<text class="summary-value">{{ totalPieceCount }}</text>
|
||||||
|
<text class="summary-label">{{ t('productOutbound.pieceCount') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="itemList.length" class="item-list">
|
||||||
|
<view v-for="(item, idx) in itemList" :key="item.id || idx" class="item-card">
|
||||||
|
<view class="item-header">
|
||||||
|
<text class="item-name">{{ textValue(item.productName) }}</text>
|
||||||
|
<text class="item-code">{{ textValue(item.productBarCode || item.productCode) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.outboundPackageCount') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(getItemInputCount(item)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.outboundPieceCount') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(getItemPieceCount(item)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.outMode') }}</text>
|
||||||
|
<text class="info-value">{{ outModeText(item.outMode) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.packagingScheme') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.packagingSchemeName) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.palletPackageQuantity') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.palletPackageQuantity) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.packageQuantity') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.packageQuantity) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="getPallets(item).length" class="pallet-list">
|
||||||
|
<view v-for="(pallet, pIndex) in getPallets(item)" :key="pallet.palletId || pallet.id || pIndex" class="pallet-card">
|
||||||
|
<view class="pallet-top">
|
||||||
|
<text class="pallet-code">{{ textValue(getPalletCode(pallet)) }}</text>
|
||||||
|
<text class="pallet-count">{{ t('productOutbound.packageUnit', { count: textValue(pallet.packageCount) }) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="pallet-grid">
|
||||||
|
<view class="pallet-cell">
|
||||||
|
<text class="pallet-label">{{ t('productOutbound.warehouse') }}</text>
|
||||||
|
<text class="pallet-value">{{ textValue(getWarehouseDisplay(pallet, item)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="pallet-cell">
|
||||||
|
<text class="pallet-label">{{ t('productOutbound.location') }}</text>
|
||||||
|
<text class="pallet-value">{{ textValue(getAreaDisplay(pallet, item)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="pallet-cell">
|
||||||
|
<text class="pallet-label">{{ t('productOutbound.pieceCount') }}</text>
|
||||||
|
<text class="pallet-value">{{ textValue(getPalletPieceCount(pallet, item)) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card">{{ loading ? t('productOutbound.loading') : t('productOutbound.emptyItemList') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="action-bar">
|
||||||
|
<view class="action-btn back-btn" @click="handleBack">{{ t('productOutbound.back') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import NavBar from '@/components/common/NavBar.vue'
|
||||||
|
import { getProductOutboundDetail } from '@/api/mes/productOutbound'
|
||||||
|
import { getWarehouseAreaSimpleList, getWarehouseSimpleList, getSimpleUserList } from '@/api/mes/moldget'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const detailId = ref(null)
|
||||||
|
const detail = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
const userOptions = ref([])
|
||||||
|
const warehouseOptions = ref([])
|
||||||
|
const warehouseAreaMap = ref({})
|
||||||
|
|
||||||
|
const itemList = computed(() => normalizeItems(detail.value))
|
||||||
|
const attachmentList = computed(() => normalizeAttachments(detail.value))
|
||||||
|
const totalPalletCount = computed(() => itemList.value.reduce((sum, item) => sum + getPallets(item).length, 0))
|
||||||
|
const totalInputCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(getItemInputCount(item)) || 0), 0))
|
||||||
|
const totalPieceCount = computed(() => itemList.value.reduce((sum, item) => sum + (Number(getItemPieceCount(item)) || 0), 0))
|
||||||
|
|
||||||
|
function normalizeResponse(res) {
|
||||||
|
return res && res.data !== undefined ? res.data : res
|
||||||
|
}
|
||||||
|
function normalizeItems(row) {
|
||||||
|
const list = row?.items || row?.itemList || row?.stockOutItems || row?.stockOutItemList || []
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
}
|
||||||
|
function normalizeAttachments(row) {
|
||||||
|
const raw = row?.fileUrl || row?.attachments || row?.attachmentList || row?.files
|
||||||
|
if (!raw) return []
|
||||||
|
if (Array.isArray(raw)) return raw
|
||||||
|
return String(raw).split(',').map((item) => item.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
function textValue(v) {
|
||||||
|
if (v === 0) return '0'
|
||||||
|
if (v == null) return '-'
|
||||||
|
const s = String(v).trim()
|
||||||
|
return s || '-'
|
||||||
|
}
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) return '-'
|
||||||
|
if (Array.isArray(value) && value.length >= 3) {
|
||||||
|
const [year, month, day] = value
|
||||||
|
return `${year}-${pad(month)}-${pad(day)}`
|
||||||
|
}
|
||||||
|
const raw = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
|
||||||
|
const date = new Date(raw)
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value)
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
|
}
|
||||||
|
function pad(n) {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
function statusText(s) {
|
||||||
|
const map = {
|
||||||
|
0: t('productOutbound.statusPending'),
|
||||||
|
10: t('productOutbound.statusAuditing'),
|
||||||
|
20: t('productOutbound.statusStored'),
|
||||||
|
1: t('productOutbound.statusRejected')
|
||||||
|
}
|
||||||
|
const num = Number(s)
|
||||||
|
return map[num] || textValue(s)
|
||||||
|
}
|
||||||
|
function statusClass(s) {
|
||||||
|
const num = Number(s)
|
||||||
|
if (num === 0) return 'text-primary'
|
||||||
|
if (num === 10) return 'text-warning'
|
||||||
|
if (num === 20) return 'text-success'
|
||||||
|
if (num === 1) return 'text-danger'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
function getStockUserName(row) {
|
||||||
|
if (row?.stockUserName) return row.stockUserName
|
||||||
|
const userId = row?.stockUserId
|
||||||
|
return userOptions.value.find((item) => String(item.value) === String(userId))?.label || userId
|
||||||
|
}
|
||||||
|
function getAttachmentName(file, index) {
|
||||||
|
if (file?.name) return file.name
|
||||||
|
const text = String(file?.url || file?.path || file || '')
|
||||||
|
const name = text.split('/').pop()
|
||||||
|
return name || `${t('productOutbound.attachment')}${index + 1}`
|
||||||
|
}
|
||||||
|
function outModeText(value) {
|
||||||
|
if (Number(value) === 1) return t('productOutbound.outModeWholePallet')
|
||||||
|
if (Number(value) === 2) return t('productOutbound.outModeSplitPallet')
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
function getItemInputCount(item) {
|
||||||
|
return item?.packageCount ?? item?.palletCount ?? item?.inputCount ?? item?.count
|
||||||
|
}
|
||||||
|
function getItemPieceCount(item) {
|
||||||
|
return item?.count ?? ((Number(item?.inputCount) || 0) * (Number(item?.packageQuantity) || 1))
|
||||||
|
}
|
||||||
|
function getPallets(item) {
|
||||||
|
if (Array.isArray(item?.pallets)) return item.pallets
|
||||||
|
if (item?.palletId || item?.palletCode) {
|
||||||
|
return [{
|
||||||
|
palletId: item.palletId,
|
||||||
|
palletCode: item.palletCode,
|
||||||
|
packageCount: item.packageCount ?? item.inputCount,
|
||||||
|
warehouseId: item.warehouseId,
|
||||||
|
warehouseName: item.warehouseName,
|
||||||
|
areaId: item.areaId,
|
||||||
|
areaName: item.areaName,
|
||||||
|
count: item.count
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
function getPalletCode(pallet) {
|
||||||
|
return pallet?.palletCode || pallet?.code || pallet?.pallet?.code || pallet?.palletId || ''
|
||||||
|
}
|
||||||
|
function getPalletPieceCount(pallet, item) {
|
||||||
|
return pallet?.count ?? ((Number(pallet?.packageCount) || 0) * (Number(item?.packageQuantity) || 1))
|
||||||
|
}
|
||||||
|
function getWarehouseDisplay(pallet, item) {
|
||||||
|
const id = pallet?.warehouseId ?? item?.warehouseId
|
||||||
|
return pallet?.warehouseName || item?.warehouseName || warehouseOptions.value.find((w) => String(w.value) === String(id))?.label || id
|
||||||
|
}
|
||||||
|
function getAreaDisplay(pallet, item) {
|
||||||
|
const warehouseId = pallet?.warehouseId ?? item?.warehouseId
|
||||||
|
const areaId = pallet?.areaId ?? item?.areaId
|
||||||
|
const areas = warehouseAreaMap.value[String(warehouseId)] || []
|
||||||
|
return pallet?.areaName || item?.areaName || areas.find((a) => String(a.value) === String(areaId))?.label || areaId
|
||||||
|
}
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const res = await getSimpleUserList()
|
||||||
|
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
|
||||||
|
userOptions.value = data.map((u) => ({
|
||||||
|
value: u.id || u.userId,
|
||||||
|
label: u.nickname || u.userName || u.name || String(u.id || '')
|
||||||
|
}))
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
async function loadWarehouses() {
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseSimpleList()
|
||||||
|
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
|
||||||
|
warehouseOptions.value = data.map((w) => ({ value: w.id, label: w.name || String(w.id || '') }))
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
async function loadAreasForWarehouse(warehouseId) {
|
||||||
|
if (!warehouseId || warehouseAreaMap.value[String(warehouseId)]) return
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseAreaSimpleList(warehouseId)
|
||||||
|
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
|
||||||
|
warehouseAreaMap.value = {
|
||||||
|
...warehouseAreaMap.value,
|
||||||
|
[String(warehouseId)]: data.map((a) => ({ value: a.id, label: a.name || a.areaName || String(a.id || '') }))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
warehouseAreaMap.value = { ...warehouseAreaMap.value, [String(warehouseId)]: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadAreasForDetail() {
|
||||||
|
const ids = new Set()
|
||||||
|
itemList.value.forEach((item) => {
|
||||||
|
if (item.warehouseId) ids.add(item.warehouseId)
|
||||||
|
getPallets(item).forEach((pallet) => {
|
||||||
|
const id = pallet.warehouseId ?? item.warehouseId
|
||||||
|
if (id) ids.add(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await Promise.all(Array.from(ids).map((id) => loadAreasForWarehouse(id)))
|
||||||
|
}
|
||||||
|
async function loadDetail() {
|
||||||
|
if (!detailId.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductOutboundDetail(detailId.value)
|
||||||
|
detail.value = normalizeResponse(res) || {}
|
||||||
|
await loadAreasForDetail()
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: t('productOutbound.detailLoadFailed'), icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleBack() {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(async (options) => {
|
||||||
|
detailId.value = options?.id || null
|
||||||
|
await Promise.all([loadUsers(), loadWarehouses()])
|
||||||
|
await loadDetail()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container { min-height: 100vh; background: #f5f7fb; }
|
||||||
|
.detail-scroll { height: calc(100vh - 172rpx); }
|
||||||
|
.content-section { padding: 20rpx 24rpx 28rpx; }
|
||||||
|
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||||
|
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
|
||||||
|
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
|
||||||
|
.section-title-wrap { display: flex; align-items: center; gap: 12rpx; min-width: 0; }
|
||||||
|
.list-header { justify-content: space-between; gap: 16rpx; }
|
||||||
|
.status-tag { margin-left: auto; flex-shrink: 0; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; line-height: 1; background: #e2e8f0; color: #64748b; }
|
||||||
|
.status-tag.text-success { color: #15803d; background: #dcfce7; }
|
||||||
|
.status-tag.text-danger { color: #dc2626; background: #fee2e2; }
|
||||||
|
.status-tag.text-warning { color: #d97706; background: #fef3c7; }
|
||||||
|
.status-tag.text-primary { color: #1d4ed8; background: #dbeafe; }
|
||||||
|
.loading-card, .empty-card { min-height: 180rpx; border: 2rpx dashed #d7dde8; border-radius: 18rpx; background: #f8fafc; display: flex; align-items: center; justify-content: center; color: #94a3b8; font-size: 27rpx; }
|
||||||
|
.readonly-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 14rpx; overflow: hidden; }
|
||||||
|
.readonly-item { min-width: 0; display: flex; flex-direction: column; gap: 8rpx; padding: 18rpx 20rpx; border-right: 1rpx solid #f1f5f9; border-bottom: 1rpx solid #f1f5f9; }
|
||||||
|
.readonly-item:nth-child(2n) { border-right: 0; }
|
||||||
|
.readonly-label { font-size: 23rpx; color: #8a94a6; }
|
||||||
|
.readonly-value { font-size: 27rpx; color: #334155; line-height: 1.35; word-break: break-all; }
|
||||||
|
.readonly-value.highlight { color: #1f4b79; font-weight: 700; }
|
||||||
|
.form-field { display: flex; flex-direction: column; gap: 12rpx; margin-top: 24rpx; }
|
||||||
|
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
|
||||||
|
.readonly-box { min-height: var(--app-form-control-height, 70rpx); padding: 18rpx 24rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; font-size: 28rpx; color: #374151; line-height: 1.45; }
|
||||||
|
.attachment-list { display: flex; flex-direction: column; gap: 10rpx; }
|
||||||
|
.attachment-item { display: flex; align-items: center; gap: 10rpx; padding: 16rpx 18rpx; background: #f8fafc; border-radius: 12rpx; }
|
||||||
|
.attachment-name { flex: 1; min-width: 0; font-size: 24rpx; color: #334155; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.summary-strip { display: grid; grid-template-columns: repeat(4, 1fr); background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 16rpx; overflow: hidden; margin-bottom: 18rpx; }
|
||||||
|
.summary-item { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 6rpx; padding: 16rpx 8rpx; border-right: 1rpx solid #eef2f7; }
|
||||||
|
.summary-item:last-child { border-right: 0; }
|
||||||
|
.summary-value { max-width: 100%; font-size: 30rpx; font-weight: 700; color: #1f4b79; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.summary-label { font-size: 22rpx; color: #8a94a6; }
|
||||||
|
.item-list { display: flex; flex-direction: column; gap: 18rpx; }
|
||||||
|
.item-card { padding: 20rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 18rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||||
|
.item-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 14rpx; }
|
||||||
|
.item-name { flex: 1; min-width: 0; font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.item-code { flex-shrink: 0; max-width: 240rpx; font-size: 24rpx; color: #8a94a6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
|
||||||
|
.info-cell { min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
|
||||||
|
.info-label { font-size: 22rpx; color: #9ca3af; }
|
||||||
|
.info-value { font-size: 26rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.pallet-list { margin-top: 18rpx; display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
|
.pallet-card { padding: 16rpx 18rpx; background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 14rpx; }
|
||||||
|
.pallet-top { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; margin-bottom: 12rpx; }
|
||||||
|
.pallet-code { flex: 1; min-width: 0; font-size: 27rpx; color: #1f2937; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.pallet-count { flex-shrink: 0; font-size: 24rpx; color: #1f4b79; font-weight: 600; }
|
||||||
|
.pallet-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10rpx; }
|
||||||
|
.pallet-cell { min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
|
||||||
|
.pallet-label { font-size: 21rpx; color: #9ca3af; }
|
||||||
|
.pallet-value { font-size: 24rpx; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99; }
|
||||||
|
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
|
||||||
|
.back-btn { background: #1f4b79; color: #ffffff; }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,485 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<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>
|
||||||
|
<view class="status-box" @click="openStatusPicker">
|
||||||
|
<text class="status-box-text">{{ selectedStatusLabel || t('productOutbound.all') }}</text>
|
||||||
|
<uni-icons type="bottom" size="14" color="#9ca3af" />
|
||||||
|
</view>
|
||||||
|
<view class="reset-filter-btn" @click="resetFilters">{{ t('productOutbound.reset') }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="list-scroll"
|
||||||
|
:scroll-top="scrollTop"
|
||||||
|
:lower-threshold="80"
|
||||||
|
@scroll="onScroll"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<view class="list-wrap">
|
||||||
|
<view v-for="item in list" :key="item.id" class="task-card" @click="openDetail(item)">
|
||||||
|
<view class="card-header">
|
||||||
|
<view class="header-main">
|
||||||
|
<text class="task-no">{{ textValue(item.no) }}</text>
|
||||||
|
<text :class="['record-tag', statusClass(item.status)]">{{ statusText(item.status) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-body">
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">{{ t('productOutbound.productInfo') }}</text>
|
||||||
|
<text class="value">{{ textValue(item.productNames || item.productName) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">{{ t('productOutbound.outboundTime') }}</text>
|
||||||
|
<text class="value">{{ formatDateTime(item.outTime || item.createTime) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">{{ t('productOutbound.operator') }}</text>
|
||||||
|
<text class="value">{{ textValue(item.stockUserName || item.creatorName || item.creator) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">{{ t('productOutbound.outboundQuantity') }}</text>
|
||||||
|
<text class="value highlight">{{ textValue(item.totalCount) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="row">
|
||||||
|
<text class="label">{{ t('productOutbound.reviewer') }}</text>
|
||||||
|
<text class="value">{{ textValue(item.auditUserName) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="Number(item.status) === 0 || Number(item.status) === 1" class="card-actions">
|
||||||
|
<view class="action-btn submit-btn" @click.stop="openSubmitAudit(item)">{{ t('productOutbound.submitAudit') }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="Number(item.status) === 10" class="card-actions">
|
||||||
|
<view class="action-btn approve-btn" @click.stop="handleApprove(item)">{{ t('productOutbound.auditPass') }}</view>
|
||||||
|
<view class="action-btn reject-btn" @click.stop="handleReject(item)">{{ t('productOutbound.auditReject') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading && pageNo === 1" class="hint">{{ t('productOutbound.loading') }}</view>
|
||||||
|
<view v-else-if="!list.length" class="hint">{{ t('productOutbound.emptyOutboundList') }}</view>
|
||||||
|
<view v-else-if="loadingMore" class="hint">{{ t('productOutbound.loadingMore') }}</view>
|
||||||
|
<view v-else-if="finished" class="hint">{{ t('productOutbound.noMoreData') }}</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
|
||||||
|
<uni-icons type="arrow-up" size="20" color="#1f4b79" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="add-btn" @click="goAdd">
|
||||||
|
<text class="add-icon">+</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showAuditModal" class="modal-overlay" @click="closeAuditModal">
|
||||||
|
<view class="modal-card" @click.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">{{ t('productOutbound.submitAudit') }}</text>
|
||||||
|
<text class="modal-close" @click="closeAuditModal">x</text>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<view class="modal-field">
|
||||||
|
<text class="modal-label"><text class="required">*</text>{{ t('productOutbound.auditor') }}</text>
|
||||||
|
<view class="modal-select-field" @click="goSelectAuditor">
|
||||||
|
<text :class="['modal-select-text', selectedAuditor ? '' : 'placeholder']">{{ selectedAuditor ? selectedAuditor.label : t('productOutbound.choose') }}</text>
|
||||||
|
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-field">
|
||||||
|
<text class="modal-label">{{ t('productOutbound.remark') }}</text>
|
||||||
|
<textarea v-model="auditRemark" class="modal-textarea" :placeholder="t('productOutbound.remarkPlaceholder')" placeholder-class="textarea-placeholder" :maxlength="200" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="modal-btn cancel-btn" @click="closeAuditModal">{{ t('productOutbound.cancel') }}</view>
|
||||||
|
<view class="modal-btn confirm-btn" @click="confirmSubmitAudit">{{ t('productOutbound.submit') }}</view>
|
||||||
|
</view>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
|
|
||||||
|
<sv-focus-no-keyboard ref="focusNoKeyboardRef"></sv-focus-no-keyboard>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, ref } from 'vue'
|
||||||
|
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'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const selectedStatus = ref('')
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const statusPickerRef = ref(null)
|
||||||
|
const statusOptions = computed(() => [
|
||||||
|
{ label: t('productOutbound.all'), value: '' },
|
||||||
|
{ label: t('productOutbound.statusPending'), value: '0' },
|
||||||
|
{ label: t('productOutbound.statusAuditing'), value: '10' },
|
||||||
|
{ label: t('productOutbound.statusStored'), value: '20' },
|
||||||
|
{ label: t('productOutbound.statusRejected'), value: '1' }
|
||||||
|
])
|
||||||
|
const selectedStatusLabel = computed(() => {
|
||||||
|
const current = statusOptions.value.find((item) => item.value === selectedStatus.value)
|
||||||
|
return current ? current.label : ''
|
||||||
|
})
|
||||||
|
const list = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const pageNo = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
const showGoTop = ref(false)
|
||||||
|
const keywordFocus = ref(false)
|
||||||
|
const focusNoKeyboardRef = ref(null)
|
||||||
|
const keywordInputSelector = '#product-outbound-keyword-input input, input#product-outbound-keyword-input'
|
||||||
|
let searchTimer = null
|
||||||
|
|
||||||
|
function textValue(v) {
|
||||||
|
if (v === 0) return '0'
|
||||||
|
if (v == null) return '-'
|
||||||
|
const s = String(v).trim()
|
||||||
|
return s || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
if (!value) return '-'
|
||||||
|
if (Array.isArray(value) && value.length >= 3) {
|
||||||
|
const [year, month, day] = value
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return `${year}-${pad(month)}-${pad(day)}`
|
||||||
|
}
|
||||||
|
const raw = typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value
|
||||||
|
const date = new Date(raw)
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value)
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusText(s) {
|
||||||
|
const map = {
|
||||||
|
0: t('productOutbound.statusPending'),
|
||||||
|
10: t('productOutbound.statusAuditing'),
|
||||||
|
20: t('productOutbound.statusStored'),
|
||||||
|
1: t('productOutbound.statusRejected')
|
||||||
|
}
|
||||||
|
const num = Number(s)
|
||||||
|
return map[num] || textValue(s)
|
||||||
|
}
|
||||||
|
function statusClass(s) {
|
||||||
|
const num = Number(s)
|
||||||
|
if (num === 0) return 'text-primary'
|
||||||
|
if (num === 10) return 'text-warning'
|
||||||
|
if (num === 20) return 'text-success'
|
||||||
|
if (num === 1) return 'text-danger'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePageData(res) {
|
||||||
|
const root = res && res.data !== undefined ? res.data : res
|
||||||
|
const candidateList = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || []
|
||||||
|
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
|
||||||
|
return {
|
||||||
|
list: Array.isArray(candidateList) ? candidateList : [],
|
||||||
|
total: Number(candidateTotal || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchList(reset) {
|
||||||
|
if (reset) {
|
||||||
|
pageNo.value = 1
|
||||||
|
finished.value = false
|
||||||
|
}
|
||||||
|
if (pageNo.value === 1) loading.value = true
|
||||||
|
else loadingMore.value = true
|
||||||
|
try {
|
||||||
|
const keyword = searchKeyword.value.trim()
|
||||||
|
const res = await getProductOutboundPage({
|
||||||
|
pageNo: pageNo.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
no: keyword || undefined,
|
||||||
|
statusList: selectedStatus.value !== '' ? [Number(selectedStatus.value)] : undefined
|
||||||
|
})
|
||||||
|
const page = normalizePageData(res)
|
||||||
|
list.value = reset ? page.list : [...list.value, ...page.list]
|
||||||
|
finished.value = list.value.length >= page.total || page.list.length < pageSize.value
|
||||||
|
} catch (e) {
|
||||||
|
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
|
||||||
|
uni.showToast({ title: t('productOutbound.loadFailed'), icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearch() {
|
||||||
|
clearSearchTimer()
|
||||||
|
uni.hideKeyboard()
|
||||||
|
await fetchList(true)
|
||||||
|
}
|
||||||
|
function activateKeywordFocus() {
|
||||||
|
keywordFocus.value = false
|
||||||
|
nextTick(() => {
|
||||||
|
keywordFocus.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function focusKeywordNoKeyboard() {
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
focusNoKeyboardRef.value?.focus(keywordInputSelector)
|
||||||
|
}, 80)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function resetFilters() {
|
||||||
|
clearSearchTimer()
|
||||||
|
searchKeyword.value = ''
|
||||||
|
selectedStatus.value = ''
|
||||||
|
activateKeywordFocus()
|
||||||
|
await fetchList(true)
|
||||||
|
}
|
||||||
|
function openStatusPicker() {
|
||||||
|
statusPickerRef.value?.open()
|
||||||
|
}
|
||||||
|
async function selectStatus(option) {
|
||||||
|
selectedStatus.value = option.value
|
||||||
|
statusPickerRef.value?.close()
|
||||||
|
await fetchList(true)
|
||||||
|
}
|
||||||
|
async function resetStatus() {
|
||||||
|
selectedStatus.value = ''
|
||||||
|
statusPickerRef.value?.close()
|
||||||
|
await fetchList(true)
|
||||||
|
}
|
||||||
|
async function loadMore() {
|
||||||
|
if (loading.value || loadingMore.value || finished.value) return
|
||||||
|
pageNo.value += 1
|
||||||
|
await fetchList(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleApprove(item) {
|
||||||
|
if (!item?.id) return
|
||||||
|
uni.showModal({
|
||||||
|
title: t('productOutbound.tip'),
|
||||||
|
content: t('productOutbound.confirmAuditPass'),
|
||||||
|
confirmColor: '#16a34a',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
try {
|
||||||
|
await auditProductOutbound({ id: item.id, status: 20 })
|
||||||
|
uni.showToast({ title: t('productOutbound.auditPassSuccess'), icon: 'success' })
|
||||||
|
fetchList(true)
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: t('productOutbound.operationFailed'), icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReject(item) {
|
||||||
|
if (!item?.id) return
|
||||||
|
uni.showModal({
|
||||||
|
title: t('productOutbound.tip'),
|
||||||
|
content: t('productOutbound.confirmAuditReject'),
|
||||||
|
confirmColor: '#dc2626',
|
||||||
|
success: async (res) => {
|
||||||
|
if (!res.confirm) return
|
||||||
|
try {
|
||||||
|
await auditProductOutbound({ id: item.id, status: 1 })
|
||||||
|
uni.showToast({ title: t('productOutbound.auditRejectSuccess'), icon: 'success' })
|
||||||
|
fetchList(true)
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: t('productOutbound.operationFailed'), icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAuditModal = ref(false)
|
||||||
|
const currentAuditItem = ref(null)
|
||||||
|
const selectedAuditor = ref(null)
|
||||||
|
const auditRemark = ref('')
|
||||||
|
|
||||||
|
function openSubmitAudit(item) {
|
||||||
|
currentAuditItem.value = item
|
||||||
|
selectedAuditor.value = null
|
||||||
|
auditRemark.value = ''
|
||||||
|
showAuditModal.value = true
|
||||||
|
}
|
||||||
|
function closeAuditModal() {
|
||||||
|
showAuditModal.value = false
|
||||||
|
currentAuditItem.value = null
|
||||||
|
}
|
||||||
|
function goSelectAuditor() {
|
||||||
|
const suffix = selectedAuditor.value?.value ? `&selectedId=${encodeURIComponent(String(selectedAuditor.value.value))}` : ''
|
||||||
|
uni.navigateTo({ url: `/pages_function/pages/moldRepair/userSelect?field=auditUser&from=productOutbound${suffix}` })
|
||||||
|
}
|
||||||
|
function consumeSelectedAuditor() {
|
||||||
|
const result = getApp().globalData?._productOutboundUserSelectResult
|
||||||
|
if (!result?.user) return
|
||||||
|
const user = result.user
|
||||||
|
selectedAuditor.value = {
|
||||||
|
value: user.id || user.userId,
|
||||||
|
label: user.nickname || user.userName || user.name || String(user.id || '')
|
||||||
|
}
|
||||||
|
getApp().globalData._productOutboundUserSelectResult = null
|
||||||
|
}
|
||||||
|
async function confirmSubmitAudit() {
|
||||||
|
if (!selectedAuditor.value) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectAuditor'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!currentAuditItem.value?.id) return
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: t('productOutbound.submitting'), mask: true })
|
||||||
|
await submitProductOutbound({
|
||||||
|
id: currentAuditItem.value.id,
|
||||||
|
auditUserId: selectedAuditor.value.value,
|
||||||
|
remark: auditRemark.value || undefined
|
||||||
|
})
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: t('productOutbound.submitAuditSuccess'), icon: 'success' })
|
||||||
|
closeAuditModal()
|
||||||
|
fetchList(true)
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: t('productOutbound.submitFailed'), icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll(event) {
|
||||||
|
showGoTop.value = (event?.detail?.scrollTop || 0) > 600
|
||||||
|
}
|
||||||
|
function goTop() {
|
||||||
|
scrollTop.value = 0
|
||||||
|
}
|
||||||
|
function goAdd() {
|
||||||
|
uni.navigateTo({ url: '/pages_function/pages/productOutbound/create' })
|
||||||
|
}
|
||||||
|
function openDetail(item) {
|
||||||
|
if (!item?.id) {
|
||||||
|
uni.showToast({ title: t('productOutbound.noDetailId'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url: `/pages_function/pages/productOutbound/detail?id=${encodeURIComponent(String(item.id))}` })
|
||||||
|
}
|
||||||
|
function clearSearchTimer() {
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer)
|
||||||
|
searchTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReady(() => {
|
||||||
|
focusKeywordNoKeyboard()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
consumeSelectedAuditor()
|
||||||
|
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) 150rpx 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,
|
||||||
|
.placeholder { font-size: 26rpx; }
|
||||||
|
.status-box-text { 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); }
|
||||||
|
.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; }
|
||||||
|
.header-main { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.task-no { flex: 1; min-width: 0; font-size: 32rpx; font-weight: 700; color: #0f172a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.record-tag { flex-shrink: 0; padding: 8rpx 18rpx; border-radius: 999rpx; font-size: 22rpx; line-height: 1; background: #e2e8f0; color: #64748b; }
|
||||||
|
.card-body .row { display: flex; justify-content: space-between; align-items: flex-start; gap: 20rpx; margin-top: 12rpx; }
|
||||||
|
.card-body .row:first-child { margin-top: 0; }
|
||||||
|
.label { width: 140rpx; font-size: 25rpx; color: #94a3b8; flex-shrink: 0; }
|
||||||
|
.value { flex: 1; text-align: right; font-size: 27rpx; color: #334155; line-height: 1.5; }
|
||||||
|
.value.highlight { color: #1f4b79; font-weight: 600; }
|
||||||
|
.card-actions { display: flex; gap: 16rpx; margin-top: 20rpx; padding-top: 20rpx; border-top: 1rpx solid #f0f0f0; }
|
||||||
|
.action-btn { flex: 1; height: 64rpx; line-height: 64rpx; text-align: center; border-radius: 10rpx; font-size: 26rpx; font-weight: 500; }
|
||||||
|
.approve-btn { background: #dcfce7; color: #16a34a; }
|
||||||
|
.reject-btn { background: #fee2e2; color: #dc2626; }
|
||||||
|
.submit-btn { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.record-tag.text-success { color: #15803d; background: #dcfce7; }
|
||||||
|
.record-tag.text-danger { color: #dc2626; background: #fee2e2; }
|
||||||
|
.record-tag.text-warning { color: #d97706; background: #fef3c7; }
|
||||||
|
.record-tag.text-primary { color: #1d4ed8; background: #dbeafe; }
|
||||||
|
.hint { padding: 36rpx 0; text-align: center; color: #94a3b8; font-size: 26rpx; }
|
||||||
|
.go-top-btn { position: fixed; right: 28rpx; bottom: calc(140rpx + env(safe-area-inset-bottom)); width: 92rpx; height: 92rpx; border-radius: 46rpx; background: rgba(255, 255, 255, 0.96); box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.12); display: flex; align-items: center; justify-content: center; }
|
||||||
|
.add-btn { position: fixed; right: 28rpx; bottom: calc(56rpx + env(safe-area-inset-bottom)); width: 92rpx; height: 92rpx; border-radius: 46rpx; background: #1f4b79; box-shadow: 0 14rpx 30rpx rgba(24, 63, 108, 0.24); display: flex; align-items: center; justify-content: center; }
|
||||||
|
.add-icon { color: #fff; font-size: 64rpx; line-height: 1; margin-top: -4rpx; }
|
||||||
|
.modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); z-index: 999; display: flex; align-items: flex-end; justify-content: center; }
|
||||||
|
.modal-card { width: 100%; background: #fff; border-radius: 28rpx 28rpx 0 0; box-shadow: 0 -12rpx 42rpx rgba(0, 0, 0, 0.14); padding-bottom: env(safe-area-inset-bottom); }
|
||||||
|
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 32rpx 20rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||||
|
.modal-title { font-size: 32rpx; font-weight: 700; color: #1a1a1a; }
|
||||||
|
.modal-close { font-size: 36rpx; color: #999; padding: 8rpx; }
|
||||||
|
.modal-body { padding: 28rpx 32rpx; }
|
||||||
|
.modal-field { margin-bottom: 28rpx; }
|
||||||
|
.modal-field:last-child { margin-bottom: 0; }
|
||||||
|
.modal-label { font-size: 28rpx; color: #374151; font-weight: 500; margin-bottom: 14rpx; display: block; }
|
||||||
|
.required { color: #ef4444; }
|
||||||
|
.modal-select-field { display: flex; align-items: center; justify-content: space-between; height: 80rpx; padding: 0 24rpx; background: #f8fafc; border: 1rpx solid #e0e0e0; border-radius: 12rpx; box-sizing: border-box; }
|
||||||
|
.modal-select-text { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.placeholder { color: #bbb; }
|
||||||
|
.modal-textarea { width: 100%; min-height: var(--app-textarea-field-height, 100rpx); padding: 20rpx; background: #f8fafc; border: 1rpx solid #e0e0e0; border-radius: 12rpx; font-size: 27rpx; color: #374151; box-sizing: border-box; }
|
||||||
|
.modal-footer { display: flex; gap: 18rpx; padding: 24rpx 32rpx; border-top: 1rpx solid #f0f0f0; }
|
||||||
|
.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>
|
||||||
@ -0,0 +1,388 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<NavBar :title="t('productOutbound.createTitle')" />
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="detail-scroll">
|
||||||
|
<view class="content-section">
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-header">
|
||||||
|
<view class="section-icon">
|
||||||
|
<uni-icons type="paperplane" size="24" color="#1f7cff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="section-title">{{ t('productOutbound.productInfo') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.product') }}<text class="required-star">*</text></text>
|
||||||
|
<view :class="['select-field', productName ? 'selected' : '']" @click="goSelectProduct">
|
||||||
|
<view class="select-content">
|
||||||
|
<text :class="productName ? 'select-value' : 'select-placeholder'">{{ productName || t('productOutbound.selectProduct') }}</text>
|
||||||
|
<text v-if="productBarCode" class="select-subtext">{{ textValue(productBarCode) }}</text>
|
||||||
|
</view>
|
||||||
|
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="productId" class="info-panel">
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">{{ t('productOutbound.product') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(productName) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">{{ t('productOutbound.code') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(productBarCode) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">{{ t('productOutbound.packagingScheme') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(packagingSchemeName) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="info-label">{{ t('productOutbound.packageQuantity') }}</text>
|
||||||
|
<text class="info-value">{{ t('productOutbound.pieceUnit', { count: textValue(packageQuantity) }) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-header">
|
||||||
|
<view class="section-icon">
|
||||||
|
<uni-icons type="list" size="24" color="#1f7cff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
<text class="section-title">{{ t('productOutbound.outboundInfo') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<text class="form-label">{{ t('productOutbound.outMode') }}<text class="required-star">*</text></text>
|
||||||
|
<view class="segment-control">
|
||||||
|
<view :class="['segment-item', outMode === 1 ? 'active' : '']" @click="setOutMode(1)">{{ t('productOutbound.outModeWholePallet') }}</view>
|
||||||
|
<view :class="['segment-item', outMode === 2 ? 'active' : '']" @click="setOutMode(2)">{{ t('productOutbound.outModeSplitPallet') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-field">
|
||||||
|
<view class="form-label-row">
|
||||||
|
<text class="form-label">{{ t('productOutbound.palletCode') }}<text class="required-star">*</text></text>
|
||||||
|
<text v-if="selectedPallets.length" class="label-tip">{{ t('productOutbound.selectedPalletCount', { count: selectedPallets.length }) }}</text>
|
||||||
|
</view>
|
||||||
|
<view :class="['select-field', selectedPallets.length ? 'selected' : '']" @click="goSelectPallet" >
|
||||||
|
<view class="select-content">
|
||||||
|
<text :class="selectedPallets.length ? 'select-value' : 'select-placeholder'">{{ palletCode || t('productOutbound.selectPallet') }}</text>
|
||||||
|
</view>
|
||||||
|
<uni-icons type="right" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="selectedPallets.length" class="pallet-list">
|
||||||
|
<view v-for="(pallet, index) in selectedPallets" :key="pallet.id || index" class="pallet-card">
|
||||||
|
<view class="pallet-main">
|
||||||
|
<view class="pallet-top">
|
||||||
|
<text class="pallet-code">{{ textValue(getPalletCode(pallet)) }}</text>
|
||||||
|
<view class="remove-btn" @click="removePallet(index)">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#ef4444"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="pallet-loc">
|
||||||
|
<view class="loc-cell">
|
||||||
|
<text class="loc-label">{{ t('productOutbound.warehouse') }}</text>
|
||||||
|
<view class="loc-picker readonly">
|
||||||
|
<text class="loc-value">{{ textValue(getWarehouseName(pallet.warehouseId)) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="loc-cell">
|
||||||
|
<text class="loc-label">{{ t('productOutbound.location') }}</text>
|
||||||
|
<view class="loc-picker readonly">
|
||||||
|
<text class="loc-value">{{ textValue(getAreaName(pallet.warehouseId, pallet.areaId)) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="pallet-count-row">
|
||||||
|
<view class="count-cell">
|
||||||
|
<text class="count-label">{{ t('productOutbound.availablePackageCount') }}</text>
|
||||||
|
<text class="count-value">{{ textValue(getAvailablePackageCount(pallet)) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="count-cell">
|
||||||
|
<text class="count-label">{{ t('productOutbound.outboundPackageCount') }}</text>
|
||||||
|
<input v-if="outMode === 2" class="count-input" type="number" :value="pallet.packageCount" @input="onPalletCountInput($event, index)" placeholder="0" />
|
||||||
|
<text v-else class="count-value">{{ textValue(pallet.packageCount) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="pallet-count-row">
|
||||||
|
<view class="count-cell">
|
||||||
|
<text class="count-label">{{ t('productOutbound.outboundPieceCount') }}</text>
|
||||||
|
<text class="count-value">{{ getPalletItemCount(pallet) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="quantity-summary">
|
||||||
|
<view class="quantity-item">
|
||||||
|
<text class="quantity-value">{{ packageCount }}</text>
|
||||||
|
<text class="quantity-label">{{ t('productOutbound.packageCount') }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="quantity-item">
|
||||||
|
<text class="quantity-value">{{ count }}</text>
|
||||||
|
<text class="quantity-label">{{ t('productOutbound.pieceCount') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card" @click="goSelectPallet">{{ t('productOutbound.selectPalletFirst') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="action-bar">
|
||||||
|
<view class="action-btn back-btn" @click="handleCancel">{{ t('productOutbound.cancel') }}</view>
|
||||||
|
<view class="action-btn submit-btn" @click="handleConfirm">{{ t('productOutbound.confirm') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import NavBar from '@/components/common/NavBar.vue'
|
||||||
|
import { getWarehouseAreaSimpleList, getWarehouseSimpleList } from '@/api/mes/moldget'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const product = ref({})
|
||||||
|
const outMode = ref(1)
|
||||||
|
const selectedPallets = ref([])
|
||||||
|
const warehouseOptions = ref([])
|
||||||
|
const warehouseAreaMap = ref({})
|
||||||
|
|
||||||
|
const productName = computed(() => product.value.name || '')
|
||||||
|
const productId = computed(() => product.value.id)
|
||||||
|
const productBarCode = computed(() => product.value.barCode || product.value.code || '')
|
||||||
|
const currentScheme = computed(() => getDefaultScheme(product.value))
|
||||||
|
const packagingSchemeName = computed(() => currentScheme.value?.packagingSchemeName || '')
|
||||||
|
const unitName = computed(() => product.value.unitName || product.value.minStockUnitName || '个')
|
||||||
|
const packageQuantity = computed(() => Number(currentScheme.value?.packageQuantity) || 1)
|
||||||
|
const palletPackageQuantity = computed(() => Number(currentScheme.value?.palletPackageQuantity) || 0)
|
||||||
|
const palletTotalQuantity = computed(() => Number(currentScheme.value?.palletTotalQuantity) || packageQuantity.value * palletPackageQuantity.value)
|
||||||
|
const packageCount = computed(() => selectedPallets.value.reduce((sum, pallet) => sum + (Number(pallet.packageCount) || 0), 0))
|
||||||
|
const count = computed(() => selectedPallets.value.reduce((sum, pallet) => sum + getPalletItemCountValue(pallet), 0))
|
||||||
|
const palletCode = computed(() => selectedPallets.value.map((item) => getPalletCode(item)).filter(Boolean).join(', '))
|
||||||
|
|
||||||
|
function textValue(v) {
|
||||||
|
if (v === 0) return '0'
|
||||||
|
if (v == null) return '-'
|
||||||
|
const s = String(v).trim()
|
||||||
|
return s || '-'
|
||||||
|
}
|
||||||
|
function getDefaultScheme(item) {
|
||||||
|
const schemes = Array.isArray(item?.packagingSchemes) ? item.packagingSchemes : []
|
||||||
|
return schemes.find((scheme) => Number(scheme?.defaultStatus) === 1) || schemes[0] || {}
|
||||||
|
}
|
||||||
|
function getPalletCode(pallet) {
|
||||||
|
return pallet?.code || pallet?.palletCode || ''
|
||||||
|
}
|
||||||
|
function getAvailablePackageCount(pallet) {
|
||||||
|
const value = Number(pallet?.productCount ?? pallet?.availablePackageCount ?? pallet?.packageCount ?? 0)
|
||||||
|
return Number.isFinite(value) ? value : 0
|
||||||
|
}
|
||||||
|
function getPalletItemCountValue(pallet) {
|
||||||
|
const c = Number(pallet?.packageCount) || 0
|
||||||
|
return c * (packageQuantity.value || 1)
|
||||||
|
}
|
||||||
|
function getPalletItemCount(pallet) {
|
||||||
|
return getPalletItemCountValue(pallet)
|
||||||
|
}
|
||||||
|
function setOutMode(value) {
|
||||||
|
if (outMode.value === value) return
|
||||||
|
outMode.value = value
|
||||||
|
selectedPallets.value = selectedPallets.value.map((pallet) => ({
|
||||||
|
...pallet,
|
||||||
|
packageCount: value === 1 ? getAvailablePackageCount(pallet) : Math.min(Number(pallet.packageCount) || 0, getAvailablePackageCount(pallet))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
function goSelectProduct() {
|
||||||
|
const suffix = product.value?.id ? `?selectedId=${product.value.id}` : ''
|
||||||
|
uni.navigateTo({ url: `/pages_function/pages/productOutbound/productSelect${suffix}` })
|
||||||
|
}
|
||||||
|
function goSelectPallet() {
|
||||||
|
if (!productId.value) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectProductFirst'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getApp().globalData._productOutboundSelectedPallets = [...selectedPallets.value]
|
||||||
|
const query = `productId=${productId.value}&outMode=${outMode.value}&packageQuantity=${packageQuantity.value || 1}`
|
||||||
|
uni.navigateTo({ url: `/pages_function/pages/productOutbound/palletSelect?${query}` })
|
||||||
|
}
|
||||||
|
function removePallet(index) {
|
||||||
|
selectedPallets.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
function onPalletCountInput(e, index) {
|
||||||
|
const pallet = selectedPallets.value[index]
|
||||||
|
const max = getAvailablePackageCount(pallet)
|
||||||
|
const v = Number(e.detail.value)
|
||||||
|
const nextValue = Number.isFinite(v) ? Math.max(0, Math.min(v, max)) : 0
|
||||||
|
selectedPallets.value[index].packageCount = nextValue
|
||||||
|
}
|
||||||
|
function getWarehouseName(id) {
|
||||||
|
return warehouseOptions.value.find((item) => String(item.value) === String(id))?.label || ''
|
||||||
|
}
|
||||||
|
function getAreaName(warehouseId, areaId) {
|
||||||
|
const areas = warehouseAreaMap.value[String(warehouseId)] || []
|
||||||
|
return areas.find((item) => String(item.value) === String(areaId))?.label || ''
|
||||||
|
}
|
||||||
|
async function loadWarehouses() {
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseSimpleList()
|
||||||
|
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
|
||||||
|
warehouseOptions.value = data.map((w) => ({ value: w.id, label: w.name || String(w.id || '') }))
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
async function loadAreasForWarehouse(warehouseId) {
|
||||||
|
if (!warehouseId || warehouseAreaMap.value[String(warehouseId)]) return
|
||||||
|
try {
|
||||||
|
const res = await getWarehouseAreaSimpleList(warehouseId)
|
||||||
|
const data = Array.isArray(res) ? res : (Array.isArray(res?.data) ? res.data : [])
|
||||||
|
warehouseAreaMap.value = {
|
||||||
|
...warehouseAreaMap.value,
|
||||||
|
[String(warehouseId)]: data.map((a) => ({ value: a.id, label: a.name || a.areaName || String(a.id || '') }))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
warehouseAreaMap.value = { ...warehouseAreaMap.value, [String(warehouseId)]: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function loadAreasForPallets(pallets) {
|
||||||
|
const warehouseIds = Array.from(new Set((pallets || []).map((p) => p.warehouseId).filter(Boolean)))
|
||||||
|
await Promise.all(warehouseIds.map((id) => loadAreasForWarehouse(id)))
|
||||||
|
}
|
||||||
|
function handleCancel() {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
function handleConfirm() {
|
||||||
|
if (!productId.value) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectProduct'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!selectedPallets.value.length) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectPallet'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const invalid = selectedPallets.value.find((p) => !p.warehouseId || !p.areaId || !Number(p.packageCount))
|
||||||
|
if (invalid) {
|
||||||
|
uni.showToast({ title: t('productOutbound.completePalletInfo'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const firstPallet = selectedPallets.value[0]
|
||||||
|
const item = {
|
||||||
|
productId: productId.value,
|
||||||
|
productName: productName.value,
|
||||||
|
productBarCode: productBarCode.value,
|
||||||
|
productUnitName: unitName.value,
|
||||||
|
inputUnitType: unitName.value,
|
||||||
|
inputCount: count.value,
|
||||||
|
count: count.value,
|
||||||
|
outMode: outMode.value,
|
||||||
|
palletCount: selectedPallets.value.length,
|
||||||
|
packageCount: packageCount.value,
|
||||||
|
pieceCount: count.value,
|
||||||
|
packagingSchemeRelationId: currentScheme.value?.id,
|
||||||
|
packagingSchemeId: currentScheme.value?.packagingSchemeId,
|
||||||
|
packagingSchemeName: packagingSchemeName.value,
|
||||||
|
packageQuantity: packageQuantity.value,
|
||||||
|
palletPackageQuantity: palletPackageQuantity.value,
|
||||||
|
palletTotalQuantity: palletTotalQuantity.value,
|
||||||
|
warehouseId: firstPallet?.warehouseId || null,
|
||||||
|
warehouseName: getWarehouseName(firstPallet?.warehouseId),
|
||||||
|
areaId: firstPallet?.areaId || null,
|
||||||
|
areaName: getAreaName(firstPallet?.warehouseId, firstPallet?.areaId),
|
||||||
|
palletCode: palletCode.value,
|
||||||
|
pallets: selectedPallets.value.map((pallet) => ({
|
||||||
|
palletId: pallet.id,
|
||||||
|
palletCode: getPalletCode(pallet),
|
||||||
|
packageCount: Number(pallet.packageCount) || 0,
|
||||||
|
warehouseId: pallet.warehouseId,
|
||||||
|
areaId: pallet.areaId,
|
||||||
|
count: getPalletItemCountValue(pallet)
|
||||||
|
})),
|
||||||
|
images: product.value.images,
|
||||||
|
_product: { ...product.value }
|
||||||
|
}
|
||||||
|
if (!getApp().globalData._productOutboundItems) getApp().globalData._productOutboundItems = []
|
||||||
|
getApp().globalData._productOutboundItems.push(item)
|
||||||
|
uni.showToast({ title: t('productOutbound.productAdded'), icon: 'success', duration: 1000 })
|
||||||
|
setTimeout(() => uni.navigateBack(), 700)
|
||||||
|
}
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
const gd = getApp().globalData
|
||||||
|
const productResult = gd?._productOutboundProductSelectResult
|
||||||
|
if (productResult) {
|
||||||
|
product.value = productResult
|
||||||
|
selectedPallets.value = []
|
||||||
|
gd._productOutboundProductSelectResult = null
|
||||||
|
}
|
||||||
|
const palletResult = gd?._productOutboundPalletSelectResult
|
||||||
|
if (Array.isArray(palletResult)) {
|
||||||
|
selectedPallets.value = palletResult.map((pallet) => ({
|
||||||
|
...pallet,
|
||||||
|
packageCount: Number(pallet.packageCount) || 0
|
||||||
|
}))
|
||||||
|
await loadAreasForPallets(selectedPallets.value)
|
||||||
|
gd._productOutboundPalletSelectResult = null
|
||||||
|
}
|
||||||
|
await loadWarehouses()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container { min-height: 100vh; background: #f5f7fb; }
|
||||||
|
.detail-scroll { height: calc(100vh - 172rpx); }
|
||||||
|
.content-section { padding: 20rpx 24rpx 28rpx; }
|
||||||
|
.section-card { background: #ffffff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 20rpx; border: 1rpx solid #eef2f7; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||||
|
.section-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 22rpx; padding-bottom: 18rpx; border-bottom: 1rpx solid #f1f5f9; }
|
||||||
|
.section-icon { width: 40rpx; height: 40rpx; border-radius: 10rpx; background: #eff6ff; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.section-title { font-size: 32rpx; font-weight: 600; color: #1f2937; }
|
||||||
|
.form-field { display: flex; flex-direction: column; gap: 12rpx; }
|
||||||
|
.form-field + .form-field { margin-top: 24rpx; }
|
||||||
|
.form-label-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.form-label { font-size: 26rpx; color: #4b5563; font-weight: 500; }
|
||||||
|
.label-tip { flex-shrink: 0; font-size: 24rpx; color: #6b7280; }
|
||||||
|
.required-star { color: #ef4444; font-size: 28rpx; margin-left: 4rpx; }
|
||||||
|
.segment-control { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; padding: 6rpx; background: #f3f4f6; border-radius: 16rpx; }
|
||||||
|
.segment-item { height: 72rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #6b7280; font-size: 28rpx; font-weight: 600; }
|
||||||
|
.segment-item.active { background: #ffffff; color: #1f7cff; box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.08); }
|
||||||
|
.select-field { min-height: 70rpx; padding: 16rpx 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 12rpx; }
|
||||||
|
.select-field.selected { background: #f9fbff; border-color: #bfdbfe; box-shadow: 0 4rpx 12rpx rgba(31, 124, 255, 0.08); }
|
||||||
|
.select-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
|
||||||
|
.select-value { font-size: 28rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.select-placeholder { font-size: 28rpx; color: #9ca3af; }
|
||||||
|
.select-subtext { font-size: 24rpx; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.info-panel { margin-top: 22rpx; padding: 20rpx; background: #f8fafc; border: 1rpx solid #e8eef6; border-radius: 16rpx; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 14rpx; overflow: hidden; }
|
||||||
|
.info-item { min-width: 0; display: flex; flex-direction: column; gap: 8rpx; padding: 18rpx 20rpx; border-right: 1rpx solid #f1f5f9; border-bottom: 1rpx solid #f1f5f9; }
|
||||||
|
.info-item:nth-child(2n) { border-right: 0; }
|
||||||
|
.info-label { font-size: 23rpx; color: #8a94a6; }
|
||||||
|
.info-value { font-size: 27rpx; color: #334155; line-height: 1.35; word-break: break-all; }
|
||||||
|
.pallet-list { margin-top: 20rpx; display: flex; flex-direction: column; gap: 14rpx; }
|
||||||
|
.pallet-card { padding: 18rpx 20rpx; border: 1rpx solid #eef2f7; border-radius: 16rpx; background: #ffffff; }
|
||||||
|
.pallet-main { display: flex; flex-direction: column; gap: 14rpx; }
|
||||||
|
.pallet-top { display: flex; align-items: center; justify-content: space-between; gap: 12rpx; }
|
||||||
|
.pallet-code { flex: 1; min-width: 0; font-size: 28rpx; color: #1f2937; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.remove-btn { width: 48rpx; height: 48rpx; border-radius: 24rpx; background: #fef2f2; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.pallet-loc { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
|
||||||
|
.loc-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
|
||||||
|
.loc-label { font-size: 22rpx; color: #9ca3af; }
|
||||||
|
.loc-picker { min-height: 64rpx; padding: 0 16rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 12rpx; display: flex; align-items: center; justify-content: space-between; gap: 8rpx; box-sizing: border-box; }
|
||||||
|
.loc-value { flex: 1; min-width: 0; font-size: 25rpx; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.loc-placeholder { flex: 1; font-size: 25rpx; color: #9ca3af; }
|
||||||
|
.pallet-count-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12rpx; }
|
||||||
|
.count-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
|
||||||
|
.count-label { font-size: 22rpx; color: #9ca3af; }
|
||||||
|
.count-input { height: 64rpx; padding: 0 16rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 12rpx; font-size: 26rpx; color: #1f2937; box-sizing: border-box; }
|
||||||
|
.count-value { height: 64rpx; line-height: 64rpx; padding: 0 16rpx; background: #f1f5f9; border-radius: 12rpx; font-size: 26rpx; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.quantity-summary { display: grid; grid-template-columns: 1fr 1fr; gap: 14rpx; }
|
||||||
|
.quantity-item { padding: 18rpx; background: #eff6ff; border-radius: 14rpx; display: flex; flex-direction: column; align-items: center; gap: 6rpx; }
|
||||||
|
.quantity-value { font-size: 34rpx; color: #1f4b79; font-weight: 700; }
|
||||||
|
.quantity-label { font-size: 23rpx; color: #64748b; }
|
||||||
|
.empty-card { height: 96rpx; border-radius: 14rpx; background: #f8fafc; color: #94a3b8; font-size: 27rpx; display: flex; align-items: center; justify-content: center;margin-top: 20rpx; }
|
||||||
|
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 18rpx; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99; }
|
||||||
|
.action-btn { flex: 1; height: 84rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
|
||||||
|
.back-btn { background: #eef2f7; color: #475569; }
|
||||||
|
.submit-btn { background: #1f4b79; color: #ffffff; }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page-container">
|
||||||
|
<NavBar :title="t('productOutbound.selectProductTitle')" />
|
||||||
|
|
||||||
|
<view class="search-bar">
|
||||||
|
<view class="search-input-wrap">
|
||||||
|
<uni-icons type="search" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="t('productOutbound.searchProductPlaceholder')"
|
||||||
|
placeholder-class="search-placeholder"
|
||||||
|
confirm-type="search"
|
||||||
|
/>
|
||||||
|
<view v-if="searchText" class="search-clear" @click="clearSearch">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#9ca3af"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="product-list" v-if="filteredList.length">
|
||||||
|
<view
|
||||||
|
v-for="item in filteredList"
|
||||||
|
:key="item.id"
|
||||||
|
:class="['product-card', isSelected(item) ? 'active' : '']"
|
||||||
|
@click="selectedId = item.id"
|
||||||
|
>
|
||||||
|
<view class="product-header">
|
||||||
|
<view class="product-title-wrap">
|
||||||
|
<view class="product-name-row">
|
||||||
|
<text class="product-name">{{ textValue(item.name) }}</text>
|
||||||
|
<text v-if="getCategoryName(item)" class="category-tag">{{ getCategoryName(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="product-code">{{ textValue(item.barCode || item.code) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="check-badge">
|
||||||
|
<uni-icons v-if="isSelected(item)" type="checkmarkempty" size="16" color="#ffffff"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.spec') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.standard || item.deviceSpec) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-cell">
|
||||||
|
<text class="info-label">{{ t('productOutbound.unit') }}</text>
|
||||||
|
<text class="info-value">{{ textValue(item.unitName) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view v-else class="empty-wrap">
|
||||||
|
<uni-icons type="info" size="30" color="#cbd5e1"></uni-icons>
|
||||||
|
<text>{{ loading ? t('productOutbound.loading') : t('productOutbound.emptyProduct') }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="action-bar">
|
||||||
|
<view :class="['action-btn', selectedId ? '' : 'action-btn-disabled']" @click="handleConfirm">{{ t('productOutbound.confirm') }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import NavBar from '@/components/common/NavBar.vue'
|
||||||
|
import { getProduct, getProductPage } from '@/api/erp/productInfo'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const productList = ref([])
|
||||||
|
const selectedId = ref(null)
|
||||||
|
const searchText = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
selectedId.value = options?.selectedId ? String(options.selectedId) : null
|
||||||
|
})
|
||||||
|
|
||||||
|
function textValue(v) {
|
||||||
|
if (v === 0) return '0'
|
||||||
|
if (v == null) return '-'
|
||||||
|
const s = String(v).trim()
|
||||||
|
return s || '-'
|
||||||
|
}
|
||||||
|
const filteredList = computed(() => {
|
||||||
|
const keyword = searchText.value.trim().toLowerCase()
|
||||||
|
if (!keyword) return productList.value
|
||||||
|
return productList.value.filter((item) =>
|
||||||
|
String(item.name || '').toLowerCase().includes(keyword) ||
|
||||||
|
String(item.barCode || item.code || '').toLowerCase().includes(keyword) ||
|
||||||
|
String(item.standard || item.deviceSpec || '').toLowerCase().includes(keyword) ||
|
||||||
|
String(getCategoryName(item)).toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
function isSelected(item) {
|
||||||
|
return String(selectedId.value) === String(item.id)
|
||||||
|
}
|
||||||
|
function getCategoryName(item) {
|
||||||
|
return item?.subCategoryName || item?.categoryName || item?.category?.name || ''
|
||||||
|
}
|
||||||
|
function clearSearch() {
|
||||||
|
searchText.value = ''
|
||||||
|
}
|
||||||
|
function normalizePageList(res) {
|
||||||
|
const root = res && res.data !== undefined ? res.data : res
|
||||||
|
return Array.isArray(root) ? root : (root?.list || root?.rows || root?.records || [])
|
||||||
|
}
|
||||||
|
async function loadProducts() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const raw = []
|
||||||
|
for (let pageNo = 1; pageNo <= 5; pageNo += 1) {
|
||||||
|
const res = await getProductPage({ pageNo, pageSize: 100, categoryType: 1 })
|
||||||
|
const list = normalizePageList(res)
|
||||||
|
if (!list.length) break
|
||||||
|
raw.push(...list)
|
||||||
|
if (list.length < 100) break
|
||||||
|
}
|
||||||
|
productList.value = raw
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({ title: t('productOutbound.loadFailed'), icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleConfirm() {
|
||||||
|
if (!selectedId.value) {
|
||||||
|
uni.showToast({ title: t('productOutbound.selectProduct'), icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let item = productList.value.find((d) => String(d.id) === String(selectedId.value))
|
||||||
|
if (!item) return
|
||||||
|
try {
|
||||||
|
const res = await getProduct(item.id)
|
||||||
|
item = { ...item, ...(res?.data || res || {}) }
|
||||||
|
} catch (e) {}
|
||||||
|
getApp().globalData._productOutboundProductSelectResult = item
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
onShow(loadProducts)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-container { min-height: 100vh; background: #f5f7fb; padding-bottom: calc(120rpx + env(safe-area-inset-bottom)); }
|
||||||
|
.search-bar { padding: 18rpx 24rpx; background: #ffffff; box-shadow: 0 4rpx 16rpx rgba(15, 23, 42, 0.03); }
|
||||||
|
.search-input-wrap { display: flex; align-items: center; gap: 12rpx; height: 76rpx; padding: 0 22rpx; background: #f8fafc; border: 1rpx solid #e5e7eb; border-radius: 14rpx; box-sizing: border-box; }
|
||||||
|
.search-input { flex: 1; min-width: 0; font-size: 28rpx; color: #374151; }
|
||||||
|
.search-placeholder { color: #9ca3af; }
|
||||||
|
.search-clear { width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.product-list { height: calc(100vh - 294rpx); }
|
||||||
|
.product-card { margin: 18rpx 24rpx 0; padding: 24rpx; background: #ffffff; border: 1rpx solid #eef2f7; border-radius: 20rpx; box-shadow: 0 6rpx 18rpx rgba(15, 23, 42, 0.04); }
|
||||||
|
.product-card.active { border-color: #bfdbfe; background: #f9fbff; box-shadow: 0 8rpx 22rpx rgba(31, 124, 255, 0.08); }
|
||||||
|
.product-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18rpx; }
|
||||||
|
.product-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
|
||||||
|
.product-name-row { display: flex; align-items: center; gap: 12rpx; min-width: 0; }
|
||||||
|
.product-name { min-width: 0; font-size: 30rpx; font-weight: 600; color: #1f2937; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.category-tag { max-width: 180rpx; padding: 5rpx 14rpx; border-radius: 999rpx; background: #eff6ff; color: #1f7cff; font-size: 22rpx; line-height: 1.3; flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.product-code { font-size: 24rpx; color: #8a94a6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.check-badge { width: 36rpx; height: 36rpx; border-radius: 18rpx; border: 1rpx solid #d1d5db; background: #ffffff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.product-card.active .check-badge { border-color: #1f7cff; background: #1f7cff; }
|
||||||
|
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14rpx; margin-top: 20rpx; padding-top: 18rpx; border-top: 1rpx solid #f1f5f9; }
|
||||||
|
.info-cell { min-width: 0; display: flex; flex-direction: column; gap: 6rpx; }
|
||||||
|
.info-label { font-size: 23rpx; color: #9ca3af; }
|
||||||
|
.info-value { font-size: 27rpx; color: #374151; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.empty-wrap { height: calc(100vh - 294rpx); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14rpx; color: #94a3b8; font-size: 27rpx; }
|
||||||
|
.action-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); background: #ffffff; box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06); z-index: 99; }
|
||||||
|
.action-btn { height: 84rpx; border-radius: 16rpx; background: #1f4b79; color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
|
||||||
|
.action-btn-disabled { background: #94a3b8; }
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue