feat(mobile-h5): wire select-only master data into business forms

Replace master-data CRUD pickers with CRM/PL selectors, require productId, drop auto vehicle create, and remove 基础资料 routes/menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-03 22:42:24 +08:00
1 parent a8f59183df
commit db06fea45a
9 files changed
+129 -725

No files matched your search

-5
View File
@@ -23,11 +23,6 @@ const simpleRoutes: any[] = [
path: '/weighing',
name: 'Weighing',
meta: { title: '地磅单管理', icon: '', isHide: false }
},
{
path: '/master-data',
name: 'MasterData',
meta: { title: '基础资料', icon: '', isHide: false }
}
]
-42
View File
@@ -88,48 +88,6 @@ const routes: RouteRecordRaw[] = [
name: 'WeighingDetailEdit',
component: () => import('@/views/weighing/detail.vue'),
meta: { title: '地磅单详情' }
},
{
path: '/master-data',
name: 'MasterData',
component: () => import('@/views/master-data/index.vue'),
meta: { title: '基础资料' }
},
{
path: '/master-data/company/detail',
name: 'CompanyDetail',
component: () => import('@/views/master-data/company-detail.vue'),
meta: { title: '公司详情' }
},
{
path: '/master-data/company/detail/:id',
name: 'CompanyDetailEdit',
component: () => import('@/views/master-data/company-detail.vue'),
meta: { title: '公司详情' }
},
{
path: '/master-data/cargo/detail',
name: 'CargoDetail',
component: () => import('@/views/master-data/cargo-detail.vue'),
meta: { title: '货物详情' }
},
{
path: '/master-data/cargo/detail/:id',
name: 'CargoDetailEdit',
component: () => import('@/views/master-data/cargo-detail.vue'),
meta: { title: '货物详情' }
},
{
path: '/master-data/vehicle/detail',
name: 'VehicleDetail',
component: () => import('@/views/master-data/vehicle-detail.vue'),
meta: { title: '车牌号详情' }
},
{
path: '/master-data/vehicle/detail/:id',
name: 'VehicleDetailEdit',
component: () => import('@/views/master-data/vehicle-detail.vue'),
meta: { title: '车牌号详情' }
}
]
+1 -2
View File
@@ -100,8 +100,7 @@ const menuList = computed(() => {
return [
{ path: '/plans', label: '计划管理', icon: '📋' },
{ path: '/inventory', label: '出入库管理', icon: '📦' },
{ path: '/weighing', label: '地磅单管理', icon: '⚖️' },
{ path: '/master-data', label: '基础资料', icon: '📚' }
{ path: '/weighing', label: '地磅单管理', icon: '⚖️' }
]
})
+41 -303
View File
@@ -28,7 +28,7 @@
<div v-if="isView" class="form-input">
{{ formData.partnerCompany || '--' }}
</div>
<div v-else class="form-input" @click="showCompanyPicker = true">
<div v-else class="form-input" @click="showCustomerPicker = true">
<span :class="{ placeholder: !formData.partnerCompany }">
{{ formData.partnerCompany || '请选择往来单位' }}
</span>
@@ -49,11 +49,11 @@
<div class="form-item">
<div class="form-label">产品 <span class="required">*</span></div>
<div v-if="isView" class="form-input">
{{ formData.product || '--' }}
{{ productDisplay || '--' }}
</div>
<div v-else class="form-input" @click="showCargoPicker = true">
<span :class="{ placeholder: !formData.product }">
{{ formData.product || '请选择产品' }}
<div v-else class="form-input" @click="showProductPicker = true">
<span :class="{ placeholder: !productDisplay }">
{{ productDisplay || '请选择产品' }}
</span>
<van-icon name="arrow" class="arrow-icon" />
</div>
@@ -79,7 +79,7 @@
</div>
<div v-else class="form-input" @click="showVehiclePicker = true">
<span :class="{ placeholder: !formData.vehicleNo }">
{{ formData.vehicleNo || '请选择或输入车牌号' }}
{{ formData.vehicleNo || '请选择车牌号' }}
</span>
<van-icon name="arrow" class="arrow-icon" />
</div>
@@ -125,175 +125,55 @@
</van-form>
</div>
<!-- 公司选择器 -->
<van-popup v-model:show="showCompanyPicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="companySearchKeyword"
placeholder="搜索往来单位名称"
@search="handleCompanySearch"
@clear="handleCompanySearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCompanyOptions"
show-toolbar
@confirm="onCompanyConfirm"
@cancel="showCompanyPicker = false"
/>
</div>
</van-popup>
<DbCustomerPicker v-model:show="showCustomerPicker" @select="onCustomerSelect" />
<DbProductPicker v-model:show="showProductPicker" @select="onProductSelect" />
<DbVehiclePicker v-model:show="showVehiclePicker" @select="onVehicleSelect" />
<!-- 货物选择器 -->
<van-popup v-model:show="showCargoPicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="cargoSearchKeyword"
placeholder="搜索产品名称"
@search="handleCargoSearch"
@clear="handleCargoSearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCargoOptions"
show-toolbar
@confirm="onCargoConfirm"
@cancel="showCargoPicker = false"
/>
</div>
</van-popup>
<!-- 日期选择器 -->
<van-calendar
v-model:show="showDateCalendar"
v-model="dateSelected"
@confirm="onDateConfirm"
/>
<!-- 车牌号选择器:支持搜索 + 手动输入 -->
<van-popup v-model:show="showVehiclePicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="vehicle-picker-container">
<div class="search-container">
<van-search
v-model="vehicleSearchKeyword"
placeholder="搜索车牌号"
@search="handleVehicleSearch"
@clear="handleVehicleSearch"
@update:model-value="handleVehicleSearch"
shape="round"
/>
</div>
<div class="vehicle-list">
<van-cell
v-for="item in filteredVehicleOptions"
:key="item.vehicleNo"
:title="item.vehicleNo"
clickable
@click="onVehicleSelect(item.vehicleNo)"
/>
<van-cell v-if="filteredVehicleOptions.length === 0 && !vehicleLoading" title="暂无匹配车牌号,可点击下方手动输入" />
</div>
<div class="manual-input-row">
<van-button block plain type="primary" @click="showManualVehicleInput = true">
手动输入新车牌号
</van-button>
</div>
</div>
</van-popup>
<!-- 手动输入车牌号弹层 -->
<van-dialog
v-model:show="showManualVehicleInput"
title="输入车牌号"
show-cancel-button
@confirm="onManualVehicleConfirm"
>
<van-field
v-model="manualVehicleNo"
placeholder="请输入车牌号"
clearable
class="dialog-field"
/>
</van-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import { inventoryApi, type InventoryRecord, type InventoryCreate } from '@/api/inventoryApi'
import { companyApi, cargoApi, vehicleApi } from '@/api/masterDataApi'
import type { CrmCustomerOption, CrmProductOption } from '@/api/crmOptionsApi'
import DbCustomerPicker from '@/components/pickers/DbCustomerPicker.vue'
import DbProductPicker from '@/components/pickers/DbProductPicker.vue'
import DbVehiclePicker from '@/components/pickers/DbVehiclePicker.vue'
import dayjs from 'dayjs'
const router = useRouter()
const route = useRoute()
const isView = computed(() => route.query.mode === 'view')
const isEdit = computed(() => route.query.mode === 'edit' || route.params.id)
// 计划带入新增时计划号锁定,避免误改
const isEdit = computed(() => route.query.mode === 'edit' || !!route.params.id)
const isPlanNoReadonly = computed(() => !!route.query.planNo && !isEdit.value)
// const isAdd = computed(() => !isView.value && !isEdit.value) // 未使用
const saving = ref(false)
const editingRecord = ref<InventoryRecord | null>(null)
const showCompanyPicker = ref(false)
const showCargoPicker = ref(false)
const showCustomerPicker = ref(false)
const showProductPicker = ref(false)
const showVehiclePicker = ref(false)
const showManualVehicleInput = ref(false)
const showDateCalendar = ref(false)
const dateSelected = ref<Date | null>(null)
const companyOptions = ref<{ text: string; value: string }[]>([])
const vehicleOptions = ref<{ vehicleNo: string }[]>([])
const vehicleSearchKeyword = ref('')
const vehicleLoading = ref(false)
const manualVehicleNo = ref('')
const productDisplay = ref((route.query.product as string) || '')
// 公司搜索相关
const companySearchKeyword = ref('')
const allCompanyOptions = ref<{ text: string; value: string }[]>([])
const filteredCompanyOptions = computed(() => {
if (!companySearchKeyword.value) {
return [allCompanyOptions.value]
}
const filtered = allCompanyOptions.value.filter(item =>
item.text.toLowerCase().includes(companySearchKeyword.value.toLowerCase())
)
return [filtered]
})
// 产品搜索相关
const cargoSearchKeyword = ref('')
const allCargoOptions = ref<{ text: string; value: string }[]>([])
const filteredCargoOptions = computed(() => {
if (!cargoSearchKeyword.value) return [allCargoOptions.value]
const filtered = allCargoOptions.value.filter(item =>
item.text.toLowerCase().includes(cargoSearchKeyword.value.toLowerCase())
)
return [filtered]
})
const filteredVehicleOptions = computed(() => {
if (!vehicleSearchKeyword.value) return vehicleOptions.value
const kw = vehicleSearchKeyword.value.toLowerCase()
return vehicleOptions.value.filter(item =>
item.vehicleNo.toLowerCase().includes(kw)
)
})
// 如果是从计划管理跳转过来的(有planNo参数),日期为空;否则使用当前日期
const initialDate = route.query.planNo ? '' : dayjs().format('YYYY-MM-DD')
const purchasePlanIdFromRoute = route.query.purchasePlanId ? Number(route.query.purchasePlanId) : undefined
const purchasePlanIdFromRoute = route.query.purchasePlanId
? String(route.query.purchasePlanId)
: undefined
const formData = reactive<InventoryCreate>({
date: initialDate,
partnerCompany: (route.query.customerName as string) || '',
businessType: (route.query.businessType as '入库' | '出库') || '入库',
product: (route.query.product as string) || '',
productId: (route.query.productId as string) || '',
quantity: 32,
vehicleNo: '',
planNo: (route.query.planNo as string) || '',
@@ -301,91 +181,19 @@ const formData = reactive<InventoryCreate>({
purchasePlanId: purchasePlanIdFromRoute
})
// 初始化日期选择器的默认值(如果日期为空,使用当前日期作为选择器的默认值)
dateSelected.value = route.query.planNo ? null : dayjs().toDate()
const loadCompanies = async () => {
try {
const companies = await companyApi.list()
allCompanyOptions.value = companies.map(item => ({
text: item.name,
value: item.name
}))
companyOptions.value = allCompanyOptions.value
} catch (error) {
console.error('加载公司列表失败', error)
}
const onCustomerSelect = (item: CrmCustomerOption) => {
formData.partnerCompany = item.customerName
}
const handleCompanySearch = () => {
// 触发计算属性更新
const onProductSelect = (item: CrmProductOption) => {
formData.productId = item.id
productDisplay.value = item.productName
}
const loadCargos = async () => {
try {
const cargos = await cargoApi.list()
allCargoOptions.value = cargos.map(item => ({
text: item.name,
value: item.name
}))
} catch (error) {
console.error('加载货物列表失败', error)
}
}
const handleCargoSearch = () => {
// 触发 computed 过滤
}
const loadVehicles = async () => {
vehicleLoading.value = true
try {
const list = await vehicleApi.list(vehicleSearchKeyword.value || undefined)
vehicleOptions.value = list.map(item => ({ vehicleNo: item.vehicleNo }))
} catch (error) {
console.error('加载车牌号列表失败', error)
} finally {
vehicleLoading.value = false
}
}
const handleVehicleSearch = () => {
loadVehicles()
}
const onVehicleSelect = (vehicleNo: string) => {
formData.vehicleNo = vehicleNo
showVehiclePicker.value = false
}
const onManualVehicleConfirm = () => {
const v = (manualVehicleNo.value || '').trim()
if (v) {
formData.vehicleNo = v
showVehiclePicker.value = false
}
showManualVehicleInput.value = false
manualVehicleNo.value = ''
}
// 保存前确保车牌号已写入车牌号表(新车牌则先创建)
const ensureVehicleExists = async (vehicleNo: string) => {
if (!vehicleNo?.trim()) return
try {
await vehicleApi.create({ vehicleNo: vehicleNo.trim(), remarks: null })
} catch (err: any) {
if (err.response?.status !== 409) throw err
}
}
const onCompanyConfirm = ({ selectedOptions }: any) => {
formData.partnerCompany = selectedOptions[0].value
showCompanyPicker.value = false
}
const onCargoConfirm = ({ selectedOptions }: any) => {
formData.product = selectedOptions[0].value
showCargoPicker.value = false
const onVehicleSelect = (licensePlate: string) => {
formData.vehicleNo = licensePlate
}
const onDateConfirm = (value: Date | Date[]) => {
@@ -396,22 +204,22 @@ const onDateConfirm = (value: Date | Date[]) => {
}
const loadData = async () => {
if (isEdit.value && route.params.id) {
if ((isEdit.value || isView.value) && route.params.id) {
try {
const record = await inventoryApi.get(Number(route.params.id))
const record = await inventoryApi.get(String(route.params.id))
editingRecord.value = record
Object.assign(formData, {
date: record.date,
partnerCompany: record.partnerCompany,
businessType: record.businessType,
product: record.product,
productId: record.productId || '',
quantity: record.quantity,
vehicleNo: record.vehicleNo || '',
planNo: record.planNo || '',
remarks: record.remarks || '',
purchasePlanId: record.purchasePlanId
})
// 初始化日期选择器的值(用于编辑时回显)
productDisplay.value = record.product || ''
if (record.date) {
dateSelected.value = dayjs(record.date).toDate()
}
@@ -423,16 +231,19 @@ const loadData = async () => {
}
const handleSubmit = async () => {
if (!formData.date || !formData.partnerCompany || !formData.product || !formData.quantity || !formData.vehicleNo) {
showToast({ type: 'fail', message: '请填写必填项' })
if (
!formData.date ||
!formData.partnerCompany ||
!formData.productId ||
!formData.quantity ||
!formData.vehicleNo
) {
showToast({ type: 'fail', message: '请填写必填项(含产品与车牌)' })
return
}
saving.value = true
try {
if (formData.vehicleNo?.trim()) {
await ensureVehicleExists(formData.vehicleNo.trim())
}
if (editingRecord.value) {
await inventoryApi.update(editingRecord.value.id, formData)
showToast({ type: 'success', message: '更新成功' })
@@ -440,7 +251,6 @@ const handleSubmit = async () => {
await inventoryApi.create(formData)
showToast({ type: 'success', message: '创建成功' })
}
// 根据来源跳转到对应页面(兼容 from=plan / from=plans)
const from = route.query.from as string
if (from === 'plan' || from === 'plans') {
router.push('/plans')
@@ -455,7 +265,6 @@ const handleSubmit = async () => {
}
const handleBack = () => {
// 根据来源跳转到对应页面(兼容 from=plan / from=plans)
const from = route.query.from as string
if (from === 'plan' || from === 'plans') {
router.push('/plans')
@@ -464,28 +273,7 @@ const handleBack = () => {
}
}
// 监听公司选择器弹窗,当打开时重置搜索关键词
watch(showCompanyPicker, (newVal) => {
if (newVal) {
companySearchKeyword.value = ''
}
})
watch(showCargoPicker, (newVal) => {
if (newVal) {
cargoSearchKeyword.value = ''
}
})
watch(showVehiclePicker, (newVal) => {
if (newVal) {
vehicleSearchKeyword.value = ''
loadVehicles()
}
})
onMounted(async () => {
await Promise.all([loadCompanies(), loadCargos()])
if (isEdit.value || isView.value) {
await loadData()
}
@@ -495,55 +283,6 @@ onMounted(async () => {
<style lang="scss" scoped>
@import '@/assets/styles/detail-page.scss';
.company-picker-container {
height: 100%;
display: flex;
flex-direction: column;
}
.search-container {
padding: 16px;
border-bottom: 1px solid #ebedf0;
background: #fff;
:deep(.van-search) {
padding: 0;
}
:deep(.van-search__content) {
background: #f7f8fa;
border-radius: 20px;
}
:deep(.van-field__control) {
font-size: 14px;
}
}
/* 不强制让 van-picker 占满剩余高度,使用 popup 的 max-height 控制总体高度 */
.vehicle-picker-container {
display: flex;
flex-direction: column;
max-height: 70vh;
}
.vehicle-picker-container .vehicle-list {
flex: 1;
overflow-y: auto;
}
.vehicle-picker-container .manual-input-row {
padding: 12px 16px;
border-top: 1px solid #ebedf0;
background: #fff;
}
.dialog-field {
padding: 12px 16px;
}
/* 数量为默认值 32 时红色标识,提醒用户关注 */
.quantity-default-red .form-label,
.quantity-default-red .form-label .required {
color: var(--van-danger-color, #ee0a24);
@@ -553,4 +292,3 @@ onMounted(async () => {
font-weight: 500;
}
</style>
+4 -3
View File
@@ -510,7 +510,7 @@ const canDeleteRecord = (item: InventoryRecord) => {
return dateStr >= today
}
const handleDelete = (id: number) => {
const handleDelete = (id: string) => {
showDialog({
title: '确认删除',
message: '确定要删除这条记录吗?',
@@ -540,13 +540,14 @@ const handleGenerateWeighing = (record: InventoryRecord) => {
inboundOutboundId: record.id.toString(),
vehicleNo: record.vehicleNo,
cargoName: record.product,
productId: record.productId || '',
businessType: record.businessType,
supplier: supplier,
consignee: consignee,
planNo: record.planNo || '',
purchasePlanId: record.purchasePlanId?.toString() || '',
batchNo: record.batchNo || '',
quantity: record.quantity.toString(), // 传递数量(吨)
quantity: record.quantity.toString(),
from: 'inventory'
}
})
@@ -555,7 +556,7 @@ const handleGenerateWeighing = (record: InventoryRecord) => {
/**
* 生成二维码图片地址:内容为出入库单 ID(与 H5 填写页一致)
*/
const getQrImageUrl = (inventoryId: number) =>
const getQrImageUrl = (inventoryId: string) =>
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(String(inventoryId))}`
/** 打开磅单二维码弹窗,供用户截图保存(不依赖 HTTPS) */
+27 -162
View File
@@ -38,7 +38,7 @@
<div v-if="isView" class="form-input">
{{ formData.customerName || '--' }}
</div>
<div v-else class="form-input" @click="showCompanyPicker = true">
<div v-else class="form-input" @click="showCustomerPicker = true">
<span :class="{ placeholder: !formData.customerName }">
{{ formData.customerName || '请选择往来单位' }}
</span>
@@ -49,11 +49,11 @@
<div class="form-item">
<div class="form-label">产品 <span class="required">*</span></div>
<div v-if="isView" class="form-input">
{{ formData.product || '--' }}
{{ productDisplay || '--' }}
</div>
<div v-else class="form-input" @click="showCargoPicker = true">
<span :class="{ placeholder: !formData.product }">
{{ formData.product || '请选择产品' }}
<div v-else class="form-input" @click="showProductPicker = true">
<span :class="{ placeholder: !productDisplay }">
{{ productDisplay || '请选择产品' }}
</span>
<van-icon name="arrow" class="arrow-icon" />
</div>
@@ -125,56 +125,15 @@
</van-form>
</div>
<!-- 公司选择器 -->
<van-popup v-model:show="showCompanyPicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="companySearchKeyword"
placeholder="搜索往来单位名称"
@search="handleCompanySearch"
@clear="handleCompanySearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCompanyOptions"
show-toolbar
@confirm="onCompanyConfirm"
@cancel="showCompanyPicker = false"
/>
</div>
</van-popup>
<DbCustomerPicker v-model:show="showCustomerPicker" @select="onCustomerSelect" />
<DbProductPicker v-model:show="showProductPicker" @select="onProductSelect" />
<!-- 货物选择器 -->
<van-popup v-model:show="showCargoPicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="cargoSearchKeyword"
placeholder="搜索产品名称"
@search="handleCargoSearch"
@clear="handleCargoSearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCargoOptions"
show-toolbar
@confirm="onCargoConfirm"
@cancel="showCargoPicker = false"
/>
</div>
</van-popup>
<!-- 开始日期选择器 -->
<van-calendar
v-model:show="showStartDateCalendar"
v-model="startDateSelected"
@confirm="onStartDateConfirm"
/>
<!-- 结束日期选择器 -->
<van-calendar
v-model:show="showEndDateCalendar"
v-model="endDateSelected"
@@ -184,52 +143,30 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import { planApi, type Plan, type PlanCreate } from '@/api/plansApi'
import { companyApi, cargoApi } from '@/api/masterDataApi'
import type { CrmCustomerOption, CrmProductOption } from '@/api/crmOptionsApi'
import DbCustomerPicker from '@/components/pickers/DbCustomerPicker.vue'
import DbProductPicker from '@/components/pickers/DbProductPicker.vue'
import dayjs from 'dayjs'
const router = useRouter()
const route = useRoute()
const isView = computed(() => route.query.mode === 'view')
const isEdit = computed(() => route.query.mode === 'edit' || route.params.id)
// const isAdd = computed(() => !isView.value && !isEdit.value) // 未使用
const isEdit = computed(() => route.query.mode === 'edit' || !!route.params.id)
const saving = ref(false)
const editingPlan = ref<Plan | null>(null)
const showCompanyPicker = ref(false)
const showCargoPicker = ref(false)
const showCustomerPicker = ref(false)
const showProductPicker = ref(false)
const showStartDateCalendar = ref(false)
const showEndDateCalendar = ref(false)
const startDateSelected = ref<Date | null>(null)
const endDateSelected = ref<Date | null>(null)
const companyOptions = ref<{ text: string; value: string }[]>([])
const cargoOptions = ref<{ text: string; value: string }[]>([])
// 搜索相关 - 往来单位
const companySearchKeyword = ref('')
const allCompanyOptions = ref<{ text: string; value: string }[]>([])
const filteredCompanyOptions = computed(() => {
if (!companySearchKeyword.value) return [allCompanyOptions.value]
const filtered = allCompanyOptions.value.filter(item =>
item.text.toLowerCase().includes(companySearchKeyword.value.toLowerCase())
)
return [filtered]
})
// 搜索相关 - 产品
const cargoSearchKeyword = ref('')
const allCargoOptions = ref<{ text: string; value: string }[]>([])
const filteredCargoOptions = computed(() => {
if (!cargoSearchKeyword.value) return [allCargoOptions.value]
const filtered = allCargoOptions.value.filter(item =>
item.text.toLowerCase().includes(cargoSearchKeyword.value.toLowerCase())
)
return [filtered]
})
const productDisplay = ref('')
const formData = reactive<PlanCreate>({
planNo: '',
@@ -237,57 +174,21 @@ const formData = reactive<PlanCreate>({
startDate: dayjs().format('YYYY-MM-DD'),
endDate: '',
businessType: '入库',
product: '',
productId: '',
plannedQuantity: 0,
customerName: '',
remarks: ''
})
// 初始化日期选择器的默认值
startDateSelected.value = dayjs().toDate()
const loadCompanies = async () => {
try {
const companies = await companyApi.list()
allCompanyOptions.value = companies.map(item => ({
text: item.name,
value: item.name
}))
companyOptions.value = allCompanyOptions.value
} catch (error) {
console.error('加载公司列表失败', error)
}
const onCustomerSelect = (item: CrmCustomerOption) => {
formData.customerName = item.customerName
}
const loadCargos = async () => {
try {
const cargos = await cargoApi.list()
allCargoOptions.value = cargos.map(item => ({
text: item.name,
value: item.name
}))
cargoOptions.value = allCargoOptions.value
} catch (error) {
console.error('加载货物列表失败', error)
}
}
const onCompanyConfirm = ({ selectedOptions }: any) => {
formData.customerName = selectedOptions[0].value
showCompanyPicker.value = false
}
const onCargoConfirm = ({ selectedOptions }: any) => {
formData.product = selectedOptions[0].value
showCargoPicker.value = false
}
const handleCompanySearch = () => {
// 触发 computed 过滤
}
const handleCargoSearch = () => {
// 触发 computed 过滤
const onProductSelect = (item: CrmProductOption) => {
formData.productId = item.id
productDisplay.value = item.productName
}
const onStartDateConfirm = (value: Date | Date[]) => {
@@ -305,9 +206,9 @@ const onEndDateConfirm = (value: Date | Date[]) => {
}
const loadData = async () => {
if (isEdit.value && route.params.id) {
if ((isEdit.value || isView.value) && route.params.id) {
try {
const plan = await planApi.get(Number(route.params.id))
const plan = await planApi.get(String(route.params.id))
editingPlan.value = plan
Object.assign(formData, {
planNo: plan.planNo || '',
@@ -315,12 +216,12 @@ const loadData = async () => {
startDate: plan.startDate || '',
endDate: plan.endDate || '',
businessType: plan.businessType,
product: plan.product,
productId: plan.productId || '',
plannedQuantity: plan.plannedQuantity,
customerName: plan.customerName || '',
remarks: plan.remarks || ''
})
// 初始化日期选择器的值
productDisplay.value = plan.product || ''
if (plan.startDate) {
startDateSelected.value = dayjs(plan.startDate).toDate()
}
@@ -335,8 +236,8 @@ const loadData = async () => {
}
const handleSubmit = async () => {
if (!formData.product || !formData.plannedQuantity) {
showToast({ type: 'fail', message: '请填写必填项' })
if (!formData.productId || !formData.plannedQuantity) {
showToast({ type: 'fail', message: '请填写必填项(含产品)' })
return
}
@@ -362,19 +263,10 @@ const handleBack = () => {
}
onMounted(async () => {
await Promise.all([loadCompanies(), loadCargos()])
if (isEdit.value || isView.value) {
await loadData()
}
})
// 打开弹窗时重置搜索关键词
watch(showCompanyPicker, (val) => {
if (val) companySearchKeyword.value = ''
})
watch(showCargoPicker, (val) => {
if (val) cargoSearchKeyword.value = ''
})
</script>
<style lang="scss" scoped>
@@ -469,7 +361,6 @@ watch(showCargoPicker, (val) => {
}
}
.radio-wrapper {
padding: 12px 0;
}
@@ -481,30 +372,4 @@ watch(showCargoPicker, (val) => {
margin-top: 24px;
padding: 0 16px;
}
.company-picker-container {
height: 100%;
display: flex;
flex-direction: column;
}
.search-container {
padding: 16px;
border-bottom: 1px solid #ebedf0;
background: #fff;
:deep(.van-search) {
padding: 0;
}
:deep(.van-search__content) {
background: #f7f8fa;
border-radius: 20px;
}
:deep(.van-field__control) {
font-size: 14px;
}
}
</style>
+3 -3
View File
@@ -437,7 +437,7 @@ const handleViewRelated = async (record: Plan) => {
}
}
const handleDelete = (id: number) => {
const handleDelete = (id: string) => {
showDialog({
title: '确认删除',
message: '确定要删除这个计划吗?',
@@ -455,17 +455,17 @@ const handleDelete = (id: number) => {
const handleGenerateInventory = (record: Plan) => {
router.push({
path: '/inventory',
path: '/inventory/detail',
query: {
planNo: record.planNo || '',
product: record.product || '',
productId: record.productId || '',
businessType: record.businessType || '',
customerName: record.customerName || '',
purchasePlanId: String(record.id),
from: 'plan'
}
})
showToast({ type: 'success', message: '已跳转到出入库页面' })
}
onMounted(() => {
+52 -204
View File
@@ -15,21 +15,22 @@
<div v-if="isView" class="form-input">
{{ formData.vehicleNo || '--' }}
</div>
<van-field
v-else
v-model="formData.vehicleNo"
placeholder="请输入车牌号"
/>
<div v-else class="form-input" @click="showVehiclePicker = true">
<span :class="{ placeholder: !formData.vehicleNo }">
{{ formData.vehicleNo || '请选择车牌号' }}
</span>
<van-icon name="arrow" class="arrow-icon" />
</div>
</div>
<div class="form-item">
<div class="form-label">货物名称 <span class="required">*</span></div>
<div v-if="isView" class="form-input">
{{ formData.cargoName || '--' }}
{{ cargoDisplay || '--' }}
</div>
<div v-else class="form-input" @click="showCargoPicker = true">
<span :class="{ placeholder: !formData.cargoName }">
{{ formData.cargoName || '请选择货物名称' }}
<span :class="{ placeholder: !cargoDisplay }">
{{ cargoDisplay || '请选择货物名称' }}
</span>
<van-icon name="arrow" class="arrow-icon" />
</div>
@@ -188,68 +189,10 @@
</van-form>
</div>
<!-- 货物选择器 -->
<van-popup v-model:show="showCargoPicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="cargoSearchKeyword"
placeholder="搜索货物名称"
@search="handleCargoSearch"
@clear="handleCargoSearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCargoOptions"
show-toolbar
@confirm="onCargoConfirm"
@cancel="showCargoPicker = false"
/>
</div>
</van-popup>
<!-- 供货单位选择器 -->
<van-popup v-model:show="showSupplierPicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="companySearchKeyword"
placeholder="搜索往来单位名称"
@search="handleCompanySearch"
@clear="handleCompanySearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCompanyOptions"
show-toolbar
@confirm="onSupplierConfirm"
@cancel="showSupplierPicker = false"
/>
</div>
</van-popup>
<!-- 收货单位选择器 -->
<van-popup v-model:show="showConsigneePicker" round position="bottom" :style="{ maxHeight: '70vh' }">
<div class="company-picker-container">
<div class="search-container">
<van-search
v-model="companySearchKeyword"
placeholder="搜索往来单位名称"
@search="handleCompanySearch"
@clear="handleCompanySearch"
shape="round"
/>
</div>
<van-picker
:columns="filteredCompanyOptions"
show-toolbar
@confirm="onConsigneeConfirm"
@cancel="showConsigneePicker = false"
/>
</div>
</van-popup>
<DbProductPicker v-model:show="showCargoPicker" @select="onProductSelect" />
<DbCustomerPicker v-model:show="showSupplierPicker" @select="onSupplierSelect" />
<DbCustomerPicker v-model:show="showConsigneePicker" @select="onConsigneeSelect" />
<DbVehiclePicker v-model:show="showVehiclePicker" @select="onVehicleSelect" />
<!-- 毛重日期选择器 -->
<van-calendar
@@ -290,50 +233,31 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import { weighingApi, type WeighingRecord, type WeighingCreate } from '@/api/weighingApi'
import { companyApi, cargoApi } from '@/api/masterDataApi'
import type { CrmCustomerOption, CrmProductOption } from '@/api/crmOptionsApi'
import { inventoryApi } from '@/api/inventoryApi'
import { planApi } from '@/api/plansApi'
import DbCustomerPicker from '@/components/pickers/DbCustomerPicker.vue'
import DbProductPicker from '@/components/pickers/DbProductPicker.vue'
import DbVehiclePicker from '@/components/pickers/DbVehiclePicker.vue'
import dayjs from 'dayjs'
const router = useRouter()
const route = useRoute()
const isView = computed(() => route.query.mode === 'view')
const isEdit = computed(() => route.query.mode === 'edit' || route.params.id)
// const isAdd = computed(() => !isView.value && !isEdit.value) // 未使用
const isEdit = computed(() => route.query.mode === 'edit' || !!route.params.id)
const saving = ref(false)
const editingRecord = ref<WeighingRecord | null>(null)
const showCargoPicker = ref(false)
const showSupplierPicker = ref(false)
const showConsigneePicker = ref(false)
const companyOptions = ref<{ text: string; value: string }[]>([])
const cargoOptions = ref<{ text: string; value: string }[]>([])
// 搜索相关
const companySearchKeyword = ref('')
const allCompanyOptions = ref<{ text: string; value: string }[]>([])
const filteredCompanyOptions = computed(() => {
if (!companySearchKeyword.value) return [allCompanyOptions.value]
const filtered = allCompanyOptions.value.filter(item =>
item.text.toLowerCase().includes(companySearchKeyword.value.toLowerCase())
)
return [filtered]
})
const cargoSearchKeyword = ref('')
const allCargoOptions = ref<{ text: string; value: string }[]>([])
const filteredCargoOptions = computed(() => {
if (!cargoSearchKeyword.value) return [allCargoOptions.value]
const filtered = allCargoOptions.value.filter(item =>
item.text.toLowerCase().includes(cargoSearchKeyword.value.toLowerCase())
)
return [filtered]
})
const showVehiclePicker = ref(false)
const cargoDisplay = ref('')
// 毛重时间选择器相关状态
const showGrossWeightDateCalendar = ref(false)
@@ -368,7 +292,7 @@ const timeColumns = generateTimeColumns()
const formData = reactive<WeighingCreate>({
vehicleNo: '',
cargoName: '',
productId: '',
businessType: '入库',
supplier: '',
consignee: '',
@@ -407,51 +331,46 @@ const initFromRoute = async () => {
if (inboundOutboundId) {
try {
// 获取出入库单数据
const inventory = await inventoryApi.get(Number(inboundOutboundId))
// 填充基础数据(优先使用路由参数,如果没有则使用出入库单数据)
const inventory = await inventoryApi.get(String(inboundOutboundId))
formData.vehicleNo = (route.query.vehicleNo as string) || inventory.vehicleNo
formData.cargoName = (route.query.cargoName as string) || inventory.product
formData.productId = (route.query.productId as string) || inventory.productId || ''
cargoDisplay.value = (route.query.cargoName as string) || inventory.product
const businessType = (route.query.businessType as '入库' | '出库') || inventory.businessType
formData.businessType = businessType
// 根据业务类型设置供货单位和收货单位
// 入库单:供货单位 = 往来单位,收货单位 = 安庆市山峡化工有限公司
// 出库单:供货单位 = 安庆市山峡化工有限公司,收货单位 = 往来单位
if (route.query.supplier) {
formData.supplier = route.query.supplier as string
} else {
formData.supplier = businessType === '入库' ? inventory.partnerCompany : '安庆市山峡化工有限公司'
}
if (route.query.consignee) {
formData.consignee = route.query.consignee as string
} else {
formData.consignee = businessType === '入库' ? '安庆市山峡化工有限公司' : inventory.partnerCompany
}
formData.planNo = (route.query.planNo as string) || inventory.planNo || ''
// 填充出入库单号(优先使用路由参数,如果没有则使用出入库单数据)
formData.batchNo = (route.query.batchNo as string) || inventory.batchNo || ''
formData.inboundOutboundId = Number(inboundOutboundId)
formData.inboundOutboundId = String(inboundOutboundId)
formData.weighingPoint = '山峡化工'
// 本次量:优先使用路由参数中的quantity(吨),如果没有则使用出入库单的quantity(吨),然后转换为kg
const quantityInTons = route.query.quantity ? Number(route.query.quantity) : inventory.quantity
formData.currentQuantity = quantityInTons * 1000
// 如果有计划ID,获取计划数据(优先使用路由参数)
const purchasePlanId = (route.query.purchasePlanId as string) || inventory.purchasePlanId?.toString()
if (purchasePlanId) {
// 先设置计划ID,确保即使获取计划数据失败也能保存计划ID
formData.purchasePlanId = Number(purchasePlanId)
formData.purchasePlanId = String(purchasePlanId)
try {
const plan = await planApi.get(Number(purchasePlanId))
// 计划量就是计划的plannedQuantity(单位:吨),需要转换为kg
const plan = await planApi.get(String(purchasePlanId))
formData.plannedQuantity = plan.plannedQuantity * 1000
if (!formData.productId && plan.productId) {
formData.productId = plan.productId
cargoDisplay.value = cargoDisplay.value || plan.product
}
} catch (error) {
console.error('获取计划数据失败', error)
// 即使获取计划数据失败,计划ID也已经设置了
}
}
} catch (error) {
@@ -513,53 +432,21 @@ const calculateNetWeight = () => {
netWeight.value // 触发计算
}
const loadCompanies = async () => {
try {
const companies = await companyApi.list()
allCompanyOptions.value = companies.map(item => ({
text: item.name,
value: item.name
}))
companyOptions.value = allCompanyOptions.value
} catch (error) {
console.error('加载公司列表失败', error)
}
const onProductSelect = (item: CrmProductOption) => {
formData.productId = item.id
cargoDisplay.value = item.productName
}
const loadCargos = async () => {
try {
const cargos = await cargoApi.list()
allCargoOptions.value = cargos.map(item => ({
text: item.name,
value: item.name
}))
cargoOptions.value = allCargoOptions.value
} catch (error) {
console.error('加载货物列表失败', error)
}
const onSupplierSelect = (item: CrmCustomerOption) => {
formData.supplier = item.customerName
}
const onCargoConfirm = ({ selectedOptions }: any) => {
formData.cargoName = selectedOptions[0].value
showCargoPicker.value = false
const onConsigneeSelect = (item: CrmCustomerOption) => {
formData.consignee = item.customerName
}
const onSupplierConfirm = ({ selectedOptions }: any) => {
formData.supplier = selectedOptions[0].value
showSupplierPicker.value = false
}
const onConsigneeConfirm = ({ selectedOptions }: any) => {
formData.consignee = selectedOptions[0].value
showConsigneePicker.value = false
}
const handleCompanySearch = () => {
// 触发 computed 过滤
}
const handleCargoSearch = () => {
// 触发 computed 过滤
const onVehicleSelect = (licensePlate: string) => {
formData.vehicleNo = licensePlate
}
// 拆分日期时间字符串为日期和时间部分
@@ -685,11 +572,11 @@ const onTareWeightTimeCancel = () => {
const loadData = async () => {
if (isEdit.value && route.params.id) {
try {
const record = await weighingApi.get(Number(route.params.id))
const record = await weighingApi.get(String(route.params.id))
editingRecord.value = record
Object.assign(formData, {
vehicleNo: record.vehicleNo,
cargoName: record.cargoName,
productId: record.productId || '',
businessType: record.businessType,
supplier: record.supplier,
consignee: record.consignee,
@@ -709,6 +596,7 @@ const loadData = async () => {
executedQuantity: record.executedQuantity,
batchNo: record.batchNo || ''
})
cargoDisplay.value = record.cargoName || ''
// 拆分并设置日期时间选择器的值(用于编辑时回显)
if (record.grossWeightTime) {
@@ -735,8 +623,8 @@ const loadData = async () => {
const handleSubmit = async () => {
// 只验证必填项:车牌号、货物名称、供货单位、收货单位
// 毛重和皮重可以分步保存,不强制要求
if (!formData.vehicleNo || !formData.cargoName || !formData.supplier || !formData.consignee) {
showToast({ type: 'fail', message: '请填写必填项' })
if (!formData.vehicleNo || !formData.productId || !formData.supplier || !formData.consignee) {
showToast({ type: 'fail', message: '请填写必填项(含产品与车牌)' })
return
}
@@ -789,25 +677,12 @@ const handleBack = () => {
}
onMounted(async () => {
await Promise.all([loadCompanies(), loadCargos()])
if (isEdit.value || isView.value) {
await loadData()
} else {
// 如果是新增模式,且是从出入库管理跳转过来的,初始化数据
await initFromRoute()
}
})
// 打开弹窗时重置搜索关键词
watch(showCargoPicker, (val) => {
if (val) cargoSearchKeyword.value = ''
})
watch(showSupplierPicker, (val) => {
if (val) companySearchKeyword.value = ''
})
watch(showConsigneePicker, (val) => {
if (val) companySearchKeyword.value = ''
})
</script>
<style lang="scss" scoped>
@@ -822,32 +697,5 @@ watch(showConsigneePicker, (val) => {
.text-danger {
color: #ee0a24;
}
.company-picker-container {
height: 100%;
display: flex;
flex-direction: column;
}
.search-container {
padding: 16px;
border-bottom: 1px solid #ebedf0;
background: #fff;
:deep(.van-search) {
padding: 0;
}
:deep(.van-search__content) {
background: #f7f8fa;
border-radius: 20px;
}
:deep(.van-field__control) {
font-size: 14px;
}
}
/* 不强制让 van-picker 占满剩余高度,使用 popup 的 max-height 控制总体高度 */
</style>
+1 -1
View File
@@ -429,7 +429,7 @@ const isWeighingRecordToday = (item: WeighingRecord) => {
return dateStr === dayjs().format('YYYY-MM-DD')
}
const handleVoid = (id: number) => {
const handleVoid = (id: string) => {
showDialog({
title: '确认作废',
message: '确定要作废这条地磅单吗?',