Compare commits

...

2 Commits

Author SHA1 Message Date
8b7d1379d1 feat: 打通业务员端创建订单链路 2026-05-14 14:24:06 +08:00
afe6993929 feat: 打通业务员端订单操作链路 2026-05-14 14:17:41 +08:00
7 changed files with 486 additions and 94 deletions

View File

@ -19,13 +19,13 @@ class OrderItemPayload(BaseModel):
product_name: str
specification: str
unit: str
quantity: float
sale_price: float
cost_price: float
rebate_amount: float = 0
freight_amount: float = 0
tax_amount: float = 0
other_fee_amount: float = 0
quantity: float = Field(gt=0)
sale_price: float = Field(ge=0)
cost_price: float = Field(ge=0)
rebate_amount: float = Field(default=0, ge=0)
freight_amount: float = Field(default=0, ge=0)
tax_amount: float = Field(default=0, ge=0)
other_fee_amount: float = Field(default=0, ge=0)
remark: str | None = None
@ -36,11 +36,11 @@ class CreateOrderRequest(BaseModel):
order_source: str | None = None
delivery_type: str | None = None
factory_id: int | None = None
commission_amount: float = 0
rebate_total: float = 0
freight_total: float = 0
tax_total: float = 0
other_fee_total: float = 0
commission_amount: float = Field(default=0, ge=0)
rebate_total: float = Field(default=0, ge=0)
freight_total: float = Field(default=0, ge=0)
tax_total: float = Field(default=0, ge=0)
other_fee_total: float = Field(default=0, ge=0)
remark: str | None = None
items: list[OrderItemPayload] = Field(default_factory=list)

View File

