- recognize_waybill 捕获所有异常返回空结果而非抛出 - 修正 SysUser 导入为 User(匹配实际模型类名) - 前端识别为空时提示"自动识别未成功,请手动输入单号" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
411 lines
13 KiB
JavaScript
411 lines
13 KiB
JavaScript
/**
|
|
* 司机端任务详情页面
|
|
* 职责:展示单个任务完整信息,支持接单、确认揽货(需上传运单号)、确认送达等操作。
|
|
*/
|
|
|
|
function mapTaskStatus(status) {
|
|
var statusMap = { pending: "待接单", accepted: "已接单", picked_up: "已揽货", delivered: "已送达", canceled: "已取消" };
|
|
return statusMap[status] || status || "-";
|
|
}
|
|
|
|
function getStatusBadge(status) {
|
|
var map = { pending: "badge-orange", accepted: "badge-blue", picked_up: "badge-blue", delivered: "badge-green", canceled: "badge-red" };
|
|
return map[status] || "badge-gray";
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
statusBarHeight: 20,
|
|
loading: true,
|
|
message: "",
|
|
actionLoading: false,
|
|
taskId: null,
|
|
taskInfo: null,
|
|
actionRemark: "",
|
|
traceList: [],
|
|
uploadedPhotos: [],
|
|
waybillList: [],
|
|
taskAttachments: [],
|
|
recognizing: false,
|
|
},
|
|
|
|
async onLoad(options) {
|
|
var app = getApp();
|
|
this.setData({
|
|
statusBarHeight: app.globalData.statusBarHeight,
|
|
taskId: Number(options.id || 0),
|
|
});
|
|
await this.loadDetail();
|
|
},
|
|
|
|
async loadDetail() {
|
|
var app = getApp();
|
|
this.setData({ loading: true, message: "", uploadedPhotos: [] });
|
|
try {
|
|
var data = await app.request({ url: "/api/driver/tasks/" + this.data.taskId, method: "GET" });
|
|
this.setData({
|
|
taskInfo: Object.assign({}, data, {
|
|
statusText: mapTaskStatus(data.status),
|
|
badgeClass: getStatusBadge(data.status),
|
|
pickupAddress: data.pickup_address || "-",
|
|
deliveryAddress: data.delivery_address || "-",
|
|
pickupContent: data.pickup_content || "-",
|
|
factoryName: data.factory_name || "-",
|
|
salesmanName: data.salesman_name || "-",
|
|
}),
|
|
});
|
|
if (data.order_id) {
|
|
try {
|
|
var traceData = await app.request({ url: "/api/logistics/" + data.order_id + "/trace", method: "GET" });
|
|
this.setData({
|
|
traceList: (traceData.trace_list || []).map(function (item) {
|
|
item.node_time = item.node_time || item.created_at || "-";
|
|
return item;
|
|
}),
|
|
});
|
|
} catch (e) {}
|
|
}
|
|
// Load existing waybills from server
|
|
if (data.status === "accepted" || data.status === "picked_up" || data.status === "delivered") {
|
|
await this.loadWaybills();
|
|
}
|
|
// Load existing attachments for completed states
|
|
if (data.status === "picked_up" || data.status === "delivered") {
|
|
await this.loadAttachments();
|
|
}
|
|
} catch (error) {
|
|
this.setData({ message: error.message || "加载任务详情失败" });
|
|
} finally {
|
|
this.setData({ loading: false });
|
|
}
|
|
},
|
|
|
|
loadWaybills: async function () {
|
|
var app = getApp();
|
|
try {
|
|
var waybills = await app.request({ url: "/api/driver/tasks/" + this.data.taskId + "/waybills", method: "GET" });
|
|
this.setData({ waybillList: waybills || [] });
|
|
} catch (e) {
|
|
// Ignore - waybills may not exist yet
|
|
}
|
|
},
|
|
|
|
loadAttachments: async function () {
|
|
var app = getApp();
|
|
try {
|
|
var data = await app.request({
|
|
url: "/api/files/attachments?biz_type=logistics_task&biz_id=" + this.data.taskId,
|
|
method: "GET",
|
|
});
|
|
this.setData({ taskAttachments: data || [] });
|
|
} catch (e) {
|
|
this.setData({ taskAttachments: [] });
|
|
}
|
|
},
|
|
|
|
handleRemarkInput: function (event) {
|
|
this.setData({ actionRemark: event.detail.value });
|
|
},
|
|
|
|
handleChoosePhoto: function () {
|
|
var that = this;
|
|
var remaining = 9 - this.data.uploadedPhotos.length;
|
|
if (remaining <= 0) { wx.showToast({ title: "最多上传9张", icon: "none" }); return; }
|
|
wx.chooseImage({
|
|
count: remaining,
|
|
sizeType: ["compressed"],
|
|
sourceType: ["camera", "album"],
|
|
success: function (res) {
|
|
res.tempFilePaths.forEach(function (path) {
|
|
that.uploadPhotoToServer(path);
|
|
});
|
|
},
|
|
});
|
|
},
|
|
|
|
uploadPhotoToServer: function (localPath) {
|
|
var that = this;
|
|
var app = getApp();
|
|
var token = app.globalData.authToken || "";
|
|
wx.showLoading({ title: "上传中..." });
|
|
wx.uploadFile({
|
|
url: app.globalData.apiBaseUrl + "/api/files/local-upload",
|
|
filePath: localPath,
|
|
name: "file",
|
|
header: { Authorization: "Bearer " + token },
|
|
success: function (uploadRes) {
|
|
wx.hideLoading();
|
|
try {
|
|
var body = JSON.parse(uploadRes.data);
|
|
if (body.code === 0 && body.data) {
|
|
var newPhoto = {
|
|
path: body.data.url,
|
|
name: body.data.file_name || localPath.split("/").pop(),
|
|
file_size: body.data.file_size || 0,
|
|
};
|
|
that.setData({ uploadedPhotos: that.data.uploadedPhotos.concat([newPhoto]) });
|
|
} else {
|
|
wx.showToast({ title: body.message || "上传失败", icon: "none" });
|
|
}
|
|
} catch (e) {
|
|
wx.showToast({ title: "上传结果解析失败", icon: "none" });
|
|
}
|
|
},
|
|
fail: function () {
|
|
wx.hideLoading();
|
|
wx.showToast({ title: "网络错误,上传失败", icon: "none" });
|
|
},
|
|
});
|
|
},
|
|
|
|
handleRemovePhoto: function (event) {
|
|
var index = event.currentTarget.dataset.index;
|
|
var photos = this.data.uploadedPhotos.slice();
|
|
photos.splice(index, 1);
|
|
this.setData({ uploadedPhotos: photos });
|
|
},
|
|
|
|
// --- Waybill methods ---
|
|
|
|
handleWaybillPhoto: function () {
|
|
var that = this;
|
|
if (this.data.recognizing) return;
|
|
wx.chooseImage({
|
|
count: 1,
|
|
sizeType: ["compressed"],
|
|
sourceType: ["camera"],
|
|
success: function (res) {
|
|
var filePath = res.tempFilePaths[0];
|
|
that.setData({ recognizing: true });
|
|
wx.showLoading({ title: "识别中..." });
|
|
that.uploadAndRecognize(filePath);
|
|
},
|
|
});
|
|
},
|
|
|
|
uploadAndRecognize: function (filePath) {
|
|
var that = this;
|
|
var app = getApp();
|
|
var token = app.globalData.authToken || "";
|
|
wx.uploadFile({
|
|
url: app.globalData.apiBaseUrl + "/api/files/local-upload",
|
|
filePath: filePath,
|
|
name: "file",
|
|
header: { Authorization: "Bearer " + token },
|
|
success: function (uploadRes) {
|
|
try {
|
|
var body = JSON.parse(uploadRes.data);
|
|
if (body.code !== 0 || !body.data) {
|
|
wx.hideLoading();
|
|
that.setData({ recognizing: false });
|
|
wx.showToast({ title: body.message || "上传失败", icon: "none" });
|
|
return;
|
|
}
|
|
var imageUrl = body.data.url || body.data;
|
|
that.callRecognizeWaybill(imageUrl, filePath);
|
|
} catch (e) {
|
|
wx.hideLoading();
|
|
that.setData({ recognizing: false });
|
|
wx.showToast({ title: "上传结果解析失败", icon: "none" });
|
|
}
|
|
},
|
|
fail: function () {
|
|
wx.hideLoading();
|
|
that.setData({ recognizing: false });
|
|
wx.showToast({ title: "文件上传失败", icon: "none" });
|
|
},
|
|
});
|
|
},
|
|
|
|
callRecognizeWaybill: function (imageUrl, localPath) {
|
|
var that = this;
|
|
var app = getApp();
|
|
app.request({
|
|
url: "/api/driver/recognize-waybill",
|
|
method: "POST",
|
|
data: { image_url: imageUrl },
|
|
}).then(function (result) {
|
|
wx.hideLoading();
|
|
that.setData({ recognizing: false });
|
|
var waybill = {
|
|
tracking_number: result.tracking_number || "",
|
|
express_company: result.express_company || "",
|
|
express_name: result.express_name || "",
|
|
image_url: imageUrl,
|
|
local_path: localPath,
|
|
confidence: result.confidence || 0,
|
|
};
|
|
var list = that.data.waybillList.slice();
|
|
list.push(waybill);
|
|
that.setData({ waybillList: list });
|
|
if (waybill.tracking_number) {
|
|
wx.showToast({ title: "识别成功", icon: "success" });
|
|
} else {
|
|
wx.showToast({ title: "自动识别未成功,请手动输入单号", icon: "none", duration: 2500 });
|
|
}
|
|
}).catch(function (err) {
|
|
wx.hideLoading();
|
|
that.setData({ recognizing: false });
|
|
wx.showToast({ title: err.message || "识别失败,请手动输入", icon: "none" });
|
|
});
|
|
},
|
|
|
|
handleManualWaybill: function () {
|
|
var that = this;
|
|
wx.showModal({
|
|
title: "手动输入运单号",
|
|
editable: true,
|
|
placeholderText: "请输入运单号",
|
|
confirmText: "添加",
|
|
confirmColor: "#2563eb",
|
|
success: function (res) {
|
|
if (res.confirm && res.content && res.content.trim()) {
|
|
var trackingNumber = res.content.trim();
|
|
var list = that.data.waybillList.slice();
|
|
list.push({
|
|
tracking_number: trackingNumber,
|
|
express_company: "",
|
|
express_name: "",
|
|
image_url: "",
|
|
local_path: "",
|
|
confidence: 0,
|
|
});
|
|
that.setData({ waybillList: list });
|
|
}
|
|
},
|
|
});
|
|
},
|
|
|
|
handleRemoveWaybill: function (event) {
|
|
var index = event.currentTarget.dataset.index;
|
|
var that = this;
|
|
var item = this.data.waybillList[index];
|
|
// If it has an id, it's saved on server - delete from server too
|
|
if (item && item.id) {
|
|
var app = getApp();
|
|
app.request({
|
|
url: "/api/driver/tasks/" + this.data.taskId + "/waybills/" + item.id,
|
|
method: "DELETE",
|
|
}).catch(function () {});
|
|
}
|
|
var list = this.data.waybillList.slice();
|
|
list.splice(index, 1);
|
|
this.setData({ waybillList: list });
|
|
},
|
|
|
|
handleEditWaybill: function (event) {
|
|
var index = event.currentTarget.dataset.index;
|
|
var item = this.data.waybillList[index];
|
|
if (!item) return;
|
|
var that = this;
|
|
wx.showModal({
|
|
title: "修改运单号",
|
|
editable: true,
|
|
placeholderText: "请输入运单号",
|
|
content: item.tracking_number || "",
|
|
confirmText: "保存",
|
|
confirmColor: "#2563eb",
|
|
success: function (res) {
|
|
if (res.confirm && res.content && res.content.trim()) {
|
|
var list = that.data.waybillList.slice();
|
|
list[index] = Object.assign({}, list[index], {
|
|
tracking_number: res.content.trim(),
|
|
});
|
|
that.setData({ waybillList: list });
|
|
}
|
|
},
|
|
});
|
|
},
|
|
|
|
getActionConfig: function () {
|
|
var status = this.data.taskInfo && this.data.taskInfo.status;
|
|
var taskId = this.data.taskId;
|
|
if (status === "pending") {
|
|
return { text: "接单", url: "/api/driver/tasks/" + taskId + "/accept", successMessage: "接单成功", confirmMsg: "确认接受此任务?", needAttachment: false };
|
|
}
|
|
if (status === "accepted") {
|
|
return { text: "确认揽货", url: "/api/driver/tasks/" + taskId + "/pickup", successMessage: "已确认揽货", confirmMsg: "确认已揽收货物?请先录入运单号。", needWaybill: true };
|
|
}
|
|
if (status === "picked_up") {
|
|
return { text: "确认送达", url: "/api/driver/tasks/" + taskId + "/deliver", successMessage: "已确认送达", confirmMsg: "确认货物已送达目的地?", needAttachment: false };
|
|
}
|
|
return null;
|
|
},
|
|
|
|
handleAction: function () {
|
|
var that = this;
|
|
var action = this.getActionConfig();
|
|
if (!action) return;
|
|
if (action.needWaybill && this.data.waybillList.length === 0) {
|
|
wx.showToast({ title: "请先录入运单号", icon: "none" });
|
|
return;
|
|
}
|
|
// Validate all waybills have tracking numbers
|
|
if (action.needWaybill) {
|
|
var hasEmpty = this.data.waybillList.some(function (w) {
|
|
return !w.tracking_number;
|
|
});
|
|
if (hasEmpty) {
|
|
wx.showToast({ title: "请填写完整的运单号", icon: "none" });
|
|
return;
|
|
}
|
|
}
|
|
wx.showModal({
|
|
title: "操作确认",
|
|
content: action.confirmMsg,
|
|
confirmText: "确认",
|
|
confirmColor: "#2563eb",
|
|
success: function (res) {
|
|
if (res.confirm) { that.executeAction(action); }
|
|
},
|
|
});
|
|
},
|
|
|
|
executeAction: async function (action) {
|
|
var app = getApp();
|
|
this.setData({ actionLoading: true, message: "" });
|
|
try {
|
|
// For accepted status, submit waybills first
|
|
if (action.needWaybill && this.data.waybillList.length > 0) {
|
|
var waybillPayload = this.data.waybillList.map(function (w) {
|
|
return {
|
|
tracking_number: w.tracking_number,
|
|
express_company: w.express_company || "",
|
|
express_name: w.express_name || "",
|
|
image_url: w.image_url || "",
|
|
};
|
|
});
|
|
await app.request({
|
|
url: "/api/driver/tasks/" + this.data.taskId + "/waybills",
|
|
method: "POST",
|
|
data: { waybills: waybillPayload },
|
|
});
|
|
}
|
|
|
|
var photoFiles = this.data.uploadedPhotos.map(function (p) {
|
|
return { file_url: p.path, file_name: p.name };
|
|
});
|
|
await app.request({
|
|
url: action.url,
|
|
method: "POST",
|
|
data: { photo_files: photoFiles, video_files: [], remark: this.data.actionRemark || action.successMessage },
|
|
});
|
|
wx.showToast({ title: action.successMessage, icon: "success" });
|
|
this.setData({ actionRemark: "", uploadedPhotos: [], waybillList: [] });
|
|
await this.loadDetail();
|
|
} catch (error) {
|
|
wx.showToast({ title: error.message || "操作失败", icon: "none" });
|
|
} finally {
|
|
this.setData({ actionLoading: false });
|
|
}
|
|
},
|
|
|
|
handleViewTracking: function () {
|
|
if (this.data.taskInfo && this.data.taskInfo.order_no) {
|
|
wx.switchTab({ url: "/pages/driver/order-tracking/order-tracking" });
|
|
}
|
|
},
|
|
|
|
handleBack: function () { wx.navigateBack({ delta: 1 }); },
|
|
});
|