Compare commits

...

6 Commits

35 changed files with 2268 additions and 295 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.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))

View File

@ -20,6 +20,7 @@ router = APIRouter(prefix="/api/orders", tags=["orders"])
@router.get("")
def list_orders(
order_no: str | None = Query(default=None),
order_status: str | None = Query(default=None),
customer_name: str | None = Query(default=None),
customer_mobile: str | None = Query(default=None),
@ -36,6 +37,7 @@ def list_orders(
orders = order_service.list_orders(
session,
{
"order_no": order_no,
"order_status": order_status,
"customer_name": customer_name,
"customer_mobile": customer_mobile,

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter
from fastapi import APIRouter, Query
from backend.app.schemas.common import success_payload
from backend.app.services.product_service import product_service
@ -22,8 +22,8 @@ def update_category(category_id: int) -> dict:
@router.get("/api/products")
def list_products() -> dict:
return success_payload(product_service.list_products())
def list_products(product_name: str | None = Query(default=None)) -> dict:
return success_payload(product_service.list_products({"product_name": product_name}))
@router.post("/api/products")

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter
from fastapi import APIRouter, Query
from backend.app.schemas.common import success_payload
from backend.app.services.supplier_service import supplier_service
@ -7,9 +7,8 @@ router = APIRouter(prefix="/api/suppliers", tags=["suppliers"])
@router.get("")
def list_suppliers() -> dict:
# 路由层只负责协议转换,具体数据来源交给 service 处理。
return success_payload(supplier_service.list_suppliers())
def list_suppliers(supplier_type: str | None = Query(default=None)) -> dict:
return success_payload(supplier_service.list_suppliers({"supplier_type": supplier_type}))
@router.post("")

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

@ -16,6 +16,8 @@ class OrderRepository:
def list_orders_by_filters(self, session: Session, filters: dict) -> list[SalesOrder]:
stmt = select(SalesOrder).where(SalesOrder.deleted == 0)
if filters.get("order_no"):
stmt = stmt.where(SalesOrder.order_no.contains(filters["order_no"]))
if filters.get("order_status"):
stmt = stmt.where(SalesOrder.order_status == filters["order_status"])
if filters.get("customer_name"):

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

@ -2,6 +2,7 @@ from pydantic import BaseModel, Field
class OrderListQuery(BaseModel):
order_no: str | None = None
order_status: str | None = None
customer_name: str | None = None
customer_mobile: str | None = None

View File

@ -28,6 +28,18 @@ class DemoStore:
"menus": [{"menu_name": "后台首页", "menu_path": "/dashboard"}],
"permissions": ["system:user:list", "system:role:list", "order:list"],
},
("driver01", "driver"): {
"user_id": 10,
"username": "driver01",
"real_name": "演示司机",
"mobile": "13900000010",
"role_id": 4,
"role_name": "司机",
"role_code": "driver",
"token": "demo-driver-token",
"menus": [{"menu_name": "我的任务", "menu_path": "/pages/task-list"}],
"permissions": ["driver:task:list", "driver:task:accept"],
},
}
self.orders = [
{
@ -36,17 +48,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 +131,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()

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:
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()

View File

@ -29,6 +29,7 @@ class OrderService:
"customer_mobile": item.customer_mobile,
"salesman_name": "",
"order_status": item.order_status,
"order_source": item.order_source,
"sale_price_total": float(item.sale_price_total or 0),
"profit_total": float(item.profit_total or 0),
"commission_amount": float(item.commission_amount or 0),

View File

@ -13,7 +13,7 @@ class ProductService:
def create_category(self) -> dict:
return {
"category_id": 3,
"category_name": "分类",
"category_name": "分类",
"category_code": "new-category",
"status": 1,
}
@ -21,41 +21,57 @@ class ProductService:
def update_category(self, category_id: int) -> dict:
return {"category_id": category_id, "updated": True}
def list_products(self) -> dict:
def list_products(self, filters: dict | None = None) -> dict:
product_list = [
{
"product_id": 2001,
"product_name": "演示产品A",
"specification": "10kg",
"unit": "",
"category_id": 1,
"category_name": "工业品",
"cost_price": 60,
"sale_price": 100,
"status": 1,
},
{
"product_id": 2002,
"product_name": "演示产品B",
"specification": "20kg",
"unit": "",
"category_id": 1,
"category_name": "工业品",
"cost_price": 120,
"sale_price": 180,
"status": 1,
},
]
if filters and filters.get("product_name"):
product_list = [item for item in product_list if filters["product_name"] in item["product_name"]]
return {
"total": 1,
"total": len(product_list),
"page_no": 1,
"page_size": 20,
"list": [
{
"product_id": 2001,
"product_name": "产品A",
"specification": "10kg",
"unit": "",
"category_id": 1,
"category_name": "工业品",
"cost_price": 60,
"sale_price": 100,
"status": 1,
}
],
"list": product_list,
}
def create_product(self) -> dict:
return {
"product_id": 2002,
"product_name": "产品B",
"specification": "20kg",
"unit": "",
"product_id": 2003,
"product_name": "新建产品",
"specification": "30kg",
"unit": "",
"status": 1,
}
def get_product(self, product_id: int) -> dict:
return {
"product_id": product_id,
"product_name": "产品A",
"product_name": "演示产品A",
"specification": "10kg",
"unit": "",
"unit": "",
"category_id": 1,
"category_name": "工业品",
"cost_price": 60,

View File

@ -1,27 +1,40 @@
class SupplierService:
# 供应商服务先返回演示数据,后续可以平滑替换为数据库查询。
def list_suppliers(self) -> dict:
def list_suppliers(self, filters: dict | None = None) -> dict:
supplier_list = [
{
"supplier_id": 1001,
"supplier_name": "工厂A",
"supplier_type": "factory",
"contact_name": "王师傅",
"contact_mobile": "13800000001",
"template_type": "default",
"status": 1,
},
{
"supplier_id": 1002,
"supplier_name": "物流合作方B",
"supplier_type": "logistics",
"contact_name": "赵调度",
"contact_mobile": "13800000002",
"template_type": "default",
"status": 1,
},
]
if filters and filters.get("supplier_type"):
supplier_list = [item for item in supplier_list if item["supplier_type"] == filters["supplier_type"]]
return {
"total": 1,
"total": len(supplier_list),
"page_no": 1,
"page_size": 20,
"list": [
{
"supplier_id": 1001,
"supplier_name": "工厂A",
"supplier_type": "factory",
"contact_name": "王五",
"contact_mobile": "13800000001",
"template_type": "default",
"status": 1,
}
],
"list": supplier_list,
}
def create_supplier(self) -> dict:
return {
"supplier_id": 1002,
"supplier_name": "工厂B",
"supplier_id": 1003,
"supplier_name": "新建供应商",
"supplier_type": "factory",
"status": 1,
}

View File

@ -1,5 +1,32 @@
App({
globalData: {
appName: "mini-driver"
appName: "mini-driver",
apiBaseUrl: "http://127.0.0.1:8000",
authToken: "",
driverUser: null,
},
request(options) {
const token = this.globalData.authToken;
return new Promise((resolve, reject) => {
wx.request({
...options,
url: `${this.globalData.apiBaseUrl}${options.url}`,
header: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.header || {}),
},
success: (response) => {
const result = response.data || {};
if (response.statusCode >= 400 || result.code !== 0) {
reject(new Error(result.message || `请求失败:${response.statusCode}`));
return;
}
resolve(result.data);
},
fail: reject,
});
});
}
});

View File

@ -1 +1,45 @@
Page({});
Page({
data: {
username: "driver01",
password: "123456",
loading: false,
message: "",
},
handleInput(event) {
const { field } = event.currentTarget.dataset;
this.setData({
[field]: event.detail.value,
});
},
async handleLogin() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const data = await app.request({
url: "/api/auth/login",
method: "POST",
data: {
username: this.data.username,
password: this.data.password,
role_type: "driver",
},
});
app.globalData.authToken = data.token;
app.globalData.driverUser = data;
this.setData({
message: `欢迎,${data.real_name || data.username}`,
});
wx.navigateTo({
url: "/pages/task-list",
});
} catch (error) {
this.setData({
message: error.message || "司机登录失败",
});
} finally {
this.setData({ loading: false });
}
},
});

View File

@ -1,4 +1,12 @@
<view class="page">
<view class="title">登录页</view>
<view class="desc">预留 username、password、role_type=driver 登录入口。</view>
<view class="title">司机登录</view>
<view class="desc">使用司机账号进入任务列表并处理配送任务。</view>
<view class="card">
<input class="input" value="{{username}}" data-field="username" bindinput="handleInput" placeholder="请输入司机账号" />
<input class="input" password value="{{password}}" data-field="password" bindinput="handleInput" placeholder="请输入密码" />
<button type="primary" loading="{{loading}}" bindtap="handleLogin">
{{ loading ? "登录中..." : "登录司机端" }}
</button>
<view wx:if="{{message}}" class="message">{{message}}</view>
</view>
</view>

View File

@ -1,3 +1,6 @@
.page { padding: 24rpx; }
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 16rpx; }
.input { border: 1rpx solid #d1d5db; border-radius: 12rpx; padding: 20rpx; background: #ffffff; }
.message { color: #047857; font-size: 26rpx; }

View File

@ -1 +1,102 @@
Page({});
function mapTaskStatus(status) {
const statusMap = {
pending: "待接单",
accepted: "已接单",
picked_up: "已揽货",
delivered: "已送达",
canceled: "已取消",
};
return statusMap[status] || status || "-";
}
Page({
data: {
loading: true,
message: "",
actionLoading: false,
taskId: null,
taskInfo: null,
},
async onLoad(options) {
this.setData({
taskId: Number(options.id || 0),
});
await this.loadDetail();
},
async loadDetail() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const data = await app.request({
url: `/api/driver/tasks/${this.data.taskId}`,
method: "GET",
});
this.setData({
taskInfo: {
...data,
statusText: mapTaskStatus(data.status),
},
});
} catch (error) {
this.setData({
message: error.message || "加载任务详情失败",
});
} finally {
this.setData({ loading: false });
}
},
getActionConfig() {
const status = this.data.taskInfo?.status;
if (status === "pending") {
return {
text: "接单",
url: `/api/driver/tasks/${this.data.taskId}/accept`,
successMessage: "接单成功",
};
}
if (status === "accepted") {
return {
text: "确认揽货",
url: `/api/driver/tasks/${this.data.taskId}/pickup`,
successMessage: "已确认揽货",
};
}
if (status === "picked_up") {
return {
text: "确认送达",
url: `/api/driver/tasks/${this.data.taskId}/deliver`,
successMessage: "已确认送达",
};
}
return null;
},
async handleAction() {
const action = this.getActionConfig();
if (!action) {
return;
}
const app = getApp();
this.setData({ actionLoading: true, message: "" });
try {
await app.request({
url: action.url,
method: "POST",
});
await this.loadDetail();
this.setData({
message: action.successMessage,
});
} catch (error) {
this.setData({
message: error.message || "任务操作失败",
});
} finally {
this.setData({ actionLoading: false });
}
},
});

