1
This commit is contained in:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -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/
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -127,7 +127,17 @@ const mainChildren = [
|
||||
meta: { title: "本地配置" },
|
||||
|
||||
},
|
||||
{
|
||||
|
||||
path: "card-activate",
|
||||
|
||||
name: "card-activate",
|
||||
|
||||
component: () => import("../views/CardActivateView.vue"),
|
||||
|
||||
meta: { title: "卡密激活" },
|
||||
|
||||
},
|
||||
{
|
||||
|
||||
path: "admin",
|
||||
|
||||
0
src/views/CardActivateView.vue
Normal file
0
src/views/CardActivateView.vue
Normal 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">
|
||||
|
||||
@@ -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;
|
||||
feedback.value = { severity: "", message: "" };
|
||||
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,8 +213,13 @@ function buildAssignPayload() {
|
||||
}
|
||||
|
||||
async function saveCard() {
|
||||
if (form.value.duration_days < 1) {
|
||||
showMessage("warn", "时长至少 1 天");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
Reference in New Issue
Block a user