Просмотр исходного кода

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>
zlwu 3 месяцев назад
Родитель
Сommit
db06fea45a

+ 0 - 5
mobile-h5/src/api/menuApi.ts

@@ -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 }
   }
 ]
 

+ 0 - 42
mobile-h5/src/router/index.ts

@@ -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
mobile-h5/src/views/dashboard/index.vue

@@ -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
mobile-h5/src/views/inventory/detail.vue

@@ -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>
-
-    <!-- 货物选择器 -->
-    <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>
+    <DbCustomerPicker v-model:show="showCustomerPicker" @select="onCustomerSelect" />
+    <DbProductPicker v-model:show="showProductPicker" @select="onProductSelect" />
+    <DbVehiclePicker v-model:show="showVehiclePicker" @select="onVehicleSelect" />
 
-    <!-- 日期选择器 -->
     <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 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)
-  )
-})
+const productDisplay = ref((route.query.product as string) || '')
 
-// 如果是从计划管理跳转过来的(有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 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 onProductSelect = (item: CrmProductOption) => {
+  formData.productId = item.id
+  productDisplay.value = item.productName
 }
 
-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
mobile-h5/src/views/inventory/index.vue

@@ -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
mobile-h5/src/views/plans/detail.vue

@@ -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>
-
-    <!-- 货物选择器 -->
-    <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>
+    <DbCustomerPicker v-model:show="showCustomerPicker" @select="onCustomerSelect" />
+    <DbProductPicker v-model:show="showProductPicker" @select="onProductSelect" />
 
-    <!-- 开始日期选择器 -->
     <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 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 onCustomerSelect = (item: CrmCustomerOption) => {
+  formData.customerName = item.customerName
 }
 
-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
mobile-h5/src/views/plans/index.vue

@@ -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
mobile-h5/src/views/weighing/detail.vue

@@ -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 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 onCargoConfirm = ({ selectedOptions }: any) => {
-  formData.cargoName = selectedOptions[0].value
-  showCargoPicker.value = false
+const onProductSelect = (item: CrmProductOption) => {
+  formData.productId = item.id
+  cargoDisplay.value = item.productName
 }
 
-const onSupplierConfirm = ({ selectedOptions }: any) => {
-  formData.supplier = selectedOptions[0].value
-  showSupplierPicker.value = false
+const onSupplierSelect = (item: CrmCustomerOption) => {
+  formData.supplier = item.customerName
 }
 
-const onConsigneeConfirm = ({ selectedOptions }: any) => {
-  formData.consignee = selectedOptions[0].value
-  showConsigneePicker.value = false
+const onConsigneeSelect = (item: CrmCustomerOption) => {
+  formData.consignee = item.customerName
 }
 
-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
mobile-h5/src/views/weighing/index.vue

@@ -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: '确定要作废这条地磅单吗?',