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 @@
车牌号 *
- +
+ + {{ formData.vehicleNo || '请选择车牌号' }} + + +
备注
@@ -87,6 +92,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 @@
车牌号 *
- +
+ + {{ createForm.vehicleNo || '请选择车牌号' }} + + +
备注
@@ -270,6 +272,13 @@
图片生成中...
+ + @@ -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 {