mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 18:10:30 +08:00
00411a5e74
Add routes, menu entries, platform API helpers, and views for delivery batches and license SN management. Made-with: Cursor
396 lines
12 KiB
Vue
396 lines
12 KiB
Vue
<template>
|
|
<el-card v-loading="loading" shadow="never">
|
|
<template #header>
|
|
<div class="toolbar">
|
|
<div class="head-left">
|
|
<el-button link type="primary" @click="goList">← 交付列表</el-button>
|
|
<span class="title">交付批次详情</span>
|
|
<el-tag v-if="batch" :type="batchStatusTagType(batch.status)" size="small">
|
|
{{ batchStatusLabel(batch.status) }}
|
|
</el-tag>
|
|
</div>
|
|
<div v-if="batch && isPending" class="head-actions">
|
|
<el-button type="primary" :loading="savingHeader" @click="saveHeader">保存抬头</el-button>
|
|
<el-button type="success" :loading="markingDelivered" @click="onMarkDelivered">标记已交付</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="batch">
|
|
<el-descriptions :column="2" border class="block">
|
|
<el-descriptions-item label="批次编码">{{ batch.batchCode ?? "—" }}</el-descriptions-item>
|
|
<el-descriptions-item label="项目">{{ displayProject }}</el-descriptions-item>
|
|
<el-descriptions-item label="合同 ID">{{ batch.contractId ?? "—" }}</el-descriptions-item>
|
|
<el-descriptions-item label="完成时间">{{ formatDateTime(batch.finishedAt) }}</el-descriptions-item>
|
|
<el-descriptions-item label="计划交付日" :span="2">
|
|
<template v-if="isPending">
|
|
<el-date-picker
|
|
v-model="headerPlannedDate"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
placeholder="选填"
|
|
style="width: 240px"
|
|
/>
|
|
</template>
|
|
<template v-else>{{ formatDate(batch.plannedDeliveryDate) }}</template>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="备注" :span="2">
|
|
<template v-if="isPending">
|
|
<el-input v-model="headerRemarks" type="textarea" :rows="2" maxlength="4000" show-word-limit />
|
|
</template>
|
|
<template v-else>{{ batch.remarks ?? "—" }}</template>
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<h3 class="section-title">交付明细</h3>
|
|
<div v-if="isPending" class="line-toolbar">
|
|
<el-button type="primary" @click="openLineDialog()">添加明细</el-button>
|
|
</div>
|
|
<el-table :data="lineRows" border stripe style="width: 100%">
|
|
<el-table-column prop="sortOrder" label="排序" width="80" />
|
|
<el-table-column prop="description" label="说明" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="quantity" label="数量" width="100" />
|
|
<el-table-column prop="contractLineId" label="合同行 ID" width="110">
|
|
<template #default="{ row }">{{ row.contractLineId ?? "—" }}</template>
|
|
</el-table-column>
|
|
<el-table-column v-if="isPending" label="操作" width="140" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="primary" link @click="openLineDialog(row)">编辑</el-button>
|
|
<el-button type="danger" link @click="onDeleteLine(row)">删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</template>
|
|
|
|
<el-empty v-else-if="!loading" description="未加载到交付批次" />
|
|
|
|
<el-dialog v-model="lineDialogVisible" :title="lineEditingId ? '编辑明细' : '添加明细'" width="520px" destroy-on-close @closed="resetLineForm">
|
|
<el-form ref="lineFormRef" :model="lineForm" :rules="lineRules" label-width="120px">
|
|
<el-form-item label="排序">
|
|
<el-input-number v-model="lineForm.sortOrder" :min="0" :max="999999" controls-position="right" style="width: 100%" />
|
|
</el-form-item>
|
|
<el-form-item label="说明" prop="description">
|
|
<el-input v-model="lineForm.description" maxlength="512" show-word-limit placeholder="description" />
|
|
</el-form-item>
|
|
<el-form-item label="数量" prop="quantity">
|
|
<el-input-number v-model="lineForm.quantity" :min="0.0001" :precision="4" controls-position="right" style="width: 100%" />
|
|
</el-form-item>
|
|
<el-form-item label="合同行 ID">
|
|
<el-input-number
|
|
v-model="lineForm.contractLineId"
|
|
:min="1"
|
|
clearable
|
|
controls-position="right"
|
|
style="width: 100%"
|
|
placeholder="选填"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="lineDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="lineSaving" @click="submitLine">保存</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</el-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
import { useAuthStore } from "../stores/auth";
|
|
import {
|
|
getDeliveryBatch,
|
|
updateDeliveryBatch,
|
|
patchDeliveryBatchStatus,
|
|
addDeliveryLine,
|
|
updateDeliveryLine,
|
|
deleteDeliveryLine,
|
|
listProjects,
|
|
} from "../api/platform";
|
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
|
|
|
const auth = useAuthStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const loading = ref(false);
|
|
const savingHeader = ref(false);
|
|
const markingDelivered = ref(false);
|
|
const batch = ref(null);
|
|
const projectMap = ref(new Map());
|
|
|
|
const headerPlannedDate = ref("");
|
|
const headerRemarks = ref("");
|
|
|
|
const lineDialogVisible = ref(false);
|
|
const lineEditingId = ref(null);
|
|
const lineFormRef = ref(null);
|
|
const lineSaving = ref(false);
|
|
const lineForm = ref({
|
|
sortOrder: 0,
|
|
description: "",
|
|
quantity: 1,
|
|
contractLineId: undefined,
|
|
});
|
|
const lineRules = {
|
|
description: [{ required: true, message: "请输入说明", trigger: "blur" }],
|
|
quantity: [{ required: true, message: "请输入数量", trigger: "change" }],
|
|
};
|
|
|
|
const batchId = computed(() => route.params.id);
|
|
|
|
const isPending = computed(() => String(batch.value?.status ?? "").toUpperCase() === "PENDING");
|
|
|
|
const lineRows = computed(() => {
|
|
const b = batch.value;
|
|
if (!b) return [];
|
|
const raw = b.lines ?? [];
|
|
return Array.isArray(raw) ? raw : [];
|
|
});
|
|
|
|
const displayProject = computed(() => {
|
|
const b = batch.value;
|
|
if (!b) return "—";
|
|
if (b.projectName) return b.projectName;
|
|
const id = b.projectId;
|
|
if (id == null) return "—";
|
|
return projectMap.value.get(id) ?? String(id);
|
|
});
|
|
|
|
watch(
|
|
() => batch.value,
|
|
(b) => {
|
|
if (!b) return;
|
|
const d = b.plannedDeliveryDate;
|
|
headerPlannedDate.value = d == null ? "" : typeof d === "string" ? d.slice(0, 10) : String(d).slice(0, 10);
|
|
headerRemarks.value = b.remarks ?? "";
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
onMounted(async () => {
|
|
auth.restoreAxiosAuth();
|
|
await loadProjectMap();
|
|
await loadBatch();
|
|
});
|
|
|
|
watch(
|
|
() => route.params.id,
|
|
async () => {
|
|
await loadBatch();
|
|
}
|
|
);
|
|
|
|
function goList() {
|
|
router.push({ name: "deliveries" });
|
|
}
|
|
|
|
function batchStatusLabel(s) {
|
|
const u = String(s ?? "").toUpperCase();
|
|
const map = { PENDING: "待交付", DELIVERED: "已交付", CANCELLED: "已取消" };
|
|
return map[u] ?? String(s ?? "—");
|
|
}
|
|
|
|
function batchStatusTagType(s) {
|
|
const u = String(s ?? "").toUpperCase();
|
|
if (u === "PENDING") return "warning";
|
|
if (u === "DELIVERED") return "success";
|
|
if (u === "CANCELLED") return "info";
|
|
return "";
|
|
}
|
|
|
|
function formatDate(v) {
|
|
if (v == null || v === "") return "—";
|
|
if (typeof v === "string") return v.slice(0, 10);
|
|
return String(v);
|
|
}
|
|
|
|
function formatDateTime(v) {
|
|
if (v == null || v === "") return "—";
|
|
if (typeof v === "string") return v.replace("T", " ").slice(0, 19);
|
|
return String(v);
|
|
}
|
|
|
|
async function loadProjectMap() {
|
|
try {
|
|
const { data } = await listProjects({ page: 0, size: 500 });
|
|
const body = data && typeof data === "object" ? data : {};
|
|
const list = Array.isArray(body.content) ? body.content : [];
|
|
const m = new Map();
|
|
for (const p of list) {
|
|
if (p?.id != null) m.set(p.id, p.name ?? String(p.id));
|
|
}
|
|
projectMap.value = m;
|
|
} catch {
|
|
projectMap.value = new Map();
|
|
}
|
|
}
|
|
|
|
async function loadBatch() {
|
|
const id = batchId.value;
|
|
if (id == null || id === "") return;
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await getDeliveryBatch(id);
|
|
batch.value = data && typeof data === "object" ? data : null;
|
|
} catch (e) {
|
|
ElMessage.error(apiErrorMessage(e, "加载交付批次失败"));
|
|
batch.value = null;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveHeader() {
|
|
const id = batchId.value;
|
|
if (id == null) return;
|
|
savingHeader.value = true;
|
|
try {
|
|
await updateDeliveryBatch(id, {
|
|
plannedDeliveryDate: headerPlannedDate.value || undefined,
|
|
remarks: headerRemarks.value?.trim() ?? "",
|
|
});
|
|
ElMessage.success("已保存");
|
|
await loadBatch();
|
|
} catch (e) {
|
|
ElMessage.error(apiErrorMessage(e, "保存失败"));
|
|
} finally {
|
|
savingHeader.value = false;
|
|
}
|
|
}
|
|
|
|
function onMarkDelivered() {
|
|
ElMessageBox.confirm("确认将该批次标记为已交付(DELIVERED)?", "标记已交付", { type: "info" })
|
|
.then(async () => {
|
|
const id = batchId.value;
|
|
if (id == null) return;
|
|
markingDelivered.value = true;
|
|
try {
|
|
await patchDeliveryBatchStatus(id, { status: "DELIVERED" });
|
|
ElMessage.success("状态已更新");
|
|
await loadBatch();
|
|
} catch (e) {
|
|
ElMessage.error(apiErrorMessage(e, "状态更新失败"));
|
|
} finally {
|
|
markingDelivered.value = false;
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
function lineIdOf(row) {
|
|
return row.id;
|
|
}
|
|
|
|
function openLineDialog(row) {
|
|
if (row) {
|
|
lineEditingId.value = lineIdOf(row);
|
|
lineForm.value = {
|
|
sortOrder: row.sortOrder ?? 0,
|
|
description: row.description ?? "",
|
|
quantity: Number(row.quantity ?? 1),
|
|
contractLineId: row.contractLineId ?? undefined,
|
|
};
|
|
} else {
|
|
lineEditingId.value = null;
|
|
lineForm.value = {
|
|
sortOrder: 0,
|
|
description: "",
|
|
quantity: 1,
|
|
contractLineId: undefined,
|
|
};
|
|
}
|
|
lineDialogVisible.value = true;
|
|
}
|
|
|
|
function resetLineForm() {
|
|
lineEditingId.value = null;
|
|
lineFormRef.value?.resetFields?.();
|
|
}
|
|
|
|
async function submitLine() {
|
|
const f = lineFormRef.value;
|
|
if (!f) return;
|
|
try {
|
|
await f.validate();
|
|
} catch {
|
|
return;
|
|
}
|
|
const id = batchId.value;
|
|
if (id == null) return;
|
|
const payload = {
|
|
sortOrder: lineForm.value.sortOrder ?? 0,
|
|
description: lineForm.value.description.trim(),
|
|
quantity: lineForm.value.quantity,
|
|
contractLineId: lineForm.value.contractLineId ?? undefined,
|
|
};
|
|
lineSaving.value = true;
|
|
try {
|
|
if (lineEditingId.value != null) {
|
|
await updateDeliveryLine(id, lineEditingId.value, payload);
|
|
ElMessage.success("已更新明细");
|
|
} else {
|
|
await addDeliveryLine(id, payload);
|
|
ElMessage.success("已添加明细");
|
|
}
|
|
lineDialogVisible.value = false;
|
|
await loadBatch();
|
|
} catch (e) {
|
|
ElMessage.error(apiErrorMessage(e, "保存明细失败"));
|
|
} finally {
|
|
lineSaving.value = false;
|
|
}
|
|
}
|
|
|
|
function onDeleteLine(row) {
|
|
const lid = lineIdOf(row);
|
|
if (lid == null) {
|
|
ElMessage.warning("该行缺少 ID,无法删除");
|
|
return;
|
|
}
|
|
ElMessageBox.confirm("确定删除该交付明细吗?", "提示", { type: "warning" })
|
|
.then(async () => {
|
|
try {
|
|
await deleteDeliveryLine(batchId.value, lid);
|
|
ElMessage.success("已删除");
|
|
await loadBatch();
|
|
} catch (e) {
|
|
ElMessage.error(apiErrorMessage(e, "删除失败"));
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
.head-left {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.title {
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
.block {
|
|
margin-bottom: 20px;
|
|
}
|
|
.section-title {
|
|
margin: 20px 0 12px;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
}
|
|
.line-toolbar {
|
|
margin-bottom: 12px;
|
|
}
|
|
</style>
|