From bf557bb00bb2e25337b28825f1b2f48ec5c65054 Mon Sep 17 00:00:00 2001 From: taiyi Date: Sat, 30 May 2026 14:08:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E8=A2=AB=E6=88=AA?= =?UTF-8?q?=E6=96=AD=E7=9A=84=E4=B8=9A=E5=8A=A1=E5=91=98=E7=AB=AF=20Vue=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E4=BF=AE=E5=A4=8D=20Vite=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OrderDetailPage.vue、OrdersPage.vue、OrderFormPage.vue 在此前的提交中被 意外截断(script 标签丢失、模板内容残缺),导致 Vite 报 Element is missing end tag。 从完整版本恢复这三个文件,并在 mockApi.js 中补回 fetchOrdersPage 函数。 Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/web-sales/src/mockApi.js | 45 + .../web-sales/src/views/OrderDetailPage.vue | 646 +++++++- .../web-sales/src/views/OrderFormPage.vue | 1302 ++++++++++++++++- frontend/web-sales/src/views/OrdersPage.vue | 747 +++++++++- 4 files changed, 2688 insertions(+), 52 deletions(-) diff --git a/frontend/web-sales/src/mockApi.js b/frontend/web-sales/src/mockApi.js index 3c5d358..38389db 100644 --- a/frontend/web-sales/src/mockApi.js +++ b/frontend/web-sales/src/mockApi.js @@ -131,6 +131,51 @@ export async function fetchCurrentUser() { return request("/api/auth/me"); } +/** + * 标准化订单列表查询参数 + * @param {Object} [filters={}] - 原始筛选条件 + * @returns {Object} 标准化后的查询参数 + */ +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, + 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: filters.page_no || 1, + page_size: filters.page_size || 20, + }; +} + +/** + * 分页查询业务员订单列表 + * @param {Object} [filters={}] - 筛选条件 + * @returns {Promise} 包含 orderRows、total、isMock 的结果 + */ +export async function fetchOrdersPage(filters = {}) { + const query = buildQuery(normalizeOrderFilters(filters)); + const data = await request(`/api/orders?${query}`); + return { + total: data.total || 0, + pageNo: data.page_no || 1, + pageSize: data.page_size || 20, + orderRows: (data.list || []).map((item) => ({ + orderId: item.order_id, + orderNo: item.order_no, + customer: item.customer_name, + mobile: maskMobile(item.customer_mobile), + status: mapOrderStatus(item.order_status), + rawStatus: item.order_status, + source: item.order_source || "-", + amount: Number(item.sale_price_total || 0).toFixed(2), + createdAt: item.created_at || "-", + })), + isMock: false, + }; +} + // ========== 下拉选项数据 ========== /** diff --git a/frontend/web-sales/src/views/OrderDetailPage.vue b/frontend/web-sales/src/views/OrderDetailPage.vue index 6f2e796..ceb16c4 100644 --- a/frontend/web-sales/src/views/OrderDetailPage.vue +++ b/frontend/web-sales/src/views/OrderDetailPage.vue @@ -1,11 +1,4 @@ - + + + + diff --git a/frontend/web-sales/src/views/OrderFormPage.vue b/frontend/web-sales/src/views/OrderFormPage.vue index 1ae17e1..9deeb4c 100644 --- a/frontend/web-sales/src/views/OrderFormPage.vue +++ b/frontend/web-sales/src/views/OrderFormPage.vue @@ -1,21 +1,1287 @@ - + + + + diff --git a/frontend/web-sales/src/views/OrdersPage.vue b/frontend/web-sales/src/views/OrdersPage.vue index ccc8c0f..1d6caa3 100644 --- a/frontend/web-sales/src/views/OrdersPage.vue +++ b/frontend/web-sales/src/views/OrdersPage.vue @@ -1,38 +1,35 @@ - + + + + From 81ab04a96463de31677ee351b514004ccab5079c Mon Sep 17 00:00:00 2001 From: taiyi Date: Sat, 30 May 2026 16:13:33 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E4=B8=9A=E5=8A=A1=E5=91=98?= =?UTF-8?q?=E7=AB=AF=E6=94=AF=E6=8C=81=E4=BF=AE=E6=94=B9=E7=89=A9=E6=B5=81?= =?UTF-8?q?=E5=8D=95=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 PUT /api/logistics/tasks/{id}/tracking 接口,业务员可在订单详情页 修改进行中订单的快递单号和快递公司,修改记录写入审计日志。 四端同步:管理后台、司机端、管理小程序下次拉取即自动获取新值。 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/api/logistics.py | 28 +++++- backend/app/schemas/logistics.py | 13 +++ backend/app/services/logistics_service.py | 64 ++++++++++++++ .../web-sales/src/views/OrderDetailPage.vue | 87 +++++++++++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) diff --git a/backend/app/api/logistics.py b/backend/app/api/logistics.py index 71239c8..41e7add 100644 --- a/backend/app/api/logistics.py +++ b/backend/app/api/logistics.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import Session from backend.app.api.deps import get_logistics_service, require_permissions, require_roles from backend.app.db import get_db_session from backend.app.schemas.common import success_payload -from backend.app.schemas.logistics import CreateLogisticsTaskRequest, CreateLogisticsTraceRequest, DriverTaskOperateRequest +from backend.app.schemas.logistics import CreateLogisticsTaskRequest, CreateLogisticsTraceRequest, DriverTaskOperateRequest, UpdateTrackingRequest from backend.app.services.logistics_service import LogisticsService router = APIRouter(tags=["logistics"]) @@ -113,6 +113,32 @@ def get_logistics_task( return success_payload(logistics_service.get_task(task_id, session, current_user)) +@router.put("/api/logistics/tasks/{task_id}/tracking") +def update_tracking_number( + task_id: int, # 物流任务ID + payload: UpdateTrackingRequest, # 修改物流单号请求体 + logistics_service: LogisticsService = Depends(get_logistics_service), + session: Session = Depends(get_db_session), + current_user: dict = Depends(require_roles("salesman", "manager", "admin")), +) -> dict: + """ + 修改物流单号 + + 业务员或管理员修改已有物流任务的快递单号和快递公司。 + 业务员仅可修改自己订单关联的物流任务。 + + 请求参数: + task_id: 路径参数,物流任务ID + payload: UpdateTrackingRequest 请求体 + + 返回值: + 更新后的物流任务信息 + + 权限要求:salesman、manager、admin 角色 + """ + return success_payload(logistics_service.update_tracking(task_id, payload.model_dump(), session, current_user)) + + @router.get("/api/driver/tasks") def list_driver_tasks( logistics_service: LogisticsService = Depends(get_logistics_service), # 物流服务实例 diff --git a/backend/app/schemas/logistics.py b/backend/app/schemas/logistics.py index 9b12f08..b9e5aaf 100644 --- a/backend/app/schemas/logistics.py +++ b/backend/app/schemas/logistics.py @@ -113,3 +113,16 @@ class CreateLogisticsTraceRequest(BaseModel): source_platform: str """来源平台""" + + +class UpdateTrackingRequest(BaseModel): + """修改物流单号请求体。被 PUT /api/logistics/tasks/{id}/tracking 路由使用。""" + + tracking_number: str + """新的物流追踪单号""" + + express_company: str | None = None + """快递公司""" + + remark: str | None = None + """修改原因/备注""" diff --git a/backend/app/services/logistics_service.py b/backend/app/services/logistics_service.py index 25a0d83..1c72863 100644 --- a/backend/app/services/logistics_service.py +++ b/backend/app/services/logistics_service.py @@ -285,6 +285,70 @@ class LogisticsService: """ return self.get_task(task_id, session=session, current_user=current_user) + def update_tracking( + self, + task_id: int, + payload: dict, + session: Session | None = None, + current_user: dict | None = None, + ) -> dict: + """修改物流单号。 + + 业务员/管理员修改已有物流任务的快递单号和快递公司。 + 业务员只能修改自己订单关联的任务。 + + Args: + task_id: 物流任务 ID + payload: 包含 tracking_number、express_company、remark 的字典 + session: 数据库会话 + current_user: 当前登录用户信息 + + Returns: + 更新后的任务信息字典 + + 被调用路由: logistics.py - PUT /logistics/tasks/{task_id}/tracking + """ + if session is None: + raise AppException(code=ErrorCode.PARAM_ERROR, message="数据库会话未初始化", status_code=500) + + task = self.logistics_repository.get_task(session, task_id) + if task is None: + raise AppException(code=ErrorCode.NOT_FOUND, message="物流任务不存在", status_code=404) + + # 权限校验:salesman 只能改自己订单的物流单号 + if current_user and current_user.get("role_code") == "salesman": + order = self.order_repository.get_order(session, task.order_id) + if order and order.salesman_id != current_user.get("user_id"): + raise AppException(code=ErrorCode.FORBIDDEN, message="无权修改他人订单的物流单号", status_code=403) + + new_tracking = (payload.get("tracking_number") or "").strip() + if not new_tracking: + raise AppException(code=ErrorCode.PARAM_ERROR, message="物流单号不能为空", status_code=400) + + old_tracking = task.tracking_number or "" + old_express = task.express_company or "" + task.tracking_number = new_tracking + task.express_company = (payload.get("express_company") or "").strip() or None + + audit_service.write_log( + session, + { + "operate_type": "tracking_update", + "biz_type": "logistics_task", + "biz_id": task.id, + "before_value": {"tracking_number": old_tracking, "express_company": old_express}, + "after_value": {"tracking_number": new_tracking, "express_company": task.express_company}, + "remark": payload.get("remark") or f"修改物流单号:{old_tracking} → {new_tracking}", + }, + ) + session.commit() + return { + "task_id": task.id, + "order_id": task.order_id, + "tracking_number": task.tracking_number, + "express_company": task.express_company or "", + } + def accept_task( self, task_id: int, diff --git a/frontend/web-sales/src/views/OrderDetailPage.vue b/frontend/web-sales/src/views/OrderDetailPage.vue index ceb16c4..94741ce 100644 --- a/frontend/web-sales/src/views/OrderDetailPage.vue +++ b/frontend/web-sales/src/views/OrderDetailPage.vue @@ -74,6 +74,17 @@ +
+
+

