11
This commit is contained in:
@@ -21,13 +21,11 @@ function agentRequest(token, path, options = {}) {
|
||||
* pageSize?: number,
|
||||
* status?: string,
|
||||
* username?: string,
|
||||
* oemId?: number | null,
|
||||
* agentId?: number | null,
|
||||
* }} [params]
|
||||
*/
|
||||
export function listAgentCardKeysApi(
|
||||
token,
|
||||
{ page = 1, pageSize = 20, status = "all", username, oemId, agentId } = {},
|
||||
{ page = 1, pageSize = 20, status = "all", username } = {},
|
||||
) {
|
||||
const query = new URLSearchParams({
|
||||
page: String(page),
|
||||
@@ -36,26 +34,13 @@ export function listAgentCardKeysApi(
|
||||
});
|
||||
const trimmed = username?.trim();
|
||||
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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {number} cardId
|
||||
* @param {object} payload
|
||||
* @param {{ agent_remark?: string | null }} payload
|
||||
*/
|
||||
export function updateAgentCardKeyApi(token, cardId, payload) {
|
||||
return agentRequest(token, `/agent/card-keys/${cardId}`, {
|
||||
@@ -63,10 +48,3 @@ export function updateAgentCardKeyApi(token, cardId, 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,
|
||||
* pageSize?: number,
|
||||
* username?: string,
|
||||
* oemId?: number | null,
|
||||
* agentId?: number | null,
|
||||
* roleId?: number | null,
|
||||
* }} [params]
|
||||
*/
|
||||
export function listAgentUsersApi(
|
||||
token,
|
||||
{ page = 1, pageSize = 20, username, oemId, agentId, roleId } = {},
|
||||
{ page = 1, pageSize = 20, username, roleId } = {},
|
||||
) {
|
||||
const query = new URLSearchParams({
|
||||
page: String(page),
|
||||
@@ -35,38 +33,6 @@ export function listAgentUsersApi(
|
||||
});
|
||||
const trimmed = username?.trim();
|
||||
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));
|
||||
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,
|
||||
* status?: string,
|
||||
* username?: string,
|
||||
* oemId?: number | null,
|
||||
* agentId?: number | null,
|
||||
* }} [params]
|
||||
*/
|
||||
export function listAdminCardKeysApi(
|
||||
export function listOemCardKeysApi(
|
||||
token,
|
||||
{ page = 1, pageSize = 20, status = "all", username, oemId, agentId } = {},
|
||||
{ page = 1, pageSize = 20, status = "all", username, agentId } = {},
|
||||
) {
|
||||
const query = new URLSearchParams({
|
||||
page: String(page),
|
||||
@@ -36,37 +35,29 @@ export function listAdminCardKeysApi(
|
||||
});
|
||||
const trimmed = username?.trim();
|
||||
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 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 {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}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} token @param {number} cardId */
|
||||
export function deleteAdminCardKeyApi(token, cardId) {
|
||||
return oemRequest(token, `/oem/card-keys/${cardId}`, {
|
||||
method: "DELETE",
|
||||
/**
|
||||
* @param {string} token
|
||||
* @param {{ card_ids: number[], agent_id: number }} payload
|
||||
*/
|
||||
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>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useAuthStore, ROLE_AGENT, ROLE_OEM } from "../../stores/auth.js";
|
||||
import {
|
||||
listAgentCardKeysApi,
|
||||
createAgentCardKeysApi,
|
||||
updateAgentCardKeyApi,
|
||||
deleteAgentCardKeyApi,
|
||||
} from "../../api/agentCardKeys.js";
|
||||
import { listAgentUsersApi } from "../../api/agentUsers.js";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { useAuthStore } from "../../stores/auth.js";
|
||||
import { listAgentCardKeysApi, updateAgentCardKeyApi } from "../../api/agentCardKeys.js";
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
@@ -17,67 +11,20 @@ const STATUS_OPTIONS = [
|
||||
{ label: "已激活", value: "used" },
|
||||
];
|
||||
|
||||
const ASSIGN_OPTIONS = [
|
||||
{ label: "不指定", value: "none" },
|
||||
{ label: "分配给 OEM", value: "oem" },
|
||||
{ label: "分配给代理", value: "agent" },
|
||||
];
|
||||
|
||||
const cards = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const statusFilter = ref("all");
|
||||
const oemOptions = ref([]);
|
||||
const agentOptions = ref([]);
|
||||
const filters = ref({
|
||||
username: "",
|
||||
oem_id: null,
|
||||
agent_id: null,
|
||||
});
|
||||
const filters = ref({ username: "" });
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogMode = ref("create");
|
||||
const remarkDialogVisible = ref(false);
|
||||
const editingId = ref(null);
|
||||
|
||||
const emptyForm = () => ({
|
||||
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}`;
|
||||
}
|
||||
const editingSerial = ref("");
|
||||
const remarkForm = ref({ agent_remark: "" });
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "—";
|
||||
@@ -90,19 +37,6 @@ function showMessage(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() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
@@ -111,8 +45,6 @@ async function loadCards() {
|
||||
pageSize: pageSize.value,
|
||||
status: statusFilter.value,
|
||||
username: filters.value.username,
|
||||
oemId: filters.value.oem_id,
|
||||
agentId: filters.value.agent_id,
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
@@ -145,32 +77,36 @@ async function applyFilters() {
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
filters.value = { username: "", oem_id: null, agent_id: null };
|
||||
filters.value = { username: "" };
|
||||
page.value = 1;
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
dialogMode.value = "create";
|
||||
editingId.value = null;
|
||||
editingActivatedAt.value = null;
|
||||
form.value = emptyForm();
|
||||
dialogVisible.value = true;
|
||||
function openRemarkDialog(row) {
|
||||
editingId.value = row.id;
|
||||
editingSerial.value = row.serial_number;
|
||||
remarkForm.value = { agent_remark: row.agent_remark || "" };
|
||||
remarkDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row) {
|
||||
dialogMode.value = "edit";
|
||||
editingId.value = row.id;
|
||||
editingActivatedAt.value = row.activated_at;
|
||||
form.value = {
|
||||
serial_number: row.serial_number,
|
||||
duration_days: row.duration_days,
|
||||
count: 1,
|
||||
remark: row.remark || "",
|
||||
assign_type: "none",
|
||||
assign_id: null,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
async function saveRemark() {
|
||||
saving.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
|
||||
const res = await updateAgentCardKeyApi(auth.token, editingId.value, {
|
||||
agent_remark: remarkForm.value.agent_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 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(
|
||||
() => form.value.assign_type,
|
||||
() => {
|
||||
form.value.assign_id = null;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFilterOptions();
|
||||
await loadCards();
|
||||
});
|
||||
onMounted(loadCards);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page admin-card-keys">
|
||||
<div class="admin-users__header">
|
||||
<div>
|
||||
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
||||
</div>
|
||||
|
||||
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
||||
</div>
|
||||
|
||||
<Message
|
||||
@@ -306,8 +146,6 @@ onMounted(async () => {
|
||||
class="admin-users__search"
|
||||
@keyup.enter="applyFilters"
|
||||
/>
|
||||
|
||||
|
||||
<label class="text-sm text-text-muted">状态</label>
|
||||
<Select
|
||||
v-model="statusFilter"
|
||||
@@ -352,16 +190,6 @@ onMounted(async () => {
|
||||
</template>
|
||||
</Column>
|
||||
<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="创建时间">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.created_at) }}
|
||||
@@ -382,97 +210,46 @@ onMounted(async () => {
|
||||
{{ data.username || "—" }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="remark" header="备注">
|
||||
<Column field="agent_remark" header="代理备注">
|
||||
<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>
|
||||
</Column>
|
||||
|
||||
<template #empty>
|
||||
<p class="py-6 text-center text-sm text-text-muted">暂无卡密</p>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="dialogVisible"
|
||||
:header="dialogTitle"
|
||||
v-model:visible="remarkDialogVisible"
|
||||
header="编辑代理备注"
|
||||
modal
|
||||
:style="{ width: '28rem' }"
|
||||
>
|
||||
<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">
|
||||
<label class="text-xs text-text-muted uppercase">备注</label>
|
||||
<Textarea v-model="form.remark" rows="2" class="w-full" />
|
||||
<label class="text-xs text-text-muted uppercase">序列号</label>
|
||||
<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>
|
||||
|
||||
<template #footer>
|
||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
||||
<Button label="保存" :loading="saving" @click="saveCard" />
|
||||
<Button label="取消" severity="secondary" text @click="remarkDialogVisible = false" />
|
||||
<Button label="保存" :loading="saving" @click="saveRemark" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useAuthStore, ROLE_ADMIN, ROLE_AGENT,ROLE_OEM } from "../../stores/auth.js";
|
||||
import {
|
||||
listAgentUsersApi,
|
||||
createAgentUserApi,
|
||||
updateAgentUserApi,
|
||||
deleteAgentUserApi,
|
||||
} from "../../api/agentUsers.js";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useAuthStore, ROLE_ADMIN, ROLE_AGENT, ROLE_OEM } from "../../stores/auth.js";
|
||||
import { listAgentUsersApi } from "../../api/agentUsers.js";
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ label: "普通用户", value: 0 },
|
||||
{ label: "管理员", value: 1 },
|
||||
{ label: "代理", value: 2 },
|
||||
{ label: "OEM", value: 3 },
|
||||
];
|
||||
|
||||
const ROLE_LABELS = {
|
||||
0: "普通用户",
|
||||
[ROLE_ADMIN]: "管理员",
|
||||
@@ -29,36 +17,9 @@ const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const loading = ref(false);
|
||||
const oemOptions = ref([]);
|
||||
const agentOptions = ref([]);
|
||||
|
||||
const filters = ref({
|
||||
username: "",
|
||||
oem_id: null,
|
||||
agent_id: null,
|
||||
});
|
||||
const saving = ref(false);
|
||||
const filters = ref({ username: "" });
|
||||
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) {
|
||||
if (!value) return "—";
|
||||
const d = new Date(value);
|
||||
@@ -66,14 +27,6 @@ function formatDateTime(value) {
|
||||
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) {
|
||||
return ROLE_LABELS[roleId] ?? `角色 ${roleId}`;
|
||||
}
|
||||
@@ -85,35 +38,10 @@ function roleSeverity(roleId) {
|
||||
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) {
|
||||
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() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
@@ -121,8 +49,6 @@ async function loadUsers() {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
username: filters.value.username,
|
||||
oemId: filters.value.oem_id,
|
||||
agentId: filters.value.agent_id,
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
@@ -150,157 +76,18 @@ async function applyFilters() {
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
filters.value = { username: "", oem_id: null, agent_id: null };
|
||||
filters.value = { username: "" };
|
||||
page.value = 1;
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
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();
|
||||
});
|
||||
onMounted(loadUsers);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page admin-users">
|
||||
<div class="admin-users__header">
|
||||
<div>
|
||||
<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" />
|
||||
<h1 class="admin-page__title gradient-text">用户管理</h1>
|
||||
</div>
|
||||
|
||||
<Message
|
||||
@@ -313,6 +100,17 @@ onMounted(async () => {
|
||||
{{ feedback.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
|
||||
:value="users"
|
||||
:loading="loading"
|
||||
@@ -342,16 +140,6 @@ onMounted(async () => {
|
||||
<Tag :value="roleLabel(data.role_id)" :severity="roleSeverity(data.role_id)" />
|
||||
</template>
|
||||
</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 到期">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.vip_end_time) }}
|
||||
@@ -362,94 +150,9 @@ onMounted(async () => {
|
||||
{{ formatDateTime(data.created_at) }}
|
||||
</template>
|
||||
</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>
|
||||
<p class="py-6 text-center text-sm text-text-muted">暂无用户</p>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
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 {
|
||||
listAdminCardKeysApi,
|
||||
createAdminCardKeysApi,
|
||||
updateAdminCardKeyApi,
|
||||
deleteAdminCardKeyApi,
|
||||
listOemCardKeysApi,
|
||||
updateOemCardKeyApi,
|
||||
assignOemCardKeysToAgentApi,
|
||||
} from "../../api/oemCardKeys.js";
|
||||
import { listOemUsersApi } from "../../api/oemUsers.js";
|
||||
|
||||
@@ -17,62 +16,41 @@ const STATUS_OPTIONS = [
|
||||
{ label: "已激活", value: "used" },
|
||||
];
|
||||
|
||||
const ASSIGN_OPTIONS = [
|
||||
{ label: "不指定", value: "none" },
|
||||
{ label: "分配给 OEM", value: "oem" },
|
||||
{ label: "分配给代理", value: "agent" },
|
||||
];
|
||||
|
||||
const cards = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const statusFilter = ref("all");
|
||||
const oemOptions = ref([]);
|
||||
const agentOptions = ref([]);
|
||||
const filters = ref({
|
||||
username: "",
|
||||
oem_id: null,
|
||||
agent_id: null,
|
||||
});
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const assigning = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogMode = ref("create");
|
||||
const selectedCards = ref([]);
|
||||
const assignDialogVisible = ref(false);
|
||||
const assignAgentId = ref(null);
|
||||
|
||||
const remarkDialogVisible = ref(false);
|
||||
const editingId = ref(null);
|
||||
|
||||
const emptyForm = () => ({
|
||||
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 editingSerial = ref("");
|
||||
const remarkForm = ref({ oem_remark: "" });
|
||||
|
||||
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 [];
|
||||
});
|
||||
const selectedUnusedCards = computed(() =>
|
||||
selectedCards.value.filter((row) => !row.activated_at),
|
||||
);
|
||||
|
||||
const canBatchAssign = computed(() => selectedUnusedCards.value.length > 0);
|
||||
|
||||
function ownerLabel(userId) {
|
||||
if (userId == null) return "—";
|
||||
@@ -86,30 +64,33 @@ function formatDateTime(value) {
|
||||
return d.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function isRowSelectable(row) {
|
||||
return !row.activated_at;
|
||||
}
|
||||
|
||||
function showMessage(severity, message) {
|
||||
feedback.value = { severity, message };
|
||||
}
|
||||
|
||||
async function loadFilterOptions() {
|
||||
const [ agentRes] = await Promise.all([
|
||||
|
||||
listOemUsersApi(auth.token, { page: 1, pageSize: 200, roleId: ROLE_AGENT }),
|
||||
]);
|
||||
|
||||
if (agentRes.ok && Array.isArray(agentRes.data?.items)) {
|
||||
agentOptions.value = agentRes.data.items;
|
||||
const res = await listOemUsersApi(auth.token, {
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
roleId: ROLE_AGENT,
|
||||
});
|
||||
if (res.ok && Array.isArray(res.data?.items)) {
|
||||
agentOptions.value = res.data.items;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
const res = await listAdminCardKeysApi(auth.token, {
|
||||
const res = await listOemCardKeysApi(auth.token, {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
status: statusFilter.value,
|
||||
username: filters.value.username,
|
||||
oemId: filters.value.oem_id,
|
||||
agentId: filters.value.agent_id,
|
||||
});
|
||||
loading.value = false;
|
||||
@@ -124,6 +105,7 @@ async function loadCards() {
|
||||
total.value = typeof data?.total === "number" ? data.total : 0;
|
||||
if (typeof data?.page === "number") page.value = data.page;
|
||||
if (typeof data?.page_size === "number") pageSize.value = data.page_size;
|
||||
selectedCards.value = [];
|
||||
}
|
||||
|
||||
async function onPage(event) {
|
||||
@@ -143,32 +125,71 @@ async function applyFilters() {
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
filters.value = { username: "", oem_id: null, agent_id: null };
|
||||
filters.value = { username: "", agent_id: null };
|
||||
page.value = 1;
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
dialogMode.value = "create";
|
||||
editingId.value = null;
|
||||
editingActivatedAt.value = null;
|
||||
form.value = emptyForm();
|
||||
dialogVisible.value = true;
|
||||
function openRemarkDialog(row) {
|
||||
editingId.value = row.id;
|
||||
editingSerial.value = row.serial_number;
|
||||
remarkForm.value = { oem_remark: row.oem_remark || "" };
|
||||
remarkDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog(row) {
|
||||
dialogMode.value = "edit";
|
||||
editingId.value = row.id;
|
||||
editingActivatedAt.value = row.activated_at;
|
||||
form.value = {
|
||||
serial_number: row.serial_number,
|
||||
duration_days: row.duration_days,
|
||||
count: 1,
|
||||
remark: row.remark || "",
|
||||
assign_type: "none",
|
||||
assign_id: null,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
function openAssignDialog() {
|
||||
if (!canBatchAssign.value) {
|
||||
showMessage("warn", "请勾选未使用的卡密");
|
||||
return;
|
||||
}
|
||||
assignAgentId.value = null;
|
||||
assignDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function saveRemark() {
|
||||
saving.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
|
||||
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) {
|
||||
@@ -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(
|
||||
() => form.value.assign_type,
|
||||
() => {
|
||||
form.value.assign_id = null;
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFilterOptions();
|
||||
await loadCards();
|
||||
@@ -284,7 +215,11 @@ onMounted(async () => {
|
||||
<div>
|
||||
<h1 class="admin-page__title gradient-text">卡密管理</h1>
|
||||
</div>
|
||||
<Button label="生成卡密" @click="openCreateDialog" />
|
||||
<Button
|
||||
label="分配给代理"
|
||||
:disabled="!canBatchAssign"
|
||||
@click="openAssignDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Message
|
||||
@@ -304,7 +239,6 @@ onMounted(async () => {
|
||||
class="admin-users__search"
|
||||
@keyup.enter="applyFilters"
|
||||
/>
|
||||
|
||||
<Select
|
||||
v-model="filters.agent_id"
|
||||
:options="agentOptions"
|
||||
@@ -328,6 +262,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
v-model:selection="selectedCards"
|
||||
:value="cards"
|
||||
:loading="loading"
|
||||
lazy
|
||||
@@ -342,8 +277,10 @@ onMounted(async () => {
|
||||
size="small"
|
||||
class="admin-users__table"
|
||||
data-key="id"
|
||||
:selectable="isRowSelectable"
|
||||
@page="onPage"
|
||||
>
|
||||
<Column selection-mode="multiple" header-style="width: 3rem" />
|
||||
<Column field="serial_number" header="序列号" style="min-width: 11rem">
|
||||
<template #body="{ data }">
|
||||
<div class="admin-card-keys__serial">
|
||||
@@ -359,7 +296,6 @@ onMounted(async () => {
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
|
||||
|
||||
<Column header="代理" style="width: 7rem">
|
||||
<template #body="{ data }">
|
||||
{{ ownerLabel(data.agent_id) }}
|
||||
@@ -385,30 +321,20 @@ onMounted(async () => {
|
||||
{{ data.username || "—" }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="remark" header="备注">
|
||||
<Column field="oem_remark" header="OEM 备注">
|
||||
<template #body="{ data }">
|
||||
{{ data.oem_remark || "—" }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="操作" style="width: 9rem">
|
||||
<Column header="操作" style="width: 6rem">
|
||||
<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.activated_at"
|
||||
@click="removeCard(data)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
label="备注"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="openRemarkDialog(data)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<template #empty>
|
||||
@@ -417,73 +343,51 @@ onMounted(async () => {
|
||||
</DataTable>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="dialogVisible"
|
||||
:header="dialogTitle"
|
||||
v-model:visible="remarkDialogVisible"
|
||||
header="编辑 OEM 备注"
|
||||
modal
|
||||
:style="{ width: '28rem' }"
|
||||
>
|
||||
<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">
|
||||
<label class="text-xs text-text-muted uppercase">备注</label>
|
||||
<Textarea v-model="form.remark" rows="2" class="w-full" />
|
||||
<label class="text-xs text-text-muted uppercase">序列号</label>
|
||||
<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>
|
||||
|
||||
<template #footer>
|
||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
||||
<Button label="保存" :loading="saving" @click="saveCard" />
|
||||
<Button label="取消" severity="secondary" text @click="remarkDialogVisible = false" />
|
||||
<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>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user