feat: public vehicle plate options for H5 fill pages; select-only plates
Add GET /api/public/db/vehicle-plates for unauthenticated fill/plan pages and remove manual plate entry plus auto-create. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
db06fea45a
commit
5f2a6f2338
6 files changed
+145
-21
No files matched your search
@@ -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
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -65,7 +65,12 @@
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label">车牌号 <span class="required">*</span></div>
|
||||
<van-field v-model="formData.vehicleNo" placeholder="请输入车牌号" />
|
||||
<div 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">备注</div>
|
||||
@@ -87,6 +92,12 @@
|
||||
</template>
|
||||
|
||||
<van-calendar v-model:show="showDatePicker" @confirm="onDateConfirm" />
|
||||
<DbVehiclePicker
|
||||
v-model:show="showVehiclePicker"
|
||||
public-mode
|
||||
:inventory-id="String(route.params.id || '')"
|
||||
@select="onVehicleSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<InventoryRecord | null>(null)
|
||||
const showDatePicker = ref(false)
|
||||
const showVehiclePicker = ref(false)
|
||||
|
||||
const formData = reactive<InventoryUpdate>({
|
||||
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: '保存成功' })
|
||||
|
||||
@@ -174,10 +174,12 @@
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label">车牌号 *</div>
|
||||
<van-field
|
||||
v-model="createForm.vehicleNo"
|
||||
placeholder="请输入车牌号"
|
||||
/>
|
||||
<div class="form-input" @click="showVehiclePicker = true">
|
||||
<span :class="{ placeholder: !createForm.vehicleNo }">
|
||||
{{ createForm.vehicleNo || '请选择车牌号' }}
|
||||
</span>
|
||||
<van-icon name="arrow" class="arrow-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label">备注</div>
|
||||
@@ -270,6 +272,13 @@
|
||||
<div class="copy-overlay__text">图片生成中...</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
|
||||
<DbVehiclePicker
|
||||
v-model:show="showVehiclePicker"
|
||||
public-mode
|
||||
:plan-id="String(route.params.id || '')"
|
||||
@select="onVehicleSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<Pick<InventoryCreate, 'quantity' | 'vehicleNo' | 'remarks'>>({
|
||||
quantity: 0,
|
||||
vehicleNo: '',
|
||||
@@ -310,7 +321,7 @@ const copyingImage = ref(false)
|
||||
const captureInventory = ref<InventoryRecord | null>(null)
|
||||
const captureRef = ref<HTMLElement | null>(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 {
|
||||
|
||||
Reference in new issue
Block a user