11
This commit is contained in:
@@ -21,13 +21,11 @@ function agentRequest(token, path, options = {}) {
|
|||||||
* pageSize?: number,
|
* pageSize?: number,
|
||||||
* status?: string,
|
* status?: string,
|
||||||
* username?: string,
|
* username?: string,
|
||||||
* oemId?: number | null,
|
|
||||||
* agentId?: number | null,
|
|
||||||
* }} [params]
|
* }} [params]
|
||||||
*/
|
*/
|
||||||
export function listAgentCardKeysApi(
|
export function listAgentCardKeysApi(
|
||||||
token,
|
token,
|
||||||
{ page = 1, pageSize = 20, status = "all", username, oemId, agentId } = {},
|
{ page = 1, pageSize = 20, status = "all", username } = {},
|
||||||
) {
|
) {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
page: String(page),
|
page: String(page),
|
||||||
@@ -36,26 +34,13 @@ export function listAgentCardKeysApi(
|
|||||||
});
|
});
|
||||||
const trimmed = username?.trim();
|
const trimmed = username?.trim();
|
||||||
if (trimmed) query.set("username", trimmed);
|
if (trimmed) query.set("username", trimmed);
|
||||||
if (oemId != null) query.set("oem_id", String(oemId));
|
|
||||||
if (agentId != null) query.set("agent_id", String(agentId));
|
|
||||||
return agentRequest(token, `/agent/card-keys?${query}`);
|
return agentRequest(token, `/agent/card-keys?${query}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} token
|
|
||||||
* @param {object} payload
|
|
||||||
*/
|
|
||||||
export function createAgentCardKeysApi(token, payload) {
|
|
||||||
return agentRequest(token, "/agent/card-keys", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} token
|
* @param {string} token
|
||||||
* @param {number} cardId
|
* @param {number} cardId
|
||||||
* @param {object} payload
|
* @param {{ agent_remark?: string | null }} payload
|
||||||
*/
|
*/
|
||||||
export function updateAgentCardKeyApi(token, cardId, payload) {
|
export function updateAgentCardKeyApi(token, cardId, payload) {
|
||||||
return agentRequest(token, `/agent/card-keys/${cardId}`, {
|
return agentRequest(token, `/agent/card-keys/${cardId}`, {
|
||||||
@@ -63,10 +48,3 @@ export function updateAgentCardKeyApi(token, cardId, payload) {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string} token @param {number} cardId */
|
|
||||||
export function deleteAgentCardKeyApi(token, cardId) {
|
|
||||||
return agentRequest(token, `/agent/card-keys/${cardId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,14 +20,12 @@ function agentRequest(token, path, options = {}) {
|
|||||||
* page?: number,
|
* page?: number,
|
||||||
* pageSize?: number,
|
* pageSize?: number,
|
||||||
* username?: string,
|
* username?: string,
|
||||||
* oemId?: number | null,
|
|
||||||
* agentId?: number | null,
|
|
||||||
* roleId?: number | null,
|
* roleId?: number | null,
|
||||||
* }} [params]
|
* }} [params]
|
||||||
*/
|
*/
|
||||||
export function listAgentUsersApi(
|
export function listAgentUsersApi(
|
||||||
token,
|
token,
|
||||||
{ page = 1, pageSize = 20, username, oemId, agentId, roleId } = {},
|
{ page = 1, pageSize = 20, username, roleId } = {},
|
||||||
) {
|
) {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
page: String(page),
|
page: String(page),
|
||||||
@@ -35,38 +33,6 @@ export function listAgentUsersApi(
|
|||||||
});
|
});
|
||||||
const trimmed = username?.trim();
|
const trimmed = username?.trim();
|
||||||
if (trimmed) query.set("username", trimmed);
|
if (trimmed) query.set("username", trimmed);
|
||||||
if (oemId != null) query.set("oem_id", String(oemId));
|
|
||||||
if (agentId != null) query.set("agent_id", String(agentId));
|
|
||||||
if (roleId != null) query.set("role_id", String(roleId));
|
if (roleId != null) query.set("role_id", String(roleId));
|
||||||
return agentRequest(token, `/agent/users?${query}`);
|
return agentRequest(token, `/agent/users?${query}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} token
|
|
||||||
* @param {object} payload
|
|
||||||
*/
|
|
||||||
export function createAgentUserApi(token, payload) {
|
|
||||||
return agentRequest(token, "/agent/users", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} token
|
|
||||||
* @param {number} userId
|
|
||||||
* @param {object} payload
|
|
||||||
*/
|
|
||||||
export function updateAgentUserApi(token, userId, payload) {
|
|
||||||
return agentRequest(token, `/agent/users/${userId}`, {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} token @param {number} userId */
|
|
||||||
export function deleteAgentUserApi(token, userId) {
|
|
||||||
return agentRequest(token, `/agent/users/${userId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ function oemRequest(token, path, options = {}) {
|
|||||||
* pageSize?: number,
|
* pageSize?: number,
|
||||||
* status?: string,
|
* status?: string,
|
||||||
* username?: string,
|
* username?: string,
|
||||||
* oemId?: number | null,
|
|
||||||
* agentId?: number | null,
|
* agentId?: number | null,
|
||||||
* }} [params]
|
* }} [params]
|
||||||
*/
|
*/
|
||||||
export function listAdminCardKeysApi(
|
export function listOemCardKeysApi(
|
||||||
token,
|
token,
|
||||||
{ page = 1, pageSize = 20, status = "all", username, oemId, agentId } = {},
|
{ page = 1, pageSize = 20, status = "all", username, agentId } = {},
|
||||||
) {
|
) {
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
page: String(page),
|
page: String(page),
|
||||||
@@ -36,37 +35,29 @@ export function listAdminCardKeysApi(
|
|||||||
});
|
});
|
||||||
const trimmed = username?.trim();
|
const trimmed = username?.trim();
|
||||||
if (trimmed) query.set("username", trimmed);
|
if (trimmed) query.set("username", trimmed);
|
||||||
if (oemId != null) query.set("oem_id", String(oemId));
|
|
||||||
if (agentId != null) query.set("agent_id", String(agentId));
|
if (agentId != null) query.set("agent_id", String(agentId));
|
||||||
return oemRequest(token, `/oem/card-keys?${query}`);
|
return oemRequest(token, `/oem/card-keys?${query}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} token
|
|
||||||
* @param {object} payload
|
|
||||||
*/
|
|
||||||
export function createAdminCardKeysApi(token, payload) {
|
|
||||||
return oemRequest(token, "/oem/card-keys", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} token
|
* @param {string} token
|
||||||
* @param {number} cardId
|
* @param {number} cardId
|
||||||
* @param {object} payload
|
* @param {{ oem_remark?: string | null }} payload
|
||||||
*/
|
*/
|
||||||
export function updateAdminCardKeyApi(token, cardId, payload) {
|
export function updateOemCardKeyApi(token, cardId, payload) {
|
||||||
return oemRequest(token, `/oem/card-keys/${cardId}`, {
|
return oemRequest(token, `/oem/card-keys/${cardId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string} token @param {number} cardId */
|
/**
|
||||||
export function deleteAdminCardKeyApi(token, cardId) {
|
* @param {string} token
|
||||||
return oemRequest(token, `/oem/card-keys/${cardId}`, {
|
* @param {{ card_ids: number[], agent_id: number }} payload
|
||||||
method: "DELETE",
|
*/
|
||||||
|
export function assignOemCardKeysToAgentApi(token, payload) {
|
||||||
|
return oemRequest(token, "/oem/card-keys/assign-agent", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { onMounted, ref, watch } from "vue";
|
||||||
import { useAuthStore, ROLE_AGENT, ROLE_OEM } from "../../stores/auth.js";
|
import { useAuthStore } from "../../stores/auth.js";
|
||||||
import {
|
import { listAgentCardKeysApi, updateAgentCardKeyApi } from "../../api/agentCardKeys.js";
|
||||||
listAgentCardKeysApi,
|
|
||||||
createAgentCardKeysApi,
|
|
||||||
updateAgentCardKeyApi,
|
|
||||||
deleteAgentCardKeyApi,
|
|
||||||
} from "../../api/agentCardKeys.js";
|
|
||||||
import { listAgentUsersApi } from "../../api/agentUsers.js";
|
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|
||||||
@@ -17,67 +11,20 @@ const STATUS_OPTIONS = [
|
|||||||
{ label: "已激活", value: "used" },
|
{ label: "已激活", value: "used" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ASSIGN_OPTIONS = [
|
|
||||||
{ label: "不指定", value: "none" },
|
|
||||||
{ label: "分配给 OEM", value: "oem" },
|
|
||||||
{ label: "分配给代理", value: "agent" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const cards = ref([]);
|
const cards = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const statusFilter = ref("all");
|
const statusFilter = ref("all");
|
||||||
const oemOptions = ref([]);
|
const filters = ref({ username: "" });
|
||||||
const agentOptions = ref([]);
|
|
||||||
const filters = ref({
|
|
||||||
username: "",
|
|
||||||
oem_id: null,
|
|
||||||
agent_id: null,
|
|
||||||
});
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const feedback = ref({ severity: "", message: "" });
|
const feedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
const remarkDialogVisible = ref(false);
|
||||||
const dialogMode = ref("create");
|
|
||||||
const editingId = ref(null);
|
const editingId = ref(null);
|
||||||
|
const editingSerial = ref("");
|
||||||
const emptyForm = () => ({
|
const remarkForm = ref({ agent_remark: "" });
|
||||||
serial_number: "",
|
|
||||||
duration_days: 30,
|
|
||||||
count: 1,
|
|
||||||
remark: "",
|
|
||||||
assign_type: "none",
|
|
||||||
assign_id: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = ref(emptyForm());
|
|
||||||
|
|
||||||
const dialogTitle = computed(() =>
|
|
||||||
dialogMode.value === "create" ? "生成卡密" : "编辑卡密",
|
|
||||||
);
|
|
||||||
|
|
||||||
const isActivated = computed(() => dialogMode.value === "edit" && !!editingActivatedAt.value);
|
|
||||||
const editingActivatedAt = ref(null);
|
|
||||||
|
|
||||||
const ownerNameById = computed(() => {
|
|
||||||
const map = new Map();
|
|
||||||
for (const u of oemOptions.value) map.set(u.id, u.username);
|
|
||||||
for (const u of agentOptions.value) map.set(u.id, u.username);
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
const assignSelectOptions = computed(() => {
|
|
||||||
if (form.value.assign_type === "oem") return oemOptions.value;
|
|
||||||
if (form.value.assign_type === "agent") return agentOptions.value;
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
function ownerLabel(userId) {
|
|
||||||
if (userId == null) return "—";
|
|
||||||
return ownerNameById.value.get(userId) ?? `#${userId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
@@ -90,19 +37,6 @@ function showMessage(severity, message) {
|
|||||||
feedback.value = { severity, message };
|
feedback.value = { severity, message };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFilterOptions() {
|
|
||||||
const [oemRes, agentRes] = await Promise.all([
|
|
||||||
listAgentUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_OEM }),
|
|
||||||
listAgentUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_AGENT }),
|
|
||||||
]);
|
|
||||||
if (oemRes.ok && Array.isArray(oemRes.data?.items)) {
|
|
||||||
oemOptions.value = oemRes.data.items;
|
|
||||||
}
|
|
||||||
if (agentRes.ok && Array.isArray(agentRes.data?.items)) {
|
|
||||||
agentOptions.value = agentRes.data.items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCards() {
|
async function loadCards() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
@@ -111,8 +45,6 @@ async function loadCards() {
|
|||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
status: statusFilter.value,
|
status: statusFilter.value,
|
||||||
username: filters.value.username,
|
username: filters.value.username,
|
||||||
oemId: filters.value.oem_id,
|
|
||||||
agentId: filters.value.agent_id,
|
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
@@ -145,32 +77,36 @@ async function applyFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetFilters() {
|
async function resetFilters() {
|
||||||
filters.value = { username: "", oem_id: null, agent_id: null };
|
filters.value = { username: "" };
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
await loadCards();
|
await loadCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openRemarkDialog(row) {
|
||||||
dialogMode.value = "create";
|
editingId.value = row.id;
|
||||||
editingId.value = null;
|
editingSerial.value = row.serial_number;
|
||||||
editingActivatedAt.value = null;
|
remarkForm.value = { agent_remark: row.agent_remark || "" };
|
||||||
form.value = emptyForm();
|
remarkDialogVisible.value = true;
|
||||||
dialogVisible.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditDialog(row) {
|
async function saveRemark() {
|
||||||
dialogMode.value = "edit";
|
saving.value = true;
|
||||||
editingId.value = row.id;
|
feedback.value = { severity: "", message: "" };
|
||||||
editingActivatedAt.value = row.activated_at;
|
|
||||||
form.value = {
|
const res = await updateAgentCardKeyApi(auth.token, editingId.value, {
|
||||||
serial_number: row.serial_number,
|
agent_remark: remarkForm.value.agent_remark.trim() || null,
|
||||||
duration_days: row.duration_days,
|
});
|
||||||
count: 1,
|
|
||||||
remark: row.remark || "",
|
saving.value = false;
|
||||||
assign_type: "none",
|
|
||||||
assign_id: null,
|
if (!res.ok) {
|
||||||
};
|
showMessage("error", res.message || "保存失败");
|
||||||
dialogVisible.value = true;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remarkDialogVisible.value = false;
|
||||||
|
showMessage("success", res.message || "备注已更新");
|
||||||
|
await loadCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copySerial(serial) {
|
async function copySerial(serial) {
|
||||||
@@ -182,111 +118,15 @@ async function copySerial(serial) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAssignPayload() {
|
|
||||||
if (form.value.assign_type === "oem") {
|
|
||||||
return { oem_id: form.value.assign_id, agent_id: null };
|
|
||||||
}
|
|
||||||
if (form.value.assign_type === "agent") {
|
|
||||||
return { oem_id: null, agent_id: form.value.assign_id };
|
|
||||||
}
|
|
||||||
return { oem_id: null, agent_id: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveCard() {
|
|
||||||
if (form.value.duration_days < 1) {
|
|
||||||
showMessage("warn", "时长至少 1 天");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = true;
|
|
||||||
feedback.value = { severity: "", message: "" };
|
|
||||||
|
|
||||||
let res;
|
|
||||||
if (dialogMode.value === "create") {
|
|
||||||
if (form.value.assign_type !== "none" && form.value.assign_id == null) {
|
|
||||||
saving.value = false;
|
|
||||||
showMessage("warn", "请选择分配对象");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = {
|
|
||||||
duration_days: form.value.duration_days,
|
|
||||||
count: form.value.count,
|
|
||||||
remark: form.value.remark.trim() || null,
|
|
||||||
...buildAssignPayload(),
|
|
||||||
};
|
|
||||||
const serial = form.value.serial_number.trim();
|
|
||||||
if (serial) {
|
|
||||||
if (form.value.count !== 1) {
|
|
||||||
saving.value = false;
|
|
||||||
showMessage("warn", "指定序列号时数量只能为 1");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload.serial_number = serial.toUpperCase();
|
|
||||||
}
|
|
||||||
res = await createAgentCardKeysApi(auth.token, payload);
|
|
||||||
} else {
|
|
||||||
res = await updateAgentCardKeyApi(auth.token, editingId.value, {
|
|
||||||
duration_days: isActivated.value ? undefined : form.value.duration_days,
|
|
||||||
remark: form.value.remark.trim() || null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = false;
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
showMessage("error", res.message || "保存失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogVisible.value = false;
|
|
||||||
showMessage("success", res.message || "保存成功");
|
|
||||||
await loadCards();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeCard(row) {
|
|
||||||
if (row.activated_at) {
|
|
||||||
showMessage("warn", "已激活卡密不能删除");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ok = window.confirm(`确定删除卡密「${row.serial_number}」?`);
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
const res = await deleteAgentCardKeyApi(auth.token, row.id);
|
|
||||||
if (!res.ok) {
|
|
||||||
showMessage("error", res.message || "删除失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage("success", res.message || "已删除");
|
|
||||||
if (cards.value.length === 1 && page.value > 1) {
|
|
||||||
page.value -= 1;
|
|
||||||
}
|
|
||||||
await loadCards();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(statusFilter, onStatusChange);
|
watch(statusFilter, onStatusChange);
|
||||||
|
|
||||||
watch(
|
onMounted(loadCards);
|
||||||
() => form.value.assign_type,
|
|
||||||
() => {
|
|
||||||
form.value.assign_id = null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadFilterOptions();
|
|
||||||
await loadCards();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-page admin-card-keys">
|
<div class="admin-page admin-card-keys">
|
||||||
<div class="admin-users__header">
|
<div class="admin-users__header">
|
||||||
<div>
|
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
||||||
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Message
|
<Message
|
||||||
@@ -306,8 +146,6 @@ onMounted(async () => {
|
|||||||
class="admin-users__search"
|
class="admin-users__search"
|
||||||
@keyup.enter="applyFilters"
|
@keyup.enter="applyFilters"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<label class="text-sm text-text-muted">状态</label>
|
<label class="text-sm text-text-muted">状态</label>
|
||||||
<Select
|
<Select
|
||||||
v-model="statusFilter"
|
v-model="statusFilter"
|
||||||
@@ -352,16 +190,6 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
|
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
|
||||||
<Column header="OEM" style="width: 7rem">
|
|
||||||
<template #body="{ data }">
|
|
||||||
{{ ownerLabel(data.oem_id) }}
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
<Column header="代理" style="width: 7rem">
|
|
||||||
<template #body="{ data }">
|
|
||||||
{{ ownerLabel(data.agent_id) }}
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
<Column field="created_at" header="创建时间">
|
<Column field="created_at" header="创建时间">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatDateTime(data.created_at) }}
|
{{ formatDateTime(data.created_at) }}
|
||||||
@@ -382,97 +210,46 @@ onMounted(async () => {
|
|||||||
{{ data.username || "—" }}
|
{{ data.username || "—" }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="remark" header="备注">
|
<Column field="agent_remark" header="代理备注">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ data.remark || "—" }}
|
{{ data.agent_remark || "—" }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="操作" style="width: 6rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button
|
||||||
|
label="备注"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
@click="openRemarkDialog(data)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<p class="py-6 text-center text-sm text-text-muted">暂无卡密</p>
|
<p class="py-6 text-center text-sm text-text-muted">暂无卡密</p>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model:visible="dialogVisible"
|
v-model:visible="remarkDialogVisible"
|
||||||
:header="dialogTitle"
|
header="编辑代理备注"
|
||||||
modal
|
modal
|
||||||
:style="{ width: '28rem' }"
|
:style="{ width: '28rem' }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<template v-if="dialogMode === 'create'">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">时长(天)</label>
|
|
||||||
<InputNumber v-model="form.duration_days" :min="1" :max="3650" class="w-full" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">生成数量</label>
|
|
||||||
<InputNumber v-model="form.count" :min="1" :max="100" class="w-full" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">自定义序列号(可选,仅 1 张)</label>
|
|
||||||
<InputText
|
|
||||||
v-model="form.serial_number"
|
|
||||||
class="w-full font-mono uppercase"
|
|
||||||
placeholder="留空则自动生成"
|
|
||||||
:disabled="form.count > 1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">预分配</label>
|
|
||||||
<Select
|
|
||||||
v-model="form.assign_type"
|
|
||||||
:options="ASSIGN_OPTIONS"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="form.assign_type !== 'none'" class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">
|
|
||||||
{{ form.assign_type === "oem" ? "选择 OEM" : "选择代理" }}
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
v-model="form.assign_id"
|
|
||||||
:options="assignSelectOptions"
|
|
||||||
option-label="username"
|
|
||||||
option-value="id"
|
|
||||||
placeholder="请选择"
|
|
||||||
filter
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">序列号</label>
|
|
||||||
<InputText
|
|
||||||
:model-value="form.serial_number"
|
|
||||||
class="w-full font-mono"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">时长(天)</label>
|
|
||||||
<InputNumber
|
|
||||||
v-model="form.duration_days"
|
|
||||||
:min="1"
|
|
||||||
:max="3650"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="isActivated"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs text-text-muted uppercase">备注</label>
|
<label class="text-xs text-text-muted uppercase">序列号</label>
|
||||||
<Textarea v-model="form.remark" rows="2" class="w-full" />
|
<InputText :model-value="editingSerial" class="w-full font-mono" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-xs text-text-muted uppercase">代理备注</label>
|
||||||
|
<Textarea v-model="remarkForm.agent_remark" rows="3" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
<Button label="取消" severity="secondary" text @click="remarkDialogVisible = false" />
|
||||||
<Button label="保存" :loading="saving" @click="saveCard" />
|
<Button label="保存" :loading="saving" @click="saveRemark" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useAuthStore, ROLE_ADMIN, ROLE_AGENT,ROLE_OEM } from "../../stores/auth.js";
|
import { useAuthStore, ROLE_ADMIN, ROLE_AGENT, ROLE_OEM } from "../../stores/auth.js";
|
||||||
import {
|
import { listAgentUsersApi } from "../../api/agentUsers.js";
|
||||||
listAgentUsersApi,
|
|
||||||
createAgentUserApi,
|
|
||||||
updateAgentUserApi,
|
|
||||||
deleteAgentUserApi,
|
|
||||||
} from "../../api/agentUsers.js";
|
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|
||||||
const ROLE_OPTIONS = [
|
|
||||||
{ label: "普通用户", value: 0 },
|
|
||||||
{ label: "管理员", value: 1 },
|
|
||||||
{ label: "代理", value: 2 },
|
|
||||||
{ label: "OEM", value: 3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const ROLE_LABELS = {
|
const ROLE_LABELS = {
|
||||||
0: "普通用户",
|
0: "普通用户",
|
||||||
[ROLE_ADMIN]: "管理员",
|
[ROLE_ADMIN]: "管理员",
|
||||||
@@ -29,36 +17,9 @@ const total = ref(0);
|
|||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const oemOptions = ref([]);
|
const filters = ref({ username: "" });
|
||||||
const agentOptions = ref([]);
|
|
||||||
|
|
||||||
const filters = ref({
|
|
||||||
username: "",
|
|
||||||
oem_id: null,
|
|
||||||
agent_id: null,
|
|
||||||
});
|
|
||||||
const saving = ref(false);
|
|
||||||
const feedback = ref({ severity: "", message: "" });
|
const feedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const dialogMode = ref("create");
|
|
||||||
const editingId = ref(null);
|
|
||||||
|
|
||||||
const emptyForm = () => ({
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
phone: "",
|
|
||||||
role_id: 0,
|
|
||||||
vip_end_time: "",
|
|
||||||
clear_vip_end_time: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = ref(emptyForm());
|
|
||||||
|
|
||||||
const dialogTitle = computed(() =>
|
|
||||||
dialogMode.value === "create" ? "新建用户" : "编辑用户",
|
|
||||||
);
|
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value) {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
const d = new Date(value);
|
const d = new Date(value);
|
||||||
@@ -66,14 +27,6 @@ function formatDateTime(value) {
|
|||||||
return d.toLocaleString("zh-CN", { hour12: false });
|
return d.toLocaleString("zh-CN", { hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDatetimeLocalValue(iso) {
|
|
||||||
if (!iso) return "";
|
|
||||||
const d = new Date(iso);
|
|
||||||
if (Number.isNaN(d.getTime())) return "";
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function roleLabel(roleId) {
|
function roleLabel(roleId) {
|
||||||
return ROLE_LABELS[roleId] ?? `角色 ${roleId}`;
|
return ROLE_LABELS[roleId] ?? `角色 ${roleId}`;
|
||||||
}
|
}
|
||||||
@@ -85,35 +38,10 @@ function roleSeverity(roleId) {
|
|||||||
return "secondary";
|
return "secondary";
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownerNameById = computed(() => {
|
|
||||||
const map = new Map();
|
|
||||||
for (const u of oemOptions.value) map.set(u.id, u.username);
|
|
||||||
for (const u of agentOptions.value) map.set(u.id, u.username);
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
function ownerLabel(userId) {
|
|
||||||
if (userId == null) return "—";
|
|
||||||
return ownerNameById.value.get(userId) ?? `#${userId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessage(severity, message) {
|
function showMessage(severity, message) {
|
||||||
feedback.value = { severity, message };
|
feedback.value = { severity, message };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFilterOptions() {
|
|
||||||
const [oemRes, agentRes] = await Promise.all([
|
|
||||||
listAgentUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_OEM }),
|
|
||||||
listAgentUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_AGENT }),
|
|
||||||
]);
|
|
||||||
if (oemRes.ok && Array.isArray(oemRes.data?.items)) {
|
|
||||||
oemOptions.value = oemRes.data.items;
|
|
||||||
}
|
|
||||||
if (agentRes.ok && Array.isArray(agentRes.data?.items)) {
|
|
||||||
agentOptions.value = agentRes.data.items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
@@ -121,8 +49,6 @@ async function loadUsers() {
|
|||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
username: filters.value.username,
|
username: filters.value.username,
|
||||||
oemId: filters.value.oem_id,
|
|
||||||
agentId: filters.value.agent_id,
|
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
@@ -150,157 +76,18 @@ async function applyFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetFilters() {
|
async function resetFilters() {
|
||||||
filters.value = { username: "", oem_id: null, agent_id: null };
|
filters.value = { username: "" };
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog() {
|
onMounted(loadUsers);
|
||||||
dialogMode.value = "create";
|
|
||||||
editingId.value = null;
|
|
||||||
form.value = emptyForm();
|
|
||||||
dialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditDialog(row) {
|
|
||||||
dialogMode.value = "edit";
|
|
||||||
editingId.value = row.id;
|
|
||||||
form.value = {
|
|
||||||
username: row.username,
|
|
||||||
password: "",
|
|
||||||
phone: row.phone || "",
|
|
||||||
role_id: row.role_id,
|
|
||||||
vip_end_time: toDatetimeLocalValue(row.vip_end_time),
|
|
||||||
clear_vip_end_time: false,
|
|
||||||
};
|
|
||||||
dialogVisible.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPayload() {
|
|
||||||
const payload = {
|
|
||||||
username: form.value.username.trim(),
|
|
||||||
phone: form.value.phone.trim() || null,
|
|
||||||
role_id: form.value.role_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (form.value.password.trim()) {
|
|
||||||
payload.password = form.value.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (form.value.clear_vip_end_time) {
|
|
||||||
payload.clear_vip_end_time = true;
|
|
||||||
} else if (form.value.vip_end_time) {
|
|
||||||
payload.vip_end_time = new Date(form.value.vip_end_time).toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUser() {
|
|
||||||
if (!form.value.username.trim()) {
|
|
||||||
showMessage("warn", "用户名不能为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dialogMode.value === "create" && form.value.password.length < 6) {
|
|
||||||
showMessage("warn", "密码至少 6 位");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = true;
|
|
||||||
feedback.value = { severity: "", message: "" };
|
|
||||||
|
|
||||||
const payload = buildPayload();
|
|
||||||
let res;
|
|
||||||
|
|
||||||
if (dialogMode.value === "create") {
|
|
||||||
if (!payload.password) {
|
|
||||||
saving.value = false;
|
|
||||||
showMessage("warn", "请设置初始密码");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res = await createAgentUserApi(auth.token, payload);
|
|
||||||
} else {
|
|
||||||
res = await updateAgentUserApi(auth.token, editingId.value, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = false;
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
showMessage("error", res.message || "保存失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogVisible.value = false;
|
|
||||||
showMessage("success", res.message || "保存成功");
|
|
||||||
await loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeUser(row) {
|
|
||||||
if (row.id === auth.currentUser?.id) {
|
|
||||||
showMessage("warn", "不能删除当前登录账号");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ok = window.confirm(`确定删除用户「${row.username}」?此操作不可恢复。`);
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
const res = await deleteAgentUserApi(auth.token, row.id);
|
|
||||||
if (!res.ok) {
|
|
||||||
showMessage("error", res.message || "删除失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage("success", res.message || "已删除");
|
|
||||||
if (users.value.length === 1 && page.value > 1) {
|
|
||||||
page.value -= 1;
|
|
||||||
}
|
|
||||||
await loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadFilterOptions();
|
|
||||||
await loadUsers();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="admin-page admin-users">
|
<div class="admin-page admin-users">
|
||||||
<div class="admin-users__header">
|
<div class="admin-users__header">
|
||||||
<div>
|
<h1 class="admin-page__title gradient-text">用户管理</h1>
|
||||||
<h1 class="admin-page__title gradient-text">用户管理</h1>
|
|
||||||
</div>
|
|
||||||
<Button label="新建用户" @click="openCreateDialog" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-users__filters">
|
|
||||||
<InputText
|
|
||||||
v-model="filters.username"
|
|
||||||
placeholder="搜索用户名"
|
|
||||||
class="admin-users__search"
|
|
||||||
@keyup.enter="applyFilters"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
v-model="filters.oem_id"
|
|
||||||
:options="oemOptions"
|
|
||||||
option-label="username"
|
|
||||||
option-value="id"
|
|
||||||
placeholder="全部 OEM"
|
|
||||||
show-clear
|
|
||||||
filter
|
|
||||||
class="admin-users__filter-select"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
v-model="filters.agent_id"
|
|
||||||
:options="agentOptions"
|
|
||||||
option-label="username"
|
|
||||||
option-value="id"
|
|
||||||
placeholder="全部代理"
|
|
||||||
show-clear
|
|
||||||
filter
|
|
||||||
class="admin-users__filter-select"
|
|
||||||
/>
|
|
||||||
<Button label="搜索" @click="applyFilters" />
|
|
||||||
<Button label="重置" severity="secondary" text @click="resetFilters" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Message
|
<Message
|
||||||
@@ -313,6 +100,17 @@ onMounted(async () => {
|
|||||||
{{ feedback.message }}
|
{{ feedback.message }}
|
||||||
</Message>
|
</Message>
|
||||||
|
|
||||||
|
<div class="admin-users__filters">
|
||||||
|
<InputText
|
||||||
|
v-model="filters.username"
|
||||||
|
placeholder="搜索用户名"
|
||||||
|
class="admin-users__search"
|
||||||
|
@keyup.enter="applyFilters"
|
||||||
|
/>
|
||||||
|
<Button label="搜索" @click="applyFilters" />
|
||||||
|
<Button label="重置" severity="secondary" text @click="resetFilters" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
:value="users"
|
:value="users"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@@ -342,16 +140,6 @@ onMounted(async () => {
|
|||||||
<Tag :value="roleLabel(data.role_id)" :severity="roleSeverity(data.role_id)" />
|
<Tag :value="roleLabel(data.role_id)" :severity="roleSeverity(data.role_id)" />
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="oem_id" header="OEM" style="width: 8rem">
|
|
||||||
<template #body="{ data }">
|
|
||||||
{{ ownerLabel(data.oem_id) }}
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
<Column field="agent_id" header="代理" style="width: 8rem">
|
|
||||||
<template #body="{ data }">
|
|
||||||
{{ ownerLabel(data.agent_id) }}
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
<Column field="vip_end_time" header="VIP 到期">
|
<Column field="vip_end_time" header="VIP 到期">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatDateTime(data.vip_end_time) }}
|
{{ formatDateTime(data.vip_end_time) }}
|
||||||
@@ -362,94 +150,9 @@ onMounted(async () => {
|
|||||||
{{ formatDateTime(data.created_at) }}
|
{{ formatDateTime(data.created_at) }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="操作" style="width: 9rem">
|
|
||||||
<template #body="{ data }">
|
|
||||||
<div class="admin-users__actions">
|
|
||||||
<Button
|
|
||||||
label="编辑"
|
|
||||||
size="small"
|
|
||||||
severity="secondary"
|
|
||||||
text
|
|
||||||
@click="openEditDialog(data)"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="删除"
|
|
||||||
size="small"
|
|
||||||
severity="danger"
|
|
||||||
text
|
|
||||||
:disabled="data.id === auth.currentUser?.id"
|
|
||||||
@click="removeUser(data)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Column>
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<p class="py-6 text-center text-sm text-text-muted">暂无用户</p>
|
<p class="py-6 text-center text-sm text-text-muted">暂无用户</p>
|
||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Dialog
|
|
||||||
v-model:visible="dialogVisible"
|
|
||||||
:header="dialogTitle"
|
|
||||||
modal
|
|
||||||
class="admin-users-dialog"
|
|
||||||
:style="{ width: '28rem' }"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">用户名</label>
|
|
||||||
<InputText v-model="form.username" class="w-full" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">
|
|
||||||
{{ dialogMode === "create" ? "密码" : "新密码(留空不修改)" }}
|
|
||||||
</label>
|
|
||||||
<Password
|
|
||||||
v-model="form.password"
|
|
||||||
:feedback="false"
|
|
||||||
toggle-mask
|
|
||||||
fluid
|
|
||||||
input-class="w-full"
|
|
||||||
:placeholder="dialogMode === 'create' ? '至少 6 位' : '留空则不修改'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">手机号(可选)</label>
|
|
||||||
<InputText v-model="form.phone" class="w-full" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">角色</label>
|
|
||||||
<Select
|
|
||||||
v-model="form.role_id"
|
|
||||||
:options="ROLE_OPTIONS"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">VIP 到期时间</label>
|
|
||||||
<InputText
|
|
||||||
v-model="form.vip_end_time"
|
|
||||||
type="datetime-local"
|
|
||||||
class="w-full"
|
|
||||||
:disabled="form.clear_vip_end_time"
|
|
||||||
/>
|
|
||||||
<label v-if="dialogMode === 'edit'" class="flex items-center gap-2 text-sm text-text-muted">
|
|
||||||
<Checkbox v-model="form.clear_vip_end_time" :binary="true" input-id="clear-vip" />
|
|
||||||
<span>清除 VIP 到期时间</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
|
||||||
<Button label="保存" :loading="saving" @click="saveUser" />
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { useAuthStore, ROLE_AGENT, ROLE_OEM } from "../../stores/auth.js";
|
import { useAuthStore, ROLE_AGENT } from "../../stores/auth.js";
|
||||||
import {
|
import {
|
||||||
listAdminCardKeysApi,
|
listOemCardKeysApi,
|
||||||
createAdminCardKeysApi,
|
updateOemCardKeyApi,
|
||||||
updateAdminCardKeyApi,
|
assignOemCardKeysToAgentApi,
|
||||||
deleteAdminCardKeyApi,
|
|
||||||
} from "../../api/oemCardKeys.js";
|
} from "../../api/oemCardKeys.js";
|
||||||
import { listOemUsersApi } from "../../api/oemUsers.js";
|
import { listOemUsersApi } from "../../api/oemUsers.js";
|
||||||
|
|
||||||
@@ -17,62 +16,41 @@ const STATUS_OPTIONS = [
|
|||||||
{ label: "已激活", value: "used" },
|
{ label: "已激活", value: "used" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ASSIGN_OPTIONS = [
|
|
||||||
{ label: "不指定", value: "none" },
|
|
||||||
{ label: "分配给 OEM", value: "oem" },
|
|
||||||
{ label: "分配给代理", value: "agent" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const cards = ref([]);
|
const cards = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const statusFilter = ref("all");
|
const statusFilter = ref("all");
|
||||||
const oemOptions = ref([]);
|
|
||||||
const agentOptions = ref([]);
|
const agentOptions = ref([]);
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
username: "",
|
username: "",
|
||||||
oem_id: null,
|
|
||||||
agent_id: null,
|
agent_id: null,
|
||||||
});
|
});
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
const assigning = ref(false);
|
||||||
const feedback = ref({ severity: "", message: "" });
|
const feedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
const dialogVisible = ref(false);
|
const selectedCards = ref([]);
|
||||||
const dialogMode = ref("create");
|
const assignDialogVisible = ref(false);
|
||||||
|
const assignAgentId = ref(null);
|
||||||
|
|
||||||
|
const remarkDialogVisible = ref(false);
|
||||||
const editingId = ref(null);
|
const editingId = ref(null);
|
||||||
|
const editingSerial = ref("");
|
||||||
const emptyForm = () => ({
|
const remarkForm = ref({ oem_remark: "" });
|
||||||
serial_number: "",
|
|
||||||
duration_days: 30,
|
|
||||||
count: 1,
|
|
||||||
remark: "",
|
|
||||||
assign_type: "none",
|
|
||||||
assign_id: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = ref(emptyForm());
|
|
||||||
|
|
||||||
const dialogTitle = computed(() =>
|
|
||||||
dialogMode.value === "create" ? "生成卡密" : "编辑卡密",
|
|
||||||
);
|
|
||||||
|
|
||||||
const isActivated = computed(() => dialogMode.value === "edit" && !!editingActivatedAt.value);
|
|
||||||
const editingActivatedAt = ref(null);
|
|
||||||
|
|
||||||
const ownerNameById = computed(() => {
|
const ownerNameById = computed(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
for (const u of oemOptions.value) map.set(u.id, u.username);
|
|
||||||
for (const u of agentOptions.value) map.set(u.id, u.username);
|
for (const u of agentOptions.value) map.set(u.id, u.username);
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
const assignSelectOptions = computed(() => {
|
const selectedUnusedCards = computed(() =>
|
||||||
if (form.value.assign_type === "oem") return oemOptions.value;
|
selectedCards.value.filter((row) => !row.activated_at),
|
||||||
if (form.value.assign_type === "agent") return agentOptions.value;
|
);
|
||||||
return [];
|
|
||||||
});
|
const canBatchAssign = computed(() => selectedUnusedCards.value.length > 0);
|
||||||
|
|
||||||
function ownerLabel(userId) {
|
function ownerLabel(userId) {
|
||||||
if (userId == null) return "—";
|
if (userId == null) return "—";
|
||||||
@@ -86,30 +64,33 @@ function formatDateTime(value) {
|
|||||||
return d.toLocaleString("zh-CN", { hour12: false });
|
return d.toLocaleString("zh-CN", { hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRowSelectable(row) {
|
||||||
|
return !row.activated_at;
|
||||||
|
}
|
||||||
|
|
||||||
function showMessage(severity, message) {
|
function showMessage(severity, message) {
|
||||||
feedback.value = { severity, message };
|
feedback.value = { severity, message };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFilterOptions() {
|
async function loadFilterOptions() {
|
||||||
const [ agentRes] = await Promise.all([
|
const res = await listOemUsersApi(auth.token, {
|
||||||
|
page: 1,
|
||||||
listOemUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_AGENT }),
|
pageSize: 200,
|
||||||
]);
|
roleId: ROLE_AGENT,
|
||||||
|
});
|
||||||
if (agentRes.ok && Array.isArray(agentRes.data?.items)) {
|
if (res.ok && Array.isArray(res.data?.items)) {
|
||||||
agentOptions.value = agentRes.data.items;
|
agentOptions.value = res.data.items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCards() {
|
async function loadCards() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
const res = await listAdminCardKeysApi(auth.token, {
|
const res = await listOemCardKeysApi(auth.token, {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
status: statusFilter.value,
|
status: statusFilter.value,
|
||||||
username: filters.value.username,
|
username: filters.value.username,
|
||||||
oemId: filters.value.oem_id,
|
|
||||||
agentId: filters.value.agent_id,
|
agentId: filters.value.agent_id,
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -124,6 +105,7 @@ async function loadCards() {
|
|||||||
total.value = typeof data?.total === "number" ? data.total : 0;
|
total.value = typeof data?.total === "number" ? data.total : 0;
|
||||||
if (typeof data?.page === "number") page.value = data.page;
|
if (typeof data?.page === "number") page.value = data.page;
|
||||||
if (typeof data?.page_size === "number") pageSize.value = data.page_size;
|
if (typeof data?.page_size === "number") pageSize.value = data.page_size;
|
||||||
|
selectedCards.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPage(event) {
|
async function onPage(event) {
|
||||||
@@ -143,32 +125,71 @@ async function applyFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetFilters() {
|
async function resetFilters() {
|
||||||
filters.value = { username: "", oem_id: null, agent_id: null };
|
filters.value = { username: "", agent_id: null };
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
await loadCards();
|
await loadCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openRemarkDialog(row) {
|
||||||
dialogMode.value = "create";
|
editingId.value = row.id;
|
||||||
editingId.value = null;
|
editingSerial.value = row.serial_number;
|
||||||
editingActivatedAt.value = null;
|
remarkForm.value = { oem_remark: row.oem_remark || "" };
|
||||||
form.value = emptyForm();
|
remarkDialogVisible.value = true;
|
||||||
dialogVisible.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditDialog(row) {
|
function openAssignDialog() {
|
||||||
dialogMode.value = "edit";
|
if (!canBatchAssign.value) {
|
||||||
editingId.value = row.id;
|
showMessage("warn", "请勾选未使用的卡密");
|
||||||
editingActivatedAt.value = row.activated_at;
|
return;
|
||||||
form.value = {
|
}
|
||||||
serial_number: row.serial_number,
|
assignAgentId.value = null;
|
||||||
duration_days: row.duration_days,
|
assignDialogVisible.value = true;
|
||||||
count: 1,
|
}
|
||||||
remark: row.remark || "",
|
|
||||||
assign_type: "none",
|
async function saveRemark() {
|
||||||
assign_id: null,
|
saving.value = true;
|
||||||
};
|
feedback.value = { severity: "", message: "" };
|
||||||
dialogVisible.value = true;
|
|
||||||
|
const res = await updateOemCardKeyApi(auth.token, editingId.value, {
|
||||||
|
oem_remark: remarkForm.value.oem_remark.trim() || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
saving.value = false;
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
showMessage("error", res.message || "保存失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remarkDialogVisible.value = false;
|
||||||
|
showMessage("success", res.message || "备注已更新");
|
||||||
|
await loadCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmAssign() {
|
||||||
|
if (assignAgentId.value == null) {
|
||||||
|
showMessage("warn", "请选择代理");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assigning.value = true;
|
||||||
|
feedback.value = { severity: "", message: "" };
|
||||||
|
|
||||||
|
const res = await assignOemCardKeysToAgentApi(auth.token, {
|
||||||
|
card_ids: selectedUnusedCards.value.map((c) => c.id),
|
||||||
|
agent_id: assignAgentId.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
assigning.value = false;
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
showMessage("error", res.message || "分配失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assignDialogVisible.value = false;
|
||||||
|
showMessage("success", res.message || "分配成功");
|
||||||
|
await loadCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copySerial(serial) {
|
async function copySerial(serial) {
|
||||||
@@ -180,98 +201,8 @@ async function copySerial(serial) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAssignPayload() {
|
|
||||||
if (form.value.assign_type === "oem") {
|
|
||||||
return { oem_id: form.value.assign_id, agent_id: null };
|
|
||||||
}
|
|
||||||
if (form.value.assign_type === "agent") {
|
|
||||||
return { oem_id: null, agent_id: form.value.assign_id };
|
|
||||||
}
|
|
||||||
return { oem_id: null, agent_id: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveCard() {
|
|
||||||
if (form.value.duration_days < 1) {
|
|
||||||
showMessage("warn", "时长至少 1 天");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = true;
|
|
||||||
feedback.value = { severity: "", message: "" };
|
|
||||||
|
|
||||||
let res;
|
|
||||||
if (dialogMode.value === "create") {
|
|
||||||
if (form.value.assign_type !== "none" && form.value.assign_id == null) {
|
|
||||||
saving.value = false;
|
|
||||||
showMessage("warn", "请选择分配对象");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = {
|
|
||||||
duration_days: form.value.duration_days,
|
|
||||||
count: form.value.count,
|
|
||||||
remark: form.value.remark.trim() || null,
|
|
||||||
...buildAssignPayload(),
|
|
||||||
};
|
|
||||||
const serial = form.value.serial_number.trim();
|
|
||||||
if (serial) {
|
|
||||||
if (form.value.count !== 1) {
|
|
||||||
saving.value = false;
|
|
||||||
showMessage("warn", "指定序列号时数量只能为 1");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload.serial_number = serial.toUpperCase();
|
|
||||||
}
|
|
||||||
res = await createAdminCardKeysApi(auth.token, payload);
|
|
||||||
} else {
|
|
||||||
res = await updateAdminCardKeyApi(auth.token, editingId.value, {
|
|
||||||
duration_days: isActivated.value ? undefined : form.value.duration_days,
|
|
||||||
remark: form.value.remark.trim() || null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saving.value = false;
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
showMessage("error", res.message || "保存失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialogVisible.value = false;
|
|
||||||
showMessage("success", res.message || "保存成功");
|
|
||||||
await loadCards();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeCard(row) {
|
|
||||||
if (row.activated_at) {
|
|
||||||
showMessage("warn", "已激活卡密不能删除");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ok = window.confirm(`确定删除卡密「${row.serial_number}」?`);
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
const res = await deleteAdminCardKeyApi(auth.token, row.id);
|
|
||||||
if (!res.ok) {
|
|
||||||
showMessage("error", res.message || "删除失败");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showMessage("success", res.message || "已删除");
|
|
||||||
if (cards.value.length === 1 && page.value > 1) {
|
|
||||||
page.value -= 1;
|
|
||||||
}
|
|
||||||
await loadCards();
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(statusFilter, onStatusChange);
|
watch(statusFilter, onStatusChange);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => form.value.assign_type,
|
|
||||||
() => {
|
|
||||||
form.value.assign_id = null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadFilterOptions();
|
await loadFilterOptions();
|
||||||
await loadCards();
|
await loadCards();
|
||||||
@@ -284,7 +215,11 @@ onMounted(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button label="生成卡密" @click="openCreateDialog" />
|
<Button
|
||||||
|
label="分配给代理"
|
||||||
|
:disabled="!canBatchAssign"
|
||||||
|
@click="openAssignDialog"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Message
|
<Message
|
||||||
@@ -304,7 +239,6 @@ onMounted(async () => {
|
|||||||
class="admin-users__search"
|
class="admin-users__search"
|
||||||
@keyup.enter="applyFilters"
|
@keyup.enter="applyFilters"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
v-model="filters.agent_id"
|
v-model="filters.agent_id"
|
||||||
:options="agentOptions"
|
:options="agentOptions"
|
||||||
@@ -328,6 +262,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
v-model:selection="selectedCards"
|
||||||
:value="cards"
|
:value="cards"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
lazy
|
lazy
|
||||||
@@ -342,8 +277,10 @@ onMounted(async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
class="admin-users__table"
|
class="admin-users__table"
|
||||||
data-key="id"
|
data-key="id"
|
||||||
|
:selectable="isRowSelectable"
|
||||||
@page="onPage"
|
@page="onPage"
|
||||||
>
|
>
|
||||||
|
<Column selection-mode="multiple" header-style="width: 3rem" />
|
||||||
<Column field="serial_number" header="序列号" style="min-width: 11rem">
|
<Column field="serial_number" header="序列号" style="min-width: 11rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="admin-card-keys__serial">
|
<div class="admin-card-keys__serial">
|
||||||
@@ -359,7 +296,6 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
|
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
|
||||||
|
|
||||||
<Column header="代理" style="width: 7rem">
|
<Column header="代理" style="width: 7rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ ownerLabel(data.agent_id) }}
|
{{ ownerLabel(data.agent_id) }}
|
||||||
@@ -385,30 +321,20 @@ onMounted(async () => {
|
|||||||
{{ data.username || "—" }}
|
{{ data.username || "—" }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="remark" header="备注">
|
<Column field="oem_remark" header="OEM 备注">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ data.oem_remark || "—" }}
|
{{ data.oem_remark || "—" }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="操作" style="width: 9rem">
|
<Column header="操作" style="width: 6rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="admin-users__actions">
|
<Button
|
||||||
<Button
|
label="备注"
|
||||||
label="编辑"
|
size="small"
|
||||||
size="small"
|
severity="secondary"
|
||||||
severity="secondary"
|
text
|
||||||
text
|
@click="openRemarkDialog(data)"
|
||||||
@click="openEditDialog(data)"
|
/>
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="删除"
|
|
||||||
size="small"
|
|
||||||
severity="danger"
|
|
||||||
text
|
|
||||||
:disabled="!!data.activated_at"
|
|
||||||
@click="removeCard(data)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
@@ -417,73 +343,51 @@ onMounted(async () => {
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model:visible="dialogVisible"
|
v-model:visible="remarkDialogVisible"
|
||||||
:header="dialogTitle"
|
header="编辑 OEM 备注"
|
||||||
modal
|
modal
|
||||||
:style="{ width: '28rem' }"
|
:style="{ width: '28rem' }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<template v-if="dialogMode === 'create'">
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">生成数量</label>
|
|
||||||
<InputNumber v-model="form.count" :min="1" :max="100" class="w-full" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">自定义序列号(可选,仅 1 张)</label>
|
|
||||||
<InputText
|
|
||||||
v-model="form.serial_number"
|
|
||||||
class="w-full font-mono uppercase"
|
|
||||||
placeholder="留空则自动生成"
|
|
||||||
:disabled="form.count > 1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">预分配</label>
|
|
||||||
<Select
|
|
||||||
v-model="form.assign_type"
|
|
||||||
:options="ASSIGN_OPTIONS"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="form.assign_type !== 'none'" class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">
|
|
||||||
{{ form.assign_type === "oem" ? "选择 OEM" : "选择代理" }}
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
v-model="form.assign_id"
|
|
||||||
:options="assignSelectOptions"
|
|
||||||
option-label="username"
|
|
||||||
option-value="id"
|
|
||||||
placeholder="请选择"
|
|
||||||
filter
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<label class="text-xs text-text-muted uppercase">序列号</label>
|
|
||||||
<InputText
|
|
||||||
:model-value="form.serial_number"
|
|
||||||
class="w-full font-mono"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs text-text-muted uppercase">备注</label>
|
<label class="text-xs text-text-muted uppercase">序列号</label>
|
||||||
<Textarea v-model="form.remark" rows="2" class="w-full" />
|
<InputText :model-value="editingSerial" class="w-full font-mono" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-xs text-text-muted uppercase">OEM 备注</label>
|
||||||
|
<Textarea v-model="remarkForm.oem_remark" rows="3" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
<Button label="取消" severity="secondary" text @click="remarkDialogVisible = false" />
|
||||||
<Button label="保存" :loading="saving" @click="saveCard" />
|
<Button label="保存" :loading="saving" @click="saveRemark" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="assignDialogVisible"
|
||||||
|
header="分配给代理"
|
||||||
|
modal
|
||||||
|
:style="{ width: '24rem' }"
|
||||||
|
>
|
||||||
|
<p class="mb-4 text-sm text-text-muted">
|
||||||
|
已选择 {{ selectedUnusedCards.length }} 张未使用卡密
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-xs text-text-muted uppercase">代理</label>
|
||||||
|
<Select
|
||||||
|
v-model="assignAgentId"
|
||||||
|
:options="agentOptions"
|
||||||
|
option-label="username"
|
||||||
|
option-value="id"
|
||||||
|
placeholder="请选择代理"
|
||||||
|
filter
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" severity="secondary" text @click="assignDialogVisible = false" />
|
||||||
|
<Button label="确认分配" :loading="assigning" @click="confirmAssign" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user