@ -346,6 +346,11 @@ class OrderService:
def create_order(self, payload: dict, session: Session | None = None) -> dict:
if session is not None:
try:
if not payload["customer_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
if not payload["customer_mobile"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
customer = self.customer_repository.find_by_name_and_mobile(
session,
payload["customer_name"],
@ -368,6 +373,9 @@ class OrderService:
)
items = payload["items"]
if not items:
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
sale_total = sum(item["quantity"] * item["sale_price"] for item in items)
cost_total = sum(item["quantity"] * item["cost_price"] for item in items)
profit_total = (

View File

@ -15,12 +15,18 @@ function wait(data) {
});
}
async function request(path) {
const response = await fetch(`${API_BASE_URL}${path}`);
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
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}`);
}
const result = await response.json();
return result.data;
}
@ -66,13 +72,19 @@ export async function fetchOrdersPage() {
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),
})),
isMock: false,
};
} catch (error) {
return wait({ orderFilters, orderActions, orderRows, isMock: true });
return wait({
orderFilters,
orderActions,
orderRows: orderRows.map((item) => ({ ...item, rawStatus: "draft" })),
isMock: true,
});
}
}
@ -80,6 +92,13 @@ export function fetchOrderForm() {
return wait({ sections: orderFormSections });
}
export async function createOrder(payload) {
return request("/api/orders", {
method: "POST",
body: JSON.stringify(payload),
});
}
export async function fetchOrderDetail(orderId) {
try {
const data = await request(`/api/orders/${orderId}`);
@ -110,7 +129,16 @@ export async function fetchOrderDetail(orderId) {
: "暂无审批记录",
},
];
return { blocks, isMock: false };
return {
blocks,
orderInfo: {
orderId: data.order_id,
orderNo: data.order_no,
rawStatus: data.order_status,
statusText: mapOrderStatus(data.order_status),
},
isMock: false,
};
} catch (error) {
return wait({
blocks: [
@ -120,11 +148,33 @@ export async function fetchOrderDetail(orderId) {
{ title: "发厂信息", content: "工厂、配送方式与备注信息当前为演示占位。" },
{ title: "审批记录", content: "暂无真实审批记录,当前为演示模式。" },
],
orderInfo: {
orderId,
orderNo: `SO${String(orderId).padStart(12, "0")}`,
rawStatus: "draft",
statusText: "草稿",
},
isMock: true,
});
}
}
export async function submitOrder(orderId) {
return request(`/api/orders/${orderId}/submit`, {
method: "POST",
});
}
export async function cancelOrder(orderId, cancelReason = "业务员取消订单") {
return request(`/api/orders/${orderId}/cancel`, {
method: "POST",
body: JSON.stringify({
cancel_reason: cancelReason,
cancel_opinion: "前端业务员端发起取消",
}),
});
}
export function fetchRemindersPage() {
return wait({ reminderTypes, reminderRows });
}

View File

@ -6,8 +6,36 @@
{{ isMock ? "演示数据" : "真实接口" }}
</span>
</div>
<p v-if="loading" class="loading">正在加载订单详情...</p>
<div v-else class="grid">
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
{{ message }}
</div>
<div v-if="!loading && orderInfo" class="toolbar">
<div class="order-brief">
<span>订单编号{{ orderInfo.orderNo }}</span>
<span>当前状态{{ orderInfo.statusText }}</span>
</div>
<div class="action-group">
<button
:disabled="actionLoading || !canSubmit(orderInfo.rawStatus) || isMock"
@click="handleSubmit"
>
{{ actionLoading === "submit" ? "提交中..." : "提交审核" }}
</button>
<button
class="danger-btn"
:disabled="actionLoading || !canCancel(orderInfo.rawStatus) || isMock"
@click="handleCancel"
>
{{ actionLoading === "cancel" ? "取消中..." : "取消订单" }}
</button>
</div>
</div>
<div v-if="!loading" class="grid">
<article v-for="block in blocks" :key="block.title" class="card">
<h3>{{ block.title }}</h3>
<p>{{ block.content }}</p>
@ -20,17 +48,76 @@
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { fetchOrderDetail } from "../mockApi";
import { cancelOrder, fetchOrderDetail, submitOrder } from "../mockApi";
const route = useRoute();
const loading = ref(true);
const isMock = ref(false);
const blocks = ref([]);
const orderInfo = ref(null);
const actionLoading = ref("");
const message = ref("");
const messageType = ref("success");
onMounted(async () => {
function canSubmit(status) {
return ["draft", "rejected"].includes(status);
}
function canCancel(status) {
return !["canceled", "settled"].includes(status);
}
async function loadDetail() {
const data = await fetchOrderDetail(route.params.id);
blocks.value = data.blocks;
orderInfo.value = data.orderInfo;
isMock.value = Boolean(data.isMock);
}
async function handleSubmit() {
if (!orderInfo.value?.orderId || isMock.value) {
message.value = "当前是演示数据模式,暂不执行真实提交。";
messageType.value = "error";
return;
}
actionLoading.value = "submit";
try {
await submitOrder(orderInfo.value.orderId);
await loadDetail();
message.value = `订单 ${orderInfo.value.orderNo} 已提交审核。`;
messageType.value = "success";
} catch (error) {
message.value = error.message || "提交审核失败";
messageType.value = "error";
} finally {
actionLoading.value = "";
}
}
async function handleCancel() {
if (!orderInfo.value?.orderId || isMock.value) {
message.value = "当前是演示数据模式,暂不执行真实取消。";
messageType.value = "error";
return;
}
actionLoading.value = "cancel";
try {
await cancelOrder(orderInfo.value.orderId);
await loadDetail();
message.value = `订单 ${orderInfo.value.orderNo} 已发起取消。`;
messageType.value = "success";
} catch (error) {
message.value = error.message || "取消订单失败";
messageType.value = "error";
} finally {
actionLoading.value = "";
}
}
onMounted(async () => {
await loadDetail();
loading.value = false;
});
</script>
@ -42,6 +129,14 @@ h2 { margin: 0; }
.mode-tag { padding: 6px 10px; border-radius: 999px; background: #dcfce7; color: #166534; font-size: 12px; }
.mode-tag.fallback { background: #fef3c7; color: #92400e; }
.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; }
.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; padding: 14px; border: 1px solid #e5e7eb; border-radius: 8px; background: #f9fafb; }
.order-brief { display: flex; flex-wrap: wrap; gap: 16px; color: #374151; }
.action-group { display: flex; gap: 10px; }
.action-group button { border: 1px solid #d1d5db; background: #fff; border-radius: 8px; padding: 8px 12px; }
.action-group button:disabled { color: #9ca3af; background: #f3f4f6; }
.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; }
h3 { margin: 0 0 10px; font-size: 16px; }

View File

@ -1,63 +1,245 @@
<template>
<section class="page">
<h2>订单录入 / 编辑页</h2>
<div class="tips">
<span>规则客户姓名客户手机号必填</span>
<span>规则至少保留一条明细</span>
<span>规则数量必须大于 0</span>
<h2>新建订单</h2>
<p class="tips">当前先接入业务员端最小可用创建流程后续再扩展多明细和更多字段</p>
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
{{ message }}
</div>
<p v-if="loading" class="loading">加载中...</p>
<div v-else class="grid">
<article v-for="section in orderFormSections" :key="section.title" class="card">
<h3>{{ section.title }}</h3>
<ul>
<li v-for="field in section.fields" :key="field">{{ field }}</li>
</ul>
</article>
</div>
<section v-if="!loading" class="actions">
<button>保存草稿</button>
<button disabled>保存并提交</button>
<button>新增明细</button>
<form class="form-grid" @submit.prevent="handleSubmit">
<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" />
</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>
</label>
</form>
<section class="item-panel">
<div class="panel-header">
<h3>订单明细</h3>
<span>当前先维护 1 条明细</span>
</div>
<div class="form-grid">
<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" />
</label>
</div>
</section>
<section v-if="!loading" class="summary">
<div>销售金额汇总100.00</div>
<div>成本金额汇总60.00</div>
<div>利润预览20.00</div>
<section class="summary">
<div>销售额{{ saleTotal.toFixed(2) }}</div>
<div>成本额{{ costTotal.toFixed(2) }}</div>
<div>预估利润{{ profitTotal.toFixed(2) }}</div>
</section>
<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">
{{ submitting ? "创建中..." : "创建订单" }}
</button>
</section>
<p v-if="!loading" class="validation">当前为演示状态提交按钮默认禁用代表必填项和金额校验未全部通过</p>
</section>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { computed, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { fetchOrderForm } from "../mockApi";
import { createOrder } from "../mockApi";
const loading = ref(true);
const orderFormSections = ref([]);
const router = useRouter();
const submitting = ref(false);
const message = ref("");
const messageType = ref("success");
onMounted(async () => {
const data = await fetchOrderForm();
orderFormSections.value = data.sections;
loading.value = false;
});
function buildDefaultForm() {
return {
customer_name: "",
customer_mobile: "",
customer_address: "",
order_source: "",
delivery_type: "",
factory_id: 1001,
commission_amount: 0,
rebate_total: 0,
freight_total: 0,
tax_total: 0,
other_fee_total: 0,
remark: "",
};
}
function buildDefaultItem() {
return {
product_name: "",
specification: "",
unit: "",
quantity: 1,
sale_price: 0,
cost_price: 0,
rebate_amount: 0,
freight_amount: 0,
tax_amount: 0,
other_fee_amount: 0,
remark: "",
};
}
const form = reactive(buildDefaultForm());
const item = reactive(buildDefaultItem());
const saleTotal = computed(() => Number(item.quantity || 0) * Number(item.sale_price || 0));
const costTotal = computed(() => Number(item.quantity || 0) * Number(item.cost_price || 0));
const profitTotal = computed(
() =>
saleTotal.value -
costTotal.value -
Number(form.rebate_total || 0) -
Number(form.freight_total || 0) -
Number(form.tax_total || 0) -
Number(form.other_fee_total || 0),
);
function resetForm() {
Object.assign(form, buildDefaultForm());
Object.assign(item, buildDefaultItem());
message.value = "";
}
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: "演示明细",
});
}
async function handleSubmit() {
submitting.value = true;
message.value = "";
try {
const result = await createOrder({
...form,
items: [{ ...item }],
});
message.value = `订单 ${result.order_no} 创建成功,当前状态:${result.order_status}`;
messageType.value = "success";
await router.push(`/orders/${result.order_id}`);
} catch (error) {
message.value = error.message || "创建订单失败";
messageType.value = "error";
} finally {
submitting.value = false;
}
}
</script>
<style scoped>
.page { background: #fff; border-radius: 8px; padding: 20px; }
h2 { margin: 0 0 12px; }
.tips { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 16px; }
.tips span { padding: 6px 10px; border-radius: 999px; background: #fff7ed; color: #c2410c; font-size: 13px; }
.loading { margin: 0; color: #6b7280; }
.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; }
ul { margin: 0; padding-left: 18px; }
li + li { margin-top: 8px; }
.tips { margin: 0 0 16px; color: #6b7280; }
.message-box { margin-bottom: 16px; padding: 12px 14px; border-radius: 8px; background: #dcfce7; color: #166534; }
.message-box.error { background: #fee2e2; color: #b91c1c; }
.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; }
.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; }
.panel-header h3 { margin: 0; font-size: 16px; }
.panel-header span { color: #6b7280; font-size: 13px; }
.summary { display: flex; gap: 20px; margin-top: 20px; padding: 14px; border: 1px solid #e5e7eb; border-radius: 8px; background: #f9fafb; }
.actions { display: flex; gap: 12px; margin-top: 20px; }
button { border: 1px solid #d1d5db; background: #fff; border-radius: 8px; padding: 10px 14px; }
button:disabled { color: #9ca3af; background: #f3f4f6; }
.summary { display: flex; gap: 20px; margin-top: 20px; padding: 14px; border: 1px solid #e5e7eb; border-radius: 8px; background: #f9fafb; }
.validation { margin: 16px 0 0; color: #6b7280; }
.secondary-btn { color: #374151; }
</style>

View File

@ -19,18 +19,8 @@
</div>
</div>
<div v-if="!loading" class="block">
<h3>快捷操作</h3>
<div class="chips">
<button
v-for="item in orderActions"
:key="item"
class="action-btn"
:disabled="item === '提交审核'"
>
{{ item }}
</button>
</div>
<div v-if="message" class="message-box" :class="{ error: messageType === 'error' }">
{{ message }}
</div>
<div v-if="!loading && !orderRows.length" class="empty-state">当前没有订单数据</div>
@ -57,42 +47,106 @@
<td><span class="status">{{ row.status }}</span></td>
<td>{{ row.source }}</td>
<td>{{ row.amount }}</td>
<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>
<button
class="inline-btn"
:disabled="submittingId === row.orderId || !canSubmit(row.rawStatus)"
@click="handleSubmit(row)"
>
{{ submittingId === row.orderId ? "提交中..." : "提交审核" }}
</button>
<button
class="inline-btn danger-btn"
:disabled="cancelingId === row.orderId || !canCancel(row.rawStatus)"
@click="handleCancel(row)"
>
{{ cancelingId === row.orderId ? "取消中..." : "取消订单" }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="!loading" class="pagination">
<button disabled>上一页</button>
<span>当前展示第一页后续接入真实翻页交互</span>
<button disabled>下一页</button>
</div>
</section>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { fetchOrdersPage } from "../mockApi";
import { cancelOrder, fetchOrdersPage, submitOrder } from "../mockApi";
import { salesStore } from "../store";
const loading = ref(true);
const isMock = ref(false);
const orderFilters = ref([]);
const orderActions = ref([]);
const orderRows = ref([]);
const user = salesStore.user;
const submittingId = ref(null);
const cancelingId = ref(null);
const message = ref("");
const messageType = ref("success");
onMounted(async () => {
function canSubmit(status) {
return ["draft", "rejected"].includes(status);
}
function canCancel(status) {
return !["canceled", "settled"].includes(status);
}
async function loadOrders() {
const data = await fetchOrdersPage();
orderFilters.value = data.orderFilters;
orderActions.value = data.orderActions;
orderRows.value = data.orderRows;
isMock.value = Boolean(data.isMock);
}
async function handleSubmit(row) {
if (!row.orderId || isMock.value) {
message.value = "当前是演示数据模式,暂不执行真实提交。";
messageType.value = "error";
return;
}
submittingId.value = row.orderId;
try {
await submitOrder(row.orderId);
await loadOrders();
message.value = `订单 ${row.orderNo} 已提交审核。`;
messageType.value = "success";
} catch (error) {
message.value = error.message || "提交审核失败";
messageType.value = "error";
} finally {
submittingId.value = null;
}
}
async function handleCancel(row) {
if (!row.orderId || isMock.value) {
message.value = "当前是演示数据模式,暂不执行真实取消。";
messageType.value = "error";
return;
}
cancelingId.value = row.orderId;
try {
await cancelOrder(row.orderId);
await loadOrders();
message.value = `订单 ${row.orderNo} 已发起取消。`;
messageType.value = "success";
} catch (error) {
message.value = error.message || "取消订单失败";
messageType.value = "error";
} finally {
cancelingId.value = null;
}
}
onMounted(async () => {
await loadOrders();
loading.value = false;
});
</script>
@ -111,16 +165,17 @@ h3 { margin: 0; font-size: 16px; }
.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; }
.action-btn { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px 14px; background: #fff; }
.action-btn:disabled { color: #9ca3af; background: #f3f4f6; }
.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; }
.table-panel { border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
.table-panel { margin-top: 20px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #e5e7eb; }
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; }
.actions-cell { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.detail-link { color: #2563eb; text-decoration: none; }
.disabled-link { color: #9ca3af; }
.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; }
.pagination button:disabled { color: #9ca3af; background: #f3f4f6; }
.inline-btn { border: 1px solid #d1d5db; border-radius: 8px; padding: 6px 10px; background: #fff; }
.danger-btn { color: #b91c1c; border-color: #fecaca; }
.inline-btn:disabled { color: #9ca3af; background: #f3f4f6; border-color: #e5e7eb; }
</style>

View File

@ -94,3 +94,5 @@
| 2026-05-14 | 已继续推进审批主线真实化:新增订单审批日志模型与仓储;`approve`、`cancel-approve` 会写入真实审批日志;订单详情 `approve_logs` 已开始返回真实数据库记录。 |
| 2026-05-14 | 已继续推进审计查询主线:`/api/audit-logs` 已支持按 `biz_type/biz_id/operate_type/start_time/end_time/page_no/page_size` 查询,并接入真实订单审批日志数据源。 |
| 2026-05-14 | 已开始补前端真实联调:`frontend/web-sales` 的订单列表与订单详情已改为优先请求真实后端接口,失败时再回退 mock同时清理了该模块关键页面中的乱码展示文本。 |
| 2026-05-14 | 已继续按前后端同步方式推进:后端订单接口已清理真实中文提示语;前端 `web-sales` 订单列表页与详情页已接入真实 `提交审核/取消订单` 动作,并补充加载态、禁用态与操作结果提示。 |
| 2026-05-14 | 已继续按前后端同步方式推进创建订单链路:后端补充创建订单参数校验;前端 `web-sales` 新建订单页已接入真实 `/api/orders` 创建接口,并支持最小可用单明细录入。 |