feat: add user creation and ban/unban for agent backend
- Backend: POST /agent/users - create user with optional card recharge - Backend: PUT /agent/users/:id/status - ban/unban user - Frontend: add create user page for agent (select app, card type) - Frontend: add ban/unban action in user list with confirm dialog - Frontend: add actions dropdown column in user data table - Fix: cloud variables records page i18n key (agent.finance.to -> agent.cloudVariables.records.to) - Add i18n keys for agent user management (create, ban, unban)
This commit is contained in:
@@ -368,7 +368,7 @@ onMounted(async () => {
|
||||
:placeholder="t('agent.cloudVariables.records.startDate')"
|
||||
class="w-[180px]"
|
||||
/>
|
||||
<span class="text-muted-foreground text-sm">{{ t('agent.finance.to') }}</span>
|
||||
<span class="text-muted-foreground text-sm">{{ t('agent.cloudVariables.records.to') }}</span>
|
||||
<DateTimePicker
|
||||
v-model="dateRange.to"
|
||||
:placeholder="t('agent.cloudVariables.records.endDate')"
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
|
||||
import { Ban, MoreHorizontal } from 'lucide-vue-next'
|
||||
import { h } from 'vue'
|
||||
|
||||
import type { User } from '../data/schema'
|
||||
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
export function getColumns(t: Composer['t']): ColumnDef<User>[] {
|
||||
export function getColumns(actions: {
|
||||
onToggleStatus: (row: User) => void
|
||||
}, t: Composer['t']): ColumnDef<User>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'username',
|
||||
@@ -90,5 +100,40 @@ export function getColumns(t: Composer['t']): ColumnDef<User>[] {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => h('span', { class: 'sr-only' }, t('common.actions')),
|
||||
cell: ({ row }) => {
|
||||
const user = row.original
|
||||
const isBanned = user.status === 'banned'
|
||||
|
||||
return h(
|
||||
DropdownMenu,
|
||||
{},
|
||||
{
|
||||
default: () => [
|
||||
h(DropdownMenuTrigger, { asChild: true }, () =>
|
||||
h(Button, { variant: 'ghost', class: 'h-8 w-8 p-0' }, () => [
|
||||
h(MoreHorizontal, { class: 'h-4 w-4' }),
|
||||
h('span', { class: 'sr-only' }, t('common.openMenu')),
|
||||
]),
|
||||
),
|
||||
h(
|
||||
DropdownMenuContent,
|
||||
{ align: 'end' },
|
||||
() => [
|
||||
h(DropdownMenuItem, { onClick: () => actions.onToggleStatus(user) }, () => [
|
||||
h(Ban, { class: 'mr-2 h-4 w-4' }),
|
||||
isBanned ? t('common.unban') : t('common.ban'),
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import DataTableToolbar from '@/pages/agent/users/components/data-table-toolbar.
|
||||
const props = defineProps<Omit<DataTableProps<User>, 'columns'> & {
|
||||
searchFilter?: string
|
||||
serverPagination?: DataTableProps<User>['serverPagination']
|
||||
onToggleStatus: (row: User) => void
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -28,7 +29,9 @@ const { t } = useI18n()
|
||||
|
||||
const columns = computed(() => [
|
||||
SelectColumn as ColumnDef<User>,
|
||||
...getColumns(t),
|
||||
...getColumns({
|
||||
onToggleStatus: props.onToggleStatus,
|
||||
}, t),
|
||||
])
|
||||
|
||||
const table = generateVueTable<User>({
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<script setup lang="ts">
|
||||
import { CreditCard, Eye, Loader2, UserPlus } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import api from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Application {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface CardType {
|
||||
id: number
|
||||
name: string
|
||||
billing_type: string
|
||||
price: number
|
||||
value: number
|
||||
duration_days: number
|
||||
}
|
||||
|
||||
const saving = ref(false)
|
||||
const applications = ref<Application[]>([])
|
||||
const cardTypes = ref<CardType[]>([])
|
||||
|
||||
const form = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
application_id: '',
|
||||
card_type_id: 'none',
|
||||
card_quantity: 1,
|
||||
})
|
||||
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await api.get<{ apps: Application[] }>('/agent/apps')
|
||||
applications.value = Array.isArray(data?.apps) ? data.apps : []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCardTypes(applicationId: string) {
|
||||
if (!applicationId) {
|
||||
cardTypes.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await api.get<{ cardTypes: CardType[] }>(`/agent/apps/${applicationId}`)
|
||||
cardTypes.value = Array.isArray(data?.cardTypes) ? data.cardTypes : []
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取卡密类型失败:', error)
|
||||
cardTypes.value = []
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => form.value.application_id, (newVal) => {
|
||||
form.value.card_type_id = 'none'
|
||||
fetchCardTypes(newVal)
|
||||
})
|
||||
|
||||
const selectedApplication = computed(() => {
|
||||
if (form.value.application_id) {
|
||||
return applications.value.find(app => String(app.id) === form.value.application_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const selectedCardType = computed(() => {
|
||||
if (form.value.card_type_id && form.value.card_type_id !== 'none') {
|
||||
return cardTypes.value.find(ct => String(ct.id) === form.value.card_type_id)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
function formatCardTypeValue(ct: CardType) {
|
||||
if (ct.value === -1)
|
||||
return t('agent.users.create.permanent')
|
||||
if (ct.billing_type === 'subscription') {
|
||||
return `${ct.value} ${t('agent.users.create.days')}`
|
||||
}
|
||||
return `${ct.value} ${t('agent.users.create.points')}`
|
||||
}
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return form.value.username && form.value.password && form.value.application_id
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
if (!form.value.username) {
|
||||
toast.error(t('agent.users.create.usernameRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.password) {
|
||||
toast.error(t('agent.users.create.passwordRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.value.application_id) {
|
||||
toast.error(t('agent.users.create.applicationRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: any = {
|
||||
username: form.value.username,
|
||||
email: form.value.email,
|
||||
password: form.value.password,
|
||||
application_id: Number(form.value.application_id),
|
||||
}
|
||||
if (form.value.card_type_id && form.value.card_type_id !== 'none') {
|
||||
payload.card_type_id = Number(form.value.card_type_id)
|
||||
payload.card_quantity = form.value.card_quantity || 1
|
||||
}
|
||||
await api.post('/agent/users', payload)
|
||||
toast.success(t('agent.users.create.success'))
|
||||
router.push('/agent/users')
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('创建用户失败:', error)
|
||||
toast.error(error.message || t('agent.users.create.failed'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchApplications()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicPage
|
||||
:title="t('agent.users.create.title')"
|
||||
:description="t('agent.users.create.description')"
|
||||
:breadcrumbs="[
|
||||
{ title: t('agent.users.title'), href: '/agent/users' },
|
||||
{ title: t('agent.users.create.title') },
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<UserPlus class="size-5" />
|
||||
{{ t('agent.users.create.basicInfo') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('agent.users.create.basicInfoDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="application">
|
||||
{{ t('agent.users.create.application') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.application_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="t('agent.users.create.selectApplication')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem v-for="app in applications" :key="app.id" :value="String(app.id)">
|
||||
{{ app.name }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="username">
|
||||
{{ t('agent.users.create.username') }}
|
||||
</UiLabel>
|
||||
<UiInput id="username" v-model="form.username" :placeholder="t('agent.users.create.usernamePlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="password">
|
||||
{{ t('agent.users.create.password') }}
|
||||
</UiLabel>
|
||||
<UiInput id="password" v-model="form.password" type="password" :placeholder="t('agent.users.create.passwordPlaceholder')" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="email">
|
||||
{{ t('agent.users.create.email') }}
|
||||
</UiLabel>
|
||||
<UiInput id="email" v-model="form.email" type="email" :placeholder="t('agent.users.create.emailPlaceholder')" />
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<CreditCard class="size-5" />
|
||||
{{ t('agent.users.create.rechargeCard') }}
|
||||
</UiCardTitle>
|
||||
<UiCardDescription>{{ t('agent.users.create.rechargeCardDesc') }}</UiCardDescription>
|
||||
</UiCardHeader>
|
||||
<UiCardContent class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<UiLabel for="card_type">
|
||||
{{ t('agent.users.create.cardType') }}
|
||||
</UiLabel>
|
||||
<UiSelect v-model="form.card_type_id">
|
||||
<UiSelectTrigger>
|
||||
<UiSelectValue :placeholder="cardTypes.length > 0 ? t('agent.users.create.selectCardType') : t('agent.users.create.selectApplicationFirst')" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent>
|
||||
<UiSelectItem value="none">
|
||||
{{ t('agent.users.create.noRecharge') }}
|
||||
</UiSelectItem>
|
||||
<UiSelectItem v-for="ct in cardTypes" :key="ct.id" :value="String(ct.id)">
|
||||
{{ ct.name }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedCardType" class="space-y-2">
|
||||
<UiLabel for="card_quantity">
|
||||
{{ t('agent.users.create.cardQuantity') }}
|
||||
</UiLabel>
|
||||
<UiNumberField v-model="form.card_quantity" :min="1" :max="100" class="max-w-[200px]">
|
||||
<UiNumberFieldContent>
|
||||
<UiNumberFieldDecrement />
|
||||
<UiNumberFieldInput />
|
||||
<UiNumberFieldIncrement />
|
||||
</UiNumberFieldContent>
|
||||
</UiNumberField>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedCardType" class="rounded-lg border bg-muted/50 p-3 space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.create.cardType') }}</span>
|
||||
<span>{{ selectedCardType.name }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.create.cardValue') }}</span>
|
||||
<span>{{ formatCardTypeValue(selectedCardType) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.create.cardQuantity') }}</span>
|
||||
<span>{{ form.card_quantity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<UiCard>
|
||||
<UiCardHeader>
|
||||
<UiCardTitle class="flex items-center gap-2">
|
||||
<Eye class="size-5" />
|
||||
{{ t('agent.users.create.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('agent.users.create.app') }}</span>
|
||||
<span>{{ selectedApplication?.name || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.create.usernameLabel') }}</span>
|
||||
<span>{{ form.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.create.emailLabel') }}</span>
|
||||
<span>{{ form.email || '-' }}</span>
|
||||
</div>
|
||||
<div v-if="selectedCardType" class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">{{ t('agent.users.create.rechargeCard') }}</span>
|
||||
<span class="text-primary">{{ selectedCardType.name }} x{{ form.card_quantity }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
|
||||
<UiCard>
|
||||
<UiCardContent class="pt-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<UiButton
|
||||
class="w-full"
|
||||
size="lg"
|
||||
:disabled="saving || !isFormValid"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Loader2 v-if="saving" class="mr-2 h-4 w-4 animate-spin" />
|
||||
<UserPlus v-else class="mr-2 h-4 w-4" />
|
||||
{{ t('agent.users.create.submit') }}
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('agent.users.create.cancel') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicPage>
|
||||
</template>
|
||||
@@ -1,15 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { Loader2, Users } from 'lucide-vue-next'
|
||||
import { Ban, Loader2, Plus, Users } from 'lucide-vue-next'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import type { User } from '@/pages/agent/users/data/schema'
|
||||
|
||||
import ConfirmDialog from '@/components/confirm-dialog.vue'
|
||||
import { BasicPage } from '@/components/global-layout'
|
||||
import DataTable from '@/pages/agent/users/components/data-table.vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const users = ref<User[]>([])
|
||||
@@ -22,6 +26,10 @@ const activeCount = ref(0)
|
||||
const disabledCount = ref(0)
|
||||
const bannedCount = ref(0)
|
||||
|
||||
const statusDialogOpen = ref(false)
|
||||
const statusTarget = ref<User | null>(null)
|
||||
const statusAction = ref<'ban' | 'unban'>('ban')
|
||||
|
||||
const serverPagination = computed(() => ({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
@@ -63,6 +71,34 @@ async function fetchUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
function goToCreate() {
|
||||
router.push('/agent/users/create')
|
||||
}
|
||||
|
||||
function toggleUserStatus(user: User) {
|
||||
statusTarget.value = user
|
||||
statusAction.value = user.status === 'banned' ? 'unban' : 'ban'
|
||||
statusDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleToggleStatus() {
|
||||
if (!statusTarget.value) return
|
||||
|
||||
const newStatus = statusAction.value === 'ban' ? 'banned' : 'active'
|
||||
try {
|
||||
await api.put(`/agent/users/${statusTarget.value.id}/status`, { status: newStatus })
|
||||
toast.success(t('agent.users.statusUpdateSuccess'))
|
||||
fetchUsers()
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('切换用户状态失败:', error)
|
||||
toast.error(error.message || t('agent.users.statusUpdateFailed'))
|
||||
}
|
||||
finally {
|
||||
statusTarget.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
})
|
||||
@@ -78,6 +114,13 @@ onMounted(() => {
|
||||
]"
|
||||
sticky
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton @click="goToCreate">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
{{ t('agent.users.addUser') }}
|
||||
</UiButton>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<UiCard>
|
||||
@@ -127,7 +170,7 @@ onMounted(() => {
|
||||
<UiCardTitle class="text-sm font-medium">
|
||||
{{ t('agent.users.status.banned') }}
|
||||
</UiCardTitle>
|
||||
<Users class="size-4 text-muted-foreground" />
|
||||
<Ban class="size-4 text-muted-foreground" />
|
||||
</UiCardHeader>
|
||||
<UiCardContent>
|
||||
<div class="text-2xl font-bold">
|
||||
@@ -150,11 +193,30 @@ onMounted(() => {
|
||||
:data="users"
|
||||
:server-pagination="serverPagination"
|
||||
:search-filter="searchFilter"
|
||||
:on-toggle-status="toggleUserStatus"
|
||||
@refresh="fetchUsers"
|
||||
@update:search-filter="searchFilter = $event; currentPage = 1; fetchUsers()"
|
||||
/>
|
||||
</UiCardContent>
|
||||
</UiCard>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="statusDialogOpen"
|
||||
:destructive="statusAction === 'ban'"
|
||||
:confirm-button-text="statusAction === 'ban' ? t('common.ban') : t('common.unban')"
|
||||
:cancel-button-text="t('common.cancel')"
|
||||
@confirm="handleToggleStatus"
|
||||
>
|
||||
<template #title>
|
||||
{{ statusAction === 'ban' ? t('agent.users.banUser') : t('agent.users.unbanUser') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{ statusAction === 'ban'
|
||||
? t('agent.users.banConfirm', { username: statusTarget?.username })
|
||||
: t('agent.users.unbanConfirm', { username: statusTarget?.username })
|
||||
}}
|
||||
</template>
|
||||
</ConfirmDialog>
|
||||
</BasicPage>
|
||||
</template>
|
||||
|
||||
@@ -2767,8 +2767,15 @@
|
||||
"lastLogin": "Last Login",
|
||||
"active": "Active",
|
||||
"totalUsers": "Total Users",
|
||||
"addUser": "Add User",
|
||||
"searchPlaceholder": "Search username or email...",
|
||||
"select": "Select",
|
||||
"banUser": "Ban User",
|
||||
"unbanUser": "Unban User",
|
||||
"banConfirm": "Are you sure you want to ban user {username}?",
|
||||
"unbanConfirm": "Are you sure you want to unban user {username}?",
|
||||
"statusUpdateSuccess": "User status updated successfully",
|
||||
"statusUpdateFailed": "Failed to update user status",
|
||||
"columns": {
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
@@ -2780,6 +2787,42 @@
|
||||
"active": "Active",
|
||||
"disabled": "Disabled",
|
||||
"banned": "Banned"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create User",
|
||||
"description": "Create a new user under your application",
|
||||
"basicInfo": "Basic Information",
|
||||
"basicInfoDesc": "Fill in user basic information",
|
||||
"application": "Application",
|
||||
"selectApplication": "Select application",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Enter username",
|
||||
"usernameRequired": "Username is required",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"passwordRequired": "Password is required",
|
||||
"email": "Email",
|
||||
"emailPlaceholder": "Enter email (optional)",
|
||||
"applicationRequired": "Please select an application",
|
||||
"rechargeCard": "Recharge Card",
|
||||
"rechargeCardDesc": "Recharge with card on creation (optional)",
|
||||
"cardType": "Card Type",
|
||||
"selectCardType": "Select card type",
|
||||
"selectApplicationFirst": "Select application first",
|
||||
"noRecharge": "No Recharge",
|
||||
"cardQuantity": "Card Quantity",
|
||||
"cardValue": "Card Value",
|
||||
"permanent": "Permanent",
|
||||
"days": "days",
|
||||
"points": "points",
|
||||
"preview": "Preview",
|
||||
"app": "Application",
|
||||
"usernameLabel": "Username",
|
||||
"emailLabel": "Email",
|
||||
"submit": "Create User",
|
||||
"cancel": "Cancel",
|
||||
"success": "User created successfully",
|
||||
"failed": "Failed to create user"
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
@@ -2929,7 +2972,8 @@
|
||||
"createdAt": "Created At",
|
||||
"searchPlaceholder": "Search records...",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date"
|
||||
"endDate": "End Date",
|
||||
"to": "to"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2117,6 +2117,7 @@
|
||||
"searchPlaceholder": "搜索记录...",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期",
|
||||
"to": "至",
|
||||
"total": "共 {count} 条记录",
|
||||
"totalRecords": "总记录数",
|
||||
"maxRecords": "最大 {count} 条",
|
||||
@@ -2768,8 +2769,15 @@
|
||||
"lastLogin": "最后登录",
|
||||
"active": "正常",
|
||||
"totalUsers": "用户总数",
|
||||
"addUser": "添加用户",
|
||||
"searchPlaceholder": "搜索用户名或邮箱...",
|
||||
"select": "选择",
|
||||
"banUser": "封禁用户",
|
||||
"unbanUser": "解封用户",
|
||||
"banConfirm": "确定要封禁用户 {username} 吗?",
|
||||
"unbanConfirm": "确定要解封用户 {username} 吗?",
|
||||
"statusUpdateSuccess": "用户状态更新成功",
|
||||
"statusUpdateFailed": "用户状态更新失败",
|
||||
"columns": {
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
@@ -2781,6 +2789,42 @@
|
||||
"active": "正常",
|
||||
"disabled": "禁用",
|
||||
"banned": "封禁"
|
||||
},
|
||||
"create": {
|
||||
"title": "创建用户",
|
||||
"description": "在您的应用下创建新用户",
|
||||
"basicInfo": "基本信息",
|
||||
"basicInfoDesc": "填写用户基本信息",
|
||||
"application": "所属应用",
|
||||
"selectApplication": "请选择应用",
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
"usernameRequired": "用户名不能为空",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"passwordRequired": "密码不能为空",
|
||||
"email": "邮箱",
|
||||
"emailPlaceholder": "请输入邮箱(选填)",
|
||||
"applicationRequired": "请选择所属应用",
|
||||
"rechargeCard": "充值卡密",
|
||||
"rechargeCardDesc": "创建时同时充值卡密(选填)",
|
||||
"cardType": "卡密类型",
|
||||
"selectCardType": "请选择卡密类型",
|
||||
"selectApplicationFirst": "请先选择应用",
|
||||
"noRecharge": "不充值",
|
||||
"cardQuantity": "卡密数量",
|
||||
"cardValue": "卡密面值",
|
||||
"permanent": "永久",
|
||||
"days": "天",
|
||||
"points": "点数",
|
||||
"preview": "预览",
|
||||
"app": "应用",
|
||||
"usernameLabel": "用户名",
|
||||
"emailLabel": "邮箱",
|
||||
"submit": "创建用户",
|
||||
"cancel": "取消",
|
||||
"success": "用户创建成功",
|
||||
"failed": "用户创建失败"
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
@@ -2930,7 +2974,8 @@
|
||||
"createdAt": "创建时间",
|
||||
"searchPlaceholder": "搜索记录...",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期"
|
||||
"endDate": "结束日期",
|
||||
"to": "至"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -444,6 +444,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/agent/users/index.vue'),
|
||||
meta: { title: '用户管理 - 代理商后台' },
|
||||
},
|
||||
{
|
||||
path: 'users/create',
|
||||
name: 'AgentUserCreate',
|
||||
component: () => import('@/pages/agent/users/create.vue'),
|
||||
meta: { title: '创建用户 - 代理商后台' },
|
||||
},
|
||||
{
|
||||
path: 'finance',
|
||||
name: 'AgentFinance',
|
||||
|
||||
Reference in New Issue
Block a user