feat: 实现前端卡密管理界面
- 卡密列表展示与分页功能 - 单个/批量创建卡密 - 卡密删除与批量删除 - 卡密导出功能 (file-saver) - 启用/禁用状态切换 - 状态判断 (有效/已使用/已失效) - Toast 通知系统 (vue-sonner) - 登录页面错误提示优化 - 后端登录错误消息中文化
This commit is contained in:
58
frontend/src/api/accounts.ts
Normal file
58
frontend/src/api/accounts.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import request from './request'
|
||||
|
||||
export interface Account {
|
||||
id: number
|
||||
team_account_id: string
|
||||
name: string
|
||||
is_active: boolean
|
||||
seats_in_use: number
|
||||
seats_entitled: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AccountsResponse {
|
||||
success: boolean
|
||||
data?: Account[]
|
||||
total?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface AccountResponse {
|
||||
success: boolean
|
||||
data?: Account
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface CreateAccountRequest {
|
||||
team_account_id: string
|
||||
auth_token: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export function getAccounts(params?: PaginationParams) {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params?.page) searchParams.set('page', String(params.page))
|
||||
if (params?.page_size) searchParams.set('page_size', String(params.page_size))
|
||||
const query = searchParams.toString()
|
||||
return request.get<AccountsResponse>(`/api/accounts${query ? `?${query}` : ''}`)
|
||||
}
|
||||
|
||||
export function createAccount(data: CreateAccountRequest) {
|
||||
return request.post<AccountResponse>('/api/accounts/create', data)
|
||||
}
|
||||
|
||||
export function refreshAccount(id: number) {
|
||||
return request.post<AccountResponse>(`/api/accounts/refresh?id=${id}`)
|
||||
}
|
||||
|
||||
export function deleteAccount(id: number) {
|
||||
return request.delete(`/api/accounts/delete?id=${id}`)
|
||||
}
|
||||
29
frontend/src/api/auth.ts
Normal file
29
frontend/src/api/auth.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import request from './request'
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean
|
||||
token?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface ProfileResponse {
|
||||
success: boolean
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function login(data: LoginRequest) {
|
||||
return request.post<LoginResponse>('/api/login', data)
|
||||
}
|
||||
|
||||
export function getProfile() {
|
||||
return request.get<ProfileResponse>('/api/profile')
|
||||
}
|
||||
73
frontend/src/api/cardkeys.ts
Normal file
73
frontend/src/api/cardkeys.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import request from './request'
|
||||
|
||||
export interface CardKey {
|
||||
id: number
|
||||
key: string
|
||||
max_uses: number
|
||||
used_count: number
|
||||
validity_type: string
|
||||
expires_at: string
|
||||
is_active: boolean
|
||||
created_by_id: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface CardKeysResponse {
|
||||
success: boolean
|
||||
keys?: CardKey[]
|
||||
total?: number
|
||||
page?: number
|
||||
page_size?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface CardKeyResponse {
|
||||
success: boolean
|
||||
data?: CardKey
|
||||
keys?: CardKey[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface CreateCardKeyRequest {
|
||||
validity_days?: number
|
||||
max_uses?: number
|
||||
}
|
||||
|
||||
export interface BatchCreateCardKeyRequest {
|
||||
count: number
|
||||
validity_days?: number
|
||||
max_uses?: number
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export function getCardKeys(params?: PaginationParams) {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params?.page) searchParams.set('page', String(params.page))
|
||||
if (params?.page_size) searchParams.set('page_size', String(params.page_size))
|
||||
const query = searchParams.toString()
|
||||
return request.get<CardKeysResponse>(`/api/cardkeys${query ? `?${query}` : ''}`)
|
||||
}
|
||||
|
||||
export function createCardKey(data: CreateCardKeyRequest) {
|
||||
return request.post<CardKeyResponse>('/api/cardkeys', data)
|
||||
}
|
||||
|
||||
export function batchCreateCardKeys(data: BatchCreateCardKeyRequest) {
|
||||
return request.post<CardKeyResponse>('/api/cardkeys/batch', data)
|
||||
}
|
||||
|
||||
export function deleteCardKey(id: number) {
|
||||
return request.delete<CardKeyResponse>(`/api/cardkeys/delete?id=${id}`)
|
||||
}
|
||||
|
||||
export function batchDeleteCardKeys(ids: number[]) {
|
||||
return request.delete<CardKeyResponse>('/api/cardkeys/batch', { data: { ids } })
|
||||
}
|
||||
|
||||
export function toggleCardKeyActive(id: number, is_active: boolean) {
|
||||
return request.post<CardKeyResponse>('/api/cardkeys/toggle', { id, is_active })
|
||||
}
|
||||
39
frontend/src/api/invite.ts
Normal file
39
frontend/src/api/invite.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import request from './request'
|
||||
|
||||
export interface InviteByCardRequest {
|
||||
email: string
|
||||
card_key: string
|
||||
}
|
||||
|
||||
export interface Invitation {
|
||||
id: number
|
||||
email: string
|
||||
account_id: number
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface InvitationsResponse {
|
||||
success: boolean
|
||||
invitations?: Invitation[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface DeleteInviteRequest {
|
||||
email: string
|
||||
account_id: number
|
||||
}
|
||||
|
||||
// Public endpoint - no auth required
|
||||
export function inviteByCard(data: InviteByCardRequest) {
|
||||
return request.post('/api/invite/card', data)
|
||||
}
|
||||
|
||||
// Admin endpoints - auth required
|
||||
export function listInvitations(accountId: number) {
|
||||
return request.get<InvitationsResponse>(`/api/invite?account_id=${accountId}`)
|
||||
}
|
||||
|
||||
export function deleteInvite(data: DeleteInviteRequest) {
|
||||
return request.delete('/api/invite', { data })
|
||||
}
|
||||
42
frontend/src/api/request.ts
Normal file
42
frontend/src/api/request.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import axios, { type InternalAxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import router from '@/router'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_URL || '',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Request interceptor - add Authorization header
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptor - handle 401/403
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
return response
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
router.push('/admin/login')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
Reference in New Issue
Block a user