Compare commits
2 Commits
4f03d18843
...
8b7d1379d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b7d1379d1 | |||
| afe6993929 |
@ -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)
|
||||
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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` 创建接口,并支持最小可用单明细录入。 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user