View File

@ -1,14 +1,24 @@
<view class="page">
<view class="title">任务详情</view>
<view class="desc">任务编号、状态、地址、取件内容、数量、工厂、业务员信息、备注</view>
<view class="card">
<view>操作:接单 / 确认揽货 / 确认送达</view>
<view>附件:图片上传必填,视频上传可选</view>
<view>按钮状态pending 显示接单accepted 显示确认揽货picked_up 显示确认送达</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载任务详情...</view>
<view wx:elif="{{taskInfo}}" class="card">
<view>任务编号:{{taskInfo.task_no}}</view>
<view>订单编号:{{taskInfo.order_no || "-"}}</view>
<view>状态:{{taskInfo.statusText}}</view>
<view>取货地址:{{taskInfo.pickup_address}}</view>
<view>送达地址:{{taskInfo.delivery_address}}</view>
<view>取件内容:{{taskInfo.pickup_content}}</view>
<view>数量:{{taskInfo.quantity}}</view>
<view>工厂:{{taskInfo.factory_name || "-"}}</view>
<view>客户:{{taskInfo.customer_name || "-"}}</view>
<view>业务员:{{taskInfo.salesman_name || "-"}}</view>
<view>备注:{{taskInfo.remark || "-"}}</view>
</view>
<view class="actions">
<button size="mini">接单</button>
<button size="mini">确认揽货</button>
<button size="mini">确认送达</button>
<view wx:if="{{taskInfo}}" class="actions">
<button size="mini" type="primary" loading="{{actionLoading}}" disabled="{{taskInfo.status === 'delivered' || taskInfo.status === 'canceled'}}" bindtap="handleAction">
{{ taskInfo.status === 'pending' ? '接单' : (taskInfo.status === 'accepted' ? '确认揽货' : (taskInfo.status === 'picked_up' ? '确认送达' : '已完成')) }}
</button>
</view>
</view>

View File

@ -3,3 +3,4 @@
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.actions { display: flex; gap: 16rpx; margin-top: 20rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }

View File

@ -1 +1,57 @@
Page({});
function mapTaskStatus(status) {
const statusMap = {
pending: "待接单",
accepted: "已接单",
picked_up: "已揽货",
delivered: "已送达",
canceled: "已取消",
};
return statusMap[status] || status || "-";
}
Page({
data: {
loading: true,
message: "",
taskRows: [],
},
async onShow() {
await this.loadTasks();
},
async loadTasks() {
const app = getApp();
this.setData({ loading: true, message: "" });
try {
const data = await app.request({
url: "/api/driver/tasks",
method: "GET",
});
this.setData({
taskRows: (data.list || []).map((item) => ({
taskId: item.task_id,
taskNo: item.task_no,
pickupAddress: item.pickup_address,
deliveryAddress: item.delivery_address,
pickupContent: item.pickup_content,
status: item.status,
statusText: mapTaskStatus(item.status),
})),
});
} catch (error) {
this.setData({
message: error.message || "加载司机任务失败",
});
} finally {
this.setData({ loading: false });
}
},
handleOpenDetail(event) {
const { id } = event.currentTarget.dataset;
wx.navigateTo({
url: `/pages/task-detail?id=${id}`,
});
},
});

View File

@ -1,11 +1,18 @@
<view class="page">
<view class="title">任务列表</view>
<view class="desc">状态:待接单、已接单、已揽货、已送达</view>
<view class="card">
<view>任务编号LT202605140001</view>
<view>取货地址工厂A</view>
<view>送达地址:客户地址</view>
<view>取件内容产品A 1袋</view>
<view>状态:待接单</view>
<view wx:if="{{message}}" class="message">{{message}}</view>
<view wx:if="{{loading}}" class="desc">正在加载任务...</view>
<view wx:elif="{{!taskRows.length}}" class="card">
<view>当前没有司机任务</view>
</view>
<view wx:else>
<view class="card" wx:for="{{taskRows}}" wx:key="taskId" data-id="{{item.taskId}}" bindtap="handleOpenDetail">
<view>任务编号:{{item.taskNo}}</view>
<view>取货地址:{{item.pickupAddress}}</view>
<view>送达地址:{{item.deliveryAddress}}</view>
<view>取件内容:{{item.pickupContent}}</view>
<view>状态:{{item.statusText}}</view>
</view>
</view>
</view>

View File

@ -2,3 +2,4 @@
.title { font-size: 36rpx; font-weight: 600; margin-bottom: 16rpx; }
.desc { font-size: 28rpx; color: #4b5563; }
.card { margin-top: 20rpx; padding: 20rpx; background: #ffffff; border-radius: 12rpx; display: grid; gap: 10rpx; }
.message { margin-top: 20rpx; color: #047857; font-size: 26rpx; }

View File

@ -4,6 +4,7 @@
<h1>管理后台</h1>
<nav>
<RouterLink to="/dashboard">工作台</RouterLink>
<RouterLink to="/approvals">审批中心</RouterLink>
<RouterLink to="/system">系统管理</RouterLink>
<RouterLink to="/master-data">基础资料</RouterLink>
<RouterLink to="/business">业务模块</RouterLink>

View File

@ -9,12 +9,83 @@ import {
taskRows
} from "./data";
const API_BASE_URL = "http://127.0.0.1:8000";
function wait(data) {
return new Promise((resolve) => {
setTimeout(() => resolve(data), 120);
});
}
function buildQuery(params = {}) {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") {
return;
}
search.set(key, String(value));
});
return search.toString();
}
async function request(path, options = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
headers: {
"Content-Type": "application/json",
...(options.headers || {}),
},
...options,
});
const result = await response.json().catch(() => ({}));
if (!response.ok || result.code !== 0) {
throw new Error(result.message || `请求失败: ${response.status}`);
}
return result.data;
}
function mapOrderStatus(status) {
const statusMap = {
draft: "草稿",
pending_approve: "待审批",
approved: "已审批",
rejected: "已驳回",
cancel_pending: "待取消审批",
canceled: "已取消",
pending_factory: "待工厂处理",
};
return statusMap[status] || status || "-";
}
function mapApproveType(type) {
const typeMap = {
order: "订单审批",
cancel_order: "取消审批",
};
return typeMap[type] || type || "-";
}
function mapApproveResult(result) {
const resultMap = {
pass: "通过",
reject: "驳回",
};
return resultMap[result] || result || "-";
}
function buildApprovalRow(item) {
return {
orderId: item.order_id,
orderNo: item.order_no,
customerName: item.customer_name,
customerMobile: item.customer_mobile,
orderStatus: item.order_status,
statusText: mapOrderStatus(item.order_status),
orderSource: item.order_source || "-",
amount: Number(item.sale_price_total || 0).toFixed(2),
createdAt: item.created_at || "-",
};
}
export function fetchDashboardData() {
return wait({ dashboardCards, pendingItems, recentOrders });
}
@ -30,3 +101,275 @@ export function fetchMasterData() {
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,
order_no: filters.order_no?.trim() || undefined,
customer_name: filters.customer_name?.trim() || undefined,
page_no: 1,
page_size: 50,
});
const data = await request(`/api/orders?${query}`);
return (data.list || []).map(buildApprovalRow);
}
export async function fetchApprovalDashboard() {
try {
const [pendingApproveRows, cancelApproveRows] = await Promise.all([
listApprovalOrdersByStatus("pending_approve"),
listApprovalOrdersByStatus("cancel_pending"),
]);
return {
summaryCards: [
{ label: "待订单审批", value: pendingApproveRows.length },
{ label: "待取消审批", value: cancelApproveRows.length },
{ label: "审批总待办", value: pendingApproveRows.length + cancelApproveRows.length },
],
recentRows: [...pendingApproveRows, ...cancelApproveRows]
.sort((first, second) => String(second.createdAt).localeCompare(String(first.createdAt)))
.slice(0, 8),
isMock: false,
};
} catch (error) {
return wait({
summaryCards: [
{ label: "待订单审批", value: 2 },
{ label: "待取消审批", value: 1 },
{ label: "审批总待办", value: 3 },
],
recentRows: recentOrders.map((item) => ({
orderId: null,
orderNo: item.orderNo,
customerName: item.customer,
customerMobile: "-",
orderStatus: "pending_approve",
statusText: item.status,
orderSource: "-",
amount: item.amount,
createdAt: "-",
})),
isMock: true,
});
}
}
export async function fetchApprovalList(filters = {}) {
try {
const targetStatus = filters.approve_status || "";
let rows = [];
if (targetStatus === "pending_approve" || targetStatus === "cancel_pending") {
rows = await listApprovalOrdersByStatus(targetStatus, filters);
} else {
const [pendingApproveRows, cancelApproveRows] = await Promise.all([
listApprovalOrdersByStatus("pending_approve", filters),
listApprovalOrdersByStatus("cancel_pending", filters),
]);
rows = [...pendingApproveRows, ...cancelApproveRows];
}
rows.sort((first, second) => String(second.createdAt).localeCompare(String(first.createdAt)));
return { rows, isMock: false };
} catch (error) {
return wait({
rows: recentOrders.map((item, index) => ({
orderId: index + 1,
orderNo: item.orderNo,
customerName: item.customer,
customerMobile: "-",
orderStatus: "pending_approve",
statusText: item.status,
orderSource: "-",
amount: item.amount,
createdAt: "-",
})),
isMock: true,
});
}
}
export async function fetchApprovalDetail(orderId) {
try {
const data = await request(`/api/orders/${orderId}`);
return {
orderInfo: {
orderId: data.order_id,
orderNo: data.order_no,
customerName: data.customer_name,
customerMobile: data.customer_mobile,
customerAddress: data.customer_address || "-",
orderStatus: data.order_status,
statusText: mapOrderStatus(data.order_status),
orderSource: data.order_source || "-",
deliveryType: data.delivery_type || "-",
factoryName: data.factory_name || "-",
salePriceTotal: Number(data.sale_price_total || 0).toFixed(2),
costPriceTotal: Number(data.cost_price_total || 0).toFixed(2),
profitTotal: Number(data.profit_total || 0).toFixed(2),
profitRate: Number(data.profit_rate || 0).toFixed(2),
remark: data.remark || "-",
},
items: (data.items || []).map((item) => ({
id: item.item_id,
productName: item.product_name,
specification: item.specification || "-",
unit: item.unit || "-",
quantity: Number(item.quantity || 0).toFixed(2),
salePrice: Number(item.sale_price || 0).toFixed(2),
costPrice: Number(item.cost_price || 0).toFixed(2),
})),
approveLogs: (data.approve_logs || []).map((item) => ({
id: item.log_id,
approveTime: item.approve_time || "-",
approveType: mapApproveType(item.approve_type),
approveResult: mapApproveResult(item.approve_result),
afterStatus: mapOrderStatus(item.after_status),
approveOpinion: item.approve_opinion || "-",
})),
isMock: false,
};
} catch (error) {
return wait({
orderInfo: {
orderId,
orderNo: `SO${String(orderId).padStart(12, "0")}`,
customerName: "演示客户",
customerMobile: "13900000000",
customerAddress: "演示地址",
orderStatus: "pending_approve",
statusText: "待审批",
orderSource: "演示来源",
deliveryType: "工厂直送",
factoryName: "演示工厂",
salePriceTotal: "100.00",
costPriceTotal: "60.00",
profitTotal: "40.00",
profitRate: "40.00",
remark: "当前为演示审批详情",
},
items: [
{
id: 1,
productName: "演示产品A",
specification: "10kg",
unit: "吨",
quantity: "1.00",
salePrice: "100.00",
costPrice: "60.00",
},
],
approveLogs: [],
isMock: true,
});
}
}
export async function approveOrder(orderId, payload) {
return request(`/api/orders/${orderId}/approve`, {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function cancelApproveOrder(orderId, payload) {
return request(`/api/orders/${orderId}/cancel-approve`, {
method: "POST",
body: JSON.stringify(payload),
});
}

View File

@ -4,10 +4,14 @@ import DashboardPage from "./views/DashboardPage.vue";
import SystemPage from "./views/SystemPage.vue";
import MasterDataPage from "./views/MasterDataPage.vue";
import BusinessPage from "./views/BusinessPage.vue";
import ApprovalsPage from "./views/ApprovalsPage.vue";
import ApprovalDetailPage from "./views/ApprovalDetailPage.vue";
const routes = [
{ path: "/", redirect: "/dashboard" },
{ path: "/dashboard", component: DashboardPage },
{ path: "/approvals", component: ApprovalsPage },
{ path: "/approvals/:id", component: ApprovalDetailPage },
{ path: "/system", component: SystemPage },
{ path: "/master-data", component: MasterDataPage },
{ path: "/business", component: BusinessPage }

View File

@ -0,0 +1,259 @@
<template>
<section class="page">
<div class="header-row">
<div>
<h2>审批详情</h2>
<p class="subline">查看订单利润审批历史并直接完成审批动作</p>
</div>
<div class="header-actions">
<span class="mode-tag" :class="{ fallback: isMock }">
{{ isMock ? "演示数据" : "真实接口" }}
</span>
<RouterLink class="back-link" to="/approvals">返回审批中心</RouterLink>
</div>
</div>
<p v-if="loading" class="loading">正在加载审批详情...</p>
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
{{ message }}
</div>
<template v-if="!loading && orderInfo">
<section class="summary-grid">
<article class="card">
<h3>订单信息</h3>
<p>订单编号{{ orderInfo.orderNo }}</p>
<p>当前状态{{ orderInfo.statusText }}</p>
<p>订单来源{{ orderInfo.orderSource }}</p>
<p>配送方式{{ orderInfo.deliveryType }}</p>
<p>工厂名称{{ orderInfo.factoryName }}</p>
</article>
<article class="card">
<h3>客户信息</h3>
<p>客户姓名{{ orderInfo.customerName }}</p>
<p>客户手机{{ orderInfo.customerMobile }}</p>
<p>客户地址{{ orderInfo.customerAddress }}</p>
<p>备注{{ orderInfo.remark }}</p>
</article>
<article class="card">
<h3>利润信息</h3>
<p>销售总额{{ orderInfo.salePriceTotal }}</p>
<p>成本总额{{ orderInfo.costPriceTotal }}</p>
<p>利润总额{{ orderInfo.profitTotal }}</p>
<p>利润率{{ orderInfo.profitRate }}%</p>
</article>
</section>
<section class="detail-panel">
<div class="panel-header">
<h3>订单明细</h3>
<span>供审批时核对商品和价格</span>
</div>
<table v-if="items.length">
<thead>
<tr>
<th>产品名称</th>
<th>规格</th>
<th>单位</th>
<th>数量</th>
<th>销售单价</th>
<th>成本单价</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td>{{ item.productName }}</td>
<td>{{ item.specification }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.salePrice }}</td>
<td>{{ item.costPrice }}</td>
</tr>
</tbody>
</table>
<div v-else class="empty-box">当前订单没有明细</div>
</section>
<section class="detail-panel">
<div class="panel-header">
<h3>审批操作</h3>
<span>{{ actionHint }}</span>
</div>
<div class="action-form">
<label>
<span>审批意见</span>
<textarea v-model.trim="approveOpinion" rows="3" placeholder="请输入审批意见"></textarea>
</label>
<div class="action-buttons">
<button
:disabled="Boolean(actionLoading) || !canApprove"
@click="handleApprove('pass')"
>
{{ primaryActionLoading ? primaryActionLoading : primaryActionText }}
</button>
<button
class="danger-btn"
:disabled="Boolean(actionLoading) || !canApprove"
@click="handleApprove('reject')"
>
{{ secondaryActionLoading ? secondaryActionLoading : secondaryActionText }}
</button>
</div>
</div>
</section>
<section class="detail-panel">
<div class="panel-header">
<h3>审批记录</h3>
<span>展示真实审批流转结果</span>
</div>
<div v-if="approveLogs.length" class="log-list">
<article v-for="item in approveLogs" :key="item.id" class="log-card">
<div class="log-row">
<strong>{{ item.approveTime }}</strong>
<span>{{ item.approveType }}</span>
<span>{{ item.approveResult }}</span>
<span>{{ item.afterStatus }}</span>
</div>
<p>{{ item.approveOpinion }}</p>
</article>
</div>
<div v-else class="empty-box">暂无审批记录</div>
</section>
</template>
</section>
</template>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { approveOrder, cancelApproveOrder, fetchApprovalDetail } from "../mockApi";
const route = useRoute();
const loading = ref(true);
const orderInfo = ref(null);
const items = ref([]);
const approveLogs = ref([]);
const approveOpinion = ref("");
const actionLoading = ref("");
const message = ref("");
const messageType = ref("success");
const isMock = ref(false);
const canApprove = computed(() =>
["pending_approve", "cancel_pending"].includes(orderInfo.value?.orderStatus || ""),
);
const actionHint = computed(() => {
if (orderInfo.value?.orderStatus === "cancel_pending") {
return "当前为取消审批流,可执行同意取消或驳回取消。";
}
return "当前为订单审批流,可执行通过或驳回。";
});
const primaryActionText = computed(() =>
orderInfo.value?.orderStatus === "cancel_pending" ? "同意取消" : "审批通过",
);
const secondaryActionText = computed(() =>
orderInfo.value?.orderStatus === "cancel_pending" ? "驳回取消" : "审批驳回",
);
const primaryActionLoading = computed(() => {
if (actionLoading.value === "pass") {
return orderInfo.value?.orderStatus === "cancel_pending" ? "处理中..." : "审批中...";
}
return "";
});
const secondaryActionLoading = computed(() => {
if (actionLoading.value === "reject") {
return orderInfo.value?.orderStatus === "cancel_pending" ? "处理中..." : "审批中...";
}
return "";
});
async function loadDetail() {
const data = await fetchApprovalDetail(route.params.id);
orderInfo.value = data.orderInfo;
items.value = data.items;
approveLogs.value = data.approveLogs;
isMock.value = Boolean(data.isMock);
}
async function handleApprove(result) {
if (!orderInfo.value) {
return;
}
actionLoading.value = result;
message.value = "";
try {
const payload = {
approve_result: result,
approve_opinion: approveOpinion.value || (result === "pass" ? "审批通过" : "审批驳回"),
};
if (orderInfo.value.orderStatus === "cancel_pending") {
await cancelApproveOrder(orderInfo.value.orderId, payload);
message.value = result === "pass" ? "取消审批已通过。" : "已驳回取消申请。";
} else {
await approveOrder(orderInfo.value.orderId, payload);
message.value = result === "pass" ? "订单审批已通过。" : "订单已驳回。";
}
messageType.value = "success";
await loadDetail();
} catch (error) {
message.value = error.message || "审批操作失败";
messageType.value = "error";
} finally {
actionLoading.value = "";
}
}
onMounted(async () => {
try {
await loadDetail();
} finally {
loading.value = false;
}
});
</script>
<style scoped>
.page { background: #fff; border-radius: 8px; padding: 20px; }
.header-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; }
.header-actions { display: flex; align-items: center; gap: 12px; }
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; }
.back-link { color: #2563eb; text-decoration: none; }
.loading { margin: 0; color: #6b7280; }
.message-box { margin-bottom: 16px; padding: 12px 14px; border-radius: 8px; background: #dcfce7; color: #166534; }
.message-box.error { background: #fee2e2; color: #b91c1c; }
.summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
.card h3 { margin: 0 0 10px; font-size: 16px; }
.card p { margin: 6px 0 0; color: #4b5563; line-height: 1.6; }
.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; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
.action-form { display: grid; gap: 12px; }
label { display: grid; gap: 6px; }
label span { color: #4b5563; font-size: 13px; }
textarea, button { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
.action-buttons { display: flex; gap: 12px; }
button:disabled { color: #9ca3af; background: #f3f4f6; }
.danger-btn { color: #b91c1c; border-color: #fecaca; }
.log-list { display: grid; gap: 10px; }
.log-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; background: #f9fafb; }
.log-row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px; color: #374151; }
.empty-box { padding: 20px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }
</style>

View File

@ -0,0 +1,145 @@
<template>
<section class="page">
<div class="header-row">
<div>
<h2>审批中心</h2>
<p class="subline">集中处理待订单审批和待取消审批任务</p>
</div>
<span class="mode-tag" :class="{ fallback: isMock }">
{{ isMock ? "演示数据" : "真实接口" }}
</span>
</div>
<div class="toolbar">
<label>
<span>审批类型</span>
<select v-model="filters.approve_status">
<option value="">全部待办</option>
<option value="pending_approve">待订单审批</option>
<option value="cancel_pending">待取消审批</option>
</select>
</label>
<label>
<span>订单编号</span>
<input v-model.trim="filters.order_no" type="text" placeholder="请输入订单编号" />
</label>
<label>
<span>客户姓名</span>
<input v-model.trim="filters.customer_name" type="text" placeholder="请输入客户姓名" />
</label>
<div class="toolbar-actions">
<button :disabled="loading" @click="handleSearch">查询待办</button>
<button :disabled="loading" @click="handleReset">重置</button>
</div>
</div>
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
{{ message }}
</div>
<p v-if="loading" class="loading">正在加载审批待办...</p>
<div v-else-if="!rows.length" class="empty-state">当前没有待审批订单</div>
<section v-else class="table-panel">
<table>
<thead>
<tr>
<th>订单编号</th>
<th>客户姓名</th>
<th>客户手机号</th>
<th>当前状态</th>
<th>订单来源</th>
<th>销售金额</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="`${row.orderStatus}-${row.orderId || row.orderNo}`">
<td>{{ row.orderNo }}</td>
<td>{{ row.customerName }}</td>
<td>{{ row.customerMobile }}</td>
<td><span class="status">{{ row.statusText }}</span></td>
<td>{{ row.orderSource }}</td>
<td>{{ row.amount }}</td>
<td>{{ row.createdAt }}</td>
<td>
<RouterLink v-if="row.orderId" class="detail-link" :to="`/approvals/${row.orderId}`">进入审批</RouterLink>
<span v-else class="detail-link disabled-link">演示数据</span>
</td>
</tr>
</tbody>
</table>
</section>
</section>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { fetchApprovalList } from "../mockApi";
const loading = ref(true);
const isMock = ref(false);
const rows = ref([]);
const message = ref("");
const messageType = ref("success");
const filters = reactive({
approve_status: "",
order_no: "",
customer_name: "",
});
async function loadRows() {
const data = await fetchApprovalList(filters);
rows.value = data.rows;
isMock.value = Boolean(data.isMock);
}
async function handleSearch() {
loading.value = true;
message.value = "";
try {
await loadRows();
} catch (error) {
message.value = error.message || "加载审批列表失败";
messageType.value = "error";
} finally {
loading.value = false;
}
}
async function handleReset() {
filters.approve_status = "";
filters.order_no = "";
filters.customer_name = "";
await handleSearch();
}
onMounted(async () => {
await handleSearch();
});
</script>
<style scoped>
.page { background: #fff; border-radius: 8px; padding: 20px; }
.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; }
.toolbar { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) auto; gap: 12px; margin-bottom: 16px; }
label { display: grid; gap: 6px; }
label span { color: #4b5563; font-size: 13px; }
input, select, button { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
.toolbar-actions { display: flex; gap: 10px; align-items: end; }
.message-box { margin-bottom: 16px; padding: 12px 14px; border-radius: 8px; background: #dcfce7; color: #166534; }
.message-box.error { background: #fee2e2; color: #b91c1c; }
.loading { margin: 0; color: #6b7280; }
.empty-state { padding: 24px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }
.table-panel { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
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: #eef2ff; color: #4338ca; }
.detail-link { color: #2563eb; text-decoration: none; }
.disabled-link { color: #9ca3af; }
</style>

View File

@ -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>

View File

@ -2,7 +2,13 @@
<section class="page">
<h2>工作台</h2>
<p v-if="loading" class="loading">加载中...</p>
<div v-else class="cards">
<div v-else class="header-row">
<span class="mode-tag" :class="{ fallback: isMock }">
{{ isMock ? "演示数据" : "真实接口" }}
</span>
<RouterLink class="jump-link" to="/approvals">进入审批中心</RouterLink>
</div>
<div v-if="!loading" class="cards">
<article v-for="card in dashboardCards" :key="card.label" class="card">
<span class="label">{{ card.label }}</span>
<strong class="value">{{ card.value }}</strong>
@ -10,28 +16,35 @@
</div>
<div v-if="!loading" class="split">
<section class="panel">
<h3>待处理事项</h3>
<h3>审批说明</h3>
<ul>
<li v-for="item in pendingItems" :key="item">{{ item }}</li>
</ul>
</section>
<section class="panel">
<h3>最近</h3>
<h3>最近审批</h3>
<table>
<thead>
<tr>
<th>订单编号</th>
<th>客户</th>
<th>状态</th>
<th>来源</th>
<th>金额</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in recentOrders" :key="row.orderNo">
<td>{{ row.orderNo }}</td>
<td>{{ row.customer }}</td>
<td>{{ row.status }}</td>
<td>{{ row.customerName }}</td>
<td>{{ row.statusText }}</td>
<td>{{ row.orderSource }}</td>
<td>{{ row.amount }}</td>
<td>
<RouterLink v-if="row.orderId" class="detail-link" :to="`/approvals/${row.orderId}`">查看审批</RouterLink>
<span v-else class="detail-link disabled-link">演示数据</span>
</td>
</tr>
</tbody>
</table>
@ -43,18 +56,24 @@
<script setup>
import { onMounted, ref } from "vue";
import { fetchDashboardData } from "../mockApi";
import { fetchApprovalDashboard } from "../mockApi";
const loading = ref(true);
const dashboardCards = ref([]);
const pendingItems = ref([]);
const recentOrders = ref([]);
const isMock = ref(false);
onMounted(async () => {
const data = await fetchDashboardData();
dashboardCards.value = data.dashboardCards;
pendingItems.value = data.pendingItems;
recentOrders.value = data.recentOrders;
const data = await fetchApprovalDashboard();
dashboardCards.value = data.summaryCards;
pendingItems.value = [
"审批中心支持待订单审批和待取消审批两条流转线。",
"进入详情后可直接执行通过、驳回、同意取消、驳回取消。",
"审批动作会回写真实订单状态和审批记录。",
];
recentOrders.value = data.recentRows;
isMock.value = Boolean(data.isMock);
loading.value = false;
});
</script>
@ -63,6 +82,10 @@ onMounted(async () => {
.page { background: #fff; border-radius: 8px; padding: 20px; }
h2 { margin: 0 0 12px; }
.loading { margin: 0; color: #6b7280; }
.header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.mode-tag { padding: 6px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; }
.mode-tag.fallback { background: #fef3c7; color: #92400e; }
.jump-link { color: #2563eb; text-decoration: none; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; background: #f9fafb; }
.label { display: block; color: #6b7280; margin-bottom: 8px; }
@ -74,4 +97,6 @@ ul { margin: 0; padding-left: 18px; }
li + li { margin-top: 8px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; }
.detail-link { color: #2563eb; text-decoration: none; }
.disabled-link { color: #9ca3af; }
</style>

View File

@ -15,6 +15,17 @@ function wait(data) {
});
}
function buildQuery(params = {}) {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") {
return;
}
search.set(key, String(value));
});
return search.toString();
}
async function request(path, options = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
headers: {
@ -60,9 +71,24 @@ export function loginSalesUser(form) {
});
}
export async function fetchOrdersPage() {
function normalizeOrderFilters(filters = {}) {
return {
order_status: filters.order_status || undefined,
customer_name: filters.customer_name?.trim() || undefined,
customer_mobile: filters.customer_mobile?.trim() || undefined,
order_source: filters.order_source?.trim() || undefined,
// 日期输入框返回 yyyy-mm-dd这里补足接口要求的完整时间格式。
start_time: filters.start_date ? `${filters.start_date} 00:00:00` : undefined,
end_time: filters.end_date ? `${filters.end_date} 23:59:59` : undefined,
page_no: 1,
page_size: 20,
};
}
export async function fetchOrdersPage(filters = {}) {
const query = buildQuery(normalizeOrderFilters(filters));
try {
const data = await request("/api/orders?page_no=1&page_size=20");
const data = await request(`/api/orders?${query}`);
return {
orderFilters,
orderActions,
@ -75,6 +101,7 @@ export async function fetchOrdersPage() {
rawStatus: item.order_status,
source: item.order_source || "-",
amount: Number(item.sale_price_total || 0).toFixed(2),
createdAt: item.created_at || "-",
})),
isMock: false,
};
@ -82,7 +109,7 @@ export async function fetchOrdersPage() {
return wait({
orderFilters,
orderActions,
orderRows: orderRows.map((item) => ({ ...item, rawStatus: "draft" })),
orderRows: orderRows.map((item) => ({ ...item, rawStatus: "draft", createdAt: "-" })),
isMock: true,
});
}
@ -92,6 +119,38 @@ export function fetchOrderForm() {
return wait({ sections: orderFormSections });
}
export async function fetchCustomerOptions() {
const data = await request("/api/customers?page_no=1&page_size=20");
return (data.list || []).map((item) => ({
value: item.customer_id,
label: `${item.customer_name} / ${item.mobile}`,
customer_name: item.customer_name,
customer_mobile: item.mobile,
customer_address: item.address || "",
}));
}
export async function fetchFactoryOptions() {
const data = await request("/api/suppliers?supplier_type=factory");
return (data.list || []).map((item) => ({
value: item.supplier_id,
label: item.supplier_name,
}));
}
export async function fetchProductOptions() {
const data = await request("/api/products");
return (data.list || []).map((item) => ({
value: item.product_id,
label: `${item.product_name} / ${item.specification}`,
product_name: item.product_name,
specification: item.specification,
unit: item.unit,
sale_price: item.sale_price,
cost_price: item.cost_price,
}));
}
export async function createOrder(payload) {
return request("/api/orders", {
method: "POST",
@ -119,15 +178,6 @@ export async function fetchOrderDetail(orderId) {
title: "发厂信息",
content: `工厂:${data.factory_name || "-"};配送方式:${data.delivery_type || "-"};备注:${data.remark || "-"}`,
},
{
title: "审批记录",
content:
data.approve_logs && data.approve_logs.length
? data.approve_logs
.map((item) => `${item.approve_time} ${item.approve_type} ${item.approve_result} ${item.approve_opinion || ""}`.trim())
.join("")
: "暂无审批记录",
},
];
return {
blocks,
@ -137,6 +187,23 @@ export async function fetchOrderDetail(orderId) {
rawStatus: data.order_status,
statusText: mapOrderStatus(data.order_status),
},
orderItems: (data.items || []).map((item) => ({
id: item.item_id,
productName: item.product_name,
specification: item.specification || "-",
unit: item.unit || "-",
quantity: Number(item.quantity || 0).toFixed(2),
salePrice: Number(item.sale_price || 0).toFixed(2),
costPrice: Number(item.cost_price || 0).toFixed(2),
})),
approveLogs: (data.approve_logs || []).map((item) => ({
id: item.log_id,
approveTime: item.approve_time || "-",
approveType: item.approve_type || "-",
approveResult: item.approve_result || "-",
afterStatus: mapOrderStatus(item.after_status),
approveOpinion: item.approve_opinion || "-",
})),
isMock: false,
};
} catch (error) {
@ -154,6 +221,18 @@ export async function fetchOrderDetail(orderId) {
rawStatus: "draft",
statusText: "草稿",
},
orderItems: [
{
id: 1,
productName: "演示产品A",
specification: "10kg",
unit: "吨",
quantity: "1.00",
salePrice: "100.00",
costPrice: "60.00",
},
],
approveLogs: [],
isMock: true,
});
}

View File

@ -41,6 +41,57 @@
<p>{{ block.content }}</p>
</article>
</div>
<section v-if="!loading" class="detail-panel">
<div class="panel-header">
<h3>订单明细</h3>
<span>展示真实订单商品内容</span>
</div>
<div v-if="orderItems.length" class="detail-table-wrap">
<table>
<thead>
<tr>
<th>产品名称</th>
<th>规格</th>
<th>单位</th>
<th>数量</th>
<th>销售单价</th>
<th>成本单价</th>
</tr>
</thead>
<tbody>
<tr v-for="item in orderItems" :key="item.id">
<td>{{ item.productName }}</td>
<td>{{ item.specification }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.salePrice }}</td>
<td>{{ item.costPrice }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="empty-box">当前订单暂无明细数据</div>
</section>
<section v-if="!loading" class="detail-panel">
<div class="panel-header">
<h3>审批记录</h3>
<span>展示真实审批流转历史</span>
</div>
<div v-if="approveLogs.length" class="log-list">
<article v-for="item in approveLogs" :key="item.id" class="log-card">
<div class="log-row">
<strong>{{ item.approveTime }}</strong>
<span>{{ item.approveType }}</span>
<span>{{ item.approveResult }}</span>
<span>{{ item.afterStatus }}</span>
</div>
<p>{{ item.approveOpinion }}</p>
</article>
</div>
<div v-else class="empty-box">暂无审批记录</div>
</section>
</section>
</template>
@ -55,6 +106,8 @@ const loading = ref(true);
const isMock = ref(false);
const blocks = ref([]);
const orderInfo = ref(null);
const orderItems = ref([]);
const approveLogs = ref([]);
const actionLoading = ref("");
const message = ref("");
const messageType = ref("success");
@ -71,6 +124,8 @@ async function loadDetail() {
const data = await fetchOrderDetail(route.params.id);
blocks.value = data.blocks;
orderInfo.value = data.orderInfo;
orderItems.value = data.orderItems || [];
approveLogs.value = data.approveLogs || [];
isMock.value = Boolean(data.isMock);
}
@ -139,6 +194,16 @@ h2 { margin: 0; }
.danger-btn { color: #b91c1c; border-color: #fecaca; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
.card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
.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 span { color: #6b7280; font-size: 13px; }
.detail-table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
.log-list { display: grid; gap: 10px; }
.log-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; background: #f9fafb; }
.log-row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px; color: #374151; }
.empty-box { padding: 20px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }
h3 { margin: 0 0 10px; font-size: 16px; }
p { margin: 0; line-height: 1.7; color: #4b5563; }
</style>

View File

@ -1,57 +1,83 @@
<template>
<section class="page">
<h2>新建订单</h2>
<p class="tips">当前先接入业务员端最小可用创建流程后续再扩展多明细和更多字段</p>
<p class="tips">本页已接入真实客户工厂产品选项当前保留单条明细的最小可用创建流程</p>
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
{{ message }}
</div>
<form class="form-grid" @submit.prevent="handleSubmit">
<label>
<span>选择客户</span>
<select v-model="selectedCustomerId" @change="handleCustomerChange">
<option value="">请选择客户</option>
<option v-for="option in customerOptions" :key="option.value" :value="String(option.value)">
{{ option.label }}
</option>
</select>
</label>
<label>
<span>客户姓名</span>
<input v-model.trim="form.customer_name" type="text" placeholder="请输入客户姓名" />
</label>
<label>
<span>客户手机号</span>
<input v-model.trim="form.customer_mobile" type="text" placeholder="请输入客户手机号" />
</label>
<label>
<span>客户地址</span>
<input v-model.trim="form.customer_address" type="text" placeholder="请输入客户地址" />
</label>
<label>
<span>订单来源</span>
<input v-model.trim="form.order_source" type="text" placeholder="例如:线下拜访" />
</label>
<label>
<span>配送方式</span>
<input v-model.trim="form.delivery_type" type="text" placeholder="例如:工厂直送" />
</label>
<label>
<span>工厂ID</span>
<input v-model.number="form.factory_id" type="number" min="1" placeholder="请输入工厂ID" />
<span>选择工厂</span>
<select v-model.number="form.factory_id">
<option :value="null">请选择工厂</option>
<option v-for="option in factoryOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</label>
<label>
<span>提成金额</span>
<input v-model.number="form.commission_amount" type="number" min="0" step="0.01" />
</label>
<label>
<span>返利合计</span>
<input v-model.number="form.rebate_total" type="number" min="0" step="0.01" />
</label>
<label>
<span>运费合计</span>
<input v-model.number="form.freight_total" type="number" min="0" step="0.01" />
</label>
<label>
<span>税费合计</span>
<input v-model.number="form.tax_total" type="number" min="0" step="0.01" />
</label>
<label>
<span>其他费用</span>
<input v-model.number="form.other_fee_total" type="number" min="0" step="0.01" />
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="form.remark" rows="3" placeholder="请输入订单备注"></textarea>
@ -61,29 +87,44 @@
<section class="item-panel">
<div class="panel-header">
<h3>订单明细</h3>
<span>当前先维护 1 明细</span>
<span>当前保留 1 条最小可用明细</span>
</div>
<div class="form-grid">
<label>
<span>选择产品</span>
<select v-model="selectedProductId" @change="handleProductChange">
<option value="">请选择产品</option>
<option v-for="option in productOptions" :key="option.value" :value="String(option.value)">
{{ option.label }}
</option>
</select>
</label>
<label>
<span>产品名称</span>
<input v-model.trim="item.product_name" type="text" placeholder="请输入产品名称" />
</label>
<label>
<span>规格</span>
<input v-model.trim="item.specification" type="text" placeholder="请输入规格" />
</label>
<label>
<span>单位</span>
<input v-model.trim="item.unit" type="text" placeholder="例如:吨" />
</label>
<label>
<span>数量</span>
<input v-model.number="item.quantity" type="number" min="0.01" step="0.01" />
</label>
<label>
<span>销售单价</span>
<input v-model.number="item.sale_price" type="number" min="0" step="0.01" />
</label>
<label>
<span>成本单价</span>
<input v-model.number="item.cost_price" type="number" min="0" step="0.01" />
@ -100,7 +141,7 @@
<section class="actions">
<button type="button" class="secondary-btn" @click="resetForm">重置表单</button>
<button type="button" class="secondary-btn" @click="fillDemoData">填充演示数据</button>
<button type="submit" :disabled="submitting" @click="handleSubmit">
<button type="submit" :disabled="submitting || optionLoading" @click="handleSubmit">
{{ submitting ? "创建中..." : "创建订单" }}
</button>
</section>
@ -108,15 +149,26 @@
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { computed, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { createOrder } from "../mockApi";
import {
createOrder,
fetchCustomerOptions,
fetchFactoryOptions,
fetchProductOptions,
} from "../mockApi";
const router = useRouter();
const submitting = ref(false);
const optionLoading = ref(true);
const message = ref("");
const messageType = ref("success");
const customerOptions = ref([]);
const factoryOptions = ref([]);
const productOptions = ref([]);
const selectedCustomerId = ref("");
const selectedProductId = ref("");
function buildDefaultForm() {
return {
@ -125,7 +177,7 @@ function buildDefaultForm() {
customer_address: "",
order_source: "",
delivery_type: "",
factory_id: 1001,
factory_id: null,
commission_amount: 0,
rebate_total: 0,
freight_total: 0,
@ -137,6 +189,7 @@ function buildDefaultForm() {
function buildDefaultItem() {
return {
product_id: null,
product_name: "",
specification: "",
unit: "",
@ -169,40 +222,110 @@ const profitTotal = computed(
function resetForm() {
Object.assign(form, buildDefaultForm());
Object.assign(item, buildDefaultItem());
selectedCustomerId.value = "";
selectedProductId.value = "";
message.value = "";
}
function handleCustomerChange() {
const selected = customerOptions.value.find((option) => String(option.value) === selectedCustomerId.value);
if (!selected) {
return;
}
form.customer_name = selected.customer_name;
form.customer_mobile = selected.customer_mobile;
form.customer_address = selected.customer_address;
}
function handleProductChange() {
const selected = productOptions.value.find((option) => String(option.value) === selectedProductId.value);
if (!selected) {
return;
}
item.product_id = selected.value;
item.product_name = selected.product_name;
item.specification = selected.specification;
item.unit = selected.unit;
item.sale_price = Number(selected.sale_price || 0);
item.cost_price = Number(selected.cost_price || 0);
}
function fillDemoData() {
Object.assign(form, {
customer_name: "演示客户",
customer_mobile: "13900000000",
customer_address: "杭州市西湖区演示地址 1 号",
order_source: "线下拜访",
delivery_type: "工厂直送",
factory_id: 1001,
commission_amount: 50,
rebate_total: 5,
freight_total: 10,
tax_total: 3,
other_fee_total: 2,
remark: "前端联调演示订单",
});
Object.assign(item, {
product_name: "演示产品A",
specification: "10kg",
unit: "吨",
quantity: 1,
sale_price: 100,
cost_price: 60,
rebate_amount: 5,
freight_amount: 10,
tax_amount: 3,
other_fee_amount: 2,
remark: "演示明细",
});
if (customerOptions.value.length) {
selectedCustomerId.value = String(customerOptions.value[0].value);
handleCustomerChange();
} else {
form.customer_name = "演示客户";
form.customer_mobile = "13900000000";
form.customer_address = "杭州市西湖区演示地址 1 号";
}
if (productOptions.value.length) {
selectedProductId.value = String(productOptions.value[0].value);
handleProductChange();
} else {
item.product_name = "演示产品A";
item.specification = "10kg";
item.unit = "吨";
item.sale_price = 100;
item.cost_price = 60;
}
form.order_source = "线下拜访";
form.delivery_type = "工厂直送";
form.factory_id = factoryOptions.value[0]?.value ?? 1001;
form.commission_amount = 50;
form.rebate_total = 5;
form.freight_total = 10;
form.tax_total = 3;
form.other_fee_total = 2;
form.remark = "前后端联调演示订单";
item.quantity = 1;
item.rebate_amount = 5;
item.freight_amount = 10;
item.tax_amount = 3;
item.other_fee_amount = 2;
item.remark = "演示明细";
}
async function loadOptions() {
optionLoading.value = true;
try {
const [customers, factories, products] = await Promise.all([
fetchCustomerOptions(),
fetchFactoryOptions(),
fetchProductOptions(),
]);
customerOptions.value = customers;
factoryOptions.value = factories;
productOptions.value = products;
} catch (error) {
message.value = error.message || "加载客户、工厂、产品选项失败";
messageType.value = "error";
} finally {
optionLoading.value = false;
}
}
async function handleSubmit() {
//
if (!form.customer_name.trim()) {
message.value = "请输入客户姓名";
messageType.value = "error";
return;
}
if (!form.customer_mobile.trim()) {
message.value = "请输入客户手机号";
messageType.value = "error";
return;
}
if (!item.product_name.trim()) {
message.value = "请输入产品名称";
messageType.value = "error";
return;
}
submitting.value = true;
message.value = "";
try {
@ -220,6 +343,10 @@ async function handleSubmit() {
submitting.value = false;
}
}
onMounted(async () => {
await loadOptions();
});
</script>
<style scoped>
@ -231,7 +358,7 @@ h2 { margin: 0 0 12px; }
.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; }
input, textarea { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
input, textarea, select { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
.full-width { grid-column: 1 / -1; }
.item-panel { margin-top: 20px; padding: 16px; border: 1px solid #e5e7eb; border-radius: 8px; }
.panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }

View File

@ -12,11 +12,26 @@
</span>
</div>
<div class="chips">
<label v-for="item in orderFilters" :key="item" class="filter">
<span>{{ item }}</span>
<input type="text" :placeholder="`请输入${item}`" disabled />
<label v-for="item in filterFields" :key="item.key" class="filter">
<span>{{ item.label }}</span>
<select v-if="item.type === 'select'" v-model="filters[item.key]">
<option value="">全部</option>
<option v-for="option in item.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<input
v-else
v-model.trim="filters[item.key]"
:type="item.type"
:placeholder="item.placeholder"
/>
</label>
</div>
<div class="filter-actions">
<button class="inline-btn" :disabled="loading" @click="handleSearch">查询订单</button>
<button class="inline-btn" :disabled="loading" @click="handleReset">重置筛选</button>
</div>
</div>
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
@ -36,6 +51,7 @@
<th>订单状态</th>
<th>订单来源</th>
<th>销售金额</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
@ -47,6 +63,7 @@
<td><span class="status">{{ row.status }}</span></td>
<td>{{ row.source }}</td>
<td>{{ row.amount }}</td>
<td>{{ row.createdAt }}</td>
<td class="actions-cell">
<RouterLink v-if="row.orderId" class="detail-link" :to="`/orders/${row.orderId}`">查看详情</RouterLink>
<span v-else class="detail-link disabled-link">演示详情</span>
@ -73,20 +90,49 @@
</template>
<script setup>
import { onMounted, ref } from "vue";
import { onMounted, reactive, ref } from "vue";
import { cancelOrder, fetchOrdersPage, submitOrder } from "../mockApi";
import { salesStore } from "../store";
const loading = ref(true);
const isMock = ref(false);
const orderFilters = ref([]);
const orderRows = ref([]);
const user = salesStore.user;
const submittingId = ref(null);
const cancelingId = ref(null);
const message = ref("");
const messageType = ref("success");
const filters = reactive({
order_status: "",
customer_name: "",
customer_mobile: "",
order_source: "",
start_date: "",
end_date: "",
});
const filterFields = [
{
key: "order_status",
label: "订单状态",
type: "select",
options: [
{ value: "draft", label: "草稿" },
{ value: "pending_approve", label: "待审批" },
{ value: "approved", label: "已审批" },
{ value: "rejected", label: "已驳回" },
{ value: "cancel_pending", label: "待取消审批" },
{ value: "canceled", label: "已取消" },
{ value: "pending_factory", label: "待工厂处理" },
],
},
{ key: "customer_name", label: "客户姓名", type: "text", placeholder: "请输入客户姓名" },
{ key: "customer_mobile", label: "客户手机号", type: "text", placeholder: "请输入客户手机号" },
{ key: "order_source", label: "订单来源", type: "text", placeholder: "请输入订单来源" },
{ key: "start_date", label: "开始日期", type: "date", placeholder: "" },
{ key: "end_date", label: "结束日期", type: "date", placeholder: "" },
];
function canSubmit(status) {
return ["draft", "rejected"].includes(status);
@ -97,12 +143,31 @@ function canCancel(status) {
}
async function loadOrders() {
const data = await fetchOrdersPage();
orderFilters.value = data.orderFilters;
const data = await fetchOrdersPage(filters);
orderRows.value = data.orderRows;
isMock.value = Boolean(data.isMock);
}
async function handleSearch() {
loading.value = true;
message.value = "";
try {
await loadOrders();
} finally {
loading.value = false;
}
}
async function handleReset() {
filters.order_status = "";
filters.customer_name = "";
filters.customer_mobile = "";
filters.order_source = "";
filters.start_date = "";
filters.end_date = "";
await handleSearch();
}
async function handleSubmit(row) {
if (!row.orderId || isMock.value) {
message.value = "当前是演示数据模式,暂不执行真实提交。";
@ -164,7 +229,8 @@ h3 { margin: 0; font-size: 16px; }
.chips { display: flex; flex-wrap: wrap; gap: 10px; }
.filter { display: grid; gap: 6px; min-width: 180px; }
.filter span { color: #4b5563; font-size: 13px; }
.filter input { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #f9fafb; }
.filter input, .filter select { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; }
.filter-actions { display: flex; gap: 10px; margin-top: 12px; }
.message-box { margin-top: 16px; padding: 12px 14px; border-radius: 8px; background: #dcfce7; color: #166534; }
.message-box.error { background: #fee2e2; color: #b91c1c; }
.empty-state { margin-top: 20px; padding: 24px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; }

View File

@ -19,8 +19,8 @@
| 任务 | 来源 | 验证方式 | 状态 |
| --- | --- | --- | --- |
| Vue WEB 业务员端页面骨架 | 01-页面级原型字段说明 | 已有路由、页面、mock 数据和基础交互 | 待完成 |
| Vue WEB 管理后台页面骨架 | 01-页面级原型字段说明 | 已有路由、页面、mock 数据和基础交互 | 待完成 |
| Vue WEB 业务员端页面骨架 | 01-页面级原型字段说明 | 已形成真实订单管理闭环:列表、筛选、建单、详情、提交、取消 | 已完成 |
| Vue WEB 管理后台页面骨架 | 01-页面级原型字段说明 | 已形成真实审批闭环与履约调度闭环:审批中心、发厂确认、司机任务创建 | 已完成 |
| 微信管理层端页面骨架 | 01-页面级原型字段说明 | 已有页面结构和按钮区,但未接真实数据流 | 待完成 |
| 微信司机端页面骨架 | 01-页面级原型字段说明 | 已有页面结构和按钮区,但未接真实数据流 | 待完成 |
@ -29,12 +29,12 @@
| 任务 | 来源 | 验证方式 | 状态 |
| --- | --- | --- | --- |
| 认证接口 | 02-API细化设计 | 已有路由、service、统一异常未接真实鉴权 | 待完成 |
| 订单接口 | 02-API细化设计 | 已有路由、service、演示数据未接真实持久化与状态流转 | 待完成 |
| 审批接口 | 02-API细化设计 | 已有接口占位;未实现真实审批流 | 待完成 |
| 订单接口 | 02-API细化设计 | 已支持真实建单、列表筛选、详情、提交、取消、发厂确认等主流程;其余延伸场景待补 | 待完成 |
| 审批接口 | 02-API细化设计 | 已支持真实订单审批、取消审批和审批日志写入;权限与更多角色场景待补 | 待完成 |
| 客户接口 | 02-API细化设计 | 已有接口与 service未接真实数据库 | 待完成 |
| 产品与分类接口 | 02-API细化设计 | 已有接口与 service未接真实数据库 | 待完成 |
| 供应商接口 | 02-API细化设计 | 已有接口与 service未接真实数据库 | 待完成 |
| 司机任务接口 | 02-API细化设计 | 已有接口与 service未接真实数据库与状态联动 | 待完成 |
| 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动;司机端页面尚待补齐 | 待完成 |
| 文件接口 | 02-API细化设计 | 只有占位返回;未接 OSS | 待完成 |
| 物流与 AI 接口 | 02-API细化设计 | 只有占位返回;未接真实轨迹与 AI 服务 | 待完成 |
| 提醒接口 | 02-API细化设计 | 只有占位返回;未接真实提醒任务 | 待完成 |
@ -64,17 +64,19 @@
- 基础工程初始化
- 前后端目录结构搭建
- 后端模型、迁移目录、service 分层骨架
- 前端页面骨架、mock 数据流、部分本地交互
- 后端模型与 service / repository 主体分层
- `web-sales` 业务员端订单管理闭环
- `web-admin` 管理端订单审批闭环
- `web-admin` 履约调度闭环
- 订单、审批、发厂确认、司机任务创建等主链路的数据库优先实现
### 只完成了骨架,还没有真正完成业务功能
### 仍未真正完成
- 所有核心业务接口
- 大部分真实数据库读写
- 真实登录鉴权
- 真实订单流转、审批、取消、司机任务联动
- 真实登录鉴权与权限体系
- 客户、产品、供应商、系统管理等完整后台能力
- 微信管理层端与微信司机端真实数据联动
- 真实 OSS / AI / 提醒 / 报表
- 小程序真实数据联动
- 更完整的物流轨迹、附件上传与异常处理
- 测试、联调、部署
## 修正记录
@ -96,3 +98,9 @@
| 2026-05-14 | 已开始补前端真实联调:`frontend/web-sales` 的订单列表与订单详情已改为优先请求真实后端接口,失败时再回退 mock同时清理了该模块关键页面中的乱码展示文本。 |
| 2026-05-14 | 已继续按前后端同步方式推进:后端订单接口已清理真实中文提示语;前端 `web-sales` 订单列表页与详情页已接入真实 `提交审核/取消订单` 动作,并补充加载态、禁用态与操作结果提示。 |
| 2026-05-14 | 已继续按前后端同步方式推进创建订单链路:后端补充创建订单参数校验;前端 `web-sales` 新建订单页已接入真实 `/api/orders` 创建接口,并支持最小可用单明细录入。 |
| 2026-05-14 | 已继续补齐创建订单的数据选择链路:后端供应商接口支持按 `supplier_type` 筛选;前端新建订单页已改为加载真实客户与工厂选项,并通过下拉选择回填表单。 |
| 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` 的联动。 |
| 2026-05-14 | 已整理 `任务状态跟踪.md` 的总览口径,使其与当前真实进度一致;并已完成一个可检查的完整任务:`frontend/mini-driver` 司机端执行闭环现已支持司机登录、查看真实任务列表、查看任务详情,并按状态执行接单、确认揽货、确认送达;后端已有司机任务状态与订单状态联动。 |