feat: 添加功能和修复问题

- 添加全局 API Token 认证支持 (环境变量 API_TOKEN)
- Team 页面添加直接邀请按钮
- Team 页面添加随机邀请按钮
- 修复已邀请用户列表字段名不匹配问题
- 修复数据库为空时错误显示 toast 的问题
This commit is contained in:
sar
2026-01-14 13:25:49 +08:00
parent a0a7640e8a
commit 93aa31219d
6 changed files with 194 additions and 7 deletions

View File

@@ -54,7 +54,8 @@ import {
} from '@/components/ui/pagination'
import { useAccountsStore } from '@/stores/accounts'
import { createAccount, refreshAccount, deleteAccount, type Account } from '@/api/accounts'
import { Plus, RefreshCw, Users, Loader2, Eye, EyeOff, Trash2 } from 'lucide-vue-next'
import { inviteByAdmin } from '@/api/invite'
import { Plus, RefreshCw, Users, Loader2, Eye, EyeOff, Trash2, UserPlus, Shuffle } from 'lucide-vue-next'
const router = useRouter()
const accountsStore = useAccountsStore()
@@ -67,6 +68,18 @@ const showToken = ref(false)
// Delete confirmation
const deleteDialogOpen = ref(false)
// Invite dialog
const inviteDialogOpen = ref(false)
const invitingAccountId = ref<number | null>(null)
const invitingAccountName = ref('')
const inviteEmail = ref('')
const inviting = ref(false)
// Random invite dialog
const randomInviteDialogOpen = ref(false)
const randomInviteEmail = ref('')
const randomInviting = ref(false)
const pendingDelete = ref<Account | null>(null)
// Pagination
@@ -185,6 +198,74 @@ function viewInvites(account: Account) {
router.push(`/admin/teams/${account.id}/invites`)
}
function openInviteDialog(account: Account) {
invitingAccountId.value = account.id
invitingAccountName.value = account.name || account.team_account_id
inviteEmail.value = ''
inviteDialogOpen.value = true
}
async function handleInvite() {
if (!inviteEmail.value.trim()) {
toast.error('请输入邮箱地址')
return
}
if (!invitingAccountId.value) return
inviting.value = true
try {
const response = await inviteByAdmin({
email: inviteEmail.value.trim(),
account_id: invitingAccountId.value,
})
if (response.data.success) {
toast.success('邀请发送成功')
inviteDialogOpen.value = false
inviteEmail.value = ''
// Refresh account to update seats
if (invitingAccountId.value) {
await handleRefresh(accountsStore.accounts.find(a => a.id === invitingAccountId.value)!)
}
} else {
toast.error(response.data.message || '邀请失败')
}
} catch (e: any) {
toast.error(e.response?.data?.message || '邀请失败')
} finally {
inviting.value = false
}
}
// Random invite - auto select available team
async function handleRandomInvite() {
if (!randomInviteEmail.value.trim()) {
toast.error('请输入邮箱地址')
return
}
randomInviting.value = true
try {
// Use account_id = 0 to let backend auto-select
const response = await inviteByAdmin({
email: randomInviteEmail.value.trim(),
account_id: 0,
})
if (response.data.success) {
toast.success(`邀请发送成功,已分配到: ${response.data.account_name || 'Team'}`)
randomInviteDialogOpen.value = false
randomInviteEmail.value = ''
// Refresh all accounts to update seats
await accountsStore.fetchAccounts()
} else {
toast.error(response.data.message || '邀请失败')
}
} catch (e: any) {
toast.error(e.response?.data?.message || '邀请失败')
} finally {
randomInviting.value = false
}
}
function goToPage(page: number) {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
@@ -203,6 +284,7 @@ function handlePageSizeChange(value: any) {
<div class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">Team 管理</h1>
<div class="flex items-center gap-2">
<Dialog v-model:open="dialogOpen">
<DialogTrigger as-child>
<Button>
@@ -268,6 +350,43 @@ function handlePageSizeChange(value: any) {
</form>
</DialogContent>
</Dialog>
<!-- Random Invite Button and Dialog -->
<Dialog v-model:open="randomInviteDialogOpen">
<DialogTrigger as-child>
<Button variant="outline">
<Shuffle class="h-4 w-4 mr-2" />
随机邀请
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>随机邀请</DialogTitle>
<DialogDescription>
系统将自动选择有空位的 Team 发送邀请
</DialogDescription>
</DialogHeader>
<form @submit.prevent="handleRandomInvite" class="space-y-4">
<div class="space-y-2">
<Label for="random_invite_email">邮箱地址 *</Label>
<Input
id="random_invite_email"
v-model="randomInviteEmail"
type="email"
placeholder="user@example.com"
:disabled="randomInviting"
/>
</div>
<DialogFooter>
<Button type="submit" :disabled="randomInviting">
<Loader2 v-if="randomInviting" class="h-4 w-4 mr-2 animate-spin" />
{{ randomInviting ? '邀请中...' : '发送邀请' }}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
<Card class="min-h-[600px] flex flex-col">
@@ -329,7 +448,10 @@ function handlePageSizeChange(value: any) {
]"
/>
</Button>
<Button variant="outline" size="sm" @click="viewInvites(account)">
<Button variant="outline" size="sm" @click="openInviteDialog(account)" title="直接邀请">
<UserPlus class="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" @click="viewInvites(account)" title="查看已邀请用户">
<Users class="h-4 w-4" />
</Button>
<Button
@@ -410,5 +532,35 @@ function handlePageSizeChange(value: any) {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- Invite dialog -->
<Dialog v-model:open="inviteDialogOpen">
<DialogContent>
<DialogHeader>
<DialogTitle>邀请用户</DialogTitle>
<DialogDescription>
邀请用户加入 Team: {{ invitingAccountName }}
</DialogDescription>
</DialogHeader>
<form @submit.prevent="handleInvite" class="space-y-4">
<div class="space-y-2">
<Label for="invite_email">邮箱地址 *</Label>
<Input
id="invite_email"
v-model="inviteEmail"
type="email"
placeholder="user@example.com"
:disabled="inviting"
/>
</div>
<DialogFooter>
<Button type="submit" :disabled="inviting">
<Loader2 v-if="inviting" class="h-4 w-4 mr-2 animate-spin" />
{{ inviting ? '邀请中...' : '发送邀请' }}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</template>