Compare commits
6 Commits
8b7d1379d1
...
cd3bda5971
| Author | SHA1 | Date | |
|---|---|---|---|
| cd3bda5971 | |||
| 181e56d627 | |||
| 33e921fa74 | |||
| c516fea427 | |||
| c319f87c61 | |||
| 502075a20c |
@ -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))
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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("")
|
||||
|
||||
50
backend/app/repositories/logistics_repository.py
Normal file
50
backend/app/repositories/logistics_repository.py
Normal file
@ -0,0 +1,50 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.models.business import LogisticsTask
|
||||
|
||||
|
||||
class LogisticsRepository:
|
||||
"""物流任务数据访问层。"""
|
||||
|
||||
def list_tasks_by_filters(self, session: Session, filters: dict) -> list[LogisticsTask]:
|
||||
stmt = select(LogisticsTask).where(LogisticsTask.deleted == 0)
|
||||
|
||||
if filters.get("order_id") is not None:
|
||||
stmt = stmt.where(LogisticsTask.order_id == filters["order_id"])
|
||||
if filters.get("task_no"):
|
||||
stmt = stmt.where(LogisticsTask.task_no.contains(filters["task_no"]))
|
||||
if filters.get("status"):
|
||||
stmt = stmt.where(LogisticsTask.status == filters["status"])
|
||||
if filters.get("driver_id") is not None:
|
||||
stmt = stmt.where(LogisticsTask.driver_id == filters["driver_id"])
|
||||
if filters.get("factory_id") is not None:
|
||||
stmt = stmt.where(LogisticsTask.factory_id == filters["factory_id"])
|
||||
|
||||
stmt = stmt.order_by(LogisticsTask.id.desc())
|
||||
return list(session.execute(stmt).scalars())
|
||||
|
||||
def get_task(self, session: Session, task_id: int) -> LogisticsTask | None:
|
||||
stmt = select(LogisticsTask).where(LogisticsTask.id == task_id, LogisticsTask.deleted == 0)
|
||||
return session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def get_task_by_order_id(self, session: Session, order_id: int) -> LogisticsTask | None:
|
||||
stmt = (
|
||||
select(LogisticsTask)
|
||||
.where(LogisticsTask.order_id == order_id, LogisticsTask.deleted == 0)
|
||||
.order_by(LogisticsTask.id.desc())
|
||||
)
|
||||
return session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def create_task(self, session: Session, payload: dict) -> LogisticsTask:
|
||||
task = LogisticsTask(**payload)
|
||||
session.add(task)
|
||||
session.flush()
|
||||
return task
|
||||
|
||||
def update_task_status(self, session: Session, task: LogisticsTask, status: str) -> LogisticsTask:
|
||||
task.status = status
|
||||
session.add(task)
|
||||
session.flush()
|
||||
return task
|
||||
|
||||
@ -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"):
|
||||
|
||||
23
backend/app/schemas/logistics.py
Normal file
23
backend/app/schemas/logistics.py
Normal file
@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LogisticsTaskListQuery(BaseModel):
|
||||
order_id: int | None = None
|
||||
task_no: str | None = None
|
||||
status: str | None = None
|
||||
driver_id: int | None = None
|
||||
factory_id: int | None = None
|
||||
page_no: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
|
||||
class CreateLogisticsTaskRequest(BaseModel):
|
||||
order_id: int
|
||||
driver_id: int = Field(gt=0)
|
||||
pickup_address: str
|
||||
delivery_address: str
|
||||
pickup_content: str
|
||||
quantity: float = Field(gt=0)
|
||||
factory_id: int | None = None
|
||||
remark: str | None = None
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
259
frontend/web-admin/src/views/ApprovalDetailPage.vue
Normal file
259
frontend/web-admin/src/views/ApprovalDetailPage.vue
Normal 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>
|
||||
145
frontend/web-admin/src/views/ApprovalsPage.vue
Normal file
145
frontend/web-admin/src/views/ApprovalsPage.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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` 司机端执行闭环现已支持司机登录、查看真实任务列表、查看任务详情,并按状态执行接单、确认揽货、确认送达;后端已有司机任务状态与订单状态联动。 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user