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:
2026-04-06 21:49:10 +08:00
parent 9df6f60a17
commit 00411a5e74
9 changed files with 1590 additions and 0 deletions
@@ -0,0 +1,162 @@
<template>
<el-card v-loading="pageLoading" shadow="never">
<template #header>
<div class="toolbar">
<span class="title">新建许可 SN</span>
<el-button @click="goBack">返回列表</el-button>
</div>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" style="max-width: 560px">
<el-form-item label="SN 编码" prop="snCode">
<el-input v-model="form.snCode" maxlength="128" show-word-limit placeholder="唯一 SN" />
</el-form-item>
<el-form-item label="项目">
<el-select
v-model="form.projectId"
clearable
filterable
placeholder="选填"
style="width: 100%"
:loading="projectsLoading"
>
<el-option v-for="p in projectOptions" :key="p.id" :label="p.name || String(p.id)" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="合同行 ID">
<el-input-number
v-model="form.contractLineId"
:min="1"
clearable
controls-position="right"
style="width: 100%"
placeholder="选填,MVP 手工录入"
/>
</el-form-item>
<el-form-item label="激活备注">
<el-input v-model="form.activationRemark" maxlength="512" show-word-limit type="textarea" :rows="2" placeholder="选填" />
</el-form-item>
</el-form>
<div class="footer-actions">
<el-button :loading="submitting" @click="submit(false)">创建并返回列表</el-button>
<el-button type="primary" :loading="submitting" @click="submit(true)">创建并进入详情</el-button>
</div>
</el-card>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useAuthStore } from "../stores/auth";
import { listProjects, createLicenseSn } from "../api/platform";
import { apiErrorMessage } from "../utils/apiErrorMessage";
const auth = useAuthStore();
const router = useRouter();
const pageLoading = ref(false);
const projectsLoading = ref(false);
const submitting = ref(false);
const projectOptions = ref([]);
const formRef = ref(null);
const form = reactive({
snCode: "",
projectId: undefined,
contractLineId: undefined,
activationRemark: "",
});
const rules = {
snCode: [{ required: true, message: "请输入 SN 编码", trigger: "blur" }],
};
onMounted(async () => {
auth.restoreAxiosAuth();
pageLoading.value = true;
try {
await loadProjects();
} finally {
pageLoading.value = false;
}
});
function goBack() {
router.push({ name: "license-sn-list" });
}
async function loadProjects() {
projectsLoading.value = true;
try {
const { data } = await listProjects({ page: 0, size: 500 });
const body = data && typeof data === "object" ? data : {};
projectOptions.value = Array.isArray(body.content) ? body.content : [];
} catch (e) {
ElMessage.error(apiErrorMessage(e, "加载项目失败"));
projectOptions.value = [];
} finally {
projectsLoading.value = false;
}
}
/**
* @param {boolean} goDetail
*/
async function submit(goDetail) {
const f = formRef.value;
if (!f) return;
try {
await f.validate();
} catch {
return;
}
submitting.value = true;
const body = {
snCode: form.snCode.trim(),
projectId: form.projectId ?? undefined,
contractLineId: form.contractLineId ?? undefined,
activationRemark: form.activationRemark?.trim() || undefined,
};
try {
const { data } = await createLicenseSn(body);
const id = data?.id;
if (id == null) {
ElMessage.error("创建成功但未返回 SN ID");
router.push({ name: "license-sn-list" });
return;
}
ElMessage.success("已创建许可 SN");
if (goDetail) {
router.push({ name: "license-sn-detail", params: { id: String(id) } });
} else {
router.push({ name: "license-sn-list" });
}
} catch (e) {
ElMessage.error(apiErrorMessage(e, "创建许可 SN 失败"));
} finally {
submitting.value = false;
}
}
</script>
<style scoped>
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.title {
font-weight: 600;
font-size: 16px;
}
.footer-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-lighter);
}
</style>