mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-10 02:20:28 +08:00
feat(web): I4 delivery and license SN UI
Add routes, menu entries, platform API helpers, and views for delivery batches and license SN management. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user