mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +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:
@@ -106,3 +106,99 @@ export function patchContractStatus(id, body) {
|
|||||||
export function listAuditEvents(params) {
|
export function listAuditEvents(params) {
|
||||||
return axios.get("/api/v1/audit-events", { params });
|
return axios.get("/api/v1/audit-events", { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ page?: number, size?: number, projectId?: string | number, keyword?: string }} params
|
||||||
|
*/
|
||||||
|
export function listDeliveryBatches(params) {
|
||||||
|
return axios.get("/api/v1/delivery-batches", { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} body
|
||||||
|
*/
|
||||||
|
export function createDeliveryBatch(body) {
|
||||||
|
return axios.post("/api/v1/delivery-batches", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeliveryBatch(id) {
|
||||||
|
return axios.get(`/api/v1/delivery-batches/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} id
|
||||||
|
* @param {Record<string, unknown>} body
|
||||||
|
*/
|
||||||
|
export function updateDeliveryBatch(id, body) {
|
||||||
|
return axios.put(`/api/v1/delivery-batches/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} id
|
||||||
|
* @param {{ status: string }} body
|
||||||
|
*/
|
||||||
|
export function patchDeliveryBatchStatus(id, body) {
|
||||||
|
return axios.patch(`/api/v1/delivery-batches/${id}/status`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} batchId
|
||||||
|
*/
|
||||||
|
export function listDeliveryLines(batchId) {
|
||||||
|
return axios.get(`/api/v1/delivery-batches/${batchId}/lines`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} batchId
|
||||||
|
* @param {Record<string, unknown>} body
|
||||||
|
*/
|
||||||
|
export function addDeliveryLine(batchId, body) {
|
||||||
|
return axios.post(`/api/v1/delivery-batches/${batchId}/lines`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} batchId
|
||||||
|
* @param {string | number} lineId
|
||||||
|
* @param {Record<string, unknown>} body
|
||||||
|
*/
|
||||||
|
export function updateDeliveryLine(batchId, lineId, body) {
|
||||||
|
return axios.put(`/api/v1/delivery-batches/${batchId}/lines/${lineId}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDeliveryLine(batchId, lineId) {
|
||||||
|
return axios.delete(`/api/v1/delivery-batches/${batchId}/lines/${lineId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ page?: number, size?: number, projectId?: string | number, keyword?: string }} params
|
||||||
|
*/
|
||||||
|
export function listLicenseSns(params) {
|
||||||
|
return axios.get("/api/v1/license-sns", { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} body
|
||||||
|
*/
|
||||||
|
export function createLicenseSn(body) {
|
||||||
|
return axios.post("/api/v1/license-sns", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLicenseSn(id) {
|
||||||
|
return axios.get(`/api/v1/license-sns/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} id
|
||||||
|
* @param {Record<string, unknown>} body
|
||||||
|
*/
|
||||||
|
export function updateLicenseSn(id, body) {
|
||||||
|
return axios.put(`/api/v1/license-sns/${id}`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} id
|
||||||
|
* @param {{ status: string }} body
|
||||||
|
*/
|
||||||
|
export function patchLicenseSnStatus(id, body) {
|
||||||
|
return axios.patch(`/api/v1/license-sns/${id}/status`, body);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
<el-menu-item index="/contracts">
|
<el-menu-item index="/contracts">
|
||||||
<span>合同管理</span>
|
<span>合同管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/deliveries">
|
||||||
|
<span>交付管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/licenses/sn">
|
||||||
|
<span>许可 SN</span>
|
||||||
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
|
|||||||
@@ -26,6 +26,42 @@ const routes = [
|
|||||||
component: () => import("../views/ProjectsView.vue"),
|
component: () => import("../views/ProjectsView.vue"),
|
||||||
meta: { roles: ["SYS_ADMIN", "DEVELOPER"] },
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"] },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "deliveries/new",
|
||||||
|
name: "delivery-new",
|
||||||
|
component: () => import("../views/DeliveryBatchWizardView.vue"),
|
||||||
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "新建交付批次" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "deliveries/:id",
|
||||||
|
name: "delivery-detail",
|
||||||
|
component: () => import("../views/DeliveryBatchDetailView.vue"),
|
||||||
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "交付批次详情" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "deliveries",
|
||||||
|
name: "deliveries",
|
||||||
|
component: () => import("../views/DeliveriesView.vue"),
|
||||||
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "交付管理" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "licenses/sn/new",
|
||||||
|
name: "license-sn-new",
|
||||||
|
component: () => import("../views/LicenseSnWizardView.vue"),
|
||||||
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "新建许可 SN" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "licenses/sn/:id",
|
||||||
|
name: "license-sn-detail",
|
||||||
|
component: () => import("../views/LicenseSnDetailView.vue"),
|
||||||
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "许可 SN 详情" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "licenses/sn",
|
||||||
|
name: "license-sn-list",
|
||||||
|
component: () => import("../views/LicenseSnListView.vue"),
|
||||||
|
meta: { roles: ["SYS_ADMIN", "DEVELOPER"], title: "许可 SN" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "contracts/new",
|
path: "contracts/new",
|
||||||
name: "contract-new",
|
name: "contract-new",
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
<template>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="toolbar">
|
||||||
|
<span class="title">交付管理</span>
|
||||||
|
<div class="actions">
|
||||||
|
<el-select
|
||||||
|
v-model="filterProjectId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="按项目筛选"
|
||||||
|
class="filter"
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option v-for="p in projectOptions" :key="p.id" :label="p.name || String(p.id)" :value="p.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="按批次编码搜索"
|
||||||
|
class="search"
|
||||||
|
@keyup.enter="load"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :loading="loading" @click="load">查询</el-button>
|
||||||
|
<el-button type="success" @click="goNew">新建交付批次</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="rows" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="batchCode" label="批次编码" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="项目" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.projectName ?? projectNameById(row.projectId) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="合同 ID" width="100">
|
||||||
|
<template #default="{ row }">{{ row.contractId ?? "—" }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="batchStatusTagType(row.status)" size="small">{{ batchStatusLabel(row.status) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="计划交付日" width="130">
|
||||||
|
<template #default="{ row }">{{ formatDate(row.plannedDeliveryDate) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="170">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="goDetail(row.id)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="onSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
import { listDeliveryBatches, listProjects } from "../api/platform";
|
||||||
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const rows = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const keyword = ref("");
|
||||||
|
const filterProjectId = ref(undefined);
|
||||||
|
const projectOptions = ref([]);
|
||||||
|
/** @type {import('vue').Ref<Map<string | number, string>>} */
|
||||||
|
const projectMap = ref(new Map());
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
auth.restoreAxiosAuth();
|
||||||
|
await loadProjects();
|
||||||
|
await load();
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSizeChange() {
|
||||||
|
page.value = 1;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectNameById(id) {
|
||||||
|
if (id == null) return "—";
|
||||||
|
return projectMap.value.get(id) ?? String(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 loadProjects() {
|
||||||
|
try {
|
||||||
|
const { data } = await listProjects({ page: 0, size: 500 });
|
||||||
|
const body = data && typeof data === "object" ? data : {};
|
||||||
|
const list = Array.isArray(body.content) ? body.content : [];
|
||||||
|
projectOptions.value = list;
|
||||||
|
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 (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "加载项目失败"));
|
||||||
|
projectOptions.value = [];
|
||||||
|
projectMap.value = new Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await listDeliveryBatches({
|
||||||
|
page: page.value - 1,
|
||||||
|
size: pageSize.value,
|
||||||
|
keyword: keyword.value?.trim() || undefined,
|
||||||
|
projectId: filterProjectId.value ?? undefined,
|
||||||
|
});
|
||||||
|
const body = data && typeof data === "object" ? data : {};
|
||||||
|
rows.value = Array.isArray(body.content) ? body.content : [];
|
||||||
|
total.value = Number(body.totalElements ?? body.total ?? 0);
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "加载交付批次失败"));
|
||||||
|
rows.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goNew() {
|
||||||
|
router.push({ name: "delivery-new" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(id) {
|
||||||
|
router.push({ name: "delivery-detail", params: { id: String(id) } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.pager {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<template>
|
||||||
|
<el-card v-loading="pageLoading" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="toolbar">
|
||||||
|
<span class="title">新建交付批次</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="项目" prop="projectId">
|
||||||
|
<el-select
|
||||||
|
v-model="form.projectId"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择项目"
|
||||||
|
style="width: 100%"
|
||||||
|
:loading="projectsLoading"
|
||||||
|
@change="onProjectChange"
|
||||||
|
>
|
||||||
|
<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="合同">
|
||||||
|
<el-select
|
||||||
|
v-model="form.contractId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="选填,可按项目筛选"
|
||||||
|
style="width: 100%"
|
||||||
|
:loading="contractsLoading"
|
||||||
|
:disabled="!form.projectId"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="c in contractsFiltered"
|
||||||
|
:key="c.id"
|
||||||
|
:label="contractOptionLabel(c)"
|
||||||
|
:value="c.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="批次编码" prop="batchCode">
|
||||||
|
<el-input v-model="form.batchCode" maxlength="64" show-word-limit placeholder="唯一批次编码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="计划交付日">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="plannedDateModel"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选填"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="form.remarks" type="textarea" :rows="3" maxlength="4000" show-word-limit 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, computed, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
import { listProjects, listContracts, createDeliveryBatch } from "../api/platform";
|
||||||
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const pageLoading = ref(false);
|
||||||
|
const projectsLoading = ref(false);
|
||||||
|
const contractsLoading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const projectOptions = ref([]);
|
||||||
|
const contractOptions = ref([]);
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const plannedDateModel = ref("");
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
projectId: undefined,
|
||||||
|
contractId: undefined,
|
||||||
|
batchCode: "",
|
||||||
|
remarks: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
projectId: [{ required: true, message: "请选择项目", trigger: "change" }],
|
||||||
|
batchCode: [{ required: true, message: "请输入批次编码", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const contractsFiltered = computed(() => {
|
||||||
|
const pid = form.projectId;
|
||||||
|
if (pid == null) return [];
|
||||||
|
return contractOptions.value.filter((c) => c.projectId == null || c.projectId === pid);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
auth.restoreAxiosAuth();
|
||||||
|
pageLoading.value = true;
|
||||||
|
try {
|
||||||
|
await Promise.all([loadProjects(), loadContracts()]);
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
router.push({ name: "deliveries" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onProjectChange() {
|
||||||
|
form.contractId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contractOptionLabel(c) {
|
||||||
|
const t = c.title ?? c.batchCode ?? String(c.id);
|
||||||
|
return `${t} (#${c.id})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadContracts() {
|
||||||
|
contractsLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await listContracts({ page: 0, size: 500 });
|
||||||
|
const body = data && typeof data === "object" ? data : {};
|
||||||
|
contractOptions.value = Array.isArray(body.content) ? body.content : [];
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "加载合同失败"));
|
||||||
|
contractOptions.value = [];
|
||||||
|
} finally {
|
||||||
|
contractsLoading.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 = {
|
||||||
|
projectId: form.projectId,
|
||||||
|
contractId: form.contractId ?? undefined,
|
||||||
|
batchCode: form.batchCode.trim(),
|
||||||
|
plannedDeliveryDate: plannedDateModel.value || undefined,
|
||||||
|
remarks: form.remarks?.trim() || undefined,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const { data } = await createDeliveryBatch(body);
|
||||||
|
const id = data?.id;
|
||||||
|
if (id == null) {
|
||||||
|
ElMessage.error("创建成功但未返回批次 ID");
|
||||||
|
router.push({ name: "deliveries" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success("已创建交付批次");
|
||||||
|
if (goDetail) {
|
||||||
|
router.push({ name: "delivery-detail", params: { id: String(id) } });
|
||||||
|
} else {
|
||||||
|
router.push({ name: "deliveries" });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "创建交付批次失败"));
|
||||||
|
} 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>
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
<template>
|
||||||
|
<el-card v-loading="loading" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="head-left">
|
||||||
|
<el-button link type="primary" @click="goList">← SN 列表</el-button>
|
||||||
|
<span class="title">许可 SN 详情</span>
|
||||||
|
<el-tag v-if="sn" :type="snStatusTagType(sn.status)" size="small">{{ snStatusLabel(sn.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="sn">
|
||||||
|
<el-descriptions :column="2" border class="block">
|
||||||
|
<el-descriptions-item label="SN 编码">{{ sn.snCode ?? "—" }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDateTime(sn.createdAt) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<h3 class="section-title">绑定与备注</h3>
|
||||||
|
<el-form label-width="120px" style="max-width: 520px">
|
||||||
|
<el-form-item label="项目">
|
||||||
|
<el-select
|
||||||
|
v-model="bindForm.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="bindForm.contractLineId"
|
||||||
|
:min="1"
|
||||||
|
clearable
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="选填"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="激活备注">
|
||||||
|
<el-input v-model="bindForm.activationRemark" type="textarea" :rows="2" maxlength="512" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="savingBind" @click="saveBind">保存绑定</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<h3 class="section-title">状态</h3>
|
||||||
|
<div class="status-row">
|
||||||
|
<el-select v-model="statusPick" placeholder="选择新状态" style="width: 220px">
|
||||||
|
<el-option
|
||||||
|
v-for="opt in statusOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-button type="primary" :loading="patchingStatus" :disabled="!statusPick" @click="applyStatus">更新状态</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!loading" description="未加载到许可 SN" />
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, computed, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
import { getLicenseSn, updateLicenseSn, patchLicenseSnStatus, listProjects } from "../api/platform";
|
||||||
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const savingBind = ref(false);
|
||||||
|
const patchingStatus = ref(false);
|
||||||
|
const sn = ref(null);
|
||||||
|
const projectsLoading = ref(false);
|
||||||
|
const projectOptions = ref([]);
|
||||||
|
|
||||||
|
const bindForm = reactive({
|
||||||
|
projectId: undefined,
|
||||||
|
contractLineId: undefined,
|
||||||
|
activationRemark: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusPick = ref("");
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: "REGISTERED", label: "已登记 (REGISTERED)" },
|
||||||
|
{ value: "ISSUED", label: "已发放 (ISSUED)" },
|
||||||
|
{ value: "ACTIVATED", label: "已激活 (ACTIVATED)" },
|
||||||
|
{ value: "SUSPENDED", label: "已暂停 (SUSPENDED)" },
|
||||||
|
{ value: "REVOKED", label: "已吊销 (REVOKED)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const snId = computed(() => route.params.id);
|
||||||
|
|
||||||
|
function snStatusLabel(s) {
|
||||||
|
const u = String(s ?? "").toUpperCase();
|
||||||
|
const map = {
|
||||||
|
REGISTERED: "已登记",
|
||||||
|
ISSUED: "已发放",
|
||||||
|
ACTIVATED: "已激活",
|
||||||
|
SUSPENDED: "已暂停",
|
||||||
|
REVOKED: "已吊销",
|
||||||
|
};
|
||||||
|
return map[u] ?? String(s ?? "—");
|
||||||
|
}
|
||||||
|
|
||||||
|
function snStatusTagType(s) {
|
||||||
|
const u = String(s ?? "").toUpperCase();
|
||||||
|
if (u === "REGISTERED") return "info";
|
||||||
|
if (u === "ISSUED") return "";
|
||||||
|
if (u === "ACTIVATED") return "success";
|
||||||
|
if (u === "SUSPENDED") return "warning";
|
||||||
|
if (u === "REVOKED") return "danger";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(v) {
|
||||||
|
if (v == null || v === "") return "—";
|
||||||
|
if (typeof v === "string") return v.replace("T", " ").slice(0, 19);
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => sn.value,
|
||||||
|
(row) => {
|
||||||
|
if (!row) return;
|
||||||
|
bindForm.projectId = row.projectId ?? undefined;
|
||||||
|
bindForm.contractLineId = row.contractLineId ?? undefined;
|
||||||
|
bindForm.activationRemark = row.activationRemark ?? "";
|
||||||
|
statusPick.value = "";
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
auth.restoreAxiosAuth();
|
||||||
|
await loadProjects();
|
||||||
|
await loadSn();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.params.id,
|
||||||
|
async () => {
|
||||||
|
await loadSn();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function goList() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSn() {
|
||||||
|
const id = snId.value;
|
||||||
|
if (id == null || id === "") return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await getLicenseSn(id);
|
||||||
|
sn.value = data && typeof data === "object" ? data : null;
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "加载许可 SN 失败"));
|
||||||
|
sn.value = null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBind() {
|
||||||
|
const id = snId.value;
|
||||||
|
if (id == null) return;
|
||||||
|
savingBind.value = true;
|
||||||
|
try {
|
||||||
|
await updateLicenseSn(id, {
|
||||||
|
projectId: bindForm.projectId ?? undefined,
|
||||||
|
contractLineId: bindForm.contractLineId ?? undefined,
|
||||||
|
activationRemark: bindForm.activationRemark?.trim() ?? "",
|
||||||
|
});
|
||||||
|
ElMessage.success("已保存");
|
||||||
|
await loadSn();
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "保存失败"));
|
||||||
|
} finally {
|
||||||
|
savingBind.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyStatus() {
|
||||||
|
const id = snId.value;
|
||||||
|
const st = statusPick.value;
|
||||||
|
if (id == null || !st) return;
|
||||||
|
patchingStatus.value = true;
|
||||||
|
try {
|
||||||
|
await patchLicenseSnStatus(id, { status: st });
|
||||||
|
ElMessage.success("状态已更新");
|
||||||
|
statusPick.value = "";
|
||||||
|
await loadSn();
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "状态更新失败"));
|
||||||
|
} finally {
|
||||||
|
patchingStatus.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="toolbar">
|
||||||
|
<span class="title">许可 SN</span>
|
||||||
|
<div class="actions">
|
||||||
|
<el-select
|
||||||
|
v-model="filterProjectId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="按项目筛选"
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<el-option v-for="p in projectOptions" :key="p.id" :label="p.name || String(p.id)" :value="p.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="按 SN 编码搜索"
|
||||||
|
class="search"
|
||||||
|
@keyup.enter="load"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :loading="loading" @click="load">查询</el-button>
|
||||||
|
<el-button type="success" @click="goNew">新建许可 SN</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="rows" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="snCode" label="SN 编码" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="项目" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.projectName ?? projectNameById(row.projectId) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="合同行 ID" width="110">
|
||||||
|
<template #default="{ row }">{{ row.contractLineId ?? "—" }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="snStatusTagType(row.status)" size="small">{{ snStatusLabel(row.status) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" width="170">
|
||||||
|
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="goDetail(row.id)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
background
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="onSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
import { listLicenseSns, listProjects } from "../api/platform";
|
||||||
|
import { apiErrorMessage } from "../utils/apiErrorMessage";
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const rows = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const keyword = ref("");
|
||||||
|
const filterProjectId = ref(undefined);
|
||||||
|
const projectOptions = ref([]);
|
||||||
|
/** @type {import('vue').Ref<Map<string | number, string>>} */
|
||||||
|
const projectMap = ref(new Map());
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
auth.restoreAxiosAuth();
|
||||||
|
await loadProjects();
|
||||||
|
await load();
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSizeChange() {
|
||||||
|
page.value = 1;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectNameById(id) {
|
||||||
|
if (id == null) return "—";
|
||||||
|
return projectMap.value.get(id) ?? String(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function snStatusLabel(s) {
|
||||||
|
const u = String(s ?? "").toUpperCase();
|
||||||
|
const map = {
|
||||||
|
REGISTERED: "已登记",
|
||||||
|
ISSUED: "已发放",
|
||||||
|
ACTIVATED: "已激活",
|
||||||
|
SUSPENDED: "已暂停",
|
||||||
|
REVOKED: "已吊销",
|
||||||
|
};
|
||||||
|
return map[u] ?? String(s ?? "—");
|
||||||
|
}
|
||||||
|
|
||||||
|
function snStatusTagType(s) {
|
||||||
|
const u = String(s ?? "").toUpperCase();
|
||||||
|
if (u === "REGISTERED") return "info";
|
||||||
|
if (u === "ISSUED") return "";
|
||||||
|
if (u === "ACTIVATED") return "success";
|
||||||
|
if (u === "SUSPENDED") return "warning";
|
||||||
|
if (u === "REVOKED") return "danger";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(v) {
|
||||||
|
if (v == null || v === "") return "—";
|
||||||
|
if (typeof v === "string") return v.replace("T", " ").slice(0, 19);
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProjects() {
|
||||||
|
try {
|
||||||
|
const { data } = await listProjects({ page: 0, size: 500 });
|
||||||
|
const body = data && typeof data === "object" ? data : {};
|
||||||
|
const list = Array.isArray(body.content) ? body.content : [];
|
||||||
|
projectOptions.value = list;
|
||||||
|
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 (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "加载项目失败"));
|
||||||
|
projectOptions.value = [];
|
||||||
|
projectMap.value = new Map();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await listLicenseSns({
|
||||||
|
page: page.value - 1,
|
||||||
|
size: pageSize.value,
|
||||||
|
keyword: keyword.value?.trim() || undefined,
|
||||||
|
projectId: filterProjectId.value ?? undefined,
|
||||||
|
});
|
||||||
|
const body = data && typeof data === "object" ? data : {};
|
||||||
|
rows.value = Array.isArray(body.content) ? body.content : [];
|
||||||
|
total.value = Number(body.totalElements ?? body.total ?? 0);
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(apiErrorMessage(e, "加载许可 SN 失败"));
|
||||||
|
rows.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goNew() {
|
||||||
|
router.push({ name: "license-sn-new" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDetail(id) {
|
||||||
|
router.push({ name: "license-sn-detail", params: { id: String(id) } });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.pager {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user