diff --git a/backend/app/api/db/public_vehicles.py b/backend/app/api/db/public_vehicles.py
new file mode 100644
index 000000000..48fd01ba7
--- /dev/null
+++ b/backend/app/api/db/public_vehicles.py
@@ -0,0 +1,66 @@
+"""地磅公共只读:免登车牌 options(现场填写页)"""
+from __future__ import annotations
+
+from typing import Optional
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session
+
+from app.core.database import get_db
+from app.models.db_models import DbInboundOutbound, DbPurchasePlan
+from app.models.pl_models import PlVehicle
+
+router = APIRouter(prefix="/public/db", tags=["地磅-公共车牌"])
+
+
+def _resolve_tenant_no(
+ db: Session,
+ inventory_id: Optional[str],
+ plan_id: Optional[str],
+) -> str:
+ if inventory_id:
+ row = (
+ db.query(DbInboundOutbound)
+ .filter(DbInboundOutbound.id == inventory_id, DbInboundOutbound.del_flag == 0)
+ .first()
+ )
+ if not row:
+ raise HTTPException(status_code=404, detail="出入库单不存在")
+ return row.tenant_no
+ if plan_id:
+ plan = (
+ db.query(DbPurchasePlan)
+ .filter(DbPurchasePlan.id == plan_id, DbPurchasePlan.del_flag == 0)
+ .first()
+ )
+ if not plan:
+ raise HTTPException(status_code=404, detail="计划不存在")
+ return plan.tenant_no
+ raise HTTPException(status_code=400, detail="需要提供 inventoryId 或 planId")
+
+
+@router.get("/vehicle-plates")
+def list_public_vehicle_plates(
+ q: Optional[str] = Query(None),
+ inventory_id: Optional[str] = Query(None, alias="inventoryId"),
+ plan_id: Optional[str] = Query(None, alias="planId"),
+ db: Session = Depends(get_db),
+):
+ """免登:按出入库/计划解析 tenant_no,返回 pl_vehicle 车牌(self + carrier)。"""
+ tenant_no = _resolve_tenant_no(db, inventory_id, plan_id)
+ query = db.query(PlVehicle).filter(
+ PlVehicle.tenant_no == tenant_no,
+ PlVehicle.del_flag == 0,
+ PlVehicle.vehicle_type.in_(["self", "carrier"]),
+ )
+ kw = (q or "").strip()
+ if kw:
+ query = query.filter(PlVehicle.license_plate.contains(kw))
+ rows = (
+ query.order_by(PlVehicle.create_time.desc()).limit(100).all()
+ )
+ return [
+ {"licensePlate": r.license_plate, "vehicleType": r.vehicle_type}
+ for r in rows
+ if r.license_plate
+ ]
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index 8fc6271fa..fe72d9c49 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -177,7 +177,8 @@ class Settings(BaseSettings):
"/api/public/pl/site,"
"/api/public/pl/inbound,"
"/api/public/pl/outbound,"
- "/api/public/db/inventory"
+ "/api/public/db/inventory,"
+ "/api/public/db/vehicle-plates"
)
class Config:
diff --git a/backend/app/main.py b/backend/app/main.py
index e52f32f5f..c0deabd0e 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -37,6 +37,7 @@ from app.api.pl.public_io_order import (
outbound_public_router as public_pl_outbound_router,
)
from app.api.db.public_inventory import router as public_db_inventory_router
+from app.api.db.public_vehicles import router as public_db_vehicles_router
@asynccontextmanager
@@ -159,6 +160,7 @@ app.include_router(public_pl_site_router, prefix=settings.API_V1_PREFIX)
app.include_router(public_pl_inbound_router, prefix=settings.API_V1_PREFIX)
app.include_router(public_pl_outbound_router, prefix=settings.API_V1_PREFIX)
app.include_router(public_db_inventory_router, prefix=settings.API_V1_PREFIX)
+app.include_router(public_db_vehicles_router, prefix=settings.API_V1_PREFIX)
app.include_router(menu_router, prefix=settings.API_V1_PREFIX)
app.include_router(admin_router, prefix=settings.API_V1_PREFIX)
app.include_router(workflow_router, prefix=settings.API_V1_PREFIX)
diff --git a/backend/tests/test_public_db_vehicle_plates.py b/backend/tests/test_public_db_vehicle_plates.py
new file mode 100644
index 000000000..20f229d63
--- /dev/null
+++ b/backend/tests/test_public_db_vehicle_plates.py
@@ -0,0 +1,11 @@
+"""免登车牌 options 接口入参校验(不依赖真实 DB 数据)。"""
+from fastapi.testclient import TestClient
+
+from app.main import app
+
+client = TestClient(app)
+
+
+def test_vehicle_plates_requires_inventory_or_plan_id():
+ resp = client.get("/api/public/db/vehicle-plates")
+ assert resp.status_code == 400
diff --git a/mobile-h5/src/views/inventory/fill.vue b/mobile-h5/src/views/inventory/fill.vue
index ec6042a26..822eda9c5 100644
--- a/mobile-h5/src/views/inventory/fill.vue
+++ b/mobile-h5/src/views/inventory/fill.vue
@@ -65,7 +65,12 @@
@@ -95,7 +106,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { showToast } from 'vant'
import { inventoryApi, type InventoryRecord, type InventoryUpdate } from '@/api/inventoryApi'
-import { vehicleApi } from '@/api/masterDataApi'
+import DbVehiclePicker from '@/components/pickers/DbVehiclePicker.vue'
import dayjs from 'dayjs'
const route = useRoute()
@@ -103,6 +114,7 @@ const loading = ref(true)
const saving = ref(false)
const record = ref(null)
const showDatePicker = ref(false)
+const showVehiclePicker = ref(false)
const formData = reactive({
date: '',
@@ -131,7 +143,7 @@ const loadData = async () => {
if (!id) return
loading.value = true
try {
- record.value = await inventoryApi.get(Number(id))
+ record.value = await inventoryApi.get(String(id))
formData.date = record.value.date || ''
formData.quantity = record.value.quantity
formData.vehicleNo = record.value.vehicleNo || ''
@@ -149,14 +161,8 @@ const onDateConfirm = (value: Date | Date[]) => {
showDatePicker.value = false
}
-// 保存前确保车牌号已写入车牌号表(新车牌则先创建,与 Web 行为一致)
-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 onVehicleSelect = (licensePlate: string) => {
+ formData.vehicleNo = licensePlate
}
const handleSubmit = async () => {
@@ -170,12 +176,10 @@ const handleSubmit = async () => {
if (!record.value) return
saving.value = true
try {
- await ensureVehicleExists(vehicle)
await inventoryApi.update(record.value.id, {
date,
quantity: Number(qty),
vehicleNo: vehicle,
- // 备注清空时传 null,否则后端不会更新该字段
remarks: formData.remarks === '' ? null : (formData.remarks || undefined)
})
showToast({ type: 'success', message: '保存成功' })
diff --git a/mobile-h5/src/views/inventory/plan.vue b/mobile-h5/src/views/inventory/plan.vue
index ea61a3931..2ed2b11fe 100644
--- a/mobile-h5/src/views/inventory/plan.vue
+++ b/mobile-h5/src/views/inventory/plan.vue
@@ -174,10 +174,12 @@
+
+
@@ -280,6 +289,7 @@ import dayjs from 'dayjs'
import { showToast } from 'vant'
import { planApi, type ThirdPartyPlanPageData } from '@/api/plansApi'
import { inventoryApi, type InventoryCreate, type InventoryRecord } from '@/api/inventoryApi'
+import DbVehiclePicker from '@/components/pickers/DbVehiclePicker.vue'
import { copyImageBlobToClipboard } from '@/utils/utils'
import html2canvas from 'html2canvas'
@@ -293,6 +303,7 @@ const todayStr = dayjs().format('YYYY-MM-DD')
const createDialogOpen = ref(false)
const creating = ref(false)
+const showVehiclePicker = ref(false)
const createForm = reactive>({
quantity: 0,
vehicleNo: '',
@@ -310,7 +321,7 @@ const copyingImage = ref(false)
const captureInventory = ref(null)
const captureRef = ref(null)
-const getQrImageUrl = (inventoryId: number) =>
+const getQrImageUrl = (inventoryId: string) =>
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(
String(inventoryId)
)}`
@@ -321,7 +332,7 @@ const qrImageSrc = computed(() => {
})
const loadPage = async () => {
- const id = Number(route.params.id)
+ const id = String(route.params.id || '')
if (!id) {
errorMessage.value = '链接无效:缺少计划ID'
loading.value = false
@@ -346,6 +357,10 @@ const openCreateDialog = () => {
createDialogOpen.value = true
}
+const onVehicleSelect = (licensePlate: string) => {
+ createForm.vehicleNo = licensePlate
+}
+
const handleCreateSubmit = async () => {
if (!pageData.value) return
const qty = createForm.quantity
@@ -354,6 +369,10 @@ const handleCreateSubmit = async () => {
showToast({ type: 'fail', message: '请填写数量和车牌号' })
return
}
+ if (!pageData.value.plan.productId) {
+ showToast({ type: 'fail', message: '计划缺少产品,无法创建出入库单' })
+ return
+ }
creating.value = true
try {
const p = pageData.value.plan
@@ -361,7 +380,7 @@ const handleCreateSubmit = async () => {
date: todayStr,
partnerCompany: p.customerName || '待填写',
businessType: p.businessType,
- product: p.product,
+ productId: p.productId,
quantity: Number(qty),
planNo: p.planNo,
vehicleNo: vehicle,
@@ -586,6 +605,27 @@ onMounted(() => {
background: #f9fafb;
border: 1px solid #e5e7eb;
}
+
+ .form-input {
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 10px 12px;
+ font-size: 14px;
+ color: #111827;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 40px;
+
+ .placeholder {
+ color: #9ca3af;
+ }
+
+ .arrow-icon {
+ color: #9ca3af;
+ }
+ }
}
.dialog-actions {