物流信息

+ +
+
+ 快递公司:{{ trackingInfo.express_company || '-' }} + 快递单号:{{ trackingInfo.tracking_number || '-' }} +
+
+

订单明细

@@ -149,6 +160,28 @@
+ +
@@ -157,8 +190,11 @@ import { computed, onMounted, ref } from "vue"; import { RouterLink, useRoute, useRouter } from "vue-router"; import ConfirmModal from "../components/ConfirmModal.vue"; +import { apiBaseUrl } from "../config"; import { cancelOrder, fetchOrderDetail, submitOrder } from "../mockApi"; +const SALES_TOKEN_KEY = "sales_token"; + const route = useRoute(); const router = useRouter(); const loading = ref(true); @@ -175,6 +211,11 @@ const message = ref(""); const messageType = ref("success"); const orderMeta = ref({ customer: "-", logistics: "-", factory: "-" }); const isModal = computed(() => route.query.modal === "1"); +const trackingInfo = ref(null); +const showTrackingEdit = ref(false); +const trackingSaving = ref(false); +const trackingForm = reactive({ tracking_number: "", express_company: "", remark: "" }); +const currentTaskId = ref(null); function canSubmit(status) { return ["draft", "rejected"].includes(status); @@ -208,6 +249,14 @@ async function loadDetail() { logistics: data.blocks?.[3]?.content || "-", factory: data.blocks?.[3]?.content || "-", }; + const task = data.logistics_info?.task; + if (task && task.task_id) { + trackingInfo.value = { tracking_number: task.tracking_number || "", express_company: task.express_company || "" }; + currentTaskId.value = task.task_id; + } else { + trackingInfo.value = null; + currentTaskId.value = null; + } } function handleSubmit() { @@ -274,6 +323,37 @@ async function doCancel() { } } +function openTrackingEdit() { + trackingForm.tracking_number = trackingInfo.value?.tracking_number || ""; + trackingForm.express_company = trackingInfo.value?.express_company || ""; + trackingForm.remark = ""; + showTrackingEdit.value = true; +} + +async function doUpdateTracking() { + if (!trackingForm.tracking_number || !currentTaskId.value) return; + trackingSaving.value = true; + try { + const token = localStorage.getItem(SALES_TOKEN_KEY) || ""; + const res = await fetch(`${apiBaseUrl}/api/logistics/tasks/${currentTaskId.value}/tracking`, { + method: "PUT", + headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: JSON.stringify({ tracking_number: trackingForm.tracking_number, express_company: trackingForm.express_company, remark: trackingForm.remark }), + }); + const result = await res.json().catch(() => ({})); + if (!res.ok || result.code !== 0) throw new Error(result.message || "修改失败"); + showTrackingEdit.value = false; + await loadDetail(); + message.value = "物流单号已更新"; + messageType.value = "success"; + } catch (error) { + message.value = error.message || "修改物流单号失败"; + messageType.value = "error"; + } finally { + trackingSaving.value = false; + } +} + function handleClose() { router.push("/orders"); } @@ -658,4 +738,11 @@ p { .confirm-btn { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: #fff; border-color: transparent; } .confirm-btn.danger { background: linear-gradient(135deg, #dc2626, #b91c1c); } .confirm-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.tracking-card { border: 1px solid #dbe3ef; border-radius: 18px; padding: 16px; background: #f0fdf4; } +.tracking-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } +.tracking-header h3 { margin: 0; font-size: 16px; } +.tracking-fields { display: flex; gap: 24px; color: #475569; } +.tracking-fields strong { color: #1d4ed8; } +.link-btn { background: none; border: none; color: #2563eb; cursor: pointer; font-size: 13px; text-decoration: underline; }