11
This commit is contained in:
369
src/views/oem/OemCardKeysView.vue
Normal file
369
src/views/oem/OemCardKeysView.vue
Normal file
@@ -0,0 +1,369 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useAuthStore } from "../../stores/auth.js";
|
||||
import {
|
||||
listAdminCardKeysApi,
|
||||
createAdminCardKeysApi,
|
||||
updateAdminCardKeyApi,
|
||||
deleteAdminCardKeyApi,
|
||||
} from "../../api/adminCardKeys.js";
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: "全部", value: "all" },
|
||||
{ label: "未使用", value: "unused" },
|
||||
{ label: "已激活", value: "used" },
|
||||
];
|
||||
|
||||
const cards = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const statusFilter = ref("all");
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogMode = ref("create");
|
||||
const editingId = ref(null);
|
||||
|
||||
const emptyForm = () => ({
|
||||
serial_number: "",
|
||||
duration_days: 30,
|
||||
count: 1,
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const form = ref(emptyForm());
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
dialogMode.value === "create" ? "生成卡密" : "编辑卡密",
|
||||
);
|
||||
|
||||
const isActivated = computed(() => dialogMode.value === "edit" && !!editingActivatedAt.value);
|
||||
const editingActivatedAt = ref(null);
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "—";
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
return d.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function showMessage(severity, message) {
|
||||
feedback.value = { severity, message };
|
||||
}
|
||||
|
||||
async function loadCards() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
const res = await listAdminCardKeysApi(auth.token, {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
status: statusFilter.value,
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
if (!res.ok) {
|
||||
showMessage("error", res.message || "加载卡密列表失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
cards.value = Array.isArray(data?.items) ? data.items : [];
|
||||
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;
|
||||
}
|
||||
|
||||
async function onPage(event) {
|
||||
page.value = event.page + 1;
|
||||
pageSize.value = event.rows;
|
||||
await loadCards();
|
||||
}
|
||||
|
||||
function onStatusChange() {
|
||||
page.value = 1;
|
||||
loadCards();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
dialogMode.value = "create";
|
||||
editingId.value = null;
|
||||
editingActivatedAt.value = null;
|
||||
form.value = emptyForm();
|
||||
dialogVisible.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 || "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function copySerial(serial) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(serial);
|
||||
showMessage("success", "序列号已复制");
|
||||
} catch {
|
||||
showMessage("warn", "复制失败,请手动复制");
|
||||
}
|
||||
}
|
||||
|
||||
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") {
|
||||
const payload = {
|
||||
duration_days: form.value.duration_days,
|
||||
count: form.value.count,
|
||||
remark: form.value.remark.trim() || null,
|
||||
};
|
||||
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);
|
||||
|
||||
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>
|
||||
<p class="text-sm text-text-muted">生成、查看与管理 VIP 激活卡密</p>
|
||||
</div>
|
||||
<Button label="生成卡密" @click="openCreateDialog" />
|
||||
</div>
|
||||
|
||||
<Message
|
||||
v-if="feedback.message"
|
||||
:severity="feedback.severity"
|
||||
:closable="true"
|
||||
class="admin-users__message"
|
||||
@close="feedback.message = ''"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</Message>
|
||||
|
||||
<div class="admin-card-keys__toolbar">
|
||||
<label class="text-sm text-text-muted">状态筛选</label>
|
||||
<Select
|
||||
v-model="statusFilter"
|
||||
:options="STATUS_OPTIONS"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
class="admin-card-keys__status-select"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
:value="cards"
|
||||
:loading="loading"
|
||||
lazy
|
||||
paginator
|
||||
:rows="pageSize"
|
||||
:total-records="total"
|
||||
:first="(page - 1) * pageSize"
|
||||
:rows-per-page-options="[10, 20, 50]"
|
||||
paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||
current-page-report-template="共 {totalRecords} 条"
|
||||
striped-rows
|
||||
size="small"
|
||||
class="admin-users__table"
|
||||
data-key="id"
|
||||
@page="onPage"
|
||||
>
|
||||
<Column field="serial_number" header="序列号" style="min-width: 11rem">
|
||||
<template #body="{ data }">
|
||||
<div class="admin-card-keys__serial">
|
||||
<code class="text-xs">{{ data.serial_number }}</code>
|
||||
<Button
|
||||
label="复制"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="copySerial(data.serial_number)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
|
||||
<Column field="created_at" header="创建时间">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.created_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="activated_at" header="激活时间">
|
||||
<template #body="{ data }">
|
||||
<Tag
|
||||
v-if="data.activated_at"
|
||||
:value="formatDateTime(data.activated_at)"
|
||||
severity="success"
|
||||
/>
|
||||
<Tag v-else value="未使用" severity="secondary" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="username" header="使用者">
|
||||
<template #body="{ data }">
|
||||
{{ data.username || "—" }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="remark" header="备注">
|
||||
<template #body="{ data }">
|
||||
{{ data.remark || "—" }}
|
||||
</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.activated_at"
|
||||
@click="removeCard(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
|
||||
: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>
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
||||
<Button label="保存" :loading="saving" @click="saveCard" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
325
src/views/oem/OemDesktopConfigView.vue
Normal file
325
src/views/oem/OemDesktopConfigView.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useAuthStore } from "../../stores/auth.js";
|
||||
import {
|
||||
listAdminDesktopConfigsApi,
|
||||
createAdminDesktopConfigApi,
|
||||
updateAdminDesktopConfigApi,
|
||||
deleteAdminDesktopConfigApi,
|
||||
} from "../../api/adminDesktopConfigs.js";
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const configs = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const keyword = ref("");
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const feedback = ref({ severity: "", message: "" });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const dialogMode = ref("create");
|
||||
const editingId = ref(null);
|
||||
|
||||
const emptyForm = () => ({
|
||||
name: "",
|
||||
value: "",
|
||||
mark: "",
|
||||
});
|
||||
|
||||
const form = ref(emptyForm());
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
dialogMode.value === "create" ? "新建配置" : "编辑配置",
|
||||
);
|
||||
|
||||
const valuePreview = computed(() => {
|
||||
const v = form.value.value || "";
|
||||
if (v.length <= 120) return v;
|
||||
return `${v.slice(0, 120)}…`;
|
||||
});
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "—";
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
return d.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function showMessage(severity, message) {
|
||||
feedback.value = { severity, message };
|
||||
}
|
||||
|
||||
function truncateValue(value, max = 48) {
|
||||
if (!value) return "—";
|
||||
if (value.length <= max) return value;
|
||||
return `${value.slice(0, max)}…`;
|
||||
}
|
||||
|
||||
async function loadConfigs() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
const res = await listAdminDesktopConfigsApi(auth.token, {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value,
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
if (!res.ok) {
|
||||
showMessage("error", res.message || "加载配置失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
configs.value = Array.isArray(data?.items) ? data.items : [];
|
||||
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;
|
||||
}
|
||||
|
||||
async function onPage(event) {
|
||||
page.value = event.page + 1;
|
||||
pageSize.value = event.rows;
|
||||
await loadConfigs();
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
page.value = 1;
|
||||
loadConfigs();
|
||||
}
|
||||
|
||||
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 = {
|
||||
name: row.name,
|
||||
value: row.value,
|
||||
mark: row.mark || "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function copyName(name) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(name);
|
||||
showMessage("success", "键名已复制");
|
||||
} catch {
|
||||
showMessage("warn", "复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
const name = form.value.name.trim();
|
||||
if (!name) {
|
||||
showMessage("warn", "配置键名不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
value: form.value.value,
|
||||
mark: form.value.mark.trim() || null,
|
||||
};
|
||||
|
||||
let res;
|
||||
if (dialogMode.value === "create") {
|
||||
res = await createAdminDesktopConfigApi(auth.token, payload);
|
||||
} else {
|
||||
res = await updateAdminDesktopConfigApi(auth.token, editingId.value, {
|
||||
value: payload.value,
|
||||
mark: payload.mark,
|
||||
});
|
||||
}
|
||||
|
||||
saving.value = false;
|
||||
|
||||
if (!res.ok) {
|
||||
showMessage("error", res.message || "保存失败");
|
||||
return;
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
showMessage("success", res.message || "保存成功");
|
||||
await loadConfigs();
|
||||
}
|
||||
|
||||
async function removeConfig(row) {
|
||||
const ok = window.confirm(`确定删除配置「${row.name}」?桌面端需重新登录后生效。`);
|
||||
if (!ok) return;
|
||||
|
||||
const res = await deleteAdminDesktopConfigApi(auth.token, row.id);
|
||||
if (!res.ok) {
|
||||
showMessage("error", res.message || "删除失败");
|
||||
return;
|
||||
}
|
||||
|
||||
showMessage("success", res.message || "已删除");
|
||||
if (configs.value.length === 1 && page.value > 1) {
|
||||
page.value -= 1;
|
||||
}
|
||||
await loadConfigs();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfigs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page admin-desktop-config">
|
||||
<div class="admin-users__header">
|
||||
<div>
|
||||
<h1 class="admin-page__title gradient-text">桌面配置</h1>
|
||||
<p class="text-sm text-text-muted">
|
||||
管理 desktop_configs 键值,下发至桌面端 Node 环境变量(AICLIENT_CFG_*)
|
||||
</p>
|
||||
</div>
|
||||
<Button label="新建配置" @click="openCreateDialog" />
|
||||
</div>
|
||||
|
||||
<Message
|
||||
v-if="feedback.message"
|
||||
:severity="feedback.severity"
|
||||
:closable="true"
|
||||
class="admin-users__message"
|
||||
@close="feedback.message = ''"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</Message>
|
||||
|
||||
<div class="admin-card-keys__toolbar">
|
||||
<InputText
|
||||
v-model="keyword"
|
||||
placeholder="按键名搜索,如 LLM_API_KEY"
|
||||
class="admin-desktop-config__search"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
<Button label="搜索" severity="secondary" @click="onSearch" />
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
:value="configs"
|
||||
:loading="loading"
|
||||
lazy
|
||||
paginator
|
||||
:rows="pageSize"
|
||||
:total-records="total"
|
||||
:first="(page - 1) * pageSize"
|
||||
:rows-per-page-options="[10, 20, 50]"
|
||||
paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||
current-page-report-template="共 {totalRecords} 条"
|
||||
striped-rows
|
||||
size="small"
|
||||
class="admin-users__table"
|
||||
data-key="id"
|
||||
@page="onPage"
|
||||
>
|
||||
<Column field="name" header="键名" style="min-width: 10rem">
|
||||
<template #body="{ data }">
|
||||
<div class="admin-card-keys__serial">
|
||||
<code class="text-xs">{{ data.name }}</code>
|
||||
<Button label="复制" size="small" severity="secondary" text @click="copyName(data.name)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="value" header="值">
|
||||
<template #body="{ data }">
|
||||
<span class="admin-desktop-config__value" :title="data.value">
|
||||
{{ truncateValue(data.value) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="mark" header="备注">
|
||||
<template #body="{ data }">
|
||||
{{ data.mark || "—" }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="updated_at" header="更新时间">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.updated_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
|
||||
@click="removeConfig(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
|
||||
:style="{ width: '32rem' }"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-text-muted uppercase">键名 (name)</label>
|
||||
<InputText
|
||||
v-model="form.name"
|
||||
class="w-full font-mono uppercase"
|
||||
placeholder="如 LLM_API_KEY"
|
||||
:disabled="dialogMode === 'edit'"
|
||||
/>
|
||||
<p v-if="dialogMode === 'edit'" class="text-xs text-text-muted">
|
||||
键名创建后不可修改,避免环境变量映射错乱
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-text-muted uppercase">值 (value)</label>
|
||||
<Textarea
|
||||
v-model="form.value"
|
||||
rows="6"
|
||||
class="w-full font-mono text-sm"
|
||||
placeholder="支持长文本或 JSON,如 OSS_CONFIG"
|
||||
/>
|
||||
<p v-if="valuePreview && form.value.length > 120" class="text-xs text-text-muted">
|
||||
预览:{{ valuePreview }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs text-text-muted uppercase">备注 (mark)</label>
|
||||
<InputText v-model="form.mark" class="w-full" placeholder="可选说明" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="取消" severity="secondary" text @click="dialogVisible = false" />
|
||||
<Button label="保存" :loading="saving" @click="saveConfig" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
14
src/views/oem/OemOverviewView.vue
Normal file
14
src/views/oem/OemOverviewView.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
import { useAuthStore } from "../../stores/auth";
|
||||
|
||||
const auth = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-page">
|
||||
<h1 class="admin-page__title gradient-text">概览</h1>
|
||||
<p class="text-sm text-text-muted">
|
||||
欢迎,{{ auth.currentUser?.username }}。在此查看系统运行概况。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
364
src/views/oem/OemUsersView.vue
Normal file
364
src/views/oem/OemUsersView.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useAuthStore, ROLE_ADMIN, ROLE_AGENT } from "../../stores/auth.js";
|
||||
import {
|
||||
listAdminUsersApi,
|
||||
createAdminUserApi,
|
||||
updateAdminUserApi,
|
||||
deleteAdminUserApi,
|
||||
} from "../../api/adminUsers.js";
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ label: "普通用户", value: 0 },
|
||||
{ label: "管理员", value: 1 },
|
||||
{ label: "代理", value: 2 },
|
||||
];
|
||||
|
||||
const ROLE_LABELS = {
|
||||
0: "普通用户",
|
||||
[ROLE_ADMIN]: "管理员",
|
||||
[ROLE_AGENT]: "代理",
|
||||
};
|
||||
|
||||
const users = ref([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
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);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
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}`;
|
||||
}
|
||||
|
||||
function roleSeverity(roleId) {
|
||||
if (roleId === ROLE_ADMIN) return "info";
|
||||
if (roleId === ROLE_AGENT) return "warn";
|
||||
return "secondary";
|
||||
}
|
||||
|
||||
function showMessage(severity, message) {
|
||||
feedback.value = { severity, message };
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
loading.value = true;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
const res = await listAdminUsersApi(auth.token, {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
if (!res.ok) {
|
||||
showMessage("error", res.message || "加载用户列表失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
users.value = Array.isArray(data?.items) ? data.items : [];
|
||||
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;
|
||||
}
|
||||
|
||||
async function onPage(event) {
|
||||
page.value = event.page + 1;
|
||||
pageSize.value = event.rows;
|
||||
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 createAdminUserApi(auth.token, payload);
|
||||
} else {
|
||||
res = await updateAdminUserApi(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 deleteAdminUserApi(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(() => {
|
||||
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>
|
||||
|
||||
<Message
|
||||
v-if="feedback.message"
|
||||
:severity="feedback.severity"
|
||||
:closable="true"
|
||||
class="admin-users__message"
|
||||
@close="feedback.message = ''"
|
||||
>
|
||||
{{ feedback.message }}
|
||||
</Message>
|
||||
|
||||
<DataTable
|
||||
:value="users"
|
||||
:loading="loading"
|
||||
lazy
|
||||
paginator
|
||||
:rows="pageSize"
|
||||
:total-records="total"
|
||||
:first="(page - 1) * pageSize"
|
||||
:rows-per-page-options="[10, 20, 50]"
|
||||
paginator-template="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||
current-page-report-template="共 {totalRecords} 条"
|
||||
striped-rows
|
||||
size="small"
|
||||
class="admin-users__table"
|
||||
data-key="id"
|
||||
@page="onPage"
|
||||
>
|
||||
<Column field="id" header="ID" style="width: 4rem" />
|
||||
<Column field="username" header="用户名" />
|
||||
<Column field="phone" header="手机号">
|
||||
<template #body="{ data }">
|
||||
{{ data.phone || "—" }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="role_id" header="角色" style="width: 7rem">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="roleLabel(data.role_id)" :severity="roleSeverity(data.role_id)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="vip_end_time" header="VIP 到期">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.vip_end_time) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="注册时间">
|
||||
<template #body="{ data }">
|
||||
{{ 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>
|
||||
Reference in New Issue
Block a user