feat: 完成履约调度闭环

This commit is contained in:
wsb1224 2026-05-14 15:17:56 +08:00
parent 33e921fa74
commit 181e56d627
8 changed files with 793 additions and 151 deletions

View File

@ -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.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 = APIRouter(tags=["logistics"])
@router.get("/api/logistics/tasks") @router.get("/api/logistics/tasks")
def list_logistics_tasks() -> dict: def list_logistics_tasks(
return success_payload(logistics_service.list_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") @router.post("/api/logistics/tasks")
def create_logistics_task() -> dict: def create_logistics_task(
return success_payload(logistics_service.create_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}") @router.get("/api/logistics/tasks/{task_id}")
def get_logistics_task(task_id: int) -> dict: def get_logistics_task(
return success_payload(logistics_service.get_task(task_id)) 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") @router.get("/api/driver/tasks")
def list_driver_tasks() -> dict: def list_driver_tasks(
return success_payload(logistics_service.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}") @router.get("/api/driver/tasks/{task_id}")
def get_driver_task(task_id: int) -> dict: def get_driver_task(
return success_payload(logistics_service.get_driver_task(task_id)) 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") @router.post("/api/driver/tasks/{task_id}/accept")
def accept_driver_task(task_id: int) -> dict: def accept_driver_task(
return success_payload(logistics_service.accept_task(task_id)) 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") @router.post("/api/driver/tasks/{task_id}/pickup")
def pickup_driver_task(task_id: int) -> dict: def pickup_driver_task(
return success_payload(logistics_service.pickup_task(task_id)) 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") @router.post("/api/driver/tasks/{task_id}/deliver")
def deliver_driver_task(task_id: int) -> dict: def deliver_driver_task(
return success_payload(logistics_service.deliver_task(task_id)) 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") @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)) return success_payload(logistics_service.get_trace(order_id))
@router.post("/api/logistics/{order_id}/trace") @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)) return success_payload(logistics_service.create_trace(order_id))

View 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

View 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

View File

@ -36,17 +36,41 @@ class DemoStore:
"customer_name": "李四", "customer_name": "李四",
"customer_mobile": "13900000000", "customer_mobile": "13900000000",
"salesman_name": "张三", "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, "sale_price_total": 100.0,
"cost_price_total": 60.0,
"profit_total": 20.0, "profit_total": 20.0,
"profit_rate": 20.0,
"commission_amount": 50.0, "commission_amount": 50.0,
"created_at": "2026-05-14 10:00:00", "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": [], "approve_logs": [],
"attachments": [], "attachments": [],
"remark": "演示订单", "remark": "演示订单",
} }
] ]
self.logistics_tasks = []
def login(self, username: str, role_type: str) -> dict | None: def login(self, username: str, role_type: str) -> dict | None:
user = self.users.get((username, role_type)) user = self.users.get((username, role_type))
@ -95,5 +119,50 @@ class DemoStore:
self.orders.append(order) self.orders.append(order)
return deepcopy(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() demo_store = DemoStore()

View File

@ -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: class LogisticsService:
def list_tasks(self) -> dict: def __init__(self) -> None:
return { self.logistics_repository = LogisticsRepository()
"total": 1, self.order_repository = OrderRepository()
"page_no": 1,
"page_size": 20, def list_tasks(self, session: Session | None = None, filters: dict | None = None) -> dict:
"list": [ if session is not None:
{ try:
"task_id": 5001, tasks = self.logistics_repository.list_tasks_by_filters(session, filters or {})
"task_no": "LT202605140001", return {
"order_id": 9001, "total": len(tasks),
"driver_name": "王司机", "page_no": 1,
"pickup_address": "工厂A", "page_size": 20,
"delivery_address": "客户地址", "list": [self._map_task_summary(task, session) for task in tasks],
"status": "pending",
} }
], except SQLAlchemyError:
} pass
return demo_store.list_logistics_tasks()
def create_task(self) -> dict: def create_task(self, payload: dict, session: Session | None = None) -> dict:
return {"task_id": 5002, "task_no": "LT202605140002", "status": "pending"} 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: existing_task = self.logistics_repository.get_task_by_order_id(session, order.id)
return { if existing_task is not None and existing_task.status != "canceled":
"task_id": task_id, raise AppException(code=ErrorCode.PARAM_ERROR, message="该订单已存在有效司机任务", status_code=400)
"task_no": "LT202605140001",
"order_id": 9001,
"driver_name": "王司机",
"pickup_address": "工厂A",
"delivery_address": "客户地址",
"pickup_content": "产品A 1袋",
"quantity": 1,
"status": "pending",
}
def list_driver_tasks(self) -> dict: task = self.logistics_repository.create_task(
return { session,
"total": 1, {
"page_no": 1, "task_no": f"LT{datetime.now().strftime('%Y%m%d%H%M%S')}",
"page_size": 20, "order_id": order.id,
"list": [ "driver_id": payload["driver_id"],
{ "factory_id": payload.get("factory_id") or order.factory_id,
"task_id": 5001, "pickup_address": payload["pickup_address"],
"task_no": "LT202605140001", "delivery_address": payload["delivery_address"],
"pickup_address": "工厂A", "pickup_content": payload["pickup_content"],
"delivery_address": "客户地址", "quantity": payload["quantity"],
"pickup_content": "产品A 1袋", "status": "pending",
"quantity": 1, "created_by": None,
"status": "pending", "remark": payload.get("remark"),
} },
], )
}
def get_driver_task(self, task_id: int) -> dict: # 司机任务创建后,订单进入待司机接单阶段。
return { self.order_repository.update_order_status(session, order, "pending_driver")
"task_id": task_id, session.commit()
"order_id": 9001, return self._map_task_detail(task, session)
"task_no": "LT202605140001", except SQLAlchemyError:
"status": "pending", session.rollback()
"pickup_address": "工厂A",
"delivery_address": "客户地址",
"pickup_content": "产品A 1袋",
"quantity": 1,
"factory_id": 1001,
"factory_name": "工厂A",
"salesman_name": "张三",
}
def accept_task(self, task_id: int) -> dict: return demo_store.create_logistics_task(payload)
return {"task_id": task_id, "status": "accepted"}
def pickup_task(self, task_id: int) -> dict: def get_task(self, task_id: int, session: Session | None = None) -> dict:
return {"task_id": task_id, "status": "picked_up"} 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: def list_driver_tasks(self, session: Session | None = None) -> dict:
return {"task_id": task_id, "status": "delivered"} 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: def get_trace(self, order_id: int) -> dict:
return { return {
@ -91,5 +113,61 @@ class LogisticsService:
def create_trace(self, order_id: int) -> dict: def create_trace(self, order_id: int) -> dict:
return {"order_id": order_id, "created": True} 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() logistics_service = LogisticsService()

View File

@ -102,6 +102,100 @@ export function fetchBusinessData() {
return wait({ modules: businessModules, tasks: taskRows }); 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 = {}) { async function listApprovalOrdersByStatus(status, filters = {}) {
const query = buildQuery({ const query = buildQuery({
order_status: status, order_status: status,

View File

@ -1,83 +1,353 @@
<template> <template>
<section class="page"> <section class="page">
<h2>业务模块</h2> <div class="header-row">
<p v-if="loading" class="loading">加载中...</p> <div>
<section v-if="!loading" class="toolbar"> <h2>履约调度中心</h2>
<input type="text" placeholder="按任务编号或订单编号筛选" /> <p class="subline">集中处理发厂确认司机任务创建和任务查看</p>
<button>新建任务</button> </div>
<button>导出任务</button> <span class="mode-tag" :class="{ fallback: isMock }">
</section> {{ isMock ? "演示数据" : "真实接口" }}
<div v-if="!loading && !taskRows.length" class="empty-state">当前没有司机任务数据</div> </span>
<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> </div>
<section v-if="!loading" class="table-panel">
<h3>司机任务示例</h3> <div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
<table> {{ message }}
<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> </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> </section>
</template> </template>
<script setup> <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 loadingOrders = ref(true);
const businessModules = ref([]); const loadingTasks = ref(true);
const isMock = ref(false);
const pendingOrders = ref([]);
const taskRows = 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 () => { onMounted(async () => {
const data = await fetchBusinessData(); try {
businessModules.value = data.modules; await refreshAll();
taskRows.value = data.tasks; } finally {
loading.value = false; loadingOrders.value = false;
loadingTasks.value = false;
}
}); });
</script> </script>
<style scoped> <style scoped>
.page { background: #fff; border-radius: 8px; padding: 20px; } .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; } .loading { margin: 0; color: #6b7280; }
.toolbar { display: flex; gap: 12px; margin: 0 0 20px; } .empty-state { padding: 24px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }
.toolbar input { min-width: 240px; border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; } .table-wrap { overflow-x: auto; }
.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; }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; } th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
.status { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #eff6ff; color: #1d4ed8; } .actions-cell { display: flex; flex-wrap: wrap; gap: 8px; }
.pagination { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 16px; } button, input, textarea { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
.pagination button { border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 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> </style>

View File

@ -100,3 +100,4 @@
| 2026-05-14 | 已继续补齐创建订单的产品选择链路:后端产品接口支持按 `product_name` 筛选;前端新建订单页已接入真实产品选项,并在选择后自动回填规格、单位、销售价、成本价。 | | 2026-05-14 | 已继续补齐创建订单的产品选择链路:后端产品接口支持按 `product_name` 筛选;前端新建订单页已接入真实产品选项,并在选择后自动回填规格、单位、销售价、成本价。 |
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-sales` 业务员端订单管理闭环现已支持登录后查看真实订单列表、按状态/客户/来源/日期筛选、创建最小可用订单、进入详情查看真实明细与审批记录,并可发起提交审核/取消订单;后端同步补齐订单列表 `order_source` 返回与前端最小校验配套。 | | 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-sales` 业务员端订单管理闭环现已支持登录后查看真实订单列表、按状态/客户/来源/日期筛选、创建最小可用订单、进入详情查看真实明细与审批记录,并可发起提交审核/取消订单;后端同步补齐订单列表 `order_source` 返回与前端最小校验配套。 |
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 管理端订单审批闭环现已支持工作台查看真实审批待办摘要、审批中心按订单编号/客户姓名/审批类型筛选待办订单、进入审批详情查看真实订单明细与审批记录,并可执行订单审批与取消审批;后端同步补齐订单列表 `order_no` 筛选能力以支撑管理端精确查询。 | | 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 管理端订单审批闭环现已支持工作台查看真实审批待办摘要、审批中心按订单编号/客户姓名/审批类型筛选待办订单、进入审批详情查看真实订单明细与审批记录,并可执行订单审批与取消审批;后端同步补齐订单列表 `order_no` 筛选能力以支撑管理端精确查询。 |
| 2026-05-14 | 已完成一个可检查的完整任务:`frontend/web-admin` 履约调度闭环现已支持查看待发厂订单、生成发厂文案、确认下发工厂、创建司机任务并查看真实任务列表;后端同步补齐物流任务请求模型、数据库仓储、真实任务创建与订单状态从 `pending_factory` 流转到 `pending_driver` 的联动。 |