feat: 完成履约调度闭环
This commit is contained in:
parent
33e921fa74
commit
181e56d627
@ -1,56 +1,113 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.api.deps import get_logistics_service
|
||||
from backend.app.db import get_db_session
|
||||
from backend.app.schemas.common import success_payload
|
||||
from backend.app.services.logistics_service import logistics_service
|
||||
from backend.app.schemas.logistics import CreateLogisticsTaskRequest
|
||||
from backend.app.services.logistics_service import LogisticsService
|
||||
|
||||
router = APIRouter(tags=["logistics"])
|
||||
|
||||
|
||||
@router.get("/api/logistics/tasks")
|
||||
def list_logistics_tasks() -> dict:
|
||||
return success_payload(logistics_service.list_tasks())
|
||||
def list_logistics_tasks(
|
||||
order_id: int | None = Query(default=None),
|
||||
task_no: str | None = Query(default=None),
|
||||
status: str | None = Query(default=None),
|
||||
driver_id: int | None = Query(default=None),
|
||||
factory_id: int | None = Query(default=None),
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(
|
||||
logistics_service.list_tasks(
|
||||
session=session,
|
||||
filters={
|
||||
"order_id": order_id,
|
||||
"task_no": task_no,
|
||||
"status": status,
|
||||
"driver_id": driver_id,
|
||||
"factory_id": factory_id,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/logistics/tasks")
|
||||
def create_logistics_task() -> dict:
|
||||
return success_payload(logistics_service.create_task())
|
||||
def create_logistics_task(
|
||||
payload: CreateLogisticsTaskRequest,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.create_task(payload.model_dump(), session))
|
||||
|
||||
|
||||
@router.get("/api/logistics/tasks/{task_id}")
|
||||
def get_logistics_task(task_id: int) -> dict:
|
||||
return success_payload(logistics_service.get_task(task_id))
|
||||
def get_logistics_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.get_task(task_id, session))
|
||||
|
||||
|
||||
@router.get("/api/driver/tasks")
|
||||
def list_driver_tasks() -> dict:
|
||||
return success_payload(logistics_service.list_driver_tasks())
|
||||
def list_driver_tasks(
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.list_driver_tasks(session))
|
||||
|
||||
|
||||
@router.get("/api/driver/tasks/{task_id}")
|
||||
def get_driver_task(task_id: int) -> dict:
|
||||
return success_payload(logistics_service.get_driver_task(task_id))
|
||||
def get_driver_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.get_driver_task(task_id, session))
|
||||
|
||||
|
||||
@router.post("/api/driver/tasks/{task_id}/accept")
|
||||
def accept_driver_task(task_id: int) -> dict:
|
||||
return success_payload(logistics_service.accept_task(task_id))
|
||||
def accept_driver_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.accept_task(task_id, session))
|
||||
|
||||
|
||||
@router.post("/api/driver/tasks/{task_id}/pickup")
|
||||
def pickup_driver_task(task_id: int) -> dict:
|
||||
return success_payload(logistics_service.pickup_task(task_id))
|
||||
def pickup_driver_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.pickup_task(task_id, session))
|
||||
|
||||
|
||||
@router.post("/api/driver/tasks/{task_id}/deliver")
|
||||
def deliver_driver_task(task_id: int) -> dict:
|
||||
return success_payload(logistics_service.deliver_task(task_id))
|
||||
def deliver_driver_task(
|
||||
task_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.deliver_task(task_id, session))
|
||||
|
||||
|
||||
@router.get("/api/logistics/{order_id}/trace")
|
||||
def get_logistics_trace(order_id: int) -> dict:
|
||||
def get_logistics_trace(
|
||||
order_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.get_trace(order_id))
|
||||
|
||||
|
||||
@router.post("/api/logistics/{order_id}/trace")
|
||||
def create_logistics_trace(order_id: int) -> dict:
|
||||
def create_logistics_trace(
|
||||
order_id: int,
|
||||
logistics_service: LogisticsService = Depends(get_logistics_service),
|
||||
) -> dict:
|
||||
return success_payload(logistics_service.create_trace(order_id))
|
||||
|
||||
|
||||
50
backend/app/repositories/logistics_repository.py
Normal file
50
backend/app/repositories/logistics_repository.py
Normal file
@ -0,0 +1,50 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.models.business import LogisticsTask
|
||||
|
||||
|
||||
class LogisticsRepository:
|
||||
"""物流任务数据访问层。"""
|
||||
|
||||
def list_tasks_by_filters(self, session: Session, filters: dict) -> list[LogisticsTask]:
|
||||
stmt = select(LogisticsTask).where(LogisticsTask.deleted == 0)
|
||||
|
||||
if filters.get("order_id") is not None:
|
||||
stmt = stmt.where(LogisticsTask.order_id == filters["order_id"])
|
||||
if filters.get("task_no"):
|
||||
stmt = stmt.where(LogisticsTask.task_no.contains(filters["task_no"]))
|
||||
if filters.get("status"):
|
||||
stmt = stmt.where(LogisticsTask.status == filters["status"])
|
||||
if filters.get("driver_id") is not None:
|
||||
stmt = stmt.where(LogisticsTask.driver_id == filters["driver_id"])
|
||||
if filters.get("factory_id") is not None:
|
||||
stmt = stmt.where(LogisticsTask.factory_id == filters["factory_id"])
|
||||
|
||||
stmt = stmt.order_by(LogisticsTask.id.desc())
|
||||
return list(session.execute(stmt).scalars())
|
||||
|
||||
def get_task(self, session: Session, task_id: int) -> LogisticsTask | None:
|
||||
stmt = select(LogisticsTask).where(LogisticsTask.id == task_id, LogisticsTask.deleted == 0)
|
||||
return session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def get_task_by_order_id(self, session: Session, order_id: int) -> LogisticsTask | None:
|
||||
stmt = (
|
||||
select(LogisticsTask)
|
||||
.where(LogisticsTask.order_id == order_id, LogisticsTask.deleted == 0)
|
||||
.order_by(LogisticsTask.id.desc())
|
||||
)
|
||||
return session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def create_task(self, session: Session, payload: dict) -> LogisticsTask:
|
||||
task = LogisticsTask(**payload)
|
||||
session.add(task)
|
||||
session.flush()
|
||||
return task
|
||||
|
||||
def update_task_status(self, session: Session, task: LogisticsTask, status: str) -> LogisticsTask:
|
||||
task.status = status
|
||||
session.add(task)
|
||||
session.flush()
|
||||
return task
|
||||
|
||||
23
backend/app/schemas/logistics.py
Normal file
23
backend/app/schemas/logistics.py
Normal file
@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LogisticsTaskListQuery(BaseModel):
|
||||
order_id: int | None = None
|
||||
task_no: str | None = None
|
||||
status: str | None = None
|
||||
driver_id: int | None = None
|
||||
factory_id: int | None = None
|
||||
page_no: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
|
||||
class CreateLogisticsTaskRequest(BaseModel):
|
||||
order_id: int
|
||||
driver_id: int = Field(gt=0)
|
||||
pickup_address: str
|
||||
delivery_address: str
|
||||
pickup_content: str
|
||||
quantity: float = Field(gt=0)
|
||||
factory_id: int | None = None
|
||||
remark: str | None = None
|
||||
|
||||
@ -36,17 +36,41 @@ class DemoStore:
|
||||
"customer_name": "李四",
|
||||
"customer_mobile": "13900000000",
|
||||
"salesman_name": "张三",
|
||||
"order_status": "draft",
|
||||
"order_status": "approved",
|
||||
"customer_address": "杭州市西湖区演示路 1 号",
|
||||
"order_source": "线下拜访",
|
||||
"delivery_type": "工厂直送",
|
||||
"factory_id": 1001,
|
||||
"factory_name": "工厂A",
|
||||
"sale_price_total": 100.0,
|
||||
"cost_price_total": 60.0,
|
||||
"profit_total": 20.0,
|
||||
"profit_rate": 20.0,
|
||||
"commission_amount": 50.0,
|
||||
"created_at": "2026-05-14 10:00:00",
|
||||
"items": [],
|
||||
"items": [
|
||||
{
|
||||
"item_id": 1,
|
||||
"product_id": 2001,
|
||||
"product_name": "产品A",
|
||||
"specification": "10kg",
|
||||
"unit": "吨",
|
||||
"quantity": 1,
|
||||
"sale_price": 100,
|
||||
"cost_price": 60,
|
||||
"rebate_amount": 0,
|
||||
"freight_amount": 0,
|
||||
"tax_amount": 0,
|
||||
"other_fee_amount": 0,
|
||||
"remark": "演示明细",
|
||||
}
|
||||
],
|
||||
"approve_logs": [],
|
||||
"attachments": [],
|
||||
"remark": "演示订单",
|
||||
}
|
||||
]
|
||||
self.logistics_tasks = []
|
||||
|
||||
def login(self, username: str, role_type: str) -> dict | None:
|
||||
user = self.users.get((username, role_type))
|
||||
@ -95,5 +119,50 @@ class DemoStore:
|
||||
self.orders.append(order)
|
||||
return deepcopy(order)
|
||||
|
||||
def list_logistics_tasks(self) -> dict:
|
||||
return {
|
||||
"total": len(self.logistics_tasks),
|
||||
"page_no": 1,
|
||||
"page_size": 20,
|
||||
"list": deepcopy(self.logistics_tasks),
|
||||
}
|
||||
|
||||
def create_logistics_task(self, payload: dict) -> dict:
|
||||
next_id = max([task["task_id"] for task in self.logistics_tasks], default=5000) + 1
|
||||
order = self.get_order(payload["order_id"])
|
||||
task = {
|
||||
"task_id": next_id,
|
||||
"task_no": f"LT{next_id}",
|
||||
"order_id": payload["order_id"],
|
||||
"order_no": order["order_no"] if order else "",
|
||||
"driver_id": payload["driver_id"],
|
||||
"driver_name": f"司机{payload['driver_id']}",
|
||||
"factory_id": payload.get("factory_id"),
|
||||
"factory_name": order.get("factory_name", "") if order else "",
|
||||
"pickup_address": payload["pickup_address"],
|
||||
"delivery_address": payload["delivery_address"],
|
||||
"pickup_content": payload["pickup_content"],
|
||||
"quantity": payload["quantity"],
|
||||
"status": "pending",
|
||||
"remark": payload.get("remark"),
|
||||
"created_at": "2026-05-14 14:00:00",
|
||||
"salesman_name": order.get("salesman_name", "") if order else "",
|
||||
"customer_name": order.get("customer_name", "") if order else "",
|
||||
}
|
||||
self.logistics_tasks.append(task)
|
||||
|
||||
for index, item in enumerate(self.orders):
|
||||
if item["order_id"] == payload["order_id"]:
|
||||
self.orders[index]["order_status"] = "pending_driver"
|
||||
break
|
||||
|
||||
return deepcopy(task)
|
||||
|
||||
def get_logistics_task(self, task_id: int) -> dict | None:
|
||||
for task in self.logistics_tasks:
|
||||
if task["task_id"] == task_id:
|
||||
return deepcopy(task)
|
||||
return None
|
||||
|
||||
|
||||
demo_store = DemoStore()
|
||||
|
||||
@ -1,79 +1,101 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.core.error_codes import ErrorCode
|
||||
from backend.app.core.exceptions import AppException
|
||||
from backend.app.repositories.logistics_repository import LogisticsRepository
|
||||
from backend.app.repositories.order_repository import OrderRepository
|
||||
from backend.app.services.demo_store import demo_store
|
||||
|
||||
|
||||
class LogisticsService:
|
||||
def list_tasks(self) -> dict:
|
||||
return {
|
||||
"total": 1,
|
||||
"page_no": 1,
|
||||
"page_size": 20,
|
||||
"list": [
|
||||
{
|
||||
"task_id": 5001,
|
||||
"task_no": "LT202605140001",
|
||||
"order_id": 9001,
|
||||
"driver_name": "王司机",
|
||||
"pickup_address": "工厂A",
|
||||
"delivery_address": "客户地址",
|
||||
"status": "pending",
|
||||
def __init__(self) -> None:
|
||||
self.logistics_repository = LogisticsRepository()
|
||||
self.order_repository = OrderRepository()
|
||||
|
||||
def list_tasks(self, session: Session | None = None, filters: dict | None = None) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
tasks = self.logistics_repository.list_tasks_by_filters(session, filters or {})
|
||||
return {
|
||||
"total": len(tasks),
|
||||
"page_no": 1,
|
||||
"page_size": 20,
|
||||
"list": [self._map_task_summary(task, session) for task in tasks],
|
||||
}
|
||||
],
|
||||
}
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
return demo_store.list_logistics_tasks()
|
||||
|
||||
def create_task(self) -> dict:
|
||||
return {"task_id": 5002, "task_no": "LT202605140002", "status": "pending"}
|
||||
def create_task(self, payload: dict, session: Session | None = None) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
order = self.order_repository.get_order(session, payload["order_id"])
|
||||
if order is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="订单不存在", status_code=404)
|
||||
if order.order_status not in {"pending_factory", "approved"}:
|
||||
raise AppException(code=ErrorCode.INVALID_STATUS, message="当前状态不允许创建司机任务", status_code=400)
|
||||
|
||||
def get_task(self, task_id: int) -> dict:
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"task_no": "LT202605140001",
|
||||
"order_id": 9001,
|
||||
"driver_name": "王司机",
|
||||
"pickup_address": "工厂A",
|
||||
"delivery_address": "客户地址",
|
||||
"pickup_content": "产品A 1袋",
|
||||
"quantity": 1,
|
||||
"status": "pending",
|
||||
}
|
||||
existing_task = self.logistics_repository.get_task_by_order_id(session, order.id)
|
||||
if existing_task is not None and existing_task.status != "canceled":
|
||||
raise AppException(code=ErrorCode.PARAM_ERROR, message="该订单已存在有效司机任务", status_code=400)
|
||||
|
||||
def list_driver_tasks(self) -> dict:
|
||||
return {
|
||||
"total": 1,
|
||||
"page_no": 1,
|
||||
"page_size": 20,
|
||||
"list": [
|
||||
{
|
||||
"task_id": 5001,
|
||||
"task_no": "LT202605140001",
|
||||
"pickup_address": "工厂A",
|
||||
"delivery_address": "客户地址",
|
||||
"pickup_content": "产品A 1袋",
|
||||
"quantity": 1,
|
||||
"status": "pending",
|
||||
}
|
||||
],
|
||||
}
|
||||
task = self.logistics_repository.create_task(
|
||||
session,
|
||||
{
|
||||
"task_no": f"LT{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"order_id": order.id,
|
||||
"driver_id": payload["driver_id"],
|
||||
"factory_id": payload.get("factory_id") or order.factory_id,
|
||||
"pickup_address": payload["pickup_address"],
|
||||
"delivery_address": payload["delivery_address"],
|
||||
"pickup_content": payload["pickup_content"],
|
||||
"quantity": payload["quantity"],
|
||||
"status": "pending",
|
||||
"created_by": None,
|
||||
"remark": payload.get("remark"),
|
||||
},
|
||||
)
|
||||
|
||||
def get_driver_task(self, task_id: int) -> dict:
|
||||
return {
|
||||
"task_id": task_id,
|
||||
"order_id": 9001,
|
||||
"task_no": "LT202605140001",
|
||||
"status": "pending",
|
||||
"pickup_address": "工厂A",
|
||||
"delivery_address": "客户地址",
|
||||
"pickup_content": "产品A 1袋",
|
||||
"quantity": 1,
|
||||
"factory_id": 1001,
|
||||
"factory_name": "工厂A",
|
||||
"salesman_name": "张三",
|
||||
}
|
||||
# 司机任务创建后,订单进入待司机接单阶段。
|
||||
self.order_repository.update_order_status(session, order, "pending_driver")
|
||||
session.commit()
|
||||
return self._map_task_detail(task, session)
|
||||
except SQLAlchemyError:
|
||||
session.rollback()
|
||||
|
||||
def accept_task(self, task_id: int) -> dict:
|
||||
return {"task_id": task_id, "status": "accepted"}
|
||||
return demo_store.create_logistics_task(payload)
|
||||
|
||||
def pickup_task(self, task_id: int) -> dict:
|
||||
return {"task_id": task_id, "status": "picked_up"}
|
||||
def get_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
task = self.logistics_repository.get_task(session, task_id)
|
||||
if task is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404)
|
||||
return self._map_task_detail(task, session)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
task = demo_store.get_logistics_task(task_id)
|
||||
if task is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404)
|
||||
return task
|
||||
|
||||
def deliver_task(self, task_id: int) -> dict:
|
||||
return {"task_id": task_id, "status": "delivered"}
|
||||
def list_driver_tasks(self, session: Session | None = None) -> dict:
|
||||
return self.list_tasks(session=session, filters={})
|
||||
|
||||
def get_driver_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
return self.get_task(task_id, session=session)
|
||||
|
||||
def accept_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
return self._change_task_status(task_id, "pending", "accepted", "accepted", session)
|
||||
|
||||
def pickup_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
return self._change_task_status(task_id, "accepted", "picked_up", "picked_up", session)
|
||||
|
||||
def deliver_task(self, task_id: int, session: Session | None = None) -> dict:
|
||||
return self._change_task_status(task_id, "picked_up", "delivered", "delivered", session)
|
||||
|
||||
def get_trace(self, order_id: int) -> dict:
|
||||
return {
|
||||
@ -91,5 +113,61 @@ class LogisticsService:
|
||||
def create_trace(self, order_id: int) -> dict:
|
||||
return {"order_id": order_id, "created": True}
|
||||
|
||||
def _change_task_status(
|
||||
self,
|
||||
task_id: int,
|
||||
current_status: str,
|
||||
target_status: str,
|
||||
order_status: str,
|
||||
session: Session | None = None,
|
||||
) -> dict:
|
||||
if session is not None:
|
||||
try:
|
||||
task = self.logistics_repository.get_task(session, task_id)
|
||||
if task is None:
|
||||
raise AppException(code=ErrorCode.NOT_FOUND, message="司机任务不存在", status_code=404)
|
||||
if task.status != current_status:
|
||||
raise AppException(code=ErrorCode.INVALID_STATUS, message="当前任务状态不允许执行该操作", status_code=400)
|
||||
|
||||
self.logistics_repository.update_task_status(session, task, target_status)
|
||||
order = self.order_repository.get_order(session, task.order_id)
|
||||
if order is not None:
|
||||
self.order_repository.update_order_status(session, order, order_status)
|
||||
session.commit()
|
||||
return {"task_id": task.id, "status": task.status}
|
||||
except SQLAlchemyError:
|
||||
session.rollback()
|
||||
|
||||
return {"task_id": task_id, "status": target_status}
|
||||
|
||||
def _map_task_summary(self, task, session: Session) -> dict:
|
||||
order = self.order_repository.get_order(session, task.order_id)
|
||||
supplier = self.order_repository.get_supplier(session, task.factory_id)
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"task_no": task.task_no,
|
||||
"order_id": task.order_id,
|
||||
"order_no": order.order_no if order else "",
|
||||
"driver_id": task.driver_id,
|
||||
"driver_name": f"司机{task.driver_id}",
|
||||
"factory_id": task.factory_id,
|
||||
"factory_name": supplier.supplier_name if supplier else "",
|
||||
"pickup_address": task.pickup_address,
|
||||
"delivery_address": task.delivery_address,
|
||||
"pickup_content": task.pickup_content,
|
||||
"quantity": float(task.quantity or 0),
|
||||
"status": task.status,
|
||||
"remark": task.remark,
|
||||
"created_at": task.created_at.strftime("%Y-%m-%d %H:%M:%S") if task.created_at else "",
|
||||
}
|
||||
|
||||
def _map_task_detail(self, task, session: Session) -> dict:
|
||||
summary = self._map_task_summary(task, session)
|
||||
order = self.order_repository.get_order(session, task.order_id)
|
||||
summary["salesman_name"] = ""
|
||||
summary["customer_name"] = order.customer_name if order else ""
|
||||
return summary
|
||||
|
||||
|
||||
logistics_service = LogisticsService()
|
||||
|
||||
|
||||
@ -102,6 +102,100 @@ export function fetchBusinessData() {
|
||||
return wait({ modules: businessModules, tasks: taskRows });
|
||||
}
|
||||
|
||||
export async function fetchFactoryPendingOrders() {
|
||||
try {
|
||||
const approvedQuery = buildQuery({
|
||||
order_status: "approved",
|
||||
page_no: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
const pendingFactoryQuery = buildQuery({
|
||||
order_status: "pending_factory",
|
||||
page_no: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
const [approvedData, pendingFactoryData] = await Promise.all([
|
||||
request(`/api/orders?${approvedQuery}`),
|
||||
request(`/api/orders?${pendingFactoryQuery}`),
|
||||
]);
|
||||
|
||||
return {
|
||||
rows: [...(approvedData.list || []), ...(pendingFactoryData.list || [])].map(buildApprovalRow),
|
||||
isMock: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return wait({
|
||||
rows: [
|
||||
{
|
||||
orderId: 9001,
|
||||
orderNo: "SO202605140001",
|
||||
customerName: "演示客户",
|
||||
customerMobile: "13900000000",
|
||||
orderStatus: "approved",
|
||||
statusText: "已审批",
|
||||
orderSource: "线下拜访",
|
||||
amount: "100.00",
|
||||
createdAt: "2026-05-14 10:00:00",
|
||||
},
|
||||
],
|
||||
isMock: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateSupplierText(orderId, supplierId) {
|
||||
return request(`/api/orders/${orderId}/supplier-text`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ supplier_id: supplierId }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmSupplierText(orderId, payload) {
|
||||
return request(`/api/orders/${orderId}/supplier-text/confirm`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchLogisticsTasks(filters = {}) {
|
||||
try {
|
||||
const query = buildQuery({
|
||||
order_id: filters.order_id || undefined,
|
||||
task_no: filters.task_no?.trim() || undefined,
|
||||
status: filters.status || undefined,
|
||||
});
|
||||
const data = await request(`/api/logistics/tasks?${query}`);
|
||||
return { rows: data.list || [], isMock: false };
|
||||
} catch (error) {
|
||||
return wait({
|
||||
rows: taskRows.map((item, index) => ({
|
||||
task_id: index + 1,
|
||||
task_no: item.taskNo,
|
||||
order_id: 9000 + index,
|
||||
order_no: item.orderNo,
|
||||
driver_id: index + 1,
|
||||
driver_name: item.driver,
|
||||
factory_name: "演示工厂",
|
||||
pickup_address: "工厂A",
|
||||
delivery_address: "客户地址",
|
||||
pickup_content: "演示产品 1 吨",
|
||||
quantity: 1,
|
||||
status: "pending",
|
||||
remark: "演示任务",
|
||||
created_at: "2026-05-14 14:00:00",
|
||||
})),
|
||||
isMock: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createLogisticsTask(payload) {
|
||||
return request("/api/logistics/tasks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
async function listApprovalOrdersByStatus(status, filters = {}) {
|
||||
const query = buildQuery({
|
||||
order_status: status,
|
||||
|
||||
@ -1,83 +1,353 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<h2>业务模块</h2>
|
||||
<p v-if="loading" class="loading">加载中...</p>
|
||||
<section v-if="!loading" class="toolbar">
|
||||
<input type="text" placeholder="按任务编号或订单编号筛选" />
|
||||
<button>新建任务</button>
|
||||
<button>导出任务</button>
|
||||
</section>
|
||||
<div v-if="!loading && !taskRows.length" class="empty-state">当前没有司机任务数据</div>
|
||||
<div v-else class="grid">
|
||||
<article v-for="module in businessModules" :key="module.title" class="card">
|
||||
<h3>{{ module.title }}</h3>
|
||||
<p>{{ module.description }}</p>
|
||||
</article>
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<h2>履约调度中心</h2>
|
||||
<p class="subline">集中处理发厂确认、司机任务创建和任务查看。</p>
|
||||
</div>
|
||||
<span class="mode-tag" :class="{ fallback: isMock }">
|
||||
{{ isMock ? "演示数据" : "真实接口" }}
|
||||
</span>
|
||||
</div>
|
||||
<section v-if="!loading" class="table-panel">
|
||||
<h3>司机任务示例</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务编号</th>
|
||||
<th>订单编号</th>
|
||||
<th>司机</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in taskRows" :key="row.taskNo">
|
||||
<td>{{ row.taskNo }}</td>
|
||||
<td>{{ row.orderNo }}</td>
|
||||
<td>{{ row.driver }}</td>
|
||||
<td><span class="status">{{ row.status }}</span></td>
|
||||
<td>{{ row.action }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div class="pagination">
|
||||
<button>上一页</button>
|
||||
<span>第 1 页 / 共 1 页</span>
|
||||
<button>下一页</button>
|
||||
|
||||
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<section class="detail-panel">
|
||||
<div class="panel-header">
|
||||
<h3>待发厂订单</h3>
|
||||
<span>仅展示已审批或待发厂订单</span>
|
||||
</div>
|
||||
<p v-if="loadingOrders" class="loading">正在加载待发厂订单...</p>
|
||||
<div v-else-if="!pendingOrders.length" class="empty-state">当前没有待发厂订单</div>
|
||||
<div v-else class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单编号</th>
|
||||
<th>客户姓名</th>
|
||||
<th>状态</th>
|
||||
<th>订单来源</th>
|
||||
<th>金额</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in pendingOrders" :key="row.orderId">
|
||||
<td>{{ row.orderNo }}</td>
|
||||
<td>{{ row.customerName }}</td>
|
||||
<td><span class="status">{{ row.statusText }}</span></td>
|
||||
<td>{{ row.orderSource }}</td>
|
||||
<td>{{ row.amount }}</td>
|
||||
<td class="actions-cell">
|
||||
<button
|
||||
:disabled="actionLoading === `text-${row.orderId}`"
|
||||
@click="handleGenerateText(row)"
|
||||
>
|
||||
{{ actionLoading === `text-${row.orderId}` ? "生成中..." : "生成发厂文案" }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="actionLoading === `confirm-${row.orderId}`"
|
||||
@click="handleConfirmFactory(row)"
|
||||
>
|
||||
{{ actionLoading === `confirm-${row.orderId}` ? "确认中..." : "确认下发工厂" }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="actionLoading === `task-${row.orderId}` || !canCreateTask(row.orderStatus)"
|
||||
@click="prepareTask(row)"
|
||||
>
|
||||
{{ actionLoading === `task-${row.orderId}` ? "创建中..." : "创建司机任务" }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel">
|
||||
<div class="panel-header">
|
||||
<h3>创建司机任务</h3>
|
||||
<span>从已确认发厂订单直接下发司机任务</span>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>订单编号</span>
|
||||
<input v-model="taskForm.order_no" type="text" disabled />
|
||||
</label>
|
||||
<label>
|
||||
<span>订单ID</span>
|
||||
<input v-model.number="taskForm.order_id" type="number" disabled />
|
||||
</label>
|
||||
<label>
|
||||
<span>司机ID</span>
|
||||
<input v-model.number="taskForm.driver_id" type="number" min="1" />
|
||||
</label>
|
||||
<label>
|
||||
<span>工厂ID</span>
|
||||
<input v-model.number="taskForm.factory_id" type="number" min="1" />
|
||||
</label>
|
||||
<label>
|
||||
<span>取货地址</span>
|
||||
<input v-model.trim="taskForm.pickup_address" type="text" placeholder="请输入取货地址" />
|
||||
</label>
|
||||
<label>
|
||||
<span>送达地址</span>
|
||||
<input v-model.trim="taskForm.delivery_address" type="text" placeholder="请输入送达地址" />
|
||||
</label>
|
||||
<label>
|
||||
<span>提货内容</span>
|
||||
<input v-model.trim="taskForm.pickup_content" type="text" placeholder="请输入提货内容" />
|
||||
</label>
|
||||
<label>
|
||||
<span>数量</span>
|
||||
<input v-model.number="taskForm.quantity" type="number" min="0.01" step="0.01" />
|
||||
</label>
|
||||
<label class="full-width">
|
||||
<span>备注</span>
|
||||
<textarea v-model.trim="taskForm.remark" rows="3" placeholder="请输入司机任务备注"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button :disabled="creatingTask || !taskForm.order_id" @click="handleCreateTask">
|
||||
{{ creatingTask ? "创建中..." : "提交司机任务" }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel">
|
||||
<div class="panel-header">
|
||||
<h3>司机任务列表</h3>
|
||||
<span>展示真实任务状态与创建结果</span>
|
||||
</div>
|
||||
<p v-if="loadingTasks" class="loading">正在加载司机任务...</p>
|
||||
<div v-else-if="!taskRows.length" class="empty-state">当前没有司机任务</div>
|
||||
<div v-else class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务编号</th>
|
||||
<th>订单编号</th>
|
||||
<th>司机</th>
|
||||
<th>工厂</th>
|
||||
<th>任务状态</th>
|
||||
<th>提货内容</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in taskRows" :key="row.task_id || row.task_no">
|
||||
<td>{{ row.task_no }}</td>
|
||||
<td>{{ row.order_no }}</td>
|
||||
<td>{{ row.driver_name }}</td>
|
||||
<td>{{ row.factory_name || "-" }}</td>
|
||||
<td><span class="status">{{ mapTaskStatus(row.status) }}</span></td>
|
||||
<td>{{ row.pickup_content }}</td>
|
||||
<td>{{ row.created_at || "-" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
|
||||
import { fetchBusinessData } from "../mockApi";
|
||||
import {
|
||||
confirmSupplierText,
|
||||
createLogisticsTask,
|
||||
fetchFactoryPendingOrders,
|
||||
fetchLogisticsTasks,
|
||||
generateSupplierText,
|
||||
} from "../mockApi";
|
||||
|
||||
const loading = ref(true);
|
||||
const businessModules = ref([]);
|
||||
const loadingOrders = ref(true);
|
||||
const loadingTasks = ref(true);
|
||||
const isMock = ref(false);
|
||||
const pendingOrders = ref([]);
|
||||
const taskRows = ref([]);
|
||||
const actionLoading = ref("");
|
||||
const creatingTask = ref(false);
|
||||
const message = ref("");
|
||||
const messageType = ref("success");
|
||||
const taskForm = reactive({
|
||||
order_id: null,
|
||||
order_no: "",
|
||||
driver_id: 10,
|
||||
factory_id: null,
|
||||
pickup_address: "",
|
||||
delivery_address: "",
|
||||
pickup_content: "",
|
||||
quantity: 1,
|
||||
remark: "",
|
||||
});
|
||||
|
||||
function mapTaskStatus(status) {
|
||||
const statusMap = {
|
||||
pending: "待接单",
|
||||
accepted: "已接单",
|
||||
picked_up: "已揽货",
|
||||
delivered: "已送达",
|
||||
canceled: "已取消",
|
||||
};
|
||||
return statusMap[status] || status || "-";
|
||||
}
|
||||
|
||||
function canCreateTask(orderStatus) {
|
||||
return orderStatus === "pending_factory";
|
||||
}
|
||||
|
||||
function resetTaskForm() {
|
||||
taskForm.order_id = null;
|
||||
taskForm.order_no = "";
|
||||
taskForm.driver_id = 10;
|
||||
taskForm.factory_id = null;
|
||||
taskForm.pickup_address = "";
|
||||
taskForm.delivery_address = "";
|
||||
taskForm.pickup_content = "";
|
||||
taskForm.quantity = 1;
|
||||
taskForm.remark = "";
|
||||
}
|
||||
|
||||
function prepareTask(row) {
|
||||
taskForm.order_id = row.orderId;
|
||||
taskForm.order_no = row.orderNo;
|
||||
taskForm.factory_id = 1001;
|
||||
taskForm.pickup_address = "工厂A";
|
||||
taskForm.delivery_address = `${row.customerName} 收货地址`;
|
||||
taskForm.pickup_content = `${row.customerName} 订单货物`;
|
||||
taskForm.quantity = 1;
|
||||
taskForm.remark = "管理端调度创建";
|
||||
message.value = `已带入订单 ${row.orderNo},请确认司机任务信息后提交。`;
|
||||
messageType.value = "success";
|
||||
}
|
||||
|
||||
async function loadPendingOrders() {
|
||||
const data = await fetchFactoryPendingOrders();
|
||||
pendingOrders.value = data.rows;
|
||||
isMock.value = Boolean(data.isMock);
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
const data = await fetchLogisticsTasks();
|
||||
taskRows.value = data.rows;
|
||||
isMock.value = Boolean(data.isMock);
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([loadPendingOrders(), loadTasks()]);
|
||||
}
|
||||
|
||||
async function handleGenerateText(row) {
|
||||
actionLoading.value = `text-${row.orderId}`;
|
||||
message.value = "";
|
||||
try {
|
||||
const result = await generateSupplierText(row.orderId, 1001);
|
||||
message.value = `订单 ${row.orderNo} 发厂文案已生成:${result.text_content}`;
|
||||
messageType.value = "success";
|
||||
} catch (error) {
|
||||
message.value = error.message || "生成发厂文案失败";
|
||||
messageType.value = "error";
|
||||
} finally {
|
||||
actionLoading.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmFactory(row) {
|
||||
actionLoading.value = `confirm-${row.orderId}`;
|
||||
message.value = "";
|
||||
try {
|
||||
await confirmSupplierText(row.orderId, {
|
||||
supplier_id: 1001,
|
||||
text_content: `订单 ${row.orderNo} 请安排生产与发货`,
|
||||
remark: "管理端确认发厂",
|
||||
});
|
||||
await refreshAll();
|
||||
message.value = `订单 ${row.orderNo} 已确认下发工厂,可继续创建司机任务。`;
|
||||
messageType.value = "success";
|
||||
} catch (error) {
|
||||
message.value = error.message || "确认下发工厂失败";
|
||||
messageType.value = "error";
|
||||
} finally {
|
||||
actionLoading.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateTask() {
|
||||
if (!taskForm.order_id) {
|
||||
message.value = "请先从待发厂订单中选择一笔订单";
|
||||
messageType.value = "error";
|
||||
return;
|
||||
}
|
||||
if (!taskForm.pickup_address.trim() || !taskForm.delivery_address.trim() || !taskForm.pickup_content.trim()) {
|
||||
message.value = "请补全取货地址、送达地址和提货内容";
|
||||
messageType.value = "error";
|
||||
return;
|
||||
}
|
||||
|
||||
creatingTask.value = true;
|
||||
message.value = "";
|
||||
try {
|
||||
const result = await createLogisticsTask({
|
||||
order_id: taskForm.order_id,
|
||||
driver_id: taskForm.driver_id,
|
||||
pickup_address: taskForm.pickup_address,
|
||||
delivery_address: taskForm.delivery_address,
|
||||
pickup_content: taskForm.pickup_content,
|
||||
quantity: taskForm.quantity,
|
||||
factory_id: taskForm.factory_id,
|
||||
remark: taskForm.remark,
|
||||
});
|
||||
await refreshAll();
|
||||
message.value = `司机任务 ${result.task_no} 创建成功,订单已进入待司机接单状态。`;
|
||||
messageType.value = "success";
|
||||
resetTaskForm();
|
||||
} catch (error) {
|
||||
message.value = error.message || "创建司机任务失败";
|
||||
messageType.value = "error";
|
||||
} finally {
|
||||
creatingTask.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await fetchBusinessData();
|
||||
businessModules.value = data.modules;
|
||||
taskRows.value = data.tasks;
|
||||
loading.value = false;
|
||||
try {
|
||||
await refreshAll();
|
||||
} finally {
|
||||
loadingOrders.value = false;
|
||||
loadingTasks.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { background: #fff; border-radius: 8px; padding: 20px; }
|
||||
h2 { margin: 0 0 12px; }
|
||||
.header-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
h2 { margin: 0 0 6px; }
|
||||
.subline { margin: 0; color: #6b7280; font-size: 13px; }
|
||||
.mode-tag { padding: 6px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; }
|
||||
.mode-tag.fallback { background: #fef3c7; color: #92400e; }
|
||||
.message-box { margin-bottom: 16px; padding: 12px 14px; border-radius: 8px; background: #dcfce7; color: #166534; }
|
||||
.message-box.error { background: #fee2e2; color: #b91c1c; }
|
||||
.detail-panel { margin-top: 20px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
|
||||
.panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; }
|
||||
.panel-header h3 { margin: 0; font-size: 16px; }
|
||||
.panel-header span { color: #6b7280; font-size: 13px; }
|
||||
.loading { margin: 0; color: #6b7280; }
|
||||
.toolbar { display: flex; gap: 12px; margin: 0 0 20px; }
|
||||
.toolbar input { min-width: 240px; border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; }
|
||||
.toolbar button { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 14px; background: #fff; }
|
||||
.empty-state { margin: 0 0 20px; padding: 24px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
|
||||
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
|
||||
h3 { margin: 0 0 10px; font-size: 16px; }
|
||||
p { margin: 0; line-height: 1.6; color: #4b5563; }
|
||||
.table-panel { margin-top: 20px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
|
||||
.empty-state { padding: 24px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; }
|
||||
.status { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #eff6ff; color: #1d4ed8; }
|
||||
.pagination { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
.pagination button { border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 12px; background: #fff; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
|
||||
.actions-cell { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
button, input, textarea { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
|
||||
button:disabled { color: #9ca3af; background: #f3f4f6; }
|
||||
.status { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #eef2ff; color: #4338ca; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
label { display: grid; gap: 6px; }
|
||||
label span { color: #4b5563; font-size: 13px; }
|
||||
.full-width { grid-column: 1 / -1; }
|
||||
.action-row { margin-top: 16px; display: flex; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
@ -100,3 +100,4 @@
|
||||
| 2026-05-14 | 已继续补齐创建订单的产品选择链路:后端产品接口支持按 `product_name` 筛选;前端新建订单页已接入真实产品选项,并在选择后自动回填规格、单位、销售价、成本价。 |
|
||||
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-sales` 业务员端订单管理闭环现已支持登录后查看真实订单列表、按状态/客户/来源/日期筛选、创建最小可用订单、进入详情查看真实明细与审批记录,并可发起提交审核/取消订单;后端同步补齐订单列表 `order_source` 返回与前端最小校验配套。 |
|
||||
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 管理端订单审批闭环现已支持工作台查看真实审批待办摘要、审批中心按订单编号/客户姓名/审批类型筛选待办订单、进入审批详情查看真实订单明细与审批记录,并可执行订单审批与取消审批;后端同步补齐订单列表 `order_no` 筛选能力以支撑管理端精确查询。 |
|
||||
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 履约调度闭环现已支持查看待发厂订单、生成发厂文案、确认下发工厂、创建司机任务并查看真实任务列表;后端同步补齐物流任务请求模型、数据库仓储、真实任务创建与订单状态从 `pending_factory` 流转到 `pending_driver` 的联动。 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user