This commit is contained in:
fengchuanhn@gmail.com
2026-05-23 09:50:35 +08:00
parent 6b81e2a94a
commit a6335ba712
8 changed files with 224 additions and 94 deletions

12
.gitignore vendored
View File

@@ -25,10 +25,12 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.pyd
src-tauri/binaries/
src-tauri/resources/nodejs/
src-tauri/resources/resources-bundles/python-runtime/
src-tauri/resources/resources-bundles/python-runtime/Lib/site-packages/
src-tauri/resources/resources-bundles/models/
src-tauri/resources/resources-bundles/python-runtimebackup/
src-tauri/resources/resources-bundles/eSpeak/
src-tauri/resources/pythonruntime/
src-tauri/resources/pythonruntime/Lib/site-packages/
src-tauri/resources/models/
src-tauri/resources/pythonruntimebackup/
src-tauri/resources/eSpeak/

View File

@@ -16,7 +16,8 @@ const baseMenuItems = [
// { name: "design", label: "设计", to: "/design", icon: "design" },
// { name: "model", label: "模型", to: "/model", icon: "model" },
{ name: "local-config", label: "本地配置", to: "/local-config", icon: "settings" },
{ name: "help", label: "帮助", to: "/help", icon: "help" },
{ name: "card-activate", label: "卡密激活", to: "/card-activate", icon: "card-activate" },
// { name: "help", label: "帮助", to: "/help", icon: "help" },
];
const menuItems = computed(() => {

View File

@@ -127,7 +127,17 @@ const mainChildren = [
meta: { title: "本地配置" },
},
{
path: "card-activate",
name: "card-activate",
component: () => import("../views/CardActivateView.vue"),
meta: { title: "卡密激活" },
},
{
path: "admin",

View File

View File

@@ -184,43 +184,40 @@ async function commitRenameAsr(item) {
</script>
<template>
<div
class="voice-workbench flex h-full min-h-0 overflow-hidden bg-[#0a0a0a] font-sans text-white"
>
<div class="voice-page flex h-full min-h-0 overflow-hidden text-slate-100">
<!-- 移动端顶栏 -->
<div
class="fixed left-0 right-0 top-0 z-50 flex h-14 items-center justify-between border-b border-white/10 bg-[#141414] px-4 md:hidden"
class="fixed left-0 right-0 top-0 z-50 flex h-14 items-center justify-between border-b border-white/10 bg-bg-elevated px-4 md:hidden"
>
<div
class="bg-gradient-to-r from-purple-400 to-pink-500 bg-clip-text text-lg font-bold text-transparent"
<h1 class="gradient-text text-lg font-bold">声音</h1>
<button
type="button"
class="text-slate-300 hover:text-slate-100"
@click="mobileSidebarOpen = !mobileSidebarOpen"
>
SOUND:AI
</div>
<button type="button" class="text-white" @click="mobileSidebarOpen = !mobileSidebarOpen">
<i class="pi pi-bars text-xl" />
</button>
</div>
<!-- 左侧栏 -->
<aside
class="fixed inset-y-0 left-0 z-40 flex w-80 flex-col border-r border-white/10 bg-[#141414] transition-transform duration-300 md:relative md:translate-x-0"
class="fixed inset-y-0 left-0 z-40 flex w-80 flex-col border-r border-white/10 bg-white/5 transition-transform duration-300 md:relative md:translate-x-0"
:class="mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'"
>
<div class="hidden h-16 shrink-0 items-center border-b border-white/10 px-6 md:flex">
<span class="text-xl font-bold tracking-wider">
SOUND<span class="text-purple-500">:AI</span>
</span>
<div class="hidden shrink-0 border-b border-white/10 px-6 py-4 md:block">
<h1 class="text-2xl font-bold text-slate-100">声音</h1>
<p class="mt-1 text-sm text-slate-500">语音合成与语音识别</p>
</div>
<div class="px-6 pb-2 pt-16 md:pt-6">
<div class="flex rounded-lg bg-[#1a1a1a] p-1">
<div class="px-5 pb-2 pt-16 md:pt-4">
<div class="flex rounded-lg border border-white/10 bg-white/5 p-1">
<button
type="button"
class="flex flex-1 items-center justify-center gap-2 rounded-md py-2 text-xs font-semibold transition-all"
:class="
sideTab === 'synthesis'
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg'
: 'text-gray-400 hover:text-white'
? 'bg-purple-500/20 text-slate-100 ring-1 ring-purple-500/40'
: 'text-slate-500 hover:text-slate-200'
"
@click="sideTab = 'synthesis'"
>
@@ -232,8 +229,8 @@ async function commitRenameAsr(item) {
class="flex flex-1 items-center justify-center gap-2 rounded-md py-2 text-xs font-semibold transition-all"
:class="
sideTab === 'asr'
? 'bg-gradient-to-r from-blue-600 to-cyan-600 text-white shadow-lg'
: 'text-gray-400 hover:text-white'
? 'bg-blue-500/20 text-slate-100 ring-1 ring-blue-500/40'
: 'text-slate-500 hover:text-slate-200'
"
@click="sideTab = 'asr'"
>
@@ -243,22 +240,23 @@ async function commitRenameAsr(item) {
</div>
</div>
<div class="px-6 pb-2">
<button
type="button"
class="flex w-full items-center justify-center gap-2 rounded-lg border border-purple-500/40 py-2.5 text-sm font-bold text-purple-300 transition-all hover:border-purple-400 hover:bg-purple-600/20 hover:text-white"
<div class="px-5 pb-2">
<Button
label="音色管理"
icon="pi pi-sliders-h"
size="small"
severity="secondary"
outlined
class="w-full"
@click="voiceManageVisible = true"
>
<i class="pi pi-sliders-h" />
音色管理
</button>
/>
</div>
<div class="custom-scrollbar flex-1 space-y-6 overflow-y-auto px-6 py-4">
<div class="custom-scrollbar flex-1 space-y-4 overflow-y-auto px-5 py-4">
<!-- 声音合成表单 -->
<div
v-if="sideTab === 'synthesis'"
class="rounded-xl border border-[#333] bg-[#141414] p-4"
class="rounded-xl border border-white/10 bg-white/5 p-4"
>
<Textarea
v-model="synthesisText"
@@ -268,7 +266,7 @@ async function commitRenameAsr(item) {
placeholder="输入语音内容开始合成"
/>
<div class="mb-3 text-sm font-bold text-gray-200">
<div class="mb-3 text-sm font-medium text-slate-200">
<i class="pi pi-cog mr-1 text-slate-400" />
语音合成模型
</div>
@@ -280,9 +278,9 @@ async function commitRenameAsr(item) {
size="small"
class="mb-2 w-full"
/>
<p v-if="ttsModelHint" class="mb-3 ml-1 text-xs text-gray-500">{{ ttsModelHint }}</p>
<p v-if="ttsModelHint" class="mb-3 ml-1 text-xs text-slate-500">{{ ttsModelHint }}</p>
<div class="mb-3 flex items-center gap-2 text-sm text-gray-200">
<div class="mb-3 flex items-center gap-2 text-sm text-slate-200">
<i class="pi pi-user text-slate-400" />
<Button
:label="voicePage.selectedVoiceLabel"
@@ -296,7 +294,7 @@ async function commitRenameAsr(item) {
<div class="mb-3 grid grid-cols-3 gap-2">
<div>
<label class="mb-1 block text-xs text-gray-500">情绪</label>
<label class="mb-1 block text-xs text-slate-500">情绪</label>
<Select
v-model="voiceEmotion"
:options="voicePage.voiceEmotions"
@@ -305,7 +303,7 @@ async function commitRenameAsr(item) {
/>
</div>
<div>
<label class="mb-1 block text-xs text-gray-500">语言</label>
<label class="mb-1 block text-xs text-slate-500">语言</label>
<Select
v-model="voiceLanguage"
:options="voicePage.languages"
@@ -314,7 +312,7 @@ async function commitRenameAsr(item) {
/>
</div>
<div>
<label class="mb-1 block text-xs text-gray-500">语速</label>
<label class="mb-1 block text-xs text-slate-500">语速</label>
<InputNumber
v-model="speechRate"
:min="0.5"
@@ -343,8 +341,8 @@ async function commitRenameAsr(item) {
</div>
<!-- 语音识别表单 -->
<div v-else class="rounded-xl border border-[#333] bg-[#141414] p-4">
<div class="mb-3 text-sm font-bold text-gray-200">
<div v-else class="rounded-xl border border-white/10 bg-white/5 p-4">
<div class="mb-3 text-sm font-medium text-slate-200">
<i class="pi pi-file-audio mr-1 text-slate-400" />
音频/视频文件
</div>
@@ -373,18 +371,18 @@ async function commitRenameAsr(item) {
/>
<!-- 右侧主区 -->
<div class="relative z-0 flex min-w-0 flex-1 flex-col bg-[#0a0a0a] pt-14 md:pt-0">
<div class="relative z-0 flex min-w-0 flex-1 flex-col pt-14 md:pt-0">
<div
class="hidden h-16 shrink-0 items-center justify-between border-b border-white/10 px-8 md:flex"
class="hidden shrink-0 items-center justify-between border-b border-white/10 px-6 py-3 md:flex"
>
<div class="flex items-center gap-6">
<div class="flex items-center gap-4">
<button
type="button"
class="-mb-5 border-b-2 pb-5 text-sm font-medium transition-colors"
class="border-b-2 pb-2 text-sm font-medium transition-colors"
:class="
workspaceTab === 'history'
? 'border-purple-500 text-white'
: 'border-transparent text-gray-400 hover:text-white'
? 'border-purple-500 text-slate-100'
: 'border-transparent text-slate-500 hover:text-slate-200'
"
@click="workspaceTab = 'history'"
>
@@ -392,11 +390,11 @@ async function commitRenameAsr(item) {
</button>
<button
type="button"
class="-mb-5 border-b-2 pb-5 text-sm font-medium transition-colors"
class="border-b-2 pb-2 text-sm font-medium transition-colors"
:class="
workspaceTab === 'workspace'
? 'border-purple-500 text-white'
: 'border-transparent text-gray-400 hover:text-white'
? 'border-purple-500 text-slate-100'
: 'border-transparent text-slate-500 hover:text-slate-200'
"
@click="workspaceTab = 'workspace'"
>
@@ -405,9 +403,9 @@ async function commitRenameAsr(item) {
</div>
<Button
icon="pi pi-trash"
severity="secondary"
severity="danger"
text
class="text-gray-400 hover:text-red-400"
size="small"
title="清空当前列表"
@click="onClearHistory"
/>
@@ -416,7 +414,11 @@ async function commitRenameAsr(item) {
<p
v-if="feedback.message"
class="mx-4 mt-2 text-sm md:mx-6"
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
:class="{
'text-red-400': feedback.severity === 'error',
'text-amber-400': feedback.severity === 'warn',
'text-emerald-400': feedback.severity === 'success',
}"
>
{{ feedback.message }}
</p>
@@ -426,7 +428,7 @@ async function commitRenameAsr(item) {
<template v-if="sideTab === 'synthesis'">
<div
v-if="synthesisHistory.length"
class="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-[#333] bg-[#141414] p-4"
class="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-white/10 bg-white/5 p-4"
>
<Checkbox
:model-value="voicePage.synthesisAllChecked"
@@ -444,21 +446,21 @@ async function commitRenameAsr(item) {
:disabled="!voicePage.checkedSynthesisItems.length"
@click="voicePage.deleteCheckedSynthesis()"
/>
<span class="ml-auto text-sm text-gray-500">
<span class="ml-auto text-sm text-slate-500">
{{ synthesisHistory.length }}
</span>
</div>
<p v-if="synthesisLoading" class="py-16 text-center text-sm text-gray-500">
<p v-if="synthesisLoading" class="py-16 text-center text-sm text-slate-500">
加载中...
</p>
<div
v-else-if="!synthesisHistory.length"
class="flex min-h-[320px] flex-col items-center justify-center rounded-2xl border border-white/5 bg-[#111] p-12 text-center"
class="flex min-h-[320px] flex-col items-center justify-center rounded-xl border border-white/10 bg-white/5 p-12 text-center"
>
<i class="pi pi-volume-up mb-4 text-5xl text-gray-600" />
<h2 class="text-2xl font-bold text-gray-300">准备好创作了吗</h2>
<p class="mt-2 max-w-sm text-gray-500">
<i class="pi pi-volume-up mb-4 text-5xl text-slate-600" />
<h2 class="text-xl font-bold text-slate-300">准备好创作了吗</h2>
<p class="mt-2 max-w-sm text-slate-500">
在左侧输入文本并选择声音开始您的创作之旅
</p>
</div>
@@ -467,16 +469,16 @@ async function commitRenameAsr(item) {
<div
v-for="item in synthesisHistory"
:key="item.id"
class="rounded-xl border border-[#333] bg-[#141414] p-4 transition-colors hover:border-[#555]"
class="rounded-xl border border-white/10 bg-white/5 p-4 shadow-sm transition-colors hover:border-purple-500/40 hover:shadow-md"
>
<div class="flex flex-wrap items-center gap-2">
<div
class="mr-2 inline-flex max-w-full items-start rounded-full border border-white/10 bg-white/5 px-2 py-1"
class="mr-2 inline-flex max-w-full items-start rounded-full bg-blue-500/15 px-2 py-1"
>
<Checkbox v-model="item._check" :binary="true" class="mr-2 mt-1" />
<div v-if="editingId !== item.id" class="min-w-0">
<span
class="block max-w-md cursor-pointer truncate text-sm"
class="block max-w-md cursor-pointer truncate text-sm font-medium text-slate-100"
:title="item.name"
@click="startRename(item)"
>
@@ -495,7 +497,7 @@ async function commitRenameAsr(item) {
</div>
<span
v-if="item.durationSec"
class="rounded-full bg-white/10 px-2 py-1 text-xs"
class="rounded-full bg-white/10 px-2 py-1 text-xs text-slate-400"
>
{{ formatDuration(item.durationSec) }}
</span>
@@ -504,7 +506,7 @@ async function commitRenameAsr(item) {
<div
v-if="item.sourceText"
class="mt-3 rounded-lg bg-white/5 p-3 text-sm text-gray-300"
class="mt-3 rounded-lg border border-white/10 bg-white/5 p-3 text-sm text-slate-300"
>
<span>{{ truncateText(item.sourceText) }}</span>
<div class="mt-2 flex gap-1">
@@ -523,7 +525,7 @@ async function commitRenameAsr(item) {
</div>
<div class="mt-4 flex items-center justify-between">
<span class="text-xs text-gray-500">
<span class="text-xs text-slate-500">
{{ formatRelativeTime(item.createdAt) }}
</span>
<div class="flex gap-1">
@@ -553,7 +555,7 @@ async function commitRenameAsr(item) {
<template v-else>
<div
v-if="asrHistory.length"
class="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-[#333] bg-[#141414] p-4"
class="mb-4 flex flex-wrap items-center gap-2 rounded-xl border border-white/10 bg-white/5 p-4"
>
<Checkbox
:model-value="voicePage.asrAllChecked"
@@ -571,17 +573,17 @@ async function commitRenameAsr(item) {
:disabled="!voicePage.checkedAsrItems.length"
@click="voicePage.deleteCheckedAsr()"
/>
<span class="ml-auto text-sm text-gray-500"> {{ asrHistory.length }} </span>
<span class="ml-auto text-sm text-slate-500"> {{ asrHistory.length }} </span>
</div>
<p v-if="asrLoading" class="py-16 text-center text-sm text-gray-500">加载中...</p>
<p v-if="asrLoading" class="py-16 text-center text-sm text-slate-500">加载中...</p>
<div
v-else-if="!asrHistory.length"
class="flex min-h-[320px] flex-col items-center justify-center rounded-2xl border border-white/5 bg-[#111] p-12 text-center"
class="flex min-h-[320px] flex-col items-center justify-center rounded-xl border border-white/10 bg-white/5 p-12 text-center"
>
<i class="pi pi-microphone mb-4 text-5xl text-gray-600" />
<h2 class="text-2xl font-bold text-gray-300">准备好识别了吗</h2>
<p class="mt-2 max-w-sm text-gray-500">
<i class="pi pi-microphone mb-4 text-5xl text-slate-600" />
<h2 class="text-xl font-bold text-slate-300">准备好识别了吗</h2>
<p class="mt-2 max-w-sm text-slate-500">
在左侧上传音频或视频文件开始您的识别之旅
</p>
</div>
@@ -590,16 +592,16 @@ async function commitRenameAsr(item) {
<div
v-for="item in asrHistory"
:key="item.id"
class="rounded-xl border border-[#333] bg-[#141414] p-4 transition-colors hover:border-[#555]"
class="rounded-xl border border-white/10 bg-white/5 p-4 shadow-sm transition-colors hover:border-purple-500/40 hover:shadow-md"
>
<div class="flex flex-wrap items-center gap-2">
<div
class="mr-2 inline-flex max-w-full items-start rounded-full border border-white/10 bg-white/5 px-2 py-1"
class="mr-2 inline-flex max-w-full items-start rounded-full bg-blue-500/15 px-2 py-1"
>
<Checkbox v-model="item._check" :binary="true" class="mr-2 mt-1" />
<div v-if="editingId !== item.id" class="min-w-0">
<span
class="block max-w-md cursor-pointer truncate text-sm"
class="block max-w-md cursor-pointer truncate text-sm font-medium text-slate-100"
@click="startRename(item)"
>
{{ item.name }}
@@ -616,13 +618,13 @@ async function commitRenameAsr(item) {
<div
v-if="item.resultText"
class="mt-3 rounded-lg bg-white/5 p-3 text-sm text-gray-300"
class="mt-3 rounded-lg border border-white/10 bg-white/5 p-3 text-sm text-slate-300"
>
{{ truncateText(item.resultText, 300) }}
</div>
<div class="mt-4 flex items-center justify-between">
<span class="text-xs text-gray-500">
<span class="text-xs text-slate-500">
{{ formatRelativeTime(item.createdAt) }}
</span>
<div class="flex gap-1">

View File

@@ -23,6 +23,11 @@ const ASSIGN_OPTIONS = [
{ label: "分配给代理", value: "agent" },
];
const CARD_TYPE_OPTIONS = [
{ label: "时长", value: "时长" },
{ label: "点数", value: "点数" },
];
const cards = ref([]);
const total = ref(0);
const page = ref(1);
@@ -45,7 +50,9 @@ const editingId = ref(null);
const emptyForm = () => ({
serial_number: "",
card_type: "时长",
duration_days: 30,
points_amount: 100,
count: 1,
remark: "",
assign_type: "none",
@@ -59,6 +66,7 @@ const dialogTitle = computed(() =>
);
const isActivated = computed(() => dialogMode.value === "edit" && !!editingActivatedAt.value);
const isDurationType = computed(() => form.value.card_type === "时长");
const editingActivatedAt = ref(null);
const ownerNameById = computed(() => {
@@ -74,6 +82,13 @@ const assignSelectOptions = computed(() => {
return [];
});
function cardValueLabel(row) {
if (row.card_type === "点数") {
return row.points_amount != null ? `${row.points_amount}` : "—";
}
return `${row.duration_days}`;
}
function ownerLabel(userId) {
if (userId == null) return "—";
return ownerNameById.value.get(userId) ?? `#${userId}`;
@@ -103,9 +118,12 @@ async function loadFilterOptions() {
}
}
async function loadCards() {
async function loadCards(options = {}) {
const { clearFeedback = true } = options;
loading.value = true;
if (clearFeedback) {
feedback.value = { severity: "", message: "" };
}
const res = await listAdminCardKeysApi(auth.token, {
page: page.value,
pageSize: pageSize.value,
@@ -164,7 +182,9 @@ function openEditDialog(row) {
editingActivatedAt.value = row.activated_at;
form.value = {
serial_number: row.serial_number,
card_type: row.card_type || "时长",
duration_days: row.duration_days,
points_amount: row.points_amount ?? 100,
count: 1,
remark: row.remark || "",
assign_type: "none",
@@ -193,10 +213,15 @@ function buildAssignPayload() {
}
async function saveCard() {
if (isDurationType.value) {
if (form.value.duration_days < 1) {
showMessage("warn", "时长至少 1 天");
return;
}
} else if (!form.value.points_amount || form.value.points_amount <= 0) {
showMessage("warn", "点数须大于 0");
return;
}
saving.value = true;
feedback.value = { severity: "", message: "" };
@@ -209,11 +234,16 @@ async function saveCard() {
return;
}
const payload = {
duration_days: form.value.duration_days,
card_type: form.value.card_type,
count: form.value.count,
remark: form.value.remark.trim() || null,
...buildAssignPayload(),
};
if (isDurationType.value) {
payload.duration_days = form.value.duration_days;
} else {
payload.points_amount = form.value.points_amount;
}
const serial = form.value.serial_number.trim();
if (serial) {
if (form.value.count !== 1) {
@@ -225,10 +255,18 @@ async function saveCard() {
}
res = await createAdminCardKeysApi(auth.token, payload);
} else {
res = await updateAdminCardKeyApi(auth.token, editingId.value, {
duration_days: isActivated.value ? undefined : form.value.duration_days,
const payload = {
remark: form.value.remark.trim() || null,
});
};
if (!isActivated.value) {
payload.card_type = form.value.card_type;
if (isDurationType.value) {
payload.duration_days = form.value.duration_days;
} else {
payload.points_amount = form.value.points_amount;
}
}
res = await updateAdminCardKeyApi(auth.token, editingId.value, payload);
}
saving.value = false;
@@ -238,9 +276,15 @@ async function saveCard() {
return;
}
const wasCreate = dialogMode.value === "create";
const successMessage = res.message || (wasCreate ? "卡密已生成" : "保存成功");
dialogVisible.value = false;
showMessage("success", res.message || "保存成功");
await loadCards();
if (wasCreate) {
page.value = 1;
}
await loadCards({ clearFeedback: false });
showMessage("success", successMessage);
}
async function removeCard(row) {
@@ -369,7 +413,12 @@ onMounted(async () => {
</div>
</template>
</Column>
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
<Column field="card_type" header="类型" style="width: 5rem" />
<Column header="面值" style="width: 7rem">
<template #body="{ data }">
{{ cardValueLabel(data) }}
</template>
</Column>
<Column header="OEM" style="width: 7rem">
<template #body="{ data }">
{{ ownerLabel(data.oem_id) }}
@@ -440,9 +489,29 @@ onMounted(async () => {
<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>
<Select
v-model="form.card_type"
:options="CARD_TYPE_OPTIONS"
option-label="label"
option-value="value"
class="w-full"
/>
</div>
<div v-if="isDurationType" 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 v-else class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">点数</label>
<InputNumber
v-model="form.points_amount"
:min="0.01"
:min-fraction-digits="0"
:max-fraction-digits="2"
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" />
@@ -491,6 +560,17 @@ onMounted(async () => {
/>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">类型</label>
<Select
v-model="form.card_type"
:options="CARD_TYPE_OPTIONS"
option-label="label"
option-value="value"
class="w-full"
:disabled="isActivated"
/>
</div>
<div v-if="isDurationType" class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">时长</label>
<InputNumber
v-model="form.duration_days"
@@ -500,6 +580,17 @@ onMounted(async () => {
:disabled="isActivated"
/>
</div>
<div v-else class="flex flex-col gap-2">
<label class="text-xs text-text-muted uppercase">点数</label>
<InputNumber
v-model="form.points_amount"
:min="0.01"
:min-fraction-digits="0"
:max-fraction-digits="2"
class="w-full"
:disabled="isActivated"
/>
</div>
</template>
<div class="flex flex-col gap-2">

View File

@@ -33,6 +33,13 @@ function formatDateTime(value) {
return d.toLocaleString("zh-CN", { hour12: false });
}
function cardValueLabel(row) {
if (row.card_type === "点数") {
return row.points_amount != null ? `${row.points_amount}` : "—";
}
return `${row.duration_days}`;
}
function showMessage(severity, message) {
feedback.value = { severity, message };
}
@@ -189,7 +196,12 @@ onMounted(loadCards);
</div>
</template>
</Column>
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
<Column field="card_type" header="类型" style="width: 5rem" />
<Column header="面值" style="width: 7rem">
<template #body="{ data }">
{{ cardValueLabel(data) }}
</template>
</Column>
<Column field="created_at" header="创建时间">
<template #body="{ data }">
{{ formatDateTime(data.created_at) }}

View File

@@ -64,6 +64,13 @@ function formatDateTime(value) {
return d.toLocaleString("zh-CN", { hour12: false });
}
function cardValueLabel(row) {
if (row.card_type === "点数") {
return row.points_amount != null ? `${row.points_amount}` : "—";
}
return `${row.duration_days}`;
}
function isRowSelectable(row) {
return !row.activated_at;
}
@@ -295,7 +302,12 @@ onMounted(async () => {
</div>
</template>
</Column>
<Column field="duration_days" header="时长(天)" style="width: 6rem" />
<Column field="card_type" header="类型" style="width: 5rem" />
<Column header="面值" style="width: 7rem">
<template #body="{ data }">
{{ cardValueLabel(data) }}
</template>
</Column>
<Column header="代理" style="width: 7rem">
<template #body="{ data }">
{{ ownerLabel(data.agent_id) }}