feat(m4): add batch SN ops, delivery gate, console link, SN tags

This commit is contained in:
2026-05-25 14:51:41 +08:00
parent 4dc8341e7e
commit 6522f02b54
8 changed files with 139 additions and 3 deletions
@@ -14,6 +14,9 @@
<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-item label="比特控制台">
<el-button type="primary" link @click="openBitConsole" :disabled="!consoleUrl">打开控制台</el-button>
</el-descriptions-item>
</el-descriptions>
<h3 class="section-title">绑定与备注</h3>
@@ -43,6 +46,13 @@
<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 label="标签">
<el-select v-model="bindForm.snTag" style="width:100%">
<el-option label="正式 (OFFICIAL)" value="OFFICIAL" />
<el-option label="试用 (TRIAL)" value="TRIAL" />
<el-option label="续期 (RENEWAL)" value="RENEWAL" />
</el-select>
</el-form-item>
<el-form-item>
<el-button v-permission="'license:sn:rw'" type="primary" :loading="savingBind" @click="saveBind">保存绑定</el-button>
</el-form-item>
@@ -81,6 +91,7 @@ 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 axios from "axios";
import { apiErrorMessage } from "../utils/apiErrorMessage";
const auth = useAuthStore();
@@ -93,11 +104,13 @@ const patchingStatus = ref(false);
const sn = ref(null);
const projectsLoading = ref(false);
const projectOptions = ref([]);
const consoleUrl = ref('');
const bindForm = reactive({
projectId: undefined,
contractLineId: undefined,
activationRemark: "",
snTag: "OFFICIAL",
});
const statusPick = ref("");
@@ -135,6 +148,22 @@ function snStatusTagType(s) {
return "";
}
function openBitConsole() {
if (consoleUrl.value) window.open(consoleUrl.value, '_blank')
}
async function loadConsoleUrl() {
if (sn.value?.projectId) {
try {
const { data: envs } = await axios.get('/api/v1/integration/environments', { params: { projectId: sn.value.projectId } })
if (envs?.content?.length > 0) {
const env = envs.content[0]
consoleUrl.value = env.bitanswerBaseUrl || ''
}
} catch (e) { /* ignore */ }
}
}
function formatDateTime(v) {
if (v == null || v === "") return "—";
if (typeof v === "string") return v.replace("T", " ").slice(0, 19);
@@ -148,6 +177,7 @@ watch(
bindForm.projectId = row.projectId ?? undefined;
bindForm.contractLineId = row.contractLineId ?? undefined;
bindForm.activationRemark = row.activationRemark ?? "";
bindForm.snTag = row.snTag ?? "OFFICIAL";
statusPick.value = "";
},
{ immediate: true }
@@ -157,6 +187,7 @@ onMounted(async () => {
auth.restoreAxiosAuth();
await loadProjects();
await loadSn();
await loadConsoleUrl();
});
watch(
@@ -208,6 +239,7 @@ async function saveBind() {
projectId: bindForm.projectId ?? undefined,
contractLineId: bindForm.contractLineId ?? undefined,
activationRemark: bindForm.activationRemark?.trim() ?? "",
snTag: bindForm.snTag || undefined,
});
ElMessage.success("已保存");
await loadSn();
@@ -23,11 +23,13 @@
<el-button type="primary" :loading="loading" @click="load">查询</el-button>
<el-button type="success" v-permission="'license:sn:rw'" @click="goNew">新建许可 SN</el-button>
<el-button v-permission="'license:sn:rw'" @click="batchDialogVisible = true">批量导入</el-button>
<el-button v-permission="'license:sn:rw'" @click="openBatchOp">批量操作</el-button>
</div>
</div>
</template>
<el-table v-loading="loading" :data="rows" stripe style="width: 100%">
<el-table v-loading="loading" :data="rows" stripe style="width: 100%" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<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 }">
@@ -42,6 +44,11 @@
<el-tag :type="snStatusTagType(row.status)" size="small">{{ snStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="标签" width="110">
<template #default="{ row }">
<el-tag size="small">{{ row.snTag ?? "OFFICIAL" }}</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
</el-table-column>
@@ -83,6 +90,22 @@
<el-button type="primary" :loading="batchImporting" @click="handleBatchImport">导入</el-button>
</template>
</el-dialog>
<el-dialog v-model="batchOpDialogVisible" title="批量操作" width="480px">
<el-form label-width="100px">
<el-form-item label="目标状态">
<el-select v-model="batchTargetStatus" style="width:100%">
<el-option label="已发放(ISSUED)" value="ISSUED" />
<el-option label="已激活(ACTIVATED)" value="ACTIVATED" />
<el-option label="已暂停(SUSPENDED)" value="SUSPENDED" />
<el-option label="已吊销(REVOKED)" value="REVOKED" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="batchOpDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleBatchOp">执行</el-button>
</template>
</el-dialog>
</el-card>
</template>
@@ -91,7 +114,7 @@ import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useAuthStore } from "../stores/auth";
import { listLicenseSns, listProjects, batchImportLicenseSns } from "../api/platform";
import { listLicenseSns, listProjects, batchImportLicenseSns, patchLicenseSnStatus } from "../api/platform";
import { apiErrorMessage } from "../utils/apiErrorMessage";
const auth = useAuthStore();
@@ -112,6 +135,9 @@ const batchSnText = ref('');
const batchProjectId = ref(undefined);
const batchRemark = ref('');
const batchImporting = ref(false);
const multipleSelection = ref([]);
const batchOpDialogVisible = ref(false);
const batchTargetStatus = ref('');
onMounted(async () => {
auth.restoreAxiosAuth();
@@ -204,6 +230,20 @@ function goDetail(id) {
router.push({ name: "license-sn-detail", params: { id: String(id) } });
}
function handleSelectionChange(val) { multipleSelection.value = val }
function openBatchOp() {
if (multipleSelection.value.length === 0) { ElMessage.warning('请先选择SN'); return }
batchOpDialogVisible.value = true
}
async function handleBatchOp() {
for (const sn of multipleSelection.value) {
try { await patchLicenseSnStatus(sn.id, { status: batchTargetStatus.value }) } catch (e) { /* continue */ }
}
ElMessage.success(`已处理 ${multipleSelection.value.length}`)
batchOpDialogVisible.value = false
load()
}
async function handleBatchImport() {
const codes = batchSnText.value.split('\n').map(s => s.trim()).filter(Boolean)
if (codes.length === 0) { ElMessage.warning('请输入 SN 编码'); return }