1
This commit is contained in:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -25,10 +25,12 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
*.pyd
|
||||||
src-tauri/binaries/
|
src-tauri/binaries/
|
||||||
src-tauri/resources/nodejs/
|
src-tauri/resources/nodejs/
|
||||||
src-tauri/resources/resources-bundles/python-runtime/
|
src-tauri/resources/pythonruntime/
|
||||||
src-tauri/resources/resources-bundles/python-runtime/Lib/site-packages/
|
src-tauri/resources/pythonruntime/Lib/site-packages/
|
||||||
src-tauri/resources/resources-bundles/models/
|
src-tauri/resources/models/
|
||||||
src-tauri/resources/resources-bundles/python-runtimebackup/
|
src-tauri/resources/pythonruntimebackup/
|
||||||
src-tauri/resources/resources-bundles/eSpeak/
|
src-tauri/resources/eSpeak/
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ const baseMenuItems = [
|
|||||||
// { name: "design", label: "设计", to: "/design", icon: "design" },
|
// { name: "design", label: "设计", to: "/design", icon: "design" },
|
||||||
// { name: "model", label: "模型", to: "/model", icon: "model" },
|
// { name: "model", label: "模型", to: "/model", icon: "model" },
|
||||||
{ name: "local-config", label: "本地配置", to: "/local-config", icon: "settings" },
|
{ 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(() => {
|
const menuItems = computed(() => {
|
||||||
|
|||||||
@@ -127,7 +127,17 @@ const mainChildren = [
|
|||||||
meta: { title: "本地配置" },
|
meta: { title: "本地配置" },
|
||||||
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
path: "card-activate",
|
||||||
|
|
||||||
|
name: "card-activate",
|
||||||
|
|
||||||
|
component: () => import("../views/CardActivateView.vue"),
|
||||||
|
|
||||||
|
meta: { title: "卡密激活" },
|
||||||
|
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
|
||||||
path: "admin",
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="voice-page flex h-full min-h-0 overflow-hidden text-slate-100">
|
||||||
class="voice-workbench flex h-full min-h-0 overflow-hidden bg-[#0a0a0a] font-sans text-white"
|
|
||||||
>
|
|
||||||
<!-- 移动端顶栏 -->
|
<!-- 移动端顶栏 -->
|
||||||
<div
|
<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
|
<h1 class="gradient-text text-lg font-bold">声音</h1>
|
||||||
class="bg-gradient-to-r from-purple-400 to-pink-500 bg-clip-text text-lg font-bold text-transparent"
|
<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" />
|
<i class="pi pi-bars text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 左侧栏 -->
|
<!-- 左侧栏 -->
|
||||||
<aside
|
<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'"
|
: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">
|
<div class="hidden shrink-0 border-b border-white/10 px-6 py-4 md:block">
|
||||||
<span class="text-xl font-bold tracking-wider">
|
<h1 class="text-2xl font-bold text-slate-100">声音</h1>
|
||||||
SOUND<span class="text-purple-500">:AI</span>
|
<p class="mt-1 text-sm text-slate-500">语音合成与语音识别</p>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-6 pb-2 pt-16 md:pt-6">
|
<div class="px-5 pb-2 pt-16 md:pt-4">
|
||||||
<div class="flex rounded-lg bg-[#1a1a1a] p-1">
|
<div class="flex rounded-lg border border-white/10 bg-white/5 p-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex flex-1 items-center justify-center gap-2 rounded-md py-2 text-xs font-semibold transition-all"
|
class="flex flex-1 items-center justify-center gap-2 rounded-md py-2 text-xs font-semibold transition-all"
|
||||||
:class="
|
:class="
|
||||||
sideTab === 'synthesis'
|
sideTab === 'synthesis'
|
||||||
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg'
|
? 'bg-purple-500/20 text-slate-100 ring-1 ring-purple-500/40'
|
||||||
: 'text-gray-400 hover:text-white'
|
: 'text-slate-500 hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="sideTab = 'synthesis'"
|
@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="flex flex-1 items-center justify-center gap-2 rounded-md py-2 text-xs font-semibold transition-all"
|
||||||
:class="
|
:class="
|
||||||
sideTab === 'asr'
|
sideTab === 'asr'
|
||||||
? 'bg-gradient-to-r from-blue-600 to-cyan-600 text-white shadow-lg'
|
? 'bg-blue-500/20 text-slate-100 ring-1 ring-blue-500/40'
|
||||||
: 'text-gray-400 hover:text-white'
|
: 'text-slate-500 hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="sideTab = 'asr'"
|
@click="sideTab = 'asr'"
|
||||||
>
|
>
|
||||||
@@ -243,22 +240,23 @@ async function commitRenameAsr(item) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-6 pb-2">
|
<div class="px-5 pb-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
label="音色管理"
|
||||||
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"
|
icon="pi pi-sliders-h"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
class="w-full"
|
||||||
@click="voiceManageVisible = true"
|
@click="voiceManageVisible = true"
|
||||||
>
|
/>
|
||||||
<i class="pi pi-sliders-h" />
|
|
||||||
音色管理
|
|
||||||
</button>
|
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="sideTab === 'synthesis'"
|
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
|
<Textarea
|
||||||
v-model="synthesisText"
|
v-model="synthesisText"
|
||||||
@@ -268,7 +266,7 @@ async function commitRenameAsr(item) {
|
|||||||
placeholder="输入语音内容开始合成"
|
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" />
|
<i class="pi pi-cog mr-1 text-slate-400" />
|
||||||
语音合成模型
|
语音合成模型
|
||||||
</div>
|
</div>
|
||||||
@@ -280,9 +278,9 @@ async function commitRenameAsr(item) {
|
|||||||
size="small"
|
size="small"
|
||||||
class="mb-2 w-full"
|
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" />
|
<i class="pi pi-user text-slate-400" />
|
||||||
<Button
|
<Button
|
||||||
:label="voicePage.selectedVoiceLabel"
|
:label="voicePage.selectedVoiceLabel"
|
||||||
@@ -296,7 +294,7 @@ async function commitRenameAsr(item) {
|
|||||||
|
|
||||||
<div class="mb-3 grid grid-cols-3 gap-2">
|
<div class="mb-3 grid grid-cols-3 gap-2">
|
||||||
<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
|
<Select
|
||||||
v-model="voiceEmotion"
|
v-model="voiceEmotion"
|
||||||
:options="voicePage.voiceEmotions"
|
:options="voicePage.voiceEmotions"
|
||||||
@@ -305,7 +303,7 @@ async function commitRenameAsr(item) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<Select
|
||||||
v-model="voiceLanguage"
|
v-model="voiceLanguage"
|
||||||
:options="voicePage.languages"
|
:options="voicePage.languages"
|
||||||
@@ -314,7 +312,7 @@ async function commitRenameAsr(item) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<InputNumber
|
||||||
v-model="speechRate"
|
v-model="speechRate"
|
||||||
:min="0.5"
|
:min="0.5"
|
||||||
@@ -343,8 +341,8 @@ async function commitRenameAsr(item) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 语音识别表单 -->
|
<!-- 语音识别表单 -->
|
||||||
<div v-else class="rounded-xl border border-[#333] bg-[#141414] p-4">
|
<div v-else class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||||
<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-file-audio mr-1 text-slate-400" />
|
<i class="pi pi-file-audio mr-1 text-slate-400" />
|
||||||
音频/视频文件
|
音频/视频文件
|
||||||
</div>
|
</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
|
<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
|
<button
|
||||||
type="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="
|
:class="
|
||||||
workspaceTab === 'history'
|
workspaceTab === 'history'
|
||||||
? 'border-purple-500 text-white'
|
? 'border-purple-500 text-slate-100'
|
||||||
: 'border-transparent text-gray-400 hover:text-white'
|
: 'border-transparent text-slate-500 hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="workspaceTab = 'history'"
|
@click="workspaceTab = 'history'"
|
||||||
>
|
>
|
||||||
@@ -392,11 +390,11 @@ async function commitRenameAsr(item) {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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="
|
:class="
|
||||||
workspaceTab === 'workspace'
|
workspaceTab === 'workspace'
|
||||||
? 'border-purple-500 text-white'
|
? 'border-purple-500 text-slate-100'
|
||||||
: 'border-transparent text-gray-400 hover:text-white'
|
: 'border-transparent text-slate-500 hover:text-slate-200'
|
||||||
"
|
"
|
||||||
@click="workspaceTab = 'workspace'"
|
@click="workspaceTab = 'workspace'"
|
||||||
>
|
>
|
||||||
@@ -405,9 +403,9 @@ async function commitRenameAsr(item) {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
severity="secondary"
|
severity="danger"
|
||||||
text
|
text
|
||||||
class="text-gray-400 hover:text-red-400"
|
size="small"
|
||||||
title="清空当前列表"
|
title="清空当前列表"
|
||||||
@click="onClearHistory"
|
@click="onClearHistory"
|
||||||
/>
|
/>
|
||||||
@@ -416,7 +414,11 @@ async function commitRenameAsr(item) {
|
|||||||
<p
|
<p
|
||||||
v-if="feedback.message"
|
v-if="feedback.message"
|
||||||
class="mx-4 mt-2 text-sm md:mx-6"
|
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 }}
|
{{ feedback.message }}
|
||||||
</p>
|
</p>
|
||||||
@@ -426,7 +428,7 @@ async function commitRenameAsr(item) {
|
|||||||
<template v-if="sideTab === 'synthesis'">
|
<template v-if="sideTab === 'synthesis'">
|
||||||
<div
|
<div
|
||||||
v-if="synthesisHistory.length"
|
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
|
<Checkbox
|
||||||
:model-value="voicePage.synthesisAllChecked"
|
:model-value="voicePage.synthesisAllChecked"
|
||||||
@@ -444,21 +446,21 @@ async function commitRenameAsr(item) {
|
|||||||
:disabled="!voicePage.checkedSynthesisItems.length"
|
:disabled="!voicePage.checkedSynthesisItems.length"
|
||||||
@click="voicePage.deleteCheckedSynthesis()"
|
@click="voicePage.deleteCheckedSynthesis()"
|
||||||
/>
|
/>
|
||||||
<span class="ml-auto text-sm text-gray-500">
|
<span class="ml-auto text-sm text-slate-500">
|
||||||
共 {{ synthesisHistory.length }} 条
|
共 {{ synthesisHistory.length }} 条
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</p>
|
||||||
<div
|
<div
|
||||||
v-else-if="!synthesisHistory.length"
|
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" />
|
<i class="pi pi-volume-up mb-4 text-5xl text-slate-600" />
|
||||||
<h2 class="text-2xl font-bold text-gray-300">准备好创作了吗?</h2>
|
<h2 class="text-xl font-bold text-slate-300">准备好创作了吗?</h2>
|
||||||
<p class="mt-2 max-w-sm text-gray-500">
|
<p class="mt-2 max-w-sm text-slate-500">
|
||||||
在左侧输入文本并选择声音,开始您的创作之旅。
|
在左侧输入文本并选择声音,开始您的创作之旅。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,16 +469,16 @@ async function commitRenameAsr(item) {
|
|||||||
<div
|
<div
|
||||||
v-for="item in synthesisHistory"
|
v-for="item in synthesisHistory"
|
||||||
:key="item.id"
|
: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="flex flex-wrap items-center gap-2">
|
||||||
<div
|
<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" />
|
<Checkbox v-model="item._check" :binary="true" class="mr-2 mt-1" />
|
||||||
<div v-if="editingId !== item.id" class="min-w-0">
|
<div v-if="editingId !== item.id" class="min-w-0">
|
||||||
<span
|
<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"
|
:title="item.name"
|
||||||
@click="startRename(item)"
|
@click="startRename(item)"
|
||||||
>
|
>
|
||||||
@@ -495,7 +497,7 @@ async function commitRenameAsr(item) {
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
v-if="item.durationSec"
|
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) }}
|
{{ formatDuration(item.durationSec) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -504,7 +506,7 @@ async function commitRenameAsr(item) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="item.sourceText"
|
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>
|
<span>{{ truncateText(item.sourceText) }}</span>
|
||||||
<div class="mt-2 flex gap-1">
|
<div class="mt-2 flex gap-1">
|
||||||
@@ -523,7 +525,7 @@ async function commitRenameAsr(item) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<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) }}
|
{{ formatRelativeTime(item.createdAt) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
@@ -553,7 +555,7 @@ async function commitRenameAsr(item) {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-if="asrHistory.length"
|
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
|
<Checkbox
|
||||||
:model-value="voicePage.asrAllChecked"
|
:model-value="voicePage.asrAllChecked"
|
||||||
@@ -571,17 +573,17 @@ async function commitRenameAsr(item) {
|
|||||||
:disabled="!voicePage.checkedAsrItems.length"
|
:disabled="!voicePage.checkedAsrItems.length"
|
||||||
@click="voicePage.deleteCheckedAsr()"
|
@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>
|
</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
|
<div
|
||||||
v-else-if="!asrHistory.length"
|
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" />
|
<i class="pi pi-microphone mb-4 text-5xl text-slate-600" />
|
||||||
<h2 class="text-2xl font-bold text-gray-300">准备好识别了吗?</h2>
|
<h2 class="text-xl font-bold text-slate-300">准备好识别了吗?</h2>
|
||||||
<p class="mt-2 max-w-sm text-gray-500">
|
<p class="mt-2 max-w-sm text-slate-500">
|
||||||
在左侧上传音频或视频文件,开始您的识别之旅。
|
在左侧上传音频或视频文件,开始您的识别之旅。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -590,16 +592,16 @@ async function commitRenameAsr(item) {
|
|||||||
<div
|
<div
|
||||||
v-for="item in asrHistory"
|
v-for="item in asrHistory"
|
||||||
:key="item.id"
|
: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="flex flex-wrap items-center gap-2">
|
||||||
<div
|
<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" />
|
<Checkbox v-model="item._check" :binary="true" class="mr-2 mt-1" />
|
||||||
<div v-if="editingId !== item.id" class="min-w-0">
|
<div v-if="editingId !== item.id" class="min-w-0">
|
||||||
<span
|
<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)"
|
@click="startRename(item)"
|
||||||
>
|
>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
@@ -616,13 +618,13 @@ async function commitRenameAsr(item) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="item.resultText"
|
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) }}
|
{{ truncateText(item.resultText, 300) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<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) }}
|
{{ formatRelativeTime(item.createdAt) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ const ASSIGN_OPTIONS = [
|
|||||||
{ label: "分配给代理", value: "agent" },
|
{ label: "分配给代理", value: "agent" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CARD_TYPE_OPTIONS = [
|
||||||
|
{ label: "时长", value: "时长" },
|
||||||
|
{ label: "点数", value: "点数" },
|
||||||
|
];
|
||||||
|
|
||||||
const cards = ref([]);
|
const cards = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
@@ -45,7 +50,9 @@ const editingId = ref(null);
|
|||||||
|
|
||||||
const emptyForm = () => ({
|
const emptyForm = () => ({
|
||||||
serial_number: "",
|
serial_number: "",
|
||||||
|
card_type: "时长",
|
||||||
duration_days: 30,
|
duration_days: 30,
|
||||||
|
points_amount: 100,
|
||||||
count: 1,
|
count: 1,
|
||||||
remark: "",
|
remark: "",
|
||||||
assign_type: "none",
|
assign_type: "none",
|
||||||
@@ -59,6 +66,7 @@ const dialogTitle = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isActivated = computed(() => dialogMode.value === "edit" && !!editingActivatedAt.value);
|
const isActivated = computed(() => dialogMode.value === "edit" && !!editingActivatedAt.value);
|
||||||
|
const isDurationType = computed(() => form.value.card_type === "时长");
|
||||||
const editingActivatedAt = ref(null);
|
const editingActivatedAt = ref(null);
|
||||||
|
|
||||||
const ownerNameById = computed(() => {
|
const ownerNameById = computed(() => {
|
||||||
@@ -74,6 +82,13 @@ const assignSelectOptions = computed(() => {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function cardValueLabel(row) {
|
||||||
|
if (row.card_type === "点数") {
|
||||||
|
return row.points_amount != null ? `${row.points_amount} 点` : "—";
|
||||||
|
}
|
||||||
|
return `${row.duration_days} 天`;
|
||||||
|
}
|
||||||
|
|
||||||
function ownerLabel(userId) {
|
function ownerLabel(userId) {
|
||||||
if (userId == null) return "—";
|
if (userId == null) return "—";
|
||||||
return ownerNameById.value.get(userId) ?? `#${userId}`;
|
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;
|
loading.value = true;
|
||||||
|
if (clearFeedback) {
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
|
}
|
||||||
const res = await listAdminCardKeysApi(auth.token, {
|
const res = await listAdminCardKeysApi(auth.token, {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
@@ -164,7 +182,9 @@ function openEditDialog(row) {
|
|||||||
editingActivatedAt.value = row.activated_at;
|
editingActivatedAt.value = row.activated_at;
|
||||||
form.value = {
|
form.value = {
|
||||||
serial_number: row.serial_number,
|
serial_number: row.serial_number,
|
||||||
|
card_type: row.card_type || "时长",
|
||||||
duration_days: row.duration_days,
|
duration_days: row.duration_days,
|
||||||
|
points_amount: row.points_amount ?? 100,
|
||||||
count: 1,
|
count: 1,
|
||||||
remark: row.remark || "",
|
remark: row.remark || "",
|
||||||
assign_type: "none",
|
assign_type: "none",
|
||||||
@@ -193,10 +213,15 @@ function buildAssignPayload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveCard() {
|
async function saveCard() {
|
||||||
|
if (isDurationType.value) {
|
||||||
if (form.value.duration_days < 1) {
|
if (form.value.duration_days < 1) {
|
||||||
showMessage("warn", "时长至少 1 天");
|
showMessage("warn", "时长至少 1 天");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (!form.value.points_amount || form.value.points_amount <= 0) {
|
||||||
|
showMessage("warn", "点数须大于 0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
@@ -209,11 +234,16 @@ async function saveCard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
duration_days: form.value.duration_days,
|
card_type: form.value.card_type,
|
||||||
count: form.value.count,
|
count: form.value.count,
|
||||||
remark: form.value.remark.trim() || null,
|
remark: form.value.remark.trim() || null,
|
||||||
...buildAssignPayload(),
|
...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();
|
const serial = form.value.serial_number.trim();
|
||||||
if (serial) {
|
if (serial) {
|
||||||
if (form.value.count !== 1) {
|
if (form.value.count !== 1) {
|
||||||
@@ -225,10 +255,18 @@ async function saveCard() {
|
|||||||
}
|
}
|
||||||
res = await createAdminCardKeysApi(auth.token, payload);
|
res = await createAdminCardKeysApi(auth.token, payload);
|
||||||
} else {
|
} else {
|
||||||
res = await updateAdminCardKeyApi(auth.token, editingId.value, {
|
const payload = {
|
||||||
duration_days: isActivated.value ? undefined : form.value.duration_days,
|
|
||||||
remark: form.value.remark.trim() || null,
|
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;
|
saving.value = false;
|
||||||
@@ -238,9 +276,15 @@ async function saveCard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wasCreate = dialogMode.value === "create";
|
||||||
|
const successMessage = res.message || (wasCreate ? "卡密已生成" : "保存成功");
|
||||||
|
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
showMessage("success", res.message || "保存成功");
|
if (wasCreate) {
|
||||||
await loadCards();
|
page.value = 1;
|
||||||
|
}
|
||||||
|
await loadCards({ clearFeedback: false });
|
||||||
|
showMessage("success", successMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeCard(row) {
|
async function removeCard(row) {
|
||||||
@@ -369,7 +413,12 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</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">
|
<Column header="OEM" style="width: 7rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ ownerLabel(data.oem_id) }}
|
{{ ownerLabel(data.oem_id) }}
|
||||||
@@ -440,9 +489,29 @@ onMounted(async () => {
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<template v-if="dialogMode === 'create'">
|
<template v-if="dialogMode === 'create'">
|
||||||
<div class="flex flex-col gap-2">
|
<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>
|
<label class="text-xs text-text-muted uppercase">时长(天)</label>
|
||||||
<InputNumber v-model="form.duration_days" :min="1" :max="3650" class="w-full" />
|
<InputNumber v-model="form.duration_days" :min="1" :max="3650" class="w-full" />
|
||||||
</div>
|
</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">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-xs text-text-muted uppercase">生成数量</label>
|
<label class="text-xs text-text-muted uppercase">生成数量</label>
|
||||||
<InputNumber v-model="form.count" :min="1" :max="100" class="w-full" />
|
<InputNumber v-model="form.count" :min="1" :max="100" class="w-full" />
|
||||||
@@ -491,6 +560,17 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<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>
|
<label class="text-xs text-text-muted uppercase">时长(天)</label>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
v-model="form.duration_days"
|
v-model="form.duration_days"
|
||||||
@@ -500,6 +580,17 @@ onMounted(async () => {
|
|||||||
:disabled="isActivated"
|
:disabled="isActivated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ function formatDateTime(value) {
|
|||||||
return d.toLocaleString("zh-CN", { hour12: false });
|
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) {
|
function showMessage(severity, message) {
|
||||||
feedback.value = { severity, message };
|
feedback.value = { severity, message };
|
||||||
}
|
}
|
||||||
@@ -189,7 +196,12 @@ onMounted(loadCards);
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</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="创建时间">
|
<Column field="created_at" header="创建时间">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ formatDateTime(data.created_at) }}
|
{{ formatDateTime(data.created_at) }}
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ function formatDateTime(value) {
|
|||||||
return d.toLocaleString("zh-CN", { hour12: false });
|
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) {
|
function isRowSelectable(row) {
|
||||||
return !row.activated_at;
|
return !row.activated_at;
|
||||||
}
|
}
|
||||||
@@ -295,7 +302,12 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</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">
|
<Column header="代理" style="width: 7rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
{{ ownerLabel(data.agent_id) }}
|
{{ ownerLabel(data.agent_id) }}
|
||||||
|
|||||||
Reference in New Issue
Block a user