fix: 订阅模式登录验证、永久会员类型区分、动态代码HTTP返回值修复、侧边栏滚动位置保持

- 修复订阅模式登录时错误检查余额的问题
- 区分无限余额和永久订阅两种永久会员类型
- 修复动态代码HTTP请求返回值在JS中无法正确访问的问题
- 添加侧边栏滚动位置保持功能
- 移除developer角色相关代码,统一使用admin
- 添加缺失的i18n翻译key
This commit is contained in:
2026-05-01 16:39:31 +08:00
parent c0edb32614
commit ea8ffb6c74
69 changed files with 554 additions and 369 deletions
@@ -1,14 +1,57 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { ref, onMounted, onUnmounted, nextTick } from "vue"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const scrollRef = ref<HTMLElement | null>(null)
const scrollPositionKey = 'sidebar-scroll-position'
let saveTimeout: ReturnType<typeof setTimeout> | null = null
function saveScrollPosition() {
if (scrollRef.value) {
sessionStorage.setItem(scrollPositionKey, scrollRef.value.scrollTop.toString())
}
}
function handleScroll() {
if (saveTimeout) {
clearTimeout(saveTimeout)
}
saveTimeout = setTimeout(saveScrollPosition, 50)
}
onMounted(() => {
const saved = sessionStorage.getItem(scrollPositionKey)
if (saved) {
nextTick(() => {
if (scrollRef.value) {
scrollRef.value.scrollTop = parseFloat(saved)
}
})
}
if (scrollRef.value) {
scrollRef.value.addEventListener('scroll', handleScroll, { passive: true })
}
})
onUnmounted(() => {
if (scrollRef.value) {
scrollRef.value.removeEventListener('scroll', handleScroll)
}
if (saveTimeout) {
clearTimeout(saveTimeout)
}
})
</script>
<template>
<div
ref="scrollRef"
data-slot="sidebar-content"
data-sidebar="content"
:class="cn('flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', props.class)"
+99 -14
View File
@@ -1,22 +1,107 @@
<script setup lang="ts">
import Footer from '@/components/marketing-layout/the-footer.vue'
import Header from '@/components/marketing-layout/the-header.vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import AdminSidebar from '@/components/admin-sidebar/index.vue'
import LanguageChange from '@/components/language-change.vue'
import ThemePopover from '@/components/custom-theme/theme-popover.vue'
import ToggleTheme from '@/components/toggle-theme.vue'
const router = useRouter()
const breadcrumbs = computed(() => {
const path = router.currentRoute.value.path
const parts = path.split('/').filter(Boolean)
const crumbs = [{ title: '控制台', path: '/admin' }]
const titleMap: Record<string, string> = {
applications: '应用管理',
cards: '卡密管理',
users: '用户管理',
devices: '设备管理',
sessions: '在线实例',
announcements: '公告管理',
versions: '版本管理',
'card-types': '卡类管理',
'agent-apps': '代理授权',
agents: '代理管理',
finance: '财务管理',
logs: '日志记录',
tickets: '工单系统',
'cloud-constants': '云端常量',
'cloud-variables': '云端变量',
'cloud-function': '云端函数',
'risk-control': '风控管理',
extension: '扩展配置',
profile: '个人中心',
settings: '基本设置',
security: '安全设置',
email: '邮箱设置',
create: '创建',
edit: '编辑',
recharge: '充值',
request: '申请授权',
invite: '邀请授权',
}
let currentPath = '/admin'
for (let i = 1; i < parts.length; i++) {
const part = parts[i]
currentPath += `/${part}`
if (titleMap[part]) {
crumbs.push({
title: titleMap[part],
path: currentPath,
})
}
else if (!isNaN(Number(part)) && i === parts.length - 1) {
crumbs.push({
title: '详情',
path: currentPath,
})
}
}
return crumbs
})
</script>
<template>
<div class="min-h-screen flex flex-col">
<Header />
<UiSidebarProvider>
<AdminSidebar />
<UiSidebarInset>
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<UiSidebarTrigger class="-ml-1" />
<UiSeparator orientation="vertical" class="mr-2 h-4" />
<UiBreadcrumb>
<UiBreadcrumbList>
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
<UiBreadcrumbItem v-if="index < breadcrumbs.length - 1">
<UiBreadcrumbLink as-child>
<router-link :to="crumb.path">
{{ crumb.title }}
</router-link>
</UiBreadcrumbLink>
</UiBreadcrumbItem>
<UiBreadcrumbItem v-else>
<UiBreadcrumbPage>{{ crumb.title }}</UiBreadcrumbPage>
</UiBreadcrumbItem>
<UiBreadcrumbSeparator v-if="index < breadcrumbs.length - 1" />
</template>
</UiBreadcrumbList>
</UiBreadcrumb>
<main class="flex-1">
<div class="relative bg-background">
<div class="py-8 lg:py-12">
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<router-view />
</div>
<div class="ml-auto flex items-center gap-2">
<LanguageChange />
<ToggleTheme />
<ThemePopover />
</div>
</div>
</main>
</header>
<Footer />
</div>
<main class="flex-1 overflow-auto p-4 md:p-6">
<router-view />
</main>
</UiSidebarInset>
</UiSidebarProvider>
</template>
-107
View File
@@ -1,107 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import AdminSidebar from '@/components/admin-sidebar/index.vue'
import LanguageChange from '@/components/language-change.vue'
import ThemePopover from '@/components/custom-theme/theme-popover.vue'
import ToggleTheme from '@/components/toggle-theme.vue'
const router = useRouter()
const breadcrumbs = computed(() => {
const path = router.currentRoute.value.path
const parts = path.split('/').filter(Boolean)
const crumbs = [{ title: '控制台', path: '/admin' }]
const titleMap: Record<string, string> = {
applications: '应用管理',
cards: '卡密管理',
users: '用户管理',
devices: '设备管理',
sessions: '在线实例',
announcements: '公告管理',
versions: '版本管理',
'card-types': '卡类管理',
'agent-apps': '代理授权',
agents: '代理管理',
finance: '财务管理',
logs: '日志记录',
tickets: '工单系统',
'cloud-constants': '云端常量',
'cloud-variables': '云端变量',
'cloud-function': '云端函数',
'risk-control': '风控管理',
extension: '扩展配置',
profile: '个人中心',
settings: '基本设置',
security: '安全设置',
email: '邮箱设置',
create: '创建',
edit: '编辑',
recharge: '充值',
request: '申请授权',
invite: '邀请授权',
}
let currentPath = '/admin'
for (let i = 1; i < parts.length; i++) {
const part = parts[i]
currentPath += `/${part}`
if (titleMap[part]) {
crumbs.push({
title: titleMap[part],
path: currentPath,
})
}
else if (!isNaN(Number(part)) && i === parts.length - 1) {
crumbs.push({
title: '详情',
path: currentPath,
})
}
}
return crumbs
})
</script>
<template>
<UiSidebarProvider>
<AdminSidebar />
<UiSidebarInset>
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<UiSidebarTrigger class="-ml-1" />
<UiSeparator orientation="vertical" class="mr-2 h-4" />
<UiBreadcrumb>
<UiBreadcrumbList>
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
<UiBreadcrumbItem v-if="index < breadcrumbs.length - 1">
<UiBreadcrumbLink as-child>
<router-link :to="crumb.path">
{{ crumb.title }}
</router-link>
</UiBreadcrumbLink>
</UiBreadcrumbItem>
<UiBreadcrumbItem v-else>
<UiBreadcrumbPage>{{ crumb.title }}</UiBreadcrumbPage>
</UiBreadcrumbItem>
<UiBreadcrumbSeparator v-if="index < breadcrumbs.length - 1" />
</template>
</UiBreadcrumbList>
</UiBreadcrumb>
<div class="ml-auto flex items-center gap-2">
<LanguageChange />
<ToggleTheme />
<ThemePopover />
</div>
</header>
<main class="flex-1 overflow-auto p-4 md:p-6">
<router-view />
</main>
</UiSidebarInset>
</UiSidebarProvider>
</template>
@@ -22,8 +22,8 @@ export const agentAppSchema = z.object({
export const agentRequestSchema = z.object({
id: z.number(),
developer_id: z.number(),
developer_name: z.string(),
admin_id: z.number(),
admin_name: z.string(),
agent_id: z.number(),
agent_name: z.string(),
application_id: z.number(),
@@ -66,8 +66,8 @@ const allItems = computed<CombinedItem[]>(() => {
.filter(item => item.status === 'pending')
.map(item => ({
id: item.id,
agent_id: item.developer_id,
agent_name: item.developer_name,
agent_id: item.admin_id,
agent_name: item.admin_name,
application_id: item.application_id,
app_name: item.app_name,
status: item.status,
@@ -13,7 +13,7 @@ const saving = ref(false)
const applications = ref<Array<{ id: number, name: string }>>([])
const form = ref({
developer_id: '',
admin_id: '',
application_id: '',
message: '',
})
@@ -35,7 +35,7 @@ async function fetchApplications() {
}
async function handleSubmit() {
if (!form.value.developer_id) {
if (!form.value.admin_id) {
toast.error('请输入管理员ID')
return
}
@@ -54,7 +54,7 @@ async function handleSubmit() {
'Content-Type': 'application/json',
},
body: JSON.stringify({
developer_id: Number(form.value.developer_id),
admin_id: Number(form.value.admin_id),
application_id: Number(form.value.application_id),
message: form.value.message,
}),
@@ -109,12 +109,12 @@ onMounted(() => {
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="developer_id">
<UiLabel for="admin_id">
管理员ID
</UiLabel>
<UiInput
id="developer_id"
v-model="form.developer_id"
id="admin_id"
v-model="form.admin_id"
type="number"
placeholder="请输入管理员ID"
/>
+16 -16
View File
@@ -211,13 +211,13 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
{{ t('admin.cardTypes.create.rechargeConfig') || '充值配置' }}
{{ t('admin.cardTypes.create.rechargeConfig') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.cardTypes.create.rechargeConfigDesc') || '设置卡密的充值类型和金额' }}</UiCardDescription>
<UiCardDescription>{{ t('admin.cardTypes.create.rechargeConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>{{ t('admin.cardTypes.create.rechargeType') || '充值类型' }}</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.rechargeType') }}</UiLabel>
<UiRadioGroup v-model="form.recharge_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
@@ -227,10 +227,10 @@ onMounted(() => {
<UiRadioGroupItem value="balance" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('admin.cardTypes.create.balanceRecharge') || '余额充值' }}
{{ t('admin.cardTypes.create.balanceRecharge') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('admin.cardTypes.create.balanceRechargeDesc') || '充值账户余额,适用于计时/计次模式' }}
{{ t('admin.cardTypes.create.balanceRechargeDesc') }}
</p>
</div>
</div>
@@ -243,10 +243,10 @@ onMounted(() => {
<UiRadioGroupItem value="subscription" class="mt-0.5" />
<div>
<p class="font-medium">
{{ t('admin.cardTypes.create.subscriptionRecharge') || '订阅充值' }}
{{ t('admin.cardTypes.create.subscriptionRecharge') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('admin.cardTypes.create.subscriptionRechargeDesc') || '充值会员时长,适用于订阅模式' }}
{{ t('admin.cardTypes.create.subscriptionRechargeDesc') }}
</p>
</div>
</div>
@@ -272,10 +272,10 @@ onMounted(() => {
<div class="flex items-center justify-between pt-4 border-t">
<div>
<p class="font-medium">
{{ t('admin.cardTypes.create.permanent') || '永久会员' }}
{{ t('admin.cardTypes.create.permanentLabel') }}
</p>
<p class="text-sm text-muted-foreground">
{{ t('admin.cardTypes.create.permanentDesc') || '开启后,使用此卡密的用户将成为永久会员' }}
{{ t('admin.cardTypes.create.permanentLabelDesc') }}
</p>
</div>
<UiSwitch v-model="form.is_permanent" />
@@ -283,7 +283,7 @@ onMounted(() => {
<div v-if="!form.is_permanent" class="space-y-4 pt-4 border-t">
<div v-if="form.recharge_type === 'balance'" class="space-y-2">
<UiLabel>{{ t('admin.cardTypes.create.rechargeAmount') || '充值金额' }}</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.rechargeAmount') }}</UiLabel>
<UiNumberField v-model="form.value" :min="0.01" :step="0.01" class="max-w-[200px]">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
@@ -292,14 +292,14 @@ onMounted(() => {
</UiNumberFieldContent>
</UiNumberField>
<p class="text-xs text-muted-foreground">
{{ t('admin.cardTypes.create.rechargeAmountDesc') || '用户充值后获得的余额数量' }}
{{ t('admin.cardTypes.create.rechargeAmountDesc') }}
</p>
</div>
<div v-if="form.recharge_type === 'subscription'" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel>{{ t('admin.cardTypes.create.rechargeDuration') || '充值时长' }}</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.rechargeDuration') }}</UiLabel>
<UiNumberField v-model="form.value" :min="1" :step="1">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
@@ -310,7 +310,7 @@ onMounted(() => {
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.cardTypes.create.durationUnit') || '时长单位' }}</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.durationUnit') }}</UiLabel>
<UiSelect v-model="form.value_unit">
<UiSelectTrigger>
<UiSelectValue />
@@ -360,8 +360,8 @@ onMounted(() => {
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.rechargeType') || '充值类型' }}</span>
<span>{{ form.recharge_type === 'balance' ? (t('admin.cardTypes.create.balanceRecharge') || '余额充值') : (t('admin.cardTypes.create.subscriptionRecharge') || '订阅充值') }}</span>
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.rechargeType') }}</span>
<span>{{ form.recharge_type === 'balance' ? t('admin.cardTypes.create.balanceRecharge') : t('admin.cardTypes.create.subscriptionRecharge') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewPrice') }}</span>
@@ -386,7 +386,7 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('admin.cardTypes.create.saveBtn') || '保存修改' }}
{{ t('admin.cardTypes.create.saveBtn') }}
</UiButton>
<UiButton
variant="outline"
+20 -20
View File
@@ -178,13 +178,13 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:credit-card" class="size-5" />
充值配置
{{ t('admin.cardTypes.create.rechargeConfig') }}
</UiCardTitle>
<UiCardDescription>设置卡密的充值类型和金额</UiCardDescription>
<UiCardDescription>{{ t('admin.cardTypes.create.rechargeConfigDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel>充值类型</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.rechargeType') }}</UiLabel>
<UiRadioGroup v-model="form.recharge_type" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
class="flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-colors"
@@ -194,10 +194,10 @@ onMounted(() => {
<UiRadioGroupItem value="balance" class="mt-0.5" />
<div>
<p class="font-medium">
余额充值
{{ t('admin.cardTypes.create.balanceRecharge') }}
</p>
<p class="text-sm text-muted-foreground">
充值账户余额适用于计时/计次模式
{{ t('admin.cardTypes.create.balanceRechargeDesc') }}
</p>
</div>
</div>
@@ -210,10 +210,10 @@ onMounted(() => {
<UiRadioGroupItem value="subscription" class="mt-0.5" />
<div>
<p class="font-medium">
订阅充值
{{ t('admin.cardTypes.create.subscriptionRecharge') }}
</p>
<p class="text-sm text-muted-foreground">
充值会员时长适用于订阅模式
{{ t('admin.cardTypes.create.subscriptionRechargeDesc') }}
</p>
</div>
</div>
@@ -239,10 +239,10 @@ onMounted(() => {
<div class="flex items-center justify-between pt-4 border-t">
<div>
<p class="font-medium">
永久会员
{{ t('admin.cardTypes.create.permanentLabel') }}
</p>
<p class="text-sm text-muted-foreground">
开启后使用此卡密的用户将成为永久会员
{{ t('admin.cardTypes.create.permanentLabelDesc') }}
</p>
</div>
<UiSwitch v-model="form.is_permanent" />
@@ -250,7 +250,7 @@ onMounted(() => {
<div v-if="!form.is_permanent" class="space-y-4 pt-4 border-t">
<div v-if="form.recharge_type === 'balance'" class="space-y-2">
<UiLabel>充值金额</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.rechargeAmount') }}</UiLabel>
<UiNumberField v-model="form.value" :min="0.01" :step="0.01" class="max-w-[200px]">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
@@ -259,14 +259,14 @@ onMounted(() => {
</UiNumberFieldContent>
</UiNumberField>
<p class="text-xs text-muted-foreground">
用户充值后获得的余额数量
{{ t('admin.cardTypes.create.rechargeAmountDesc') }}
</p>
</div>
<div v-if="form.recharge_type === 'subscription'" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<UiLabel>充值时长</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.rechargeDuration') }}</UiLabel>
<UiNumberField v-model="form.value" :min="1" :step="1">
<UiNumberFieldContent>
<UiNumberFieldDecrement />
@@ -277,26 +277,26 @@ onMounted(() => {
</div>
<div class="space-y-2">
<UiLabel>时长单位</UiLabel>
<UiLabel>{{ t('admin.cardTypes.create.durationUnit') }}</UiLabel>
<UiSelect v-model="form.value_unit">
<UiSelectTrigger>
<UiSelectValue />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="minute">
分钟
{{ t('admin.cardTypes.units.minute') }}
</UiSelectItem>
<UiSelectItem value="hour">
小时
{{ t('admin.cardTypes.units.hour') }}
</UiSelectItem>
<UiSelectItem value="day">
{{ t('admin.cardTypes.units.day') }}
</UiSelectItem>
<UiSelectItem value="month">
{{ t('admin.cardTypes.units.month') }}
</UiSelectItem>
<UiSelectItem value="year">
{{ t('admin.cardTypes.units.year') }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
@@ -327,8 +327,8 @@ onMounted(() => {
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">充值类型</span>
<span>{{ form.recharge_type === 'balance' ? '余额充值' : '订阅充值' }}</span>
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.rechargeType') }}</span>
<span>{{ form.recharge_type === 'balance' ? t('admin.cardTypes.create.balanceRecharge') : t('admin.cardTypes.create.subscriptionRecharge') }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.cardTypes.create.previewPrice') }}</span>
@@ -389,7 +389,7 @@ onMounted(() => {
<div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.developerOnly') }}
{{ t('admin.cloudVariables.create.adminOnly') }}
</p>
</div>
</div>
@@ -467,7 +467,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.writePermission') }}</span>
<UiBadge variant="secondary">
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.adminOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
</UiBadge>
</div>
<div class="flex justify-between text-sm items-center">
@@ -74,7 +74,7 @@ export function getColumns(actions: {
if (permission === 'user') {
return h(Badge, { variant: 'outline', class: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' }, () => t('admin.cloudVariables.create.userWritable'))
}
return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.create.developerOnly'))
return h(Badge, { variant: 'outline' }, () => t('admin.cloudVariables.create.adminOnly'))
},
},
{
@@ -364,11 +364,11 @@ onMounted(() => {
<div class="flex items-center gap-2">
<Icon icon="lucide:lock" class="size-4 text-primary" />
<p class="font-medium">
{{ t('admin.cloudVariables.create.developerOnly') }}
{{ t('admin.cloudVariables.create.adminOnly') }}
</p>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ form.scope === 'app' ? t('admin.cloudVariables.create.developerOnlyAppDesc') : t('admin.cloudVariables.create.developerOnlyDesc') }}
{{ form.scope === 'app' ? t('admin.cloudVariables.create.adminOnlyAppDesc') : t('admin.cloudVariables.create.adminOnlyDesc') }}
</p>
</div>
</div>
@@ -451,7 +451,7 @@ onMounted(() => {
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.cloudVariables.create.writePermission') }}</span>
<UiBadge variant="secondary">
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.developerOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
{{ form.write_permission === 'admin' ? t('admin.cloudVariables.create.adminOnly') : form.write_permission === 'app_user' ? '应用用户可写' : t('admin.cloudVariables.create.userWritable') }}
</UiBadge>
</div>
<div class="flex justify-between text-sm items-center">
+1 -1
View File
@@ -479,7 +479,7 @@ onUnmounted(() => {
<template>
<BasicPage
:title="t('admin.dashboard.title')"
:description="`${t('admin.welcomeBack')}, ${currentUser?.username || t('admin.developer')}`"
:description="`${t('admin.welcomeBack')}, ${currentUser?.username || t('admin.adminRole')}`"
>
<div v-if="loading" class="flex items-center justify-center py-12">
<div class="text-center">
+26 -26
View File
@@ -42,7 +42,7 @@ async function fetchUser() {
}
catch (error) {
console.error('获取用户信息失败:', error)
toast.error(t('admin.users.editFailed'))
toast.error(t('admin.users.edit.failed'))
}
finally {
loading.value = false
@@ -68,15 +68,15 @@ const selectedApplication = computed(() => {
async function handleSave() {
if (!form.value.username) {
toast.error(t('admin.users.create.usernameRequired'))
toast.error(t('admin.users.edit.usernameRequired'))
return
}
if (!form.value.email) {
toast.error(t('admin.users.create.emailRequired'))
toast.error(t('admin.users.edit.emailRequired'))
return
}
if (!form.value.application_id) {
toast.error(t('admin.users.create.applicationRequired'))
toast.error(t('admin.users.edit.applicationRequired'))
return
}
@@ -93,12 +93,12 @@ async function handleSave() {
}
await api.put(`/dev/app-users/${userId.value}`, payload)
toast.success(t('admin.users.editSuccess'))
toast.success(t('admin.users.edit.success'))
router.push('/admin/users')
}
catch (error: any) {
console.error('更新用户失败:', error)
toast.error(error.message || t('admin.users.editFailed'))
toast.error(error.message || t('admin.users.edit.failed'))
}
finally {
saving.value = false
@@ -113,11 +113,11 @@ onMounted(() => {
<template>
<BasicPage
:title="t('admin.users.edit')"
:description="t('admin.users.create.basicInfoDesc')"
:title="t('admin.users.edit.title')"
:description="t('admin.users.edit.basicInfoDesc')"
:breadcrumbs="[
{ title: t('admin.users.title'), href: '/admin/users' },
{ title: t('admin.users.edit') },
{ title: t('admin.users.edit.title') },
]"
sticky
>
@@ -132,18 +132,18 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:user-plus" class="size-5" />
{{ t('admin.users.create.basicInfo') }}
{{ t('admin.users.edit.basicInfo') }}
</UiCardTitle>
<UiCardDescription>{{ t('admin.users.create.basicInfoDesc') }}</UiCardDescription>
<UiCardDescription>{{ t('admin.users.edit.basicInfoDesc') }}</UiCardDescription>
</UiCardHeader>
<UiCardContent class="space-y-6">
<div class="space-y-2">
<UiLabel for="application">
{{ t('admin.users.create.application') }}
{{ t('admin.users.edit.application') }}
</UiLabel>
<UiSelect v-model="form.application_id">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.users.create.selectApplication')" />
<UiSelectValue :placeholder="t('admin.users.edit.selectApplication')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
@@ -155,25 +155,25 @@ onMounted(() => {
<div class="space-y-2">
<UiLabel for="username">
{{ t('admin.users.create.username') }}
{{ t('admin.users.edit.username') }}
</UiLabel>
<UiInput id="username" v-model="form.username" :placeholder="t('admin.users.create.usernamePlaceholder')" />
<UiInput id="username" v-model="form.username" :placeholder="t('admin.users.edit.usernamePlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel for="email">
{{ t('admin.users.create.email') }}
{{ t('admin.users.edit.email') }}
</UiLabel>
<UiInput id="email" v-model="form.email" type="email" :placeholder="t('admin.users.create.emailPlaceholder')" />
<UiInput id="email" v-model="form.email" type="email" :placeholder="t('admin.users.edit.emailPlaceholder')" />
</div>
<div class="space-y-2">
<UiLabel for="password">
{{ t('admin.users.create.password') }}
{{ t('admin.users.edit.password') }}
</UiLabel>
<UiInput id="password" v-model="form.password" type="password" :placeholder="t('admin.users.create.passwordPlaceholder')" />
<UiInput id="password" v-model="form.password" type="password" :placeholder="t('admin.users.edit.passwordPlaceholder')" />
<p class="text-xs text-muted-foreground">
留空则不修改密码
{{ t('admin.users.edit.passwordHint') }}
</p>
</div>
</UiCardContent>
@@ -185,21 +185,21 @@ onMounted(() => {
<UiCardHeader>
<UiCardTitle class="flex items-center gap-2">
<Icon icon="lucide:eye" class="size-5" />
{{ t('admin.users.create.preview') }}
{{ t('admin.users.edit.preview') }}
</UiCardTitle>
</UiCardHeader>
<UiCardContent class="space-y-4">
<div class="space-y-3">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.app') }}</span>
<span class="text-muted-foreground">{{ t('admin.users.edit.app') }}</span>
<span>{{ selectedApplication?.name || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.usernameLabel') }}</span>
<span class="text-muted-foreground">{{ t('admin.users.edit.usernameLabel') }}</span>
<span>{{ form.username || '-' }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.users.create.emailLabel') }}</span>
<span class="text-muted-foreground">{{ t('admin.users.edit.emailLabel') }}</span>
<span>{{ form.email || '-' }}</span>
</div>
</div>
@@ -217,14 +217,14 @@ onMounted(() => {
>
<Icon v-if="saving" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
<Icon v-else icon="lucide:check" class="mr-2 h-4 w-4" />
{{ t('admin.users.create.saveBtn') || '保存修改' }}
{{ t('admin.users.edit.saveBtn') }}
</UiButton>
<UiButton
variant="outline"
class="w-full"
@click="router.back()"
>
{{ t('admin.users.create.cancel') }}
{{ t('admin.users.edit.cancel') }}
</UiButton>
</div>
</UiCardContent>
+2 -2
View File
@@ -92,8 +92,8 @@ onMounted(() => {
<template>
<BasicPage
title="创建用户"
description="为应用创建新的用户账号,设置基本信息和应用关联"
:title="t('admin.users.create.title')"
:description="t('admin.users.create.description')"
:breadcrumbs="[
{ title: t('admin.users.title'), href: '/admin/users' },
{ title: t('admin.users.create.title') },
+71 -11
View File
@@ -27,6 +27,7 @@
"ban": "Ban",
"unban": "Unban",
"cancel": "Cancel",
"confirm": "Confirm",
"refresh": "Refresh",
"loading": "Loading...",
"totalRecords": "{count} records in total",
@@ -194,7 +195,7 @@
"activeRate": "Active Rate",
"searchPlaceholder": "Search username, email, device ID...",
"allRoles": "All Roles",
"developer": "Admin",
"admin": "Admin",
"agent": "Agent",
"allStatus": "All Status",
"active": "Active",
@@ -219,6 +220,15 @@
"password": "Password",
"passwordPlaceholder": "Leave empty to keep current",
"passwordHint": "Leave empty to keep current password",
"preview": "Preview",
"app": "Application",
"usernameLabel": "Username",
"emailLabel": "Email",
"saveBtn": "Save Changes",
"cancel": "Cancel",
"usernameRequired": "Please enter username",
"emailRequired": "Please enter email",
"applicationRequired": "Please select application",
"deviceId": "Device ID",
"deviceIdPlaceholder": "Device ID (optional)",
"preview": "Preview",
@@ -259,6 +269,35 @@
"balanceUpdateFailed": "Failed to update",
"invalidAmount": "Please enter a valid value"
},
"create": {
"title": "Create User",
"description": "Create a new user account for an application with basic information and application association",
"basicInfo": "Basic Information",
"basicInfoDesc": "Fill in user basic information",
"application": "Application",
"selectApplication": "Select application",
"username": "Username",
"usernamePlaceholder": "Enter username",
"email": "Email",
"emailPlaceholder": "Enter email",
"password": "Password",
"passwordPlaceholder": "Enter password",
"preview": "Preview",
"app": "Application",
"usernameLabel": "Username",
"emailLabel": "Email",
"submit": "Create User",
"cancel": "Cancel",
"usernameRequired": "Please enter username",
"emailRequired": "Please enter email",
"passwordRequired": "Please enter password",
"applicationRequired": "Please select application",
"success": "User created successfully",
"failed": "Failed to create user",
"saveBtn": "Save Changes"
},
"editFailed": "Failed to load user info",
"editSuccess": "User updated successfully",
"resetPassword": "Reset Password",
"resetPasswordDesc": "Set a new password for user \"{username}\"",
"newPassword": "New Password",
@@ -561,7 +600,7 @@
"totalOrders": "Total Orders",
"totalRevenue": "Total Revenue",
"onlineUsers": "Online Users",
"totalDevelopers": "Admins",
"totalAdmins": "Admins",
"totalApps": "Applications",
"totalVisitors": "Visitors",
"totalKeys": "Keys",
@@ -580,7 +619,7 @@
"loading": "Loading...",
"charts": {
"userGrowth": "User Growth",
"userGrowthDesc": "Showing user and developer growth trends",
"userGrowthDesc": "Showing user and admin growth trends",
"orderTrend": "Order Trend",
"orderTrendDesc": "Showing order count and revenue trends",
"selectTimeRange": "Select time range",
@@ -900,7 +939,7 @@
}
},
"welcomeBack": "Welcome back",
"developer": "Admin",
"adminRole": "Admin",
"loading": "Loading",
"totalApplications": "Total Applications",
"totalCards": "Total Cards",
@@ -995,7 +1034,7 @@
"loginPolicy": "Login Policy",
"loginPolicyDesc": "Set user login verification policy",
"looseMode": "Loose Mode",
"looseModeDesc": "Expired users can login, developer controls features",
"looseModeDesc": "Expired users can login, admin controls features",
"strictMode": "Strict Mode",
"strictModeDesc": "Must be unexpired to login",
"hybridMode": "Hybrid Mode",
@@ -1081,8 +1120,11 @@
"batchUpdateSuccess": "Batch updated successfully",
"batchUpdateFailed": "Failed to batch update",
"select": "Select",
"editFailed": "Failed to load card type",
"editSuccess": "Card type updated successfully",
"create": {
"title": "Create Card Type",
"description": "Create a new card type with basic information and recharge configuration",
"basicInfo": "Basic Information",
"basicInfoDesc": "Fill in the basic information of the card type",
"billingConfig": "Billing Configuration",
@@ -1127,7 +1169,22 @@
"createFailed": "Failed to create",
"saveSuccess": "Saved successfully",
"saveFailed": "Failed to save",
"editTitle": "Edit Card Type"
"editTitle": "Edit Card Type",
"rechargeConfig": "Recharge Configuration",
"rechargeConfigDesc": "Set card recharge type and amount",
"rechargeType": "Recharge Type",
"balanceRecharge": "Balance Recharge",
"balanceRechargeDesc": "Recharge account balance, suitable for time/count mode",
"subscriptionRecharge": "Subscription Recharge",
"subscriptionRechargeDesc": "Recharge membership duration, suitable for subscription mode",
"permanentLabel": "Permanent Member",
"permanentLabelDesc": "When enabled, users with this card will become permanent members",
"rechargeAmount": "Recharge Amount",
"rechargeAmountDesc": "Balance amount user receives after recharge",
"rechargeDuration": "Recharge Duration",
"durationUnit": "Duration Unit",
"saveBtn": "Save Changes",
"passwordHint": "Leave empty to keep current password"
},
"columns": {
"cardKey": "Card Key",
@@ -1872,9 +1929,9 @@
"permissionTitle": "Write Permission",
"permissionDesc": "Set who can modify this variable's value",
"writePermission": "Write Permission",
"developerOnly": "Developer Only",
"developerOnlyDesc": "Only developers can modify this variable's value via API or dashboard",
"developerOnlyAppDesc": "Only developers can modify this global variable's value via API or dashboard",
"adminOnly": "Admin Only",
"adminOnlyDesc": "Only admins can modify this variable's value via API or dashboard",
"adminOnlyAppDesc": "Only admins can modify this global variable's value via API or dashboard",
"userWritable": "User Writable",
"userWritableDesc": "Users can modify their own variable values via client API",
"userWritableAppDesc": "Users can modify this global variable value via client API, changes affect all users",
@@ -1921,6 +1978,9 @@
"batchUpdateSuccess": "Batch update successful",
"batchUpdateFailed": "Batch update failed",
"select": "Select",
"edit": "Edit",
"editSuccess": "Updated successfully",
"editFailed": "Failed to fetch cloud function",
"columns": {
"name": "Name",
"application": "Application",
@@ -1951,6 +2011,7 @@
"status": "Status",
"preview": "Preview",
"submitBtn": "Submit",
"saveBtn": "Save",
"cancelBtn": "Cancel",
"submitSuccess": "Created successfully",
"submitFailed": "Failed to create"
@@ -2248,9 +2309,8 @@
"smsSettings": "SMS Service",
"paymentSettings": "Payment Settings",
"profile": "Profile",
"developerDashboard": "Admin Dashboard",
"agentDashboard": "Agent Dashboard",
"adminDashboard": "Admin Dashboard",
"agentDashboard": "Agent Dashboard",
"logout": "Logout",
"login": "Login",
"register": "Register",
+69 -9
View File
@@ -27,6 +27,7 @@
"ban": "封禁",
"unban": "解封",
"cancel": "取消",
"confirm": "确认",
"refresh": "刷新",
"loading": "加载中...",
"totalRecords": "共 {count} 条记录",
@@ -194,7 +195,7 @@
"activeRate": "活跃率",
"searchPlaceholder": "搜索用户名、邮箱、设备指纹...",
"allRoles": "全部角色",
"developer": "管理员",
"admin": "管理员",
"agent": "代理商",
"allStatus": "全部状态",
"active": "正常",
@@ -219,6 +220,15 @@
"password": "密码",
"passwordPlaceholder": "留空则不修改",
"passwordHint": "留空则不修改密码",
"preview": "预览",
"app": "应用",
"usernameLabel": "用户名",
"emailLabel": "邮箱",
"saveBtn": "保存修改",
"cancel": "取消",
"usernameRequired": "请输入用户名",
"emailRequired": "请输入邮箱",
"applicationRequired": "请选择应用",
"deviceId": "设备指纹",
"deviceIdPlaceholder": "设备指纹(可选)",
"preview": "预览",
@@ -259,6 +269,35 @@
"balanceUpdateFailed": "更新失败",
"invalidAmount": "请输入有效的数值"
},
"create": {
"title": "创建用户",
"description": "为应用创建新的用户账号,设置基本信息和应用关联",
"basicInfo": "基本信息",
"basicInfoDesc": "填写用户的基本信息",
"application": "所属应用",
"selectApplication": "请选择应用",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"email": "邮箱",
"emailPlaceholder": "请输入邮箱",
"password": "密码",
"passwordPlaceholder": "请输入密码",
"preview": "预览",
"app": "应用",
"usernameLabel": "用户名",
"emailLabel": "邮箱",
"submit": "创建用户",
"cancel": "取消",
"usernameRequired": "请输入用户名",
"emailRequired": "请输入邮箱",
"passwordRequired": "请输入密码",
"applicationRequired": "请选择应用",
"success": "创建成功",
"failed": "创建失败",
"saveBtn": "保存修改"
},
"editFailed": "获取用户信息失败",
"editSuccess": "更新用户成功",
"resetPassword": "重置密码",
"resetPasswordDesc": "为用户「{username}」设置新密码",
"newPassword": "新密码",
@@ -562,7 +601,7 @@
"totalOrders": "总订单",
"totalRevenue": "总收入",
"onlineUsers": "在线用户",
"totalDevelopers": "管理员",
"totalAdmins": "管理员",
"totalApps": "应用",
"totalVisitors": "访客",
"totalKeys": "卡密",
@@ -901,7 +940,7 @@
}
},
"welcomeBack": "欢迎回来",
"developer": "管理员",
"adminRole": "管理员",
"loading": "加载中",
"totalApplications": "应用总数",
"totalCards": "卡密总数",
@@ -1355,8 +1394,11 @@
"batchUpdateSuccess": "批量更新成功",
"batchUpdateFailed": "批量更新失败",
"select": "选择",
"editFailed": "获取卡类信息失败",
"editSuccess": "更新卡类成功",
"create": {
"title": "创建卡类",
"description": "创建新的卡密类型,设置基本信息和充值配置",
"basicInfo": "基本信息",
"basicInfoDesc": "填写卡类的基本信息",
"billingConfig": "充值配置",
@@ -1390,7 +1432,22 @@
"createFailed": "创建失败",
"saveSuccess": "保存成功",
"saveFailed": "保存失败",
"editTitle": "编辑卡类"
"editTitle": "编辑卡类",
"rechargeConfig": "充值配置",
"rechargeConfigDesc": "设置卡密的充值类型和金额",
"rechargeType": "充值类型",
"balanceRecharge": "余额充值",
"balanceRechargeDesc": "充值账户余额,适用于计时/计次模式",
"subscriptionRecharge": "订阅充值",
"subscriptionRechargeDesc": "充值会员时长,适用于订阅模式",
"permanentLabel": "永久会员",
"permanentLabelDesc": "开启后,使用此卡密的用户将成为永久会员",
"rechargeAmount": "充值金额",
"rechargeAmountDesc": "用户充值后获得的余额数量",
"rechargeDuration": "充值时长",
"durationUnit": "时长单位",
"saveBtn": "保存修改",
"passwordHint": "留空则不修改密码"
},
"columns": {
"name": "类型名称",
@@ -1859,9 +1916,9 @@
"permissionTitle": "写入权限",
"permissionDesc": "设置谁可以修改此变量的值",
"writePermission": "写入权限",
"developerOnly": "仅管理员",
"developerOnlyDesc": "只有管理员可以通过API或后台修改此变量的值",
"developerOnlyAppDesc": "只有管理员可以通过API或后台修改此全局变量的值",
"adminOnly": "仅管理员",
"adminOnlyDesc": "只有管理员可以通过API或后台修改此变量的值",
"adminOnlyAppDesc": "只有管理员可以通过API或后台修改此全局变量的值",
"userWritable": "用户可写",
"userWritableDesc": "用户可以通过客户端API修改自己的变量值",
"userWritableAppDesc": "用户可以通过客户端API修改此全局变量的值,修改后对所有用户生效",
@@ -1908,6 +1965,9 @@
"batchUpdateSuccess": "批量更新成功",
"batchUpdateFailed": "批量更新失败",
"select": "选择",
"edit": "编辑",
"editSuccess": "更新成功",
"editFailed": "获取云端函数失败",
"columns": {
"name": "名称",
"application": "应用",
@@ -1938,6 +1998,7 @@
"status": "状态",
"preview": "预览",
"submitBtn": "提交",
"saveBtn": "保存",
"cancelBtn": "取消",
"submitSuccess": "创建成功",
"submitFailed": "创建失败"
@@ -2235,9 +2296,8 @@
"smsSettings": "短信服务",
"paymentSettings": "支付配置",
"profile": "个人中心",
"developerDashboard": "管理后台",
"agentDashboard": "代理后台",
"adminDashboard": "管理后台",
"agentDashboard": "代理后台",
"logout": "退出登录",
"login": "登录",
"register": "注册",
+1 -1
View File
@@ -25,7 +25,7 @@ const routes: RouteRecordRaw[] = [
},
{
path: '/admin',
component: () => import('@/layouts/developer.vue'),
component: () => import('@/layouts/admin.vue'),
meta: { auth: true, role: 'admin' },
children: [
{