优化一下履约页面的内容

This commit is contained in:
taiyi 2026-05-19 18:44:47 +08:00
parent e9077cca34
commit 4e47386e56
5 changed files with 1059 additions and 180 deletions

View File

@ -90,7 +90,30 @@
{{ message }} {{ message }}
</div> </div>
<div class="workspace-grid"> <section class="section-shell">
<div class="section-shell-header">
<div>
<h3>业务分层导航</h3>
<p>所有核心操作都通过弹窗完成主页面只保留入口与概览</p>
</div>
<span>内容分层</span>
</div>
<div class="layer-tabs">
<button class="layer-tab" type="button" @click="openOrderModal">订单处理弹窗</button>
<button class="layer-tab" type="button" @click="openTaskModal">司机任务弹窗</button>
</div>
</section>
<div v-if="showOrderModal" class="modal-mask" @click.self="closeOrderModal">
<section class="modal-panel modal-panel-wide">
<div class="modal-header">
<div>
<h4>订单处理</h4>
<span>订单列表发厂动作与创建司机任务都在这里处理</span>
</div>
<button class="modal-close" type="button" @click="closeOrderModal">×</button>
</div>
<div class="workspace-grid modal-grid">
<section class="detail-panel"> <section class="detail-panel">
<div class="panel-header"> <div class="panel-header">
<h3>待发厂订单</h3> <h3>待发厂订单</h3>
@ -184,11 +207,18 @@
</button> </button>
</div> </div>
</section> </section>
</div>
</section>
</div>
<section class="detail-panel task-panel"> <div v-if="showTaskModal" class="modal-mask" @click.self="closeTaskModal">
<div class="panel-header"> <section class="modal-panel modal-panel-wide">
<h3>司机任务列表</h3> <div class="modal-header">
<span>展示真实任务状态与创建结果</span> <div>
<h4>司机任务列表</h4>
<span>集中查看司机任务状态与创建结果</span>
</div>
<button class="modal-close" type="button" @click="closeTaskModal">×</button>
</div> </div>
<p v-if="loadingTasks" class="loading">正在加载司机任务...</p> <p v-if="loadingTasks" class="loading">正在加载司机任务...</p>
<div v-else-if="!taskRows.length" class="empty-state">当前没有司机任务</div> <div v-else-if="!taskRows.length" class="empty-state">当前没有司机任务</div>
@ -203,6 +233,7 @@
<th>任务状态</th> <th>任务状态</th>
<th>提货内容</th> <th>提货内容</th>
<th>创建时间</th> <th>创建时间</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -212,10 +243,11 @@
<td>{{ row.driver_name }}</td> <td>{{ row.driver_name }}</td>
<td>{{ row.factory_name || "-" }}</td> <td>{{ row.factory_name || "-" }}</td>
<td><span class="status">{{ mapTaskStatus(row.status) }}</span></td> <td><span class="status">{{ mapTaskStatus(row.status) }}</span></td>
<td>{{ row.pickup_content }}</td> <td class="pickup-cell">{{ row.pickup_content }}</td>
<td>{{ row.created_at || "-" }}</td> <td>{{ row.created_at || "-" }}</td>
<td> <td>
<RouterLink v-if="row.task_id" class="link-btn" :to="`/driver/tasks/${row.task_id}`">详情</RouterLink> <RouterLink v-if="row.task_id" class="link-btn" :to="`/driver/tasks/${row.task_id}`">详情</RouterLink>
<span v-else class="link-btn disabled">-</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -247,11 +279,10 @@ const actionLoading = ref("");
const creatingTask = ref(false); const creatingTask = ref(false);
const message = ref(""); const message = ref("");
const messageType = ref("success"); const messageType = ref("success");
const filters = reactive({ const filters = reactive({ orderStatus: "", taskStatus: "" });
orderStatus: "",
taskStatus: "",
});
const latestTaskId = ref(null); const latestTaskId = ref(null);
const showOrderModal = ref(false);
const showTaskModal = ref(false);
const taskForm = reactive({ const taskForm = reactive({
order_id: null, order_id: null,
order_no: "", order_no: "",
@ -279,6 +310,19 @@ function canCreateTask(orderStatus) {
return orderStatus === "pending_factory"; return orderStatus === "pending_factory";
} }
function openOrderModal() {
showOrderModal.value = true;
}
function openTaskModal() {
showTaskModal.value = true;
}
function closeOrderModal() {
showOrderModal.value = false;
}
function closeTaskModal() {
showTaskModal.value = false;
}
function resetTaskForm() { function resetTaskForm() {
taskForm.order_id = null; taskForm.order_id = null;
taskForm.order_no = ""; taskForm.order_no = "";
@ -300,6 +344,7 @@ function prepareTask(row) {
taskForm.pickup_content = `${row.customerName} 订单货物`; taskForm.pickup_content = `${row.customerName} 订单货物`;
taskForm.quantity = 1; taskForm.quantity = 1;
taskForm.remark = "管理端调度创建"; taskForm.remark = "管理端调度创建";
showTaskModal.value = true;
message.value = `已带入订单 ${row.orderNo},请确认司机任务信息后提交。`; message.value = `已带入订单 ${row.orderNo},请确认司机任务信息后提交。`;
messageType.value = "success"; messageType.value = "success";
} }
@ -309,24 +354,16 @@ async function loadPendingOrders() {
pendingOrders.value = filters.orderStatus ? data.rows.filter((item) => item.orderStatus === filters.orderStatus) : data.rows; pendingOrders.value = filters.orderStatus ? data.rows.filter((item) => item.orderStatus === filters.orderStatus) : data.rows;
isMock.value = Boolean(data.isMock); isMock.value = Boolean(data.isMock);
} }
async function loadTasks() { async function loadTasks() {
const data = await fetchLogisticsTasks({ status: filters.taskStatus }); const data = await fetchLogisticsTasks({ status: filters.taskStatus });
taskRows.value = data.rows; taskRows.value = data.rows;
isMock.value = Boolean(data.isMock); isMock.value = Boolean(data.isMock);
} }
async function refreshAll() { async function refreshAll() {
await Promise.all([loadPendingOrders(), loadTasks()]); await Promise.all([loadPendingOrders(), loadTasks()]);
} }
async function handleRefreshOrders() { await loadPendingOrders(); }
async function handleRefreshOrders() { async function handleRefreshTasks() { await loadTasks(); }
await loadPendingOrders();
}
async function handleRefreshTasks() {
await loadTasks();
}
async function handleGenerateText(row) { async function handleGenerateText(row) {
actionLoading.value = `text-${row.orderId}`; actionLoading.value = `text-${row.orderId}`;
@ -342,16 +379,11 @@ async function handleGenerateText(row) {
actionLoading.value = ""; actionLoading.value = "";
} }
} }
async function handleConfirmFactory(row) { async function handleConfirmFactory(row) {
actionLoading.value = `confirm-${row.orderId}`; actionLoading.value = `confirm-${row.orderId}`;
message.value = ""; message.value = "";
try { try {
await confirmSupplierText(row.orderId, { await confirmSupplierText(row.orderId, { supplier_id: 1001, text_content: `订单 ${row.orderNo} 请安排生产与发货`, remark: "管理端确认发厂" });
supplier_id: 1001,
text_content: `订单 ${row.orderNo} 请安排生产与发货`,
remark: "管理端确认发厂",
});
await refreshAll(); await refreshAll();
message.value = `订单 ${row.orderNo} 已确认下发工厂,可继续创建司机任务。`; message.value = `订单 ${row.orderNo} 已确认下发工厂,可继续创建司机任务。`;
messageType.value = "success"; messageType.value = "success";
@ -362,19 +394,11 @@ async function handleConfirmFactory(row) {
actionLoading.value = ""; actionLoading.value = "";
} }
} }
async function handleCreateTask() { async function handleCreateTask() {
if (!taskForm.order_id) { if (!taskForm.order_id) { message.value = "请先从待发厂订单中选择一笔订单"; messageType.value = "error"; return; }
message.value = "请先从待发厂订单中选择一笔订单";
messageType.value = "error";
return;
}
if (!taskForm.pickup_address.trim() || !taskForm.delivery_address.trim() || !taskForm.pickup_content.trim()) { if (!taskForm.pickup_address.trim() || !taskForm.delivery_address.trim() || !taskForm.pickup_content.trim()) {
message.value = "请补全取货地址、送达地址和提货内容"; message.value = "请补全取货地址、送达地址和提货内容"; messageType.value = "error"; return;
messageType.value = "error";
return;
} }
creatingTask.value = true; creatingTask.value = true;
message.value = ""; message.value = "";
try { try {
@ -392,9 +416,7 @@ async function handleCreateTask() {
await refreshAll(); await refreshAll();
message.value = `司机任务 ${result.task_no} 创建成功,订单已进入待司机接单状态。`; message.value = `司机任务 ${result.task_no} 创建成功,订单已进入待司机接单状态。`;
messageType.value = "success"; messageType.value = "success";
if (result.task_id) { if (result.task_id) message.value += " 可点击任务详情继续查看。";
message.value += ` 可点击任务详情继续查看。`;
}
resetTaskForm(); resetTaskForm();
} catch (error) { } catch (error) {
message.value = error.message || "创建司机任务失败"; message.value = error.message || "创建司机任务失败";
@ -415,55 +437,103 @@ onMounted(async () => {
</script> </script>
<style scoped> <style scoped>
.page { padding: 0; } .page { padding: 0; display: grid; gap: 16px; }
.header-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; } .header-row {
h2 { margin: 0 0 6px; } display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;
.subline { margin: 0; color: #6b7280; font-size: 13px; } padding: 18px 20px; border: 1px solid #dbe3ef; border-radius: 20px;
.mode-tag { padding: 6px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; white-space: nowrap; } background: linear-gradient(180deg, #ffffff, #f8fbff);
}
h2 { margin: 0 0 6px; font-size: 24px; color: #0f172a; }
.subline { margin: 0; color: #64748b; line-height: 1.6; }
.mode-tag {
display: inline-flex; align-items: center; padding: 8px 12px; border-radius: 999px;
background: #dcfce7; color: #166534; font-size: 12px; font-weight: 600; white-space: nowrap;
}
.mode-tag.fallback { background: #fef3c7; color: #92400e; } .mode-tag.fallback { background: #fef3c7; color: #92400e; }
.summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.summary-card { border: 1px solid #dbe3ef; border-radius: 14px; padding: 14px; background: #fff; min-width: 0; } .summary-card {
border: 1px solid #dbe3ef; border-radius: 18px; padding: 16px;
background: linear-gradient(180deg, #fff, #f8fbff); min-width: 0;
}
.summary-card span { display: block; color: #64748b; font-size: 13px; margin-bottom: 8px; } .summary-card span { display: block; color: #64748b; font-size: 13px; margin-bottom: 8px; }
.summary-card strong { font-size: 22px; color: #0f172a; word-break: break-all; } .summary-card strong { font-size: 24px; color: #0f172a; word-break: break-word; }
.message-box { margin-bottom: 16px; padding: 12px 14px; border-radius: 8px; background: #dcfce7; color: #166534; } .detail-panel, .section-shell {
border: 1px solid #dbe3ef; border-radius: 18px; padding: 18px; background: #fff; min-width: 0;
}
.section-shell { display: grid; gap: 12px; }
.panel-header, .section-shell-header {
display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
}
.panel-header { margin-bottom: 14px; }
.panel-header h3, .section-shell-header h3 { margin: 0; font-size: 16px; color: #0f172a; }
.panel-header span, .section-shell-header p { color: #64748b; font-size: 13px; line-height: 1.5; margin: 0; }
.section-shell-header span {
color: #1d4ed8; font-size: 12px; font-weight: 600; padding: 6px 10px; border-radius: 999px; background: #eff6ff;
}
.link-grid, .layer-tabs { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }
.layer-tab, .jump-link, .link-btn, .ghost-btn, .primary-btn, .modal-close, button, input, textarea, select { border-radius: 12px; }
.layer-tab {
border: 1px solid #cbd5e1; padding: 14px; background: #f8fafc; color: #334155; font-weight: 700;
}
.jump-link {
display: inline-flex; align-items: center; justify-content: center; padding: 10px 14px;
background: #2563eb; color: #fff; text-decoration: none; font-weight: 600;
}
.jump-link.secondary { background: #f8fafc; color: #334155; border: 1px solid #cbd5e1; }
.filter-grid, .order-quick-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
.order-quick-grid article { padding: 14px; border-radius: 14px; border: 1px solid #dbe3ef; background: #f8fbff; }
.order-quick-grid span { display: block; color: #64748b; font-size: 13px; margin-bottom: 8px; }
.order-quick-grid strong { font-size: 22px; color: #0f172a; }
.message-box { padding: 12px 14px; border-radius: 12px; background: #dcfce7; color: #166534; }
.message-box.error { background: #fee2e2; color: #b91c1c; } .message-box.error { background: #fee2e2; color: #b91c1c; }
.workspace-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.9fr); gap: 16px; align-items: start; } .modal-mask {
.form-column { display: grid; gap: 12px; position: sticky; top: 16px; } position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center;
.task-panel { grid-column: 1 / -1; } padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px);
.detail-panel { border: 1px solid #e5e7eb; border-radius: 12px; padding: 14px; background: #fff; min-width: 0; } }
.panel-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; } .modal-panel {
.panel-header h3 { margin: 0; font-size: 16px; } width: min(760px, 100%); max-height: min(88vh, 860px); overflow: auto;
.panel-header span { color: #6b7280; font-size: 13px; } border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 24px;
.loading { margin: 0; color: #6b7280; } box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24);
.empty-state { padding: 24px; border: 1px dashed #d1d5db; border-radius: 8px; color: #6b7280; text-align: center; } }
.modal-panel-wide { width: min(1100px, 100%); }
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
.modal-header h4 { margin: 0 0 6px; font-size: 18px; }
.modal-header span { color: #64748b; font-size: 13px; }
.modal-close { width: 36px; height: 36px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; }
.workspace-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(320px, 0.95fr); gap: 16px; align-items: start; }
.form-column { display: grid; gap: 12px; }
.table-wrap { overflow-x: auto; } .table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; min-width: 820px; } table { width: 100%; border-collapse: collapse; min-width: 860px; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; vertical-align: top; } th, td { text-align: left; padding: 12px 10px; border-bottom: 1px solid #e5e7eb; vertical-align: top; }
th { color: #334155; font-size: 13px; background: #f8fafc; }
.actions-cell { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; } .actions-cell { display: flex; flex-wrap: wrap; gap: 8px; min-width: 0; }
button, input, textarea { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 12px; background: #fff; } button, input, textarea, select {
border: 1px solid #cbd5e1; padding: 10px 12px; background: #fff; min-width: 0;
}
button:disabled { color: #9ca3af; background: #f3f4f6; } button:disabled { color: #9ca3af; background: #f3f4f6; }
.primary-btn { background: #2563eb; color: #fff; border-color: #2563eb; } .primary-btn { background: #2563eb; color: #fff; border-color: #2563eb; }
.primary-btn.small, .ghost-btn.small { padding: 8px 10px; } .primary-btn.small, .ghost-btn.small { padding: 8px 10px; }
.ghost-btn { background: #fff; color: #334155; } .ghost-btn { background: #f8fafc; color: #334155; }
.link-btn { color: #2563eb; } .link-btn { color: #2563eb; text-decoration: none; font-weight: 600; }
.link-btn.disabled { color: #9ca3af; }
.status { display: inline-flex; padding: 4px 10px; border-radius: 999px; background: #eef2ff; color: #4338ca; white-space: nowrap; } .status { display: inline-flex; padding: 4px 10px; border-radius: 999px; background: #eef2ff; color: #4338ca; white-space: nowrap; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.form-grid.compact { grid-template-columns: repeat(2, minmax(0, 1fr)); } .form-grid.compact { grid-template-columns: repeat(2, minmax(0, 1fr)); }
label { display: grid; gap: 6px; min-width: 0; } label { display: grid; gap: 6px; min-width: 0; }
label span { color: #4b5563; font-size: 13px; } label span { color: #475569; font-size: 13px; font-weight: 600; }
.full-width { grid-column: 1 / -1; } .full-width { grid-column: 1 / -1; }
.action-row { margin-top: 16px; display: flex; justify-content: flex-end; } .action-row { margin-top: 16px; display: flex; justify-content: flex-end; }
.loading { margin: 0; color: #6b7280; }
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
.money { font-weight: 700; color: #0f172a; white-space: nowrap; } .money { font-weight: 700; color: #0f172a; white-space: nowrap; }
.pickup-cell { min-width: 180px; }
@media (max-width: 1180px) { @media (max-width: 1180px) {
.workspace-grid { grid-template-columns: 1fr; } .workspace-grid { grid-template-columns: 1fr; }
.form-column { position: static; }
.task-panel { grid-column: auto; }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.header-row, .panel-header { flex-direction: column; align-items: flex-start; } .header-row, .panel-header, .section-shell-header, .modal-header { flex-direction: column; align-items: flex-start; }
.summary-grid, .form-grid, .form-grid.compact { grid-template-columns: 1fr; } .summary-grid, .link-grid, .layer-tabs, .filter-grid, .order-quick-grid, .form-grid, .form-grid.compact { grid-template-columns: 1fr; }
.actions-cell { flex-direction: column; align-items: stretch; } .actions-cell { flex-direction: column; align-items: stretch; }
.actions-cell button { width: 100%; } .actions-cell button { width: 100%; }
} }

View File

@ -278,11 +278,11 @@
</select> </select>
</label> </label>
<div class="toolbar-actions"> <div class="toolbar-actions">
<button :disabled="loadingCategories" @click="handleSearchCategories"> <button class="primary-btn" :disabled="loadingCategories" @click="handleSearchCategories">
{{ loadingCategories ? "查询中..." : "查询分类" }} {{ loadingCategories ? "查询中..." : "查询分类" }}
</button> </button>
<button :disabled="loadingCategories" @click="handleResetCategories">重置</button> <button class="ghost-btn" :disabled="loadingCategories" @click="handleResetCategories">重置</button>
<button @click="openCreateCategoryModal">新增分类</button> <button class="ghost-btn" @click="openCreateCategoryModal">新增分类</button>
</div> </div>
</section> </section>
@ -433,11 +433,11 @@
</select> </select>
</label> </label>
<div class="toolbar-actions"> <div class="toolbar-actions">
<button :disabled="loadingProducts" @click="handleSearchProducts"> <button class="primary-btn" :disabled="loadingProducts" @click="handleSearchProducts">
{{ loadingProducts ? "查询中..." : "查询产品" }} {{ loadingProducts ? "查询中..." : "查询产品" }}
</button> </button>
<button :disabled="loadingProducts" @click="handleResetProducts">重置</button> <button class="ghost-btn" :disabled="loadingProducts" @click="handleResetProducts">重置</button>
<button @click="openCreateProductModal">新增产品</button> <button class="ghost-btn" @click="openCreateProductModal">新增产品</button>
</div> </div>
</section> </section>
@ -591,11 +591,11 @@
</select> </select>
</label> </label>
<div class="toolbar-actions"> <div class="toolbar-actions">
<button :disabled="loadingSuppliers" @click="handleSearchSuppliers"> <button class="primary-btn" :disabled="loadingSuppliers" @click="handleSearchSuppliers">
{{ loadingSuppliers ? "查询中..." : "查询供应商" }} {{ loadingSuppliers ? "查询中..." : "查询供应商" }}
</button> </button>
<button :disabled="loadingSuppliers" @click="handleResetSuppliers">重置</button> <button class="ghost-btn" :disabled="loadingSuppliers" @click="handleResetSuppliers">重置</button>
<button @click="openCreateSupplierModal">新增供应商</button> <button class="ghost-btn" @click="openCreateSupplierModal">新增供应商</button>
</div> </div>
</section> </section>
@ -1423,7 +1423,8 @@ h2 { margin: 0 0 6px; font-size: 24px; }
textarea, textarea,
.link-btn, .link-btn,
.ghost-btn, .ghost-btn,
.primary-btn { .primary-btn,
.modal-close {
border-radius: 12px; border-radius: 12px;
padding: 10px 12px; padding: 10px 12px;
min-width: 0; min-width: 0;
@ -1439,7 +1440,7 @@ textarea,
background: #fff; background: #fff;
} }
.primary-btn { border: 1px solid #2563eb; background: #2563eb; color: #fff; } .primary-btn { border: 1px solid #2563eb; background: #2563eb; color: #fff; }
.ghost-btn { color: #334155; } .ghost-btn { background: #f8fafc; color: #334155; }
.toolbar-actions { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; } .toolbar-actions { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
.loading { margin: 0; color: #6b7280; } .loading { margin: 0; color: #6b7280; }
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; } .empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
@ -1459,23 +1460,25 @@ th { color: #334155; font-size: 13px; background: #f8fafc; }
.modal-panel-wide { width: min(980px, 100%); } .modal-panel-wide { width: min(980px, 100%); }
.detail-modal { width: min(860px, 100%); } .detail-modal { width: min(860px, 100%); }
.modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px); } .modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px); }
.modal-panel { width: min(720px, 100%); max-height: min(85vh, 860px); overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 20px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24); } .modal-panel { width: min(760px, 100%); max-height: min(88vh, 860px); overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 24px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24); }
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; } .modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
.modal-header h4 { margin: 0 0 6px; font-size: 18px; } .modal-header h4 { margin: 0 0 6px; font-size: 18px; }
.modal-header span { color: #64748b; font-size: 13px; } .modal-header span { color: #64748b; font-size: 13px; }
.modal-close { width: 36px; height: 36px; border-radius: 999px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; } .modal-close { width: 36px; height: 36px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; }
.link-btn { color: #2563eb; cursor: pointer; } .link-btn { color: #2563eb; cursor: pointer; }
.link-btn:disabled { color: #9ca3af; background: #f3f4f6; cursor: not-allowed; } .link-btn:disabled { color: #9ca3af; background: #f3f4f6; cursor: not-allowed; }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.toolbar-4, .toolbar-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } .toolbar-4, .toolbar-3, .toolbar-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.toolbar-actions { grid-column: 1 / -1; } .toolbar-actions { grid-column: 1 / -1; }
.modal-panel, .detail-modal, .modal-panel-wide { width: min(100%, 760px); }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.page-header, .panel-header { flex-direction: column; align-items: stretch; } .page-header, .panel-header, .action-row.split { flex-direction: column; align-items: stretch; }
.toolbar, .form-grid, .detail-grid, .toolbar-2, .toolbar-3, .toolbar-4 { grid-template-columns: 1fr; } .toolbar, .form-grid, .detail-grid, .toolbar-2, .toolbar-3, .toolbar-4 { grid-template-columns: 1fr; }
.modal-panel { padding: 16px; } .modal-mask { padding: 12px; }
.modal-panel, .detail-modal, .modal-panel-wide { width: 100%; max-height: 92vh; padding: 18px; }
table { min-width: 780px; } table { min-width: 780px; }
} }
</style> </style>

View File

@ -1236,15 +1236,26 @@ h2 { margin: 0 0 6px; font-size: 24px; }
.form-grid select, .form-grid select,
textarea, textarea,
.ghost-btn, .ghost-btn,
.link-btn { .link-btn,
border: 1px solid #cbd5e1; .primary-btn,
.modal-close {
border-radius: 12px; border-radius: 12px;
padding: 10px 12px; padding: 10px 12px;
min-width: 0;
}
.toolbar input,
.toolbar select,
.form-grid input,
.form-grid select,
textarea,
.ghost-btn,
.link-btn {
border: 1px solid #cbd5e1;
background: #fff; background: #fff;
} }
.primary-btn { border: 1px solid #2563eb; background: #2563eb; color: #fff; } .primary-btn { border: 1px solid #2563eb; background: #2563eb; color: #fff; }
.ghost-btn { background: #f8fafc; color: #334155; } .ghost-btn { background: #f8fafc; color: #334155; }
.toolbar-actions { display: flex; gap: 10px; align-items: end; } .toolbar-actions { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; }
.loading { margin: 0; color: #6b7280; } .loading { margin: 0; color: #6b7280; }
.empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; } .empty-state { padding: 24px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #fff; }
.table-wrap { overflow-x: auto; } .table-wrap { overflow-x: auto; }
@ -1257,12 +1268,13 @@ th { color: #334155; font-size: 13px; background: #f8fafc; }
.action-row.split { justify-content: space-between; align-items: center; } .action-row.split { justify-content: space-between; align-items: center; }
.inline-actions { display: flex; gap: 10px; flex-wrap: wrap; } .inline-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px); } .modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.48); backdrop-filter: blur(2px); }
.modal-panel { width: min(720px, 100%); max-height: min(85vh, 860px); overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 20px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24); } .modal-panel { width: min(760px, 100%); max-height: min(88vh, 860px); overflow: auto; border-radius: 20px; border: 1px solid #dbe3ef; background: #fff; padding: 24px; box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24); }
.modal-panel-wide { width: min(980px, 100%); } .modal-panel-wide { width: min(980px, 100%); }
.detail-modal { width: min(860px, 100%); }
.modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; } .modal-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; }
.modal-header h4 { margin: 0 0 6px; font-size: 18px; } .modal-header h4 { margin: 0 0 6px; font-size: 18px; }
.modal-header span { color: #64748b; font-size: 13px; } .modal-header span { color: #64748b; font-size: 13px; }
.modal-close { width: 36px; height: 36px; border-radius: 999px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; } .modal-close { width: 36px; height: 36px; border: 1px solid #d1d5db; background: #f8fafc; font-size: 22px; line-height: 1; color: #334155; }
.permission-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .permission-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.permission-item { display: grid; gap: 4px; padding: 12px; border: 1px solid #dbe3ef; border-radius: 14px; background: #f8fbff; } .permission-item { display: grid; gap: 4px; padding: 12px; border: 1px solid #dbe3ef; border-radius: 14px; background: #f8fbff; }
.permission-item small { color: #64748b; } .permission-item small { color: #64748b; }

363
未完成功能清单.md Normal file
View File

@ -0,0 +1,363 @@
# 未完成功能清单
> 说明:本清单基于《确定版需求说明书.md》《开发细节/》系列文档,并结合当前系统代码扫描结果,从“需求应有能力”与“现有代码实现”两侧做差集整理。这里把“未完成”定义为:需求已明确、文档已细化,但当前代码中缺少实现、只做了占位、只实现了部分链路、或实现与需求存在明显偏差的功能。
---
## 1. 结论摘要
当前系统已经覆盖了订单、客户、产品、工厂、司机任务、提醒、报表、配置、审计、系统用户与角色菜单等主干模块,但**仍有不少关键能力处于“半成品”或“缺失”状态**。
最核心的未完成项集中在以下几类:
1. **订单流程未闭环**:提交、审批、取消、确认下发工厂、司机履约之间的状态和数据联动还不完整。
2. **权限与字段隔离不完整**:部分端/角色的字段可见性、数据范围、按钮权限仍有缺口。
3. **系统管理不完整**菜单、角色、用户、授权虽已存在但分页、筛选、CRUD 完整性仍不足。
4. **物流、AI、提醒、报表等高级能力大多为“有接口壳、缺实际业务联动”**
5. **数据库级、审计级、配置级要求尚未完全落实到代码行为中**
---
## 2. 按需求域整理的未完成功能
### 2.1 登录鉴权与权限体系
#### 未完成项
- **角色登录态与菜单/按钮权限的完整联动不足**
- 需求要求登录后返回 `menus`、`permissions`,并由前端动态控制页面入口和按钮权限。
- 当前代码里有登录和权限校验,但从端到端看,权限菜单树、按钮权限、角色授权后的即时生效链路仍不够完整。
- **权限体系存在“接口有了,细粒度控制未完全落地”的问题**
- 例如订单详情、列表、司机任务详情等接口在需求中要求按角色隐藏不同字段,但目前更多是业务员端做了简单过滤,其他端的字段隔离与页面级按钮联动还不充分。
- **管理员/管理层/业务员/司机的角色能力边界尚未完全收敛**
- 需求里每个角色的可见范围、可操作范围定义较明确,但当前实现中仍可能存在接口共享、前端页面复用过宽的情况。
---
### 2.2 系统用户、角色、菜单管理
#### 未完成项
- **用户管理的分页、筛选与完整 CRUD 仍不完整**
- 目前代码中已有用户列表、新增、更新、重置密码,但列表分页是由外层切片实现,接口层未形成完整分页查询能力。
- 删除、停用/启用切换等操作在需求中被提到,但代码层未形成完整闭环。
- **角色管理缺少完整生命周期操作**
- 目前可新增、更新、授权菜单,但缺少更完整的角色停用/启用流程与异常约束的全量实现。
- **菜单管理未完全覆盖需求字段**
- 需求细化中提到 `icon`、`sort_no`、`status`、多级菜单等能力;现有实现虽有菜单树,但仍偏基础。
- **角色授权后的“权限码 + 菜单树”同步返回与前端刷新机制不完整**
- 需求要求授权后登录刷新生效,但代码层没有看到完整的权限缓存刷新、会话失效或即时同步机制。
---
### 2.3 客户管理
#### 未完成项
- **客户导入能力未见完整代码闭环**
- 需求要求支持导入模板、去重、重复提示、失败行返回等。
- 当前代码里客户创建、列表、详情有实现,但导入链路在代码中没有看到完整接口/服务实现。
- **客户去重规则未完全按“姓名 + 手机号”强约束落地**
- 需求明确要求以“姓名 + 手机号”做重复校验,并用于新客户自动同步。
- 代码在订单创建时会查找客户,但是否在所有客户新增/导入路径统一强校验,还不完整。
- **月结信息、欠款信息的维护和展示不完整**
- 客户资料中要求有结算方式、账期起算口径、欠款金额等字段。
- 现有实现中可见基础字段,但围绕欠款、结算模式、账期配置的管理页面与联动还不完整。
---
### 2.4 产品与产品分类管理
#### 未完成项
- **产品分类独立管理虽已出现接口痕迹,但整体功能仍不完整**
- 需求明确要有独立 `product_category` 及其维护页面。
- 目前代码虽出现产品分类相关 API但前端页面、联动选择、停用约束、导入/展示等未形成完整闭环。
- **产品状态对订单下单的约束不足**
- 需求测试中要求禁用产品不可下单。
- 当前代码中未见订单创建时对产品启用状态、分类状态的完整校验链路。
- **产品快照机制未完全前置到所有场景**
- 需求要求订单明细保存名称、规格、单位快照,历史订单不随产品变化。
- 当前订单明细读取已有快照字段,但产品维护、价格变更、分类变更后的历史一致性校验仍不完整。
---
### 2.5 工厂/供应商管理与下发文本
#### 未完成项
- **工厂下发文本的“生成 + 确认 + 留痕”链路未完全闭环**
- 需求要求审批通过后生成可复制文本,且需要确认下发、记录文本日志。
- 当前 `get_supplier_text` / `confirm_supplier_text` 已有基础实现,但:
- 下发文本历史记录表的实际持久化链路不完整;
- 按不同工厂模板生成内容的规则还偏简单;
- “复制文本体验”前端未完全落实。
- **工厂模板类型的业务差异未完全实现**
- 需求强调不同工厂可有不同模板,但本期以文本复制为主。
- 当前实现多为统一模板拼接,模板可配置性不足。
---
### 2.6 销售订单录入、提交、审批
#### 未完成项
- **订单创建接口与需求字段存在明显缺口**
- 需求要求订单录入包括客户、物流、回扣、费用、订单明细等完整结构。
- 当前创建接口虽然支持核心字段,但很多需求中的派生字段、校验细节、权限字段和审核前信息展示仍不完整。
- **订单编辑/草稿保存的完整能力未完全实现**
- 需求中明确有“保存草稿”“编辑草稿”“保存并提交”等页面操作。
- 代码目前主要看到创建、提交、取消、审批,缺少完整的更新/编辑接口和前端联动。
- **订单提交审核的完整校验不充分**
- 需求要求提交时检查明细完整性、客户匹配、新客户入库、状态流转、通知等。
- 当前已实现基础提交,但订单完整性校验和通知联动不全面。
- **审批页所需的利润计算过程展示未完全落地**
- 需求要求管理层审批时看到利润、利润率以及计算过程。
- 当前接口能返回结果值,但“计算过程明细”和页面级展示还未完整实现。
- **审批退回后的业务员通知未完整落地**
- 需求明确要求退回后通知业务员。
- 代码中有审计写入,但通知中心/消息推送联动不完整。
- **订单取消的特殊审批逻辑未完全实现**
- 需求要求:草稿、待审核直接取消;已通过后需何总确认;履约阶段走特殊审批。
- 当前实现中有 `cancel_pending`,但特殊审批规则、履约中分支和状态恢复链路还不够完整。
- **取消原因/意见/前状态的审计留痕不够完整**
- 数据库设计要求保存取消前状态、申请人、申请时间等。
- 代码有部分字段更新,但与取消审批、恢复状态、日志完整链路仍未完全闭环。
- **订单状态机仍是“部分可改状态”,不是完整流程驱动**
- 需求中状态流转非常明确。
- 当前代码允许通过一个通用状态变更接口改到多个状态,但缺少严密的状态机约束与自动联动。
- **订单通知规则未完整实现**
- 需求要求订单状态变化时相关角色收到通知。
- 目前只有订单事件写审计,未见完整消息通知写入、推送或站内信列表。
---
### 2.7 司机任务管理与履约
#### 未完成项
- **司机任务的创建、分配与订单状态联动不完整**
- 需求要求管理层或后台创建任务,订单审批后进入司机接单环节。
- 目前虽有物流任务相关模块,但任务创建的完整接口、分配策略与状态联动还不充分。
- **司机任务列表/详情的可见性控制未完全细化**
- 需求要求司机仅能看取货地址、取件内容、数量、工厂、部分业务员信息。
- 当前已有脱敏思路,但司机端显示字段、接口过滤与页面控制并未完全统一。
- **司机接单、揽货、送达的业务约束不完整**
- 需求要求:
- 接单后才能揽货;
- 揽货需图片上传;
- 提交前可撤回,提交后不可撤回;
- 送达后更新任务和订单状态。
- 当前代码可能存在基本状态流转,但“提交前可撤回、提交后不可撤回”与图片必填约束还未完整闭环。
- **COS/OSS 图片存储与附件管理未完全统一**
- 需求对图片上传、附件元数据、存储路径、类型有明确约束。
- 当前文件服务存在,但司机揽货上传、附件表、对象存储元信息的统一落实仍不足。
- **微信订阅消息通知未实现**
- 需求明确提到通过微信订阅消息通知相关人员。
- 当前没有看到实际推送实现。
---
### 2.8 物流与 AI 辅助识别
#### 未完成项
- **物流轨迹对接快递100的完整实现未落地**
- 需求要求物流信息对接快递100接口。
- 当前代码更像是手动维护/查询轨迹的雏形,第三方对接、失败重试、节点同步尚未完整。
- **物流超时提醒的“一次性提醒”策略未完全实现**
- 需求要求首个物流节点超过 2 天无流转仅提醒一次。
- 目前提醒模块存在,但是否按“首节点 + 仅一次 + 去重”规则执行,还需补齐。
- **AI 图片识别链路不完整**
- 需求要求AI 仅做辅助识别,保留原始结果与人工修正结果。
- 当前已有 AI 接口和记录思路,但:
- OCR/识别结果人工确认界面未完全实现;
- 相似订单人工判断流程未完全实现;
- 原始结果与修正结果的可追踪展示不够完整。
- **识别失败、置信度过低、结果冲突的人工兜底流程未闭环**
- 需求强调最终由人工确认。
- 目前更多是接口层记录,缺少业务流程层兜底。
---
### 2.9 提醒中心、欠款提醒、长时间未下单提醒
#### 未完成项
- **提醒中心页面与接口联动不完整**
- 需求中提醒中心应支持列表、查看、标记已读、筛选等。
- 目前虽有提醒接口,但页面展示和状态流转未见完整实现。
- **欠款提醒的起算规则未完全落地**
- 需求允许按订单日期、对账日期、发货日期配置起算口径。
- 现有代码中欠款同步逻辑存在,但不同口径切换与账期计算未完整实现。
- **欠款提醒“提醒对象为业务员”的规则未完全验证**
- 需要确保提醒分发给对应业务员,而不是泛化通知。
- 当前缺少完整的接收人策略展示。
- **长时间未下单提醒的金额阈值和沉默周期配置未完全形成业务闭环**
- 需求要求低于金额阈值的小客户不纳入提醒。
- 代码中配置接口和提醒接口有,但定时检查、筛选逻辑、重复提醒控制未完全看到。
---
### 2.10 业绩统计报表
#### 未完成项
- **报表口径统一还未完全消除差异**
- 需求要求按月/季/年、工业品/日用品拆分统计,剔除电商订单,支持提成核对。
- 代码中已有报表接口,但分类映射、来源标签剔除、提成核对字段的完整统计口径仍需联调。
- **报表导出能力可能仍是简化版本**
- 需求要求支持导出文件,并与页面一致。
- 目前可见报表接口和导出路径,但异步导出、文件持久化、权限控制和大数据量性能处理不足。
- **报表图表与明细维度未完全实现**
- 页面原型要求柱状图、折线图、表格等。
- 代码中更多是数据接口层,前端图表表现仍需补齐。
---
### 2.11 配置管理
#### 未完成项
- **系统配置项仅覆盖基础字段,未形成统一配置中心体验**
- 需求要求支持物流超时、沉默客户周期、欠款起算、金额阈值、报表分类映射等。
- 代码中有配置服务,但配置项的初始化、展示、编辑、即时生效和审计闭环未完全统一。
- **敏感配置的安全存储未完全体现**
- 例如 OSS、AI 凭证不应进入明文配置。
- 当前代码层尚未完整体现环境变量/密钥管理策略。
---
### 2.12 审计日志
#### 未完成项
- **审计日志写入覆盖面仍不够全面**
- 需求要求所有新增、修改、删除、提交、审批、取消、接单、揽货、送达等关键操作都写日志。
- 目前订单、系统用户、角色、菜单等有部分写入但物流任务、提醒、导入、导出、AI 修正等关键动作未完全覆盖。
- **审计日志查询和前后值展示未完整落地**
- 需求要求能查看前后值差异。
- 现有代码更偏基础查询,前后值 diff 展示与页面化浏览未完整实现。
---
### 2.13 前端各端页面
#### 未完成项
- **WEB 业务员端仍有较多页面是“能用但不完整”**
- 订单列表、录单、详情、提醒页有基础结构,但按钮状态、字段脱敏、草稿编辑、审批后文本复制等体验未完全落地。
- **WEB 管理后台的业务面板未完全覆盖需求页面**
- 用户、角色、菜单、客户、产品、工厂、订单、任务、提醒、报表、配置、审计等页面需要统一入口和联动。
- 当前页面结构存在,但细节操作、分页筛选、批量导入、导出、权限控制仍有缺口。
- **微信小程序管理层端存在若干页面仅实现骨架**
- 审批列表、审批详情、取消审批、下发文本、任务创建、经营数据等页面基本存在,但与真实接口和业务规则的深度联动不足。
- **微信小程序司机端页面需要补齐“任务状态驱动 UI”**
- 任务列表、任务详情、接单、揽货、送达、附件上传等页面的状态控制还不够严谨。
---
## 3. 按“第一性原理”归纳的本质缺口
从第一性原理看,当前系统真正未完成的,不是“某几个按钮没做”,而是以下 6 个基础闭环还没完全建立:
### 3.1 数据闭环未闭合
- 订单创建后,客户、产品、工厂、任务、附件、审计、提醒、报表之间的关联没有完全形成单一事实来源。
- 许多模块是“各自能查”,但还没有做到“一个事件触发全链路更新”。
### 3.2 状态机未完全收敛
- 订单状态、任务状态、提醒状态、欠款状态、审批状态之间存在多个可变点。
- 目前更像“允许改状态”,而不是“严格按状态机推进”。
### 3.3 权限隔离未完全落地
- 需求要求不同角色看到不同字段、不同按钮、不同菜单、不同数据范围。
- 现在已有角色判断,但字段级、按钮级、行级的权限隔离还不彻底。
### 3.4 配置驱动未完全实现
- 需求中大量规则本应由配置驱动,例如提醒周期、欠款起算、报表分类、模板类型。
- 当前仍有不少规则写死或半写死。
### 3.5 外部能力未完全接通
- 快递100、AI OCR、OSS、微信订阅消息等外部能力多数停留在接口壳或简化实现。
- 真实生产可用需要:鉴权、失败重试、幂等、日志、降级、人工兜底。
### 3.6 体验层未完全收口
- 页面原型要求的可见字段、按钮时机、复制体验、导出体验、空态/错误态、权限态,还没有完全体现在前端。
---
## 4. 建议优先级
### P0必须优先补齐
1. 订单编辑/草稿保存
2. 订单取消特殊审批闭环
3. 订单下发工厂文本日志化与确认闭环
4. 司机任务创建、接单、揽货、送达状态联动
5. 物流轨迹与超时提醒一次性去重
6. 欠款生成口径与提醒对象规则
7. 字段级权限与角色数据隔离
### P1尽快补齐
1. 客户导入与去重
2. 产品分类完整 CRUD
3. 审批退回通知
4. AI 识别结果人工修正页面
5. 报表口径统一与导出
6. 配置中心完整化
### P2体验与增强
1. 工厂文本复制体验优化
2. 审计日志前后值展示
3. 通知中心/消息中心
4. 页面空态、加载态、错误态统一
5. 报表图表化展示
---
## 5. 结语
如果把当前系统理解成一条业务流水线,那么现在已经有了“入口、若干节点、若干出口”,但**真正还没完全完成的是“状态驱动的完整闭环”**。建议后续优先围绕订单状态机、任务状态机、提醒状态机、权限隔离、配置驱动这五条主线补齐。

431
缺口定位清单.md Normal file
View File

@ -0,0 +1,431 @@
# 缺口定位清单
> 说明:本清单基于《确定版需求说明书.md》《开发细节/》系列文档,并结合当前代码扫描结果,按“未完成项 → 具体文件 → 具体函数/页面位置”的方式定位。目的不是复述需求,而是明确“缺口落点在哪里”,方便后续逐项补齐。
---
## 1. 订单流程缺口
### 1.1 订单编辑 / 草稿保存已补齐部分闭环
- **当前状态**:已完成后端更新接口和服务层骨架,仍需继续补前端联动与更完整的状态校验。
- **已落点文件**
- `backend/app/api/orders.py`
- `backend/app/schemas/orders.py`
- `backend/app/services/order_service.py`
- `backend/app/repositories/order_repository.py`
- **已落点位置**
- `backend/app/api/orders.py` 新增 `update_order()`,对应 `PUT /api/orders/{order_id}`
- `backend/app/schemas/orders.py` 新增 `UpdateOrderRequest`
- `backend/app/services/order_service.py` 新增 `OrderService.update_order()`,支持草稿/驳回单编辑。
- `backend/app/repositories/order_repository.py` 新增 `update_order_with_items()`
- **仍待继续完善**
- `frontend/web-sales/src/views/OrderFormPage.vue`
- `frontend/web-sales/src/views/OrdersPage.vue`
- `frontend/web-sales/src/views/OrderDetailPage.vue`
- **相关函数**
- `OrderService.update_order()`
- `OrderRepository.update_order_with_items()`
- `OrderService.create_order()`
- `OrderService.submit_order()`
- `OrderService.cancel_order()`
### 1.2 订单审批退回后的通知缺口
- **缺口描述**:需求要求审批退回后通知业务员;当前只有审批日志和审计日志,没有看到完整通知联动。
- **定位文件**
- `backend/app/services/order_service.py`
- `backend/app/services/reminder_service.py`
- `backend/app/api/reminders.py`
- **具体位置**
- `OrderService.approve_order()` 里写了审批日志,但未触发提醒创建或消息推送。
- `reminder_service` 目前只实现了欠款、沉默客户、物流超时三类提醒检查,没有订单审批通知入口。
- **相关函数**
- `OrderService.approve_order()`
- `ReminderService.check_all()`
- `ReminderService.list_reminders()`
### 1.3 取消订单的特殊审批链路仍不完整
- **缺口描述**:需求要求草稿/待审直接取消,已通过/履约中进入 `cancel_pending`,并支持取消审批通过/驳回恢复原状态;当前逻辑有状态字段,但完整业务规则还不够严密。
- **定位文件**
- `backend/app/api/orders.py`
- `backend/app/services/order_service.py`
- `backend/app/repositories/order_repository.py`
- **具体位置**
- `backend/app/services/order_service.py``cancel_order()`、`cancel_approve_order()` 已有基础实现,但对 `cancel_previous_status`、审批拒绝恢复、流程通知的闭环不完整。
- `backend/app/api/orders.py` 中虽有 `/cancel``/cancel-approve`,但前端取消弹窗、审批页与状态机联动仍需补齐。
- **相关函数**
- `OrderService.cancel_order()`
- `OrderService.cancel_approve_order()`
- `OrderRepository.update_cancel_fields()`
### 1.4 确认下发工厂文本的落库与留痕缺口
- **缺口描述**:需求要求生成工厂文本、复制、确认下发,并记录文本日志;当前接口返回文本和确认结果,但日志表/历史记录链路未完全形成闭环。
- **定位文件**
- `backend/app/api/orders.py`
- `backend/app/services/order_service.py`
- `backend/app/repositories/order_repository.py`
- `frontend/mini-manager/pages/supplier-text.js`
- **具体位置**
- `OrderService.get_supplier_text()` 仅返回文本内容,没有看到明确写入 `order_supplier_text_log` 的持久化动作。
- `OrderService.confirm_supplier_text()` 只更新订单状态,文本内容、模板类型、确认人/时间的完整日志留痕还不充分。
- `frontend/mini-manager/pages/supplier-text.js` 需要对照确认下发流程做页面联动。
- **相关函数**
- `OrderService.get_supplier_text()`
- `OrderService.confirm_supplier_text()`
### 1.5 订单状态机仍偏“手动改状态”
- **缺口描述**:需求中的订单状态是严格流程型状态机;当前有通用状态变更接口,但校验和联动不足。
- **定位文件**
- `backend/app/services/order_service.py`
- `backend/app/api/orders.py`
- **具体位置**
- `OrderService.change_order_status()` 允许多个目标状态直接切换,但没有严格限制状态路径。
- `backend/app/api/orders.py``/status` 接口暴露了通用状态修改能力。
- **相关函数**
- `OrderService.change_order_status()`
---
## 2. 司机任务与物流缺口
### 2.1 司机任务创建后订单联动已补齐部分闭环
- **当前状态**:已补充任务取消接口,并继续强化创建/接单/揽货/送达的状态联动与审计。
- **已落点文件**
- `backend/app/api/logistics.py`
- `backend/app/services/logistics_service.py`
- `backend/app/repositories/logistics_repository.py`
- `backend/app/models/business.py`
- **已落点位置**
- `LogisticsService.create_task()`:创建任务后将订单状态切到 `pending_driver`
- `LogisticsService.accept_task()` / `pickup_task()` / `deliver_task()`:形成司机状态流转闭环。
- `LogisticsService.cancel_task()`:新增任务取消入口,并回写订单状态。
- `backend/app/api/logistics.py` 新增 `/api/logistics/tasks/{task_id}/cancel`
- `backend/app/models/business.py` 新增 `OrderSupplierTextLog` 之外的任务取消字段模型支撑。
- **仍待继续完善**
- `frontend/mini-manager/pages/task-create.js`
- `frontend/web-admin/src/views/BusinessPage.vue`
- `frontend/mini-driver/pages/task-list.js`
- `frontend/mini-driver/pages/task-detail.js`
- **相关函数**
- `LogisticsService.create_task()`
- `LogisticsService.accept_task()`
- `LogisticsService.pickup_task()`
- `LogisticsService.deliver_task()`
- `LogisticsService.cancel_task()`
### 2.2 司机端任务可见字段与权限隔离仍需加强
- **缺口描述**:司机只能看到本人任务和脱敏信息;目前有基础限制,但页面与接口的字段控制仍不完全统一。
- **定位文件**
- `backend/app/services/logistics_service.py`
- `backend/app/api/logistics.py`
- `frontend/mini-driver/pages/task-detail.js`
- `frontend/mini-driver/pages/task-list.js`
- **具体位置**
- `LogisticsService._ensure_task_access()`、`LogisticsService._ensure_trace_access()` 已做基础权限控制。
- `LogisticsService._map_task_detail()` 仍返回较多业务字段,司机端页面是否完全隐藏敏感字段需要继续核对。
- **相关函数**
- `LogisticsService._ensure_task_access()`
- `LogisticsService._ensure_trace_access()`
- `LogisticsService._map_task_detail()`
### 2.3 物流轨迹第三方对接未完整上线
- **缺口描述**需求提到快递100/第三方物流轨迹;当前代码仅保留了远程抓取能力,更多是占位式接入。
- **定位文件**
- `backend/app/services/logistics_service.py`
- `backend/app/repositories/logistics_repository.py`
- **具体位置**
- `LogisticsService._load_third_party_traces()`
- `LogisticsService._fetch_remote_traces()`
- `LogisticsService.get_trace()`
- **相关函数**
- `LogisticsService._fetch_remote_traces()`
- `LogisticsService._merge_trace_list()`
### 2.4 司机揽货附件上传与 OSS 元数据未完全统一
- **缺口描述**:需求要求图片/视频上传到 OSS写入附件元数据当前已有附件写入但字段级元数据与实际 OSS 约束仍需核对。
- **定位文件**
- `backend/app/services/logistics_service.py`
- `backend/app/services/file_service.py`
- `backend/app/api/files.py`
- **具体位置**
- `LogisticsService._persist_task_attachments()` 将图片/视频落到附件表,但未完全看出对 `storage_provider`、`bucket_name`、`object_key` 的全链路使用。
- `file_service` 负责附件封装,需核对是否满足需求中的 OSS 字段。
- **相关函数**
- `LogisticsService._persist_task_attachments()`
- `FileService.build_attachment_payload()`
---
## 3. 提醒与配置缺口
### 3.1 提醒中心页面与接口联动已补齐
- **当前状态**:提醒列表筛选与已读操作已补齐前端联动,后端读接口已接通。
- **已落点文件**
- `backend/app/api/reminders.py`
- `backend/app/services/reminder_service.py`
- `frontend/web-sales/src/views/RemindersPage.vue`
- `frontend/web-sales/src/mockApi.js`
- **已落点位置**
- `ReminderService.list_reminders()`、`ReminderService.read_reminder()` 已支持列表与已读闭环。
- `frontend/web-sales/src/views/RemindersPage.vue` 已支持筛选和标记已读。
- **相关函数**
- `ReminderService.list_reminders()`
- `ReminderService.read_reminder()`
- `readReminder()`
### 3.2 欠款生成与提醒口径未完全固化
- **缺口描述**:需求明确欠款按 `shipped/delivered` 配置生成,且提醒只生成一次;当前提醒逻辑有雏形,但需要和订单状态/欠款状态完全对齐。
- **定位文件**
- `backend/app/services/reminder_service.py`
- `backend/app/services/arrears_service.py`
- `backend/app/repositories/config_repository.py`
- `backend/app/api/configs.py`
- **具体位置**
- `ReminderService.check_arrears()`、`check_inactive_customers()`、`check_logistics_timeout()` 有基本规则。
- 配置读取依赖 `ConfigRepository.get_by_key()`,需要确认欠款起算、物流超时、沉默客户阈值已在所有业务点统一引用。
- **相关函数**
- `ReminderService.check_arrears()`
- `ReminderService.check_inactive_customers()`
- `ReminderService.check_logistics_timeout()`
- `ReminderService._get_config_value()`
### 3.3 配置中心页面与即时生效闭环不足
- **缺口描述**:需求要求系统配置可视化管理并即时生效;当前后端有配置服务,但页面与缓存刷新未完全闭合。
- **定位文件**
- `backend/app/api/configs.py`
- `backend/app/services/config_service.py`
- `frontend/web-admin/src/views/SystemPage.vue`
- **具体位置**
- `ConfigService` 负责读取/写入配置,但前端系统配置页是否覆盖全部配置项、是否支持联动刷新,还需继续补齐。
- **相关函数**
- `ConfigService.get_config()`
- `ConfigService.update_config()`
---
## 4. 客户、产品、供应商缺口
### 4.1 客户导入接口未形成完整闭环
- **缺口描述**:需求要求客户导入、去重、失败返回;当前代码中客户创建和详情存在,但导入链路缺口明显。
- **定位文件**
- `backend/app/api/customers.py`
- `backend/app/services/customer_service.py`
- `frontend/web-admin/src/views/BusinessPage.vue`
- **具体位置**
- `CustomerService` 需要核对是否存在批量导入函数;从当前代码扫描看,导入链路未形成完整页面与接口闭环。
- **相关函数**
- `CustomerService.create_customer()`
- `CustomerService.list_customers()`
- `CustomerService.get_customer()`
### 4.2 产品分类独立维护未完全落地
- **缺口描述**:需求要求独立产品分类管理页面和接口;当前产品模块存在分类字段,但独立 CRUD 与前端页面还需补齐。
- **定位文件**
- `backend/app/api/products.py`
- `backend/app/services/product_service.py`
- `backend/app/repositories/product_repository.py`
- `frontend/web-admin/src/views/MasterDataPage.vue`
- **具体位置**
- `ProductService` 负责产品列表/详情/创建,但分类独立维护能力仍需核对。
- `frontend/web-admin/src/views/MasterDataPage.vue` 承担主数据页,分类管理是否完整展示需要补齐。
- **相关函数**
- `ProductService.list_products()`
- `ProductService.create_product()`
- `ProductService.get_product()`
### 4.3 工厂/供应商模板类型差异未完全实现
- **缺口描述**:需求要求工厂模板类型可配置并参与下发文案生成;当前只有基础 supplier 管理。
- **定位文件**
- `backend/app/api/suppliers.py`
- `backend/app/services/supplier_service.py`
- `backend/app/repositories/supplier_repository.py`
- **具体位置**
- `SupplierService` 负责供应商 CRUD但模板类型、下发文本模板差异的业务规则需要继续确认。
- **相关函数**
- `SupplierService.list_suppliers()`
- `SupplierService.create_supplier()`
- `SupplierService.update_supplier()`
---
## 5. 系统管理缺口
### 5.1 用户管理列表分页已补齐前端联动,后端仍保留简化分页
- **当前状态**:前端用户/角色/菜单管理页已接通真实接口;后端仍采用接口层切片分页,但已满足当前功能闭环。
- **已落点文件**
- `backend/app/api/system.py`
- `backend/app/services/system_service.py`
- `frontend/web-admin/src/views/SystemPage.vue`
- `frontend/web-admin/src/mockApi.js`
- **已落点位置**
- `SystemPage.vue` 已支持用户、角色、菜单的查询、新增、编辑、授权、重置密码。
- `mockApi.js` 已按真实接口路径完成联调封装。
- **相关函数**
- `SystemService.list_users()`
- `SystemService.list_roles()`
- `SystemService.list_menus()`
### 5.2 用户停用/启用与重置密码以外的生命周期未完整
- **缺口描述**:需求要求用户、角色、菜单的停用启用、权限变更、异常约束完整;当前实现还偏基础。
- **定位文件**
- `backend/app/services/system_service.py`
- `backend/app/api/system.py`
- **具体位置**
- `SystemService.create_user()`、`update_user()`、`reset_password()` 有实现,但用户停用/启用、批量操作等未完整。
- `SystemService.update_role()` 已拦截管理员角色停用,但其他角色生命周期还需补齐。
- **相关函数**
- `SystemService.create_user()`
- `SystemService.update_user()`
- `SystemService.reset_password()`
- `SystemService.update_role()`
### 5.3 菜单权限即时生效/缓存刷新缺口
- **缺口描述**:需求要求角色授权后菜单和按钮权限立即生效;当前代码能授权,但没有看到 token/缓存层的即时刷新。
- **定位文件**
- `backend/app/services/system_service.py`
- `backend/app/api/auth.py`
- `frontend/web-admin/src/store.js`
- **具体位置**
- `SystemService.assign_role_menus()` 写库后未见缓存失效逻辑。
- 登录后前端 store/路由刷新机制需继续核对。
- **相关函数**
- `SystemService.assign_role_menus()`
- `SystemService.get_role_menu_assignment()`
---
## 6. 报表、审计、导出缺口
### 6.1 报表口径与导出前端闭环已接通,细分口径仍可继续优化
- **当前状态**:报表查询与导出接口已接通前端;口径统一已作为后续优化项保留。
- **已落点文件**
- `backend/app/api/reports.py`
- `backend/app/services/report_service.py`
- `backend/app/repositories/report_repository.py`
- `frontend/web-admin/src/views/ReportsPage.vue`
- `frontend/web-admin/src/mockApi.js`
- **已落点位置**
- `ReportService.get_performance_report()`
- `ReportService.export_performance_report()`
- `fetchPerformanceReport()`
- `exportPerformanceReport()`
- **相关函数**
- `ReportService.get_performance_report()`
- `ReportService.export_performance_report()`
### 6.2 审计日志覆盖面已覆盖主要闭环,细节仍可继续增强
- **当前状态**:订单、物流、提醒、系统管理、报表导出均已具备审计写入;后续可继续扩展到更多细粒度场景。
- **已落点文件**
- `backend/app/services/audit_service.py`
- `backend/app/api/audit_logs.py`
- `backend/app/services/order_service.py`
- `backend/app/services/logistics_service.py`
- `backend/app/services/reminder_service.py`
- `backend/app/services/report_service.py`
- `backend/app/services/system_service.py`
- **已落点位置**
- `OrderService.create_order()` / `approve_order()` / `cancel_order()` / `update_order()`
- `LogisticsService.create_task()` / `_persist_task_attachments()` / `cancel_task()`
- `ReminderService.read_reminder()`
- `ReportService.export_performance_report()`
- `SystemService.create_user()` / `update_user()` / `reset_password()` / `create_role()` / `update_role()` / `assign_role_menus()` / `create_menu()` / `update_menu()`
- **相关函数**
- `audit_service.write_log()` 的各调用点
---
## 7. 前端页面缺口
### 7.1 业务员端订单编辑与提醒中心已补齐主要闭环
- **当前状态**:业务员端已支持订单新建、编辑、详情、提交、取消,以及提醒筛选和已读。
- **已落点文件**
- `frontend/web-sales/src/views/OrderFormPage.vue`
- `frontend/web-sales/src/views/OrdersPage.vue`
- `frontend/web-sales/src/views/OrderDetailPage.vue`
- `frontend/web-sales/src/views/RemindersPage.vue`
- **已落点位置**
- 订单编辑/草稿保存按钮已接通更新链路。
- 订单详情页已补充编辑入口。
- 提醒中心已补充筛选、已读和状态展示。
### 7.2 管理后台主数据、业务、系统页已补入订单相关信息
- **当前状态**:管理后台已补入订单业务入口、订单详情入口和物流调度信息,不再只停留在客户/产品/系统配置层。
- **已落点文件**
- `frontend/web-admin/src/views/MasterDataPage.vue`
- `frontend/web-admin/src/views/SystemPage.vue`
- `frontend/web-admin/src/views/BusinessPage.vue`
- `frontend/web-admin/src/views/ReportsPage.vue`
- `frontend/web-admin/src/views/ApprovalDetailPage.vue`
- **已落点位置**
- `BusinessPage.vue` 现可查看待发厂订单、订单详情入口、生成发厂文案、确认下发、创建司机任务,并支持订单/任务状态筛选。
- `SystemPage.vue` 已覆盖用户、角色、菜单的查询、新增、编辑、授权、重置密码。
- `ReportsPage.vue` 已接通真实导出查看入口。
- `MasterDataPage.vue` 已新增订单快捷入口,客户详情可直达审批与履约。
- `ApprovalDetailPage.vue` 已补充取消上下文、审批后去向说明及订单/履约/报表联动入口。
- **仍待继续优化**
- 主数据页的分类/产品细节与订单联动统计
- 任务详情页与业务调度页的更深跳转联动
### 7.3 小程序管理层与司机端页面已接入主流程,仍可继续细化
- **当前状态**:管理层小程序和司机端已接入发厂文案、司机任务创建与执行主流程。
- **定位文件**
- `frontend/mini-manager/pages/approve-list.js`
- `frontend/mini-manager/pages/approve-detail.js`
- `frontend/mini-manager/pages/supplier-text.js`
- `frontend/mini-manager/pages/task-create.js`
- `frontend/mini-manager/pages/dashboard.js`
- `frontend/mini-driver/pages/task-list.js`
- `frontend/mini-driver/pages/task-detail.js`
- **仍待继续优化**
- 按订单状态控制按钮显隐
- 附件上传的错误提示与重试
- 审批意见与取消上下文的展示密度
---
## 8. 可直接作为后续开发入口的优先文件
如果要按优先级补齐,建议先从以下文件开始:
1. `backend/app/services/order_service.py`
2. `backend/app/api/orders.py`
3. `backend/app/services/logistics_service.py`
4. `backend/app/api/logistics.py`
5. `backend/app/services/reminder_service.py`
6. `backend/app/services/system_service.py`
7. `frontend/web-sales/src/views/OrderFormPage.vue`
8. `frontend/web-admin/src/views/SystemPage.vue`
9. `frontend/mini-manager/pages/supplier-text.js`
10. `frontend/mini-driver/pages/task-detail.js`
---
## 9. 说明
本清单是“定位清单”,不是最终修复方案。下一步如果需要,我可以继续把这些缺口整理成:
- **按 P0/P1/P2 排序的修复计划**
- **按文件拆分的改造任务卡**
- **按模块输出的逐项代码修改建议**