feat(teams): fix checkbox multi-select and improve batch operations UI

- Fix checkbox binding using :model-value instead of :checked
- Change selectedIds from Set to reactive array for proper Vue reactivity
- Move batch refresh/delete buttons to top bar (matching CardKeysPage layout)
- Buttons show selection count like 'Refresh (2)' when items selected
- Swap position of 'Add Team' and 'Random Invite' buttons
- Remove unused isIndeterminate computed property
This commit is contained in:
sar
2026-01-16 11:53:04 +08:00
parent 59f5a87275
commit 474f592dcd
32 changed files with 405 additions and 67 deletions

View File

@@ -15,13 +15,15 @@ import (
// ChatGPTAccountHandler ChatGPT 账号处理器
type ChatGPTAccountHandler struct {
repo *repository.ChatGPTAccountRepository
invitationRepo *repository.InvitationRepository
chatgptService *service.ChatGPTService
}
// NewChatGPTAccountHandler 创建处理器
func NewChatGPTAccountHandler(repo *repository.ChatGPTAccountRepository, chatgptService *service.ChatGPTService) *ChatGPTAccountHandler {
func NewChatGPTAccountHandler(repo *repository.ChatGPTAccountRepository, invitationRepo *repository.InvitationRepository, chatgptService *service.ChatGPTService) *ChatGPTAccountHandler {
return &ChatGPTAccountHandler{
repo: repo,
invitationRepo: invitationRepo,
chatgptService: chatgptService,
}
}
@@ -307,10 +309,19 @@ func (h *ChatGPTAccountHandler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
// 先删除相关的邀请记录,避免外键约束失败
if err := h.invitationRepo.DeleteByAccountID(id); err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to delete related invitations: " + err.Error(),
})
return
}
if err := h.repo.Delete(id); err != nil {
respondJSON(w, http.StatusInternalServerError, AccountResponse{
Success: false,
Message: "Failed to delete account",
Message: "Failed to delete account: " + err.Error(),
})
return
}
@@ -320,3 +331,150 @@ func (h *ChatGPTAccountHandler) Delete(w http.ResponseWriter, r *http.Request) {
Message: "Account deleted successfully",
})
}
// BatchDeleteRequest 批量删除请求
type BatchDeleteAccountRequest struct {
IDs []int `json:"ids"`
}
// BatchDeleteResponse 批量操作响应
type BatchOperationResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
SuccessCount int `json:"success_count"`
FailedCount int `json:"failed_count"`
}
// BatchDelete 批量删除账号
func (h *ChatGPTAccountHandler) BatchDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
respondJSON(w, http.StatusMethodNotAllowed, BatchOperationResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req BatchDeleteAccountRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, BatchOperationResponse{
Success: false,
Message: "Invalid request body",
})
return
}
if len(req.IDs) == 0 {
respondJSON(w, http.StatusBadRequest, BatchOperationResponse{
Success: false,
Message: "No accounts selected",
})
return
}
successCount := 0
failedCount := 0
for _, id := range req.IDs {
// 先删除相关的邀请记录
if err := h.invitationRepo.DeleteByAccountID(id); err != nil {
failedCount++
continue
}
// 再删除账号
if err := h.repo.Delete(id); err != nil {
failedCount++
continue
}
successCount++
}
respondJSON(w, http.StatusOK, BatchOperationResponse{
Success: failedCount == 0,
Message: "Batch delete completed",
SuccessCount: successCount,
FailedCount: failedCount,
})
}
// BatchRefreshRequest 批量刷新请求
type BatchRefreshRequest struct {
IDs []int `json:"ids"`
}
// BatchRefresh 批量刷新账号
func (h *ChatGPTAccountHandler) BatchRefresh(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondJSON(w, http.StatusMethodNotAllowed, BatchOperationResponse{
Success: false,
Message: "Method not allowed",
})
return
}
var req BatchRefreshRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondJSON(w, http.StatusBadRequest, BatchOperationResponse{
Success: false,
Message: "Invalid request body",
})
return
}
if len(req.IDs) == 0 {
respondJSON(w, http.StatusBadRequest, BatchOperationResponse{
Success: false,
Message: "No accounts selected",
})
return
}
successCount := 0
failedCount := 0
for _, id := range req.IDs {
account, err := h.repo.FindByID(id)
if err != nil || account == nil {
failedCount++
continue
}
// 调用 ChatGPT API 获取订阅信息
subInfo, err := h.chatgptService.GetSubscription(account.TeamAccountID, account.AuthToken)
if err != nil {
account.ConsecutiveFailures++
account.IsActive = false
account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true}
h.repo.Update(account)
failedCount++
continue
}
// 更新账号信息
if subInfo.IsValid {
account.SeatsInUse = subInfo.SeatsInUse
account.SeatsEntitled = subInfo.SeatsEntitled
account.ActiveStart = sql.NullTime{Time: subInfo.ActiveStart, Valid: !subInfo.ActiveStart.IsZero()}
account.ActiveUntil = sql.NullTime{Time: subInfo.ActiveUntil, Valid: !subInfo.ActiveUntil.IsZero()}
account.IsActive = true
account.ConsecutiveFailures = 0
} else {
account.IsActive = false
account.ConsecutiveFailures++
}
account.LastCheck = sql.NullTime{Time: time.Now(), Valid: true}
if err := h.repo.Update(account); err != nil {
failedCount++
continue
}
successCount++
}
respondJSON(w, http.StatusOK, BatchOperationResponse{
Success: failedCount == 0,
Message: "Batch refresh completed",
SuccessCount: successCount,
FailedCount: failedCount,
})
}

View File

@@ -82,6 +82,12 @@ func (r *InvitationRepository) DeleteByEmailAndAccountID(email string, accountID
return err
}
// DeleteByAccountID 根据账号ID删除所有邀请记录
func (r *InvitationRepository) DeleteByAccountID(accountID int) error {
_, err := r.db.Exec(`DELETE FROM invitations WHERE account_id = $1`, accountID)
return err
}
// FindByAccountID 根据账号ID查找邀请记录
func (r *InvitationRepository) FindByAccountID(accountID int) ([]*models.Invitation, error) {
rows, err := r.db.Query(`

View File

@@ -26,7 +26,7 @@ func SetupRoutes(db *sql.DB) http.Handler {
// 初始化处理器
authHandler := handler.NewAuthHandler(adminRepo)
accountHandler := handler.NewChatGPTAccountHandler(chatgptAccountRepo, chatgptService)
accountHandler := handler.NewChatGPTAccountHandler(chatgptAccountRepo, invitationRepo, chatgptService)
inviteHandler := handler.NewInviteHandler(chatgptAccountRepo, invitationRepo, cardKeyRepo, chatgptService)
cardKeyHandler := handler.NewCardKeyHandler(cardKeyRepo)
@@ -47,6 +47,8 @@ func SetupRoutes(db *sql.DB) http.Handler {
protectedMux.HandleFunc("/api/accounts/create", accountHandler.Create)
protectedMux.HandleFunc("/api/accounts/refresh", accountHandler.Refresh)
protectedMux.HandleFunc("/api/accounts/delete", accountHandler.Delete)
protectedMux.HandleFunc("/api/accounts/batch/delete", accountHandler.BatchDelete)
protectedMux.HandleFunc("/api/accounts/batch/refresh", accountHandler.BatchRefresh)
// 邀请接口 (管理员) - GET: 列表, POST: 邀请, DELETE: 移除
protectedMux.HandleFunc("/api/invite", func(w http.ResponseWriter, r *http.Request) {

View File

@@ -1 +1 @@
import{d as t,h as o,n as r,u as n,v as c,x as l,o as p}from"./index-BIETROXK.js";const i=t({__name:"CardDescription",props:{class:{}},setup(s){const e=s;return(a,d)=>(p(),o("p",{"data-slot":"card-description",class:r(n(c)("text-muted-foreground text-sm",e.class))},[l(a.$slots,"default")],2))}});export{i as _};
import{d as t,h as o,n as r,u as n,v as c,x as l,o as p}from"./index-B0FmaMuw.js";const i=t({__name:"CardDescription",props:{class:{}},setup(s){const e=s;return(a,d)=>(p(),o("p",{"data-slot":"card-description",class:r(n(c)("text-muted-foreground text-sm",e.class))},[l(a.$slots,"default")],2))}});export{i as _};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{d as r,h as e,x as o,n as c,u as n,v as d,o as l}from"./index-BIETROXK.js";const _=r({__name:"Card",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("div",{"data-slot":"card",class:c(n(d)("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",a.class))},[o(t.$slots,"default")],2))}}),i=r({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("div",{"data-slot":"card-content",class:c(n(d)("px-6",a.class))},[o(t.$slots,"default")],2))}}),m=r({__name:"CardHeader",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("div",{"data-slot":"card-header",class:c(n(d)("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",a.class))},[o(t.$slots,"default")],2))}}),f=r({__name:"CardTitle",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("h3",{"data-slot":"card-title",class:c(n(d)("leading-none font-semibold",a.class))},[o(t.$slots,"default")],2))}});export{_,m as a,f as b,i as c};
import{d as r,h as e,x as o,n as c,u as n,v as d,o as l}from"./index-B0FmaMuw.js";const _=r({__name:"Card",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("div",{"data-slot":"card",class:c(n(d)("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",a.class))},[o(t.$slots,"default")],2))}}),i=r({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("div",{"data-slot":"card-content",class:c(n(d)("px-6",a.class))},[o(t.$slots,"default")],2))}}),m=r({__name:"CardHeader",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("div",{"data-slot":"card-header",class:c(n(d)("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",a.class))},[o(t.$slots,"default")],2))}}),f=r({__name:"CardTitle",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),e("h3",{"data-slot":"card-title",class:c(n(d)("leading-none font-semibold",a.class))},[o(t.$slots,"default")],2))}});export{_,m as a,f as b,i as c};

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{c as b,d as w,p as k,j as g,h as i,f as d,a as r,i as v,b as t,w as s,u as e,_ as h,e as n,n as C,U as j,t as c,o}from"./index-BIETROXK.js";import{_ as f,a as m,b as _,c as u}from"./CardTitle.vue_vue_type_script_setup_true_lang-BZWhSLyf.js";import{_ as p}from"./Skeleton.vue_vue_type_script_setup_true_lang-DLBxrxfg.js";import{u as M}from"./accounts-DexZCWCe.js";import{R as V}from"./refresh-cw-x2W3gvqp.js";import{C as $}from"./circle-x-d27Sm-GD.js";const z=b("armchair",[["path",{d:"M19 9V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v3",key:"irtipd"}],["path",{d:"M3 16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z",key:"1qyhux"}],["path",{d:"M5 18v2",key:"ppbyun"}],["path",{d:"M19 18v2",key:"gy7782"}]]);const A=b("circle-check-big",[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]),B={class:"space-y-6"},T={class:"flex items-center justify-between"},D={class:"grid gap-4 md:grid-cols-2 lg:grid-cols-4"},N={key:1,class:"text-2xl font-bold"},S={key:1,class:"text-2xl font-bold text-green-600"},L={key:1,class:"text-2xl font-bold text-red-600"},R={key:1,class:"text-2xl font-bold"},U={class:"flex items-center justify-between"},q={class:"text-destructive"},J=w({__name:"DashboardPage",setup(E){const l=M();k(()=>{x()});async function x(){try{await l.fetchAccounts()}catch(y){g.error(y.message||"加载数据失败")}}return(y,a)=>(o(),i("div",B,[d("div",T,[a[1]||(a[1]=d("h1",{class:"text-2xl font-bold"},"Dashboard",-1)),t(e(h),{variant:"outline",size:"sm",onClick:x,disabled:e(l).loading},{default:s(()=>[t(e(V),{class:C(["h-4 w-4 mr-2",e(l).loading&&"animate-spin"])},null,8,["class"]),a[0]||(a[0]=n(" 刷新 ",-1))]),_:1},8,["disabled"])]),d("div",D,[t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[2]||(a[2]=[n("Team 总数",-1)])]),_:1}),t(e(j),{class:"h-4 w-4 text-muted-foreground"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",N,c(e(l).totalTeams),1))]),_:1})]),_:1}),t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[3]||(a[3]=[n("有效订阅",-1)])]),_:1}),t(e(A),{class:"h-4 w-4 text-green-500"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",S,c(e(l).validTeams),1))]),_:1})]),_:1}),t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[4]||(a[4]=[n("无效订阅",-1)])]),_:1}),t(e($),{class:"h-4 w-4 text-red-500"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",L,c(e(l).invalidTeams),1))]),_:1})]),_:1}),t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[5]||(a[5]=[n("剩余席位",-1)])]),_:1}),t(e(z),{class:"h-4 w-4 text-muted-foreground"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",R,c(e(l).totalAvailableSeats),1))]),_:1})]),_:1})]),e(l).error?(o(),r(e(f),{key:0,class:"border-destructive"},{default:s(()=>[t(e(u),{class:"pt-6"},{default:s(()=>[d("div",U,[d("p",q,c(e(l).error),1),t(e(h),{variant:"outline",size:"sm",onClick:x},{default:s(()=>[...a[6]||(a[6]=[n(" 重试 ",-1)])]),_:1})])]),_:1})]),_:1})):v("",!0)]))}});export{J as default};
import{c as b,d as w,p as k,j as g,h as i,f as d,a as r,i as v,b as t,w as s,u as e,_ as h,e as n,n as C,U as j,t as c,o}from"./index-B0FmaMuw.js";import{_ as f,a as m,b as _,c as u}from"./CardTitle.vue_vue_type_script_setup_true_lang-D0guZCre.js";import{_ as p}from"./Skeleton.vue_vue_type_script_setup_true_lang-CypbIxgo.js";import{u as M}from"./accounts-CLfPgj8J.js";import{R as V}from"./refresh-cw-Bst35UPe.js";import{C as $}from"./circle-x-C8-4gjQR.js";const z=b("armchair",[["path",{d:"M19 9V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v3",key:"irtipd"}],["path",{d:"M3 16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z",key:"1qyhux"}],["path",{d:"M5 18v2",key:"ppbyun"}],["path",{d:"M19 18v2",key:"gy7782"}]]);const A=b("circle-check-big",[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335",key:"yps3ct"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]),B={class:"space-y-6"},T={class:"flex items-center justify-between"},D={class:"grid gap-4 md:grid-cols-2 lg:grid-cols-4"},N={key:1,class:"text-2xl font-bold"},S={key:1,class:"text-2xl font-bold text-green-600"},L={key:1,class:"text-2xl font-bold text-red-600"},R={key:1,class:"text-2xl font-bold"},U={class:"flex items-center justify-between"},q={class:"text-destructive"},J=w({__name:"DashboardPage",setup(E){const l=M();k(()=>{x()});async function x(){try{await l.fetchAccounts()}catch(y){g.error(y.message||"加载数据失败")}}return(y,a)=>(o(),i("div",B,[d("div",T,[a[1]||(a[1]=d("h1",{class:"text-2xl font-bold"},"Dashboard",-1)),t(e(h),{variant:"outline",size:"sm",onClick:x,disabled:e(l).loading},{default:s(()=>[t(e(V),{class:C(["h-4 w-4 mr-2",e(l).loading&&"animate-spin"])},null,8,["class"]),a[0]||(a[0]=n(" 刷新 ",-1))]),_:1},8,["disabled"])]),d("div",D,[t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[2]||(a[2]=[n("Team 总数",-1)])]),_:1}),t(e(j),{class:"h-4 w-4 text-muted-foreground"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",N,c(e(l).totalTeams),1))]),_:1})]),_:1}),t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[3]||(a[3]=[n("有效订阅",-1)])]),_:1}),t(e(A),{class:"h-4 w-4 text-green-500"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",S,c(e(l).validTeams),1))]),_:1})]),_:1}),t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[4]||(a[4]=[n("无效订阅",-1)])]),_:1}),t(e($),{class:"h-4 w-4 text-red-500"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",L,c(e(l).invalidTeams),1))]),_:1})]),_:1}),t(e(f),null,{default:s(()=>[t(e(m),{class:"flex flex-row items-center justify-between space-y-0 pb-2"},{default:s(()=>[t(e(_),{class:"text-sm font-medium"},{default:s(()=>[...a[5]||(a[5]=[n("剩余席位",-1)])]),_:1}),t(e(z),{class:"h-4 w-4 text-muted-foreground"})]),_:1}),t(e(u),null,{default:s(()=>[e(l).loading?(o(),r(e(p),{key:0,class:"h-8 w-16"})):(o(),i("div",R,c(e(l).totalAvailableSeats),1))]),_:1})]),_:1})]),e(l).error?(o(),r(e(f),{key:0,class:"border-destructive"},{default:s(()=>[t(e(u),{class:"pt-6"},{default:s(()=>[d("div",U,[d("p",q,c(e(l).error),1),t(e(h),{variant:"outline",size:"sm",onClick:x},{default:s(()=>[...a[6]||(a[6]=[n(" 重试 ",-1)])]),_:1})])]),_:1})]),_:1})):v("",!0)]))}});export{J as default};

View File

@@ -1 +0,0 @@
import{d,a as c,o as l,w as r,x as i,I as v,J as h,u as e,p as w,D as p,P,c as D,v as f,b as y,i as $,f as k,h as x,n as B}from"./index-BIETROXK.js";import{L as O,M as z,F as M,J as C,N as T,O as F,Q as q,R as E,S as b,U as I,W as R}from"./PaginationPrevious.vue_vue_type_script_setup_true_lang-BYnju-Ld.js";import{u as N,r as m}from"./index-D7j6Bokf.js";var A=d({__name:"DialogPortal",props:{to:{type:null,required:!1},disabled:{type:Boolean,required:!1},defer:{type:Boolean,required:!1},forceMount:{type:Boolean,required:!1}},setup(s){const t=s;return(a,o)=>(l(),c(e(O),v(h(t)),{default:r(()=>[i(a.$slots,"default")]),_:3},16))}}),V=A,j=d({__name:"DialogTrigger",props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:"button"}},setup(s){const t=s,a=z(),{forwardRef:o,currentElement:n}=N();return a.contentId||=M(void 0,"reka-dialog-content"),w(()=>{a.triggerElement.value=n.value}),(u,g)=>(l(),c(e(P),p(t,{ref:e(o),type:u.as==="button"?"button":void 0,"aria-haspopup":"dialog","aria-expanded":e(a).open.value||!1,"aria-controls":e(a).open.value?e(a).contentId:void 0,"data-state":e(a).open.value?"open":"closed",onClick:e(a).onOpenToggle}),{default:r(()=>[i(u.$slots,"default")]),_:3},16,["type","aria-expanded","aria-controls","data-state","onClick"]))}}),J=j;const U=D("plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);const L=D("x",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]),W=d({__name:"Dialog",props:{open:{type:Boolean},defaultOpen:{type:Boolean},modal:{type:Boolean}},emits:["update:open"],setup(s,{emit:t}){const n=C(s,t);return(u,g)=>(l(),c(e(T),p({"data-slot":"dialog"},e(n)),{default:r(_=>[i(u.$slots,"default",v(h(_)))]),_:3},16))}}),S=d({__name:"DialogOverlay",props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{},class:{}},setup(s){const t=s,a=m(t,"class");return(o,n)=>(l(),c(e(F),p({"data-slot":"dialog-overlay"},e(a),{class:e(f)("data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",t.class)}),{default:r(()=>[i(o.$slots,"default")]),_:3},16,["class"]))}}),X=d({inheritAttrs:!1,__name:"DialogContent",props:{forceMount:{type:Boolean},disableOutsidePointerEvents:{type:Boolean},asChild:{type:Boolean},as:{},class:{},showCloseButton:{type:Boolean,default:!0}},emits:["escapeKeyDown","pointerDownOutside","focusOutside","interactOutside","openAutoFocus","closeAutoFocus"],setup(s,{emit:t}){const a=s,o=t,n=m(a,"class"),u=C(n,o);return(g,_)=>(l(),c(e(V),null,{default:r(()=>[y(S),y(e(q),p({"data-slot":"dialog-content"},{...g.$attrs,...e(u)},{class:e(f)("bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",a.class)}),{default:r(()=>[i(g.$slots,"default"),s.showCloseButton?(l(),c(e(E),{key:0,"data-slot":"dialog-close",class:"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"},{default:r(()=>[y(e(L)),_[0]||(_[0]=k("span",{class:"sr-only"},"Close",-1))]),_:1})):$("",!0)]),_:3},16,["class"])]),_:3}))}}),G=d({__name:"DialogDescription",props:{asChild:{type:Boolean},as:{},class:{}},setup(s){const t=s,a=m(t,"class"),o=b(a);return(n,u)=>(l(),c(e(I),p({"data-slot":"dialog-description"},e(o),{class:e(f)("text-muted-foreground text-sm",t.class)}),{default:r(()=>[i(n.$slots,"default")]),_:3},16,["class"]))}}),Y=d({__name:"DialogFooter",props:{class:{}},setup(s){const t=s;return(a,o)=>(l(),x("div",{"data-slot":"dialog-footer",class:B(e(f)("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",t.class))},[i(a.$slots,"default")],2))}}),Z=d({__name:"DialogHeader",props:{class:{}},setup(s){const t=s;return(a,o)=>(l(),x("div",{"data-slot":"dialog-header",class:B(e(f)("flex flex-col gap-2 text-center sm:text-left",t.class))},[i(a.$slots,"default")],2))}}),ee=d({__name:"DialogTitle",props:{asChild:{type:Boolean},as:{},class:{}},setup(s){const t=s,a=m(t,"class"),o=b(a);return(n,u)=>(l(),c(e(R),p({"data-slot":"dialog-title"},e(o),{class:e(f)("text-lg leading-none font-semibold",t.class)}),{default:r(()=>[i(n.$slots,"default")]),_:3},16,["class"]))}}),ae=d({__name:"DialogTrigger",props:{asChild:{type:Boolean},as:{}},setup(s){const t=s;return(a,o)=>(l(),c(e(J),p({"data-slot":"dialog-trigger"},t),{default:r(()=>[i(a.$slots,"default")]),_:3},16))}});export{U as P,W as _,ae as a,X as b,Z as c,ee as d,G as e,Y as f};

View File

@@ -1 +1 @@
import{c as C,d as w,r as c,a as f,u as a,w as t,o as d,b as l,e as n,f as p,g as V,h as $,i as _,n as h,t as y,_ as B,j as i}from"./index-BIETROXK.js";import{_ as N,a as T,b as K,c as L}from"./CardTitle.vue_vue_type_script_setup_true_lang-BZWhSLyf.js";import{_ as S}from"./CardDescription.vue_vue_type_script_setup_true_lang-PYRWivA-.js";import{_ as g,a as k}from"./Label.vue_vue_type_script_setup_true_lang-C4mrJ3gg.js";import{i as z}from"./invite-rZ0cGmoH.js";import{C as P}from"./circle-x-d27Sm-GD.js";import{L as U}from"./index-D7j6Bokf.js";const j=C("circle-check",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]),D={class:"space-y-2"},E={class:"space-y-2"},F=w({__name:"JoinPage",setup(G){const u=c(""),o=c(""),r=c(!1),s=c(null),x=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;async function b(){if(!u.value.trim()){i.error("请输入邮箱");return}if(!x.test(u.value)){i.error("邮箱格式不正确");return}if(!o.value.trim()){i.error("请输入卡密");return}r.value=!0,s.value=null;try{const m=await z({email:u.value.trim(),card_key:o.value.trim()});m.data.success?(s.value={success:!0,message:"提交成功,已发起邀请!"},i.success("提交成功"),u.value="",o.value=""):(s.value={success:!1,message:m.data.message||"提交失败,请检查卡密或邮箱"},i.error(s.value.message))}catch(m){const e=m.response?.data?.message||"提交失败,请检查卡密或邮箱";s.value={success:!1,message:e},i.error(e)}finally{r.value=!1}}return(m,e)=>(d(),f(a(N),{class:"w-full max-w-md mx-4"},{default:t(()=>[l(a(T),{class:"text-center"},{default:t(()=>[l(a(K),{class:"text-2xl"},{default:t(()=>[...e[2]||(e[2]=[n("加入 Team",-1)])]),_:1}),l(a(S),null,{default:t(()=>[...e[3]||(e[3]=[n("输入邮箱和卡密,即可加入 ChatGPT Team",-1)])]),_:1})]),_:1}),l(a(L),null,{default:t(()=>[p("form",{onSubmit:V(b,["prevent"]),class:"space-y-4"},[p("div",D,[l(a(g),{for:"email"},{default:t(()=>[...e[4]||(e[4]=[n("邮箱",-1)])]),_:1}),l(a(k),{id:"email",modelValue:u.value,"onUpdate:modelValue":e[0]||(e[0]=v=>u.value=v),type:"email",placeholder:"your@email.com",disabled:r.value},null,8,["modelValue","disabled"])]),p("div",E,[l(a(g),{for:"cardKey"},{default:t(()=>[...e[5]||(e[5]=[n("卡密",-1)])]),_:1}),l(a(k),{id:"cardKey",modelValue:o.value,"onUpdate:modelValue":e[1]||(e[1]=v=>o.value=v),type:"text",placeholder:"请输入卡密",disabled:r.value},null,8,["modelValue","disabled"])]),s.value?(d(),$("div",{key:0,class:h(["flex items-center gap-2 p-3 rounded-lg text-sm",s.value.success?"bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300":"bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300"])},[s.value.success?(d(),f(a(j),{key:0,class:"h-4 w-4 shrink-0"})):(d(),f(a(P),{key:1,class:"h-4 w-4 shrink-0"})),p("span",null,y(s.value.message),1)],2)):_("",!0),l(a(B),{type:"submit",class:"w-full",disabled:r.value},{default:t(()=>[r.value?(d(),f(a(U),{key:0,class:"mr-2 h-4 w-4 animate-spin"})):_("",!0),n(" "+y(r.value?"提交中...":"提交"),1)]),_:1},8,["disabled"])],32)]),_:1})]),_:1}))}});export{F as default};
import{c as C,d as w,r as c,a as f,u as a,w as t,o as d,b as l,e as n,f as p,g as V,h as $,i as _,n as h,t as y,_ as B,j as i}from"./index-B0FmaMuw.js";import{_ as N,a as T,b as K,c as L}from"./CardTitle.vue_vue_type_script_setup_true_lang-D0guZCre.js";import{_ as S}from"./CardDescription.vue_vue_type_script_setup_true_lang-BYlDBycT.js";import{_ as g,a as k}from"./Label.vue_vue_type_script_setup_true_lang-duvmWwej.js";import{i as z}from"./invite-DvsN2S4N.js";import{C as P}from"./circle-x-C8-4gjQR.js";import{L as U}from"./index-DwEwynZa.js";const j=C("circle-check",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]),D={class:"space-y-2"},E={class:"space-y-2"},F=w({__name:"JoinPage",setup(G){const u=c(""),o=c(""),r=c(!1),s=c(null),x=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;async function b(){if(!u.value.trim()){i.error("请输入邮箱");return}if(!x.test(u.value)){i.error("邮箱格式不正确");return}if(!o.value.trim()){i.error("请输入卡密");return}r.value=!0,s.value=null;try{const m=await z({email:u.value.trim(),card_key:o.value.trim()});m.data.success?(s.value={success:!0,message:"提交成功,已发起邀请!"},i.success("提交成功"),u.value="",o.value=""):(s.value={success:!1,message:m.data.message||"提交失败,请检查卡密或邮箱"},i.error(s.value.message))}catch(m){const e=m.response?.data?.message||"提交失败,请检查卡密或邮箱";s.value={success:!1,message:e},i.error(e)}finally{r.value=!1}}return(m,e)=>(d(),f(a(N),{class:"w-full max-w-md mx-4"},{default:t(()=>[l(a(T),{class:"text-center"},{default:t(()=>[l(a(K),{class:"text-2xl"},{default:t(()=>[...e[2]||(e[2]=[n("加入 Team",-1)])]),_:1}),l(a(S),null,{default:t(()=>[...e[3]||(e[3]=[n("输入邮箱和卡密,即可加入 ChatGPT Team",-1)])]),_:1})]),_:1}),l(a(L),null,{default:t(()=>[p("form",{onSubmit:V(b,["prevent"]),class:"space-y-4"},[p("div",D,[l(a(g),{for:"email"},{default:t(()=>[...e[4]||(e[4]=[n("邮箱",-1)])]),_:1}),l(a(k),{id:"email",modelValue:u.value,"onUpdate:modelValue":e[0]||(e[0]=v=>u.value=v),type:"email",placeholder:"your@email.com",disabled:r.value},null,8,["modelValue","disabled"])]),p("div",E,[l(a(g),{for:"cardKey"},{default:t(()=>[...e[5]||(e[5]=[n("卡密",-1)])]),_:1}),l(a(k),{id:"cardKey",modelValue:o.value,"onUpdate:modelValue":e[1]||(e[1]=v=>o.value=v),type:"text",placeholder:"请输入卡密",disabled:r.value},null,8,["modelValue","disabled"])]),s.value?(d(),$("div",{key:0,class:h(["flex items-center gap-2 p-3 rounded-lg text-sm",s.value.success?"bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300":"bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300"])},[s.value.success?(d(),f(a(j),{key:0,class:"h-4 w-4 shrink-0"})):(d(),f(a(P),{key:1,class:"h-4 w-4 shrink-0"})),p("span",null,y(s.value.message),1)],2)):_("",!0),l(a(B),{type:"submit",class:"w-full",disabled:r.value},{default:t(()=>[r.value?(d(),f(a(U),{key:0,class:"mr-2 h-4 w-4 animate-spin"})):_("",!0),n(" "+y(r.value?"提交中...":"提交"),1)]),_:1},8,["disabled"])],32)]),_:1})]),_:1}))}});export{F as default};

View File

@@ -1 +0,0 @@
import{d as m,a as V,o as v,w as $,x as C,D as B,u as d,P as O,L as P,r as q,C as h,q as D,B as E,M,N as F,h as J,n as k,v as S,O as I}from"./index-BIETROXK.js";import{u as T,i as U,r as z}from"./index-D7j6Bokf.js";var R=m({__name:"Label",props:{for:{type:String,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:"label"}},setup(a){const s=a;return T(),(i,n)=>(v(),V(d(O),B(s,{onMousedown:n[0]||(n[0]=l=>{!l.defaultPrevented&&l.detail>1&&l.preventDefault()})}),{default:$(()=>[C(i.$slots,"default")]),_:3},16))}}),j=R;function A(a){return JSON.parse(JSON.stringify(a))}function G(a,s,i,n={}){var l,r;const{clone:o=!1,passive:c=!1,eventName:L,deep:b=!1,defaultValue:N,shouldEmit:g}=n,e=P(),_=i||e?.emit||(e==null||(l=e.$emit)===null||l===void 0?void 0:l.bind(e))||(e==null||(r=e.proxy)===null||r===void 0||(r=r.$emit)===null||r===void 0?void 0:r.bind(e?.proxy));let u=L;u=u||`update:${s.toString()}`;const x=t=>o?typeof o=="function"?o(t):A(t):t,y=()=>U(a[s])?x(a[s]):N,w=t=>{g?g(t)&&_(u,t):_(u,t)};if(c){const t=q(y());let p=!1;return h(()=>a[s],f=>{p||(p=!0,t.value=x(f),E(()=>p=!1))}),h(t,f=>{!p&&(f!==a[s]||b)&&w(f)},{deep:b}),t}else return D({get(){return y()},set(t){w(t)}})}const Q=m({__name:"Input",props:{defaultValue:{},modelValue:{},class:{}},emits:["update:modelValue"],setup(a,{emit:s}){const i=a,l=G(i,"modelValue",s,{passive:!0,defaultValue:i.defaultValue});return(r,o)=>M((v(),J("input",{"onUpdate:modelValue":o[0]||(o[0]=c=>I(l)?l.value=c:null),"data-slot":"input",class:k(d(S)("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm","focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]","aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",i.class))},null,2)),[[F,d(l)]])}}),W=m({__name:"Label",props:{for:{},asChild:{type:Boolean},as:{},class:{}},setup(a){const s=a,i=z(s,"class");return(n,l)=>(v(),V(d(j),B({"data-slot":"label"},d(i),{class:d(S)("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",s.class)}),{default:$(()=>[C(n.$slots,"default")]),_:3},16,["class"]))}});export{W as _,Q as a};

View File

@@ -0,0 +1 @@
import{d as m,a as V,o as v,w as $,x as B,B as C,u as d,P as O,L as P,r as q,I as h,q as E,H as M,M as D,N as F,h as I,n as J,v as S,O as k}from"./index-B0FmaMuw.js";import{a as T,i as U,r as z}from"./index-DwEwynZa.js";var H=m({__name:"Label",props:{for:{type:String,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:"label"}},setup(a){const s=a;return T(),(i,n)=>(v(),V(d(O),C(s,{onMousedown:n[0]||(n[0]=l=>{!l.defaultPrevented&&l.detail>1&&l.preventDefault()})}),{default:$(()=>[B(i.$slots,"default")]),_:3},16))}}),R=H;function j(a){return JSON.parse(JSON.stringify(a))}function A(a,s,i,n={}){var l,r;const{clone:o=!1,passive:c=!1,eventName:L,deep:b=!1,defaultValue:N,shouldEmit:g}=n,e=P(),_=i||e?.emit||(e==null||(l=e.$emit)===null||l===void 0?void 0:l.bind(e))||(e==null||(r=e.proxy)===null||r===void 0||(r=r.$emit)===null||r===void 0?void 0:r.bind(e?.proxy));let u=L;u=u||`update:${s.toString()}`;const x=t=>o?typeof o=="function"?o(t):j(t):t,y=()=>U(a[s])?x(a[s]):N,w=t=>{g?g(t)&&_(u,t):_(u,t)};if(c){const t=q(y());let p=!1;return h(()=>a[s],f=>{p||(p=!0,t.value=x(f),M(()=>p=!1))}),h(t,f=>{!p&&(f!==a[s]||b)&&w(f)},{deep:b}),t}else return E({get(){return y()},set(t){w(t)}})}const Q=m({__name:"Input",props:{defaultValue:{},modelValue:{},class:{}},emits:["update:modelValue"],setup(a,{emit:s}){const i=a,l=A(i,"modelValue",s,{passive:!0,defaultValue:i.defaultValue});return(r,o)=>D((v(),I("input",{"onUpdate:modelValue":o[0]||(o[0]=c=>k(l)?l.value=c:null),"data-slot":"input",class:J(d(S)("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm","focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]","aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",i.class))},null,2)),[[F,d(l)]])}}),W=m({__name:"Label",props:{for:{},asChild:{type:Boolean},as:{},class:{}},setup(a){const s=a,i=z(s,"class");return(n,l)=>(v(),V(d(R),C({"data-slot":"label"},d(i),{class:d(S)("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",s.class)}),{default:$(()=>[B(n.$slots,"default")]),_:3},16,["class"]))}});export{W as _,Q as a};

View File

@@ -1 +1 @@
import{d as V,k as g,r as i,a as c,w as t,u as a,l as $,m as k,o as p,b as s,e as o,f as m,g as C,_ as L,i as N,t as S,j as f}from"./index-BIETROXK.js";import{_ as B,a as R,b as U,c as h}from"./CardTitle.vue_vue_type_script_setup_true_lang-BZWhSLyf.js";import{_ as j}from"./CardDescription.vue_vue_type_script_setup_true_lang-PYRWivA-.js";import{_,a as v}from"./Label.vue_vue_type_script_setup_true_lang-C4mrJ3gg.js";import{L as q}from"./index-D7j6Bokf.js";const A={class:"space-y-2"},D={class:"space-y-2"},G=V({__name:"LoginPage",setup(M){const w=$(),b=k(),x=g(),r=i(""),u=i(""),l=i(!1);async function y(){if(!r.value.trim()||!u.value.trim()){f.error("请输入账号和密码");return}l.value=!0;const d=await x.login({username:r.value.trim(),password:u.value});if(l.value=!1,d.success){f.success("登录成功");const e=b.query.redirect;w.push(e||"/admin/dashboard")}else f.error(d.message||"登录失败")}return(d,e)=>(p(),c(a(B),{class:"w-full max-w-md mx-4"},{default:t(()=>[s(a(R),{class:"text-center"},{default:t(()=>[s(a(U),{class:"text-2xl"},{default:t(()=>[...e[2]||(e[2]=[o("管理后台登录",-1)])]),_:1}),s(a(j),null,{default:t(()=>[...e[3]||(e[3]=[o("请输入您的账号和密码",-1)])]),_:1})]),_:1}),s(a(h),null,{default:t(()=>[m("form",{onSubmit:C(y,["prevent"]),class:"space-y-4"},[m("div",A,[s(a(_),{for:"username"},{default:t(()=>[...e[4]||(e[4]=[o("账号",-1)])]),_:1}),s(a(v),{id:"username",modelValue:r.value,"onUpdate:modelValue":e[0]||(e[0]=n=>r.value=n),type:"text",placeholder:"请输入账号",disabled:l.value,autocomplete:"username"},null,8,["modelValue","disabled"])]),m("div",D,[s(a(_),{for:"password"},{default:t(()=>[...e[5]||(e[5]=[o("密码",-1)])]),_:1}),s(a(v),{id:"password",modelValue:u.value,"onUpdate:modelValue":e[1]||(e[1]=n=>u.value=n),type:"password",placeholder:"请输入密码",disabled:l.value,autocomplete:"current-password"},null,8,["modelValue","disabled"])]),s(a(L),{type:"submit",class:"w-full",disabled:l.value},{default:t(()=>[l.value?(p(),c(a(q),{key:0,class:"mr-2 h-4 w-4 animate-spin"})):N("",!0),o(" "+S(l.value?"登录中...":"登录"),1)]),_:1},8,["disabled"])],32)]),_:1})]),_:1}))}});export{G as default};
import{d as V,k as g,r as i,a as c,w as t,u as a,l as $,m as k,o as p,b as s,e as o,f as m,g as C,_ as L,i as N,t as S,j as f}from"./index-B0FmaMuw.js";import{_ as B,a as R,b as U,c as h}from"./CardTitle.vue_vue_type_script_setup_true_lang-D0guZCre.js";import{_ as j}from"./CardDescription.vue_vue_type_script_setup_true_lang-BYlDBycT.js";import{_,a as v}from"./Label.vue_vue_type_script_setup_true_lang-duvmWwej.js";import{L as q}from"./index-DwEwynZa.js";const A={class:"space-y-2"},D={class:"space-y-2"},G=V({__name:"LoginPage",setup(M){const w=$(),b=k(),x=g(),r=i(""),u=i(""),l=i(!1);async function y(){if(!r.value.trim()||!u.value.trim()){f.error("请输入账号和密码");return}l.value=!0;const d=await x.login({username:r.value.trim(),password:u.value});if(l.value=!1,d.success){f.success("登录成功");const e=b.query.redirect;w.push(e||"/admin/dashboard")}else f.error(d.message||"登录失败")}return(d,e)=>(p(),c(a(B),{class:"w-full max-w-md mx-4"},{default:t(()=>[s(a(R),{class:"text-center"},{default:t(()=>[s(a(U),{class:"text-2xl"},{default:t(()=>[...e[2]||(e[2]=[o("管理后台登录",-1)])]),_:1}),s(a(j),null,{default:t(()=>[...e[3]||(e[3]=[o("请输入您的账号和密码",-1)])]),_:1})]),_:1}),s(a(h),null,{default:t(()=>[m("form",{onSubmit:C(y,["prevent"]),class:"space-y-4"},[m("div",A,[s(a(_),{for:"username"},{default:t(()=>[...e[4]||(e[4]=[o("账号",-1)])]),_:1}),s(a(v),{id:"username",modelValue:r.value,"onUpdate:modelValue":e[0]||(e[0]=n=>r.value=n),type:"text",placeholder:"请输入账号",disabled:l.value,autocomplete:"username"},null,8,["modelValue","disabled"])]),m("div",D,[s(a(_),{for:"password"},{default:t(()=>[...e[5]||(e[5]=[o("密码",-1)])]),_:1}),s(a(v),{id:"password",modelValue:u.value,"onUpdate:modelValue":e[1]||(e[1]=n=>u.value=n),type:"password",placeholder:"请输入密码",disabled:l.value,autocomplete:"current-password"},null,8,["modelValue","disabled"])]),s(a(L),{type:"submit",class:"w-full",disabled:l.value},{default:t(()=>[l.value?(p(),c(a(q),{key:0,class:"mr-2 h-4 w-4 animate-spin"})):N("",!0),o(" "+S(l.value?"登录中...":"登录"),1)]),_:1},8,["disabled"])],32)]),_:1})]),_:1}))}});export{G as default};

View File

@@ -1 +1 @@
import{d as a,h as n,n as o,u as t,v as r,o as l}from"./index-BIETROXK.js";const u=a({__name:"Skeleton",props:{class:{}},setup(s){const e=s;return(c,p)=>(l(),n("div",{"data-slot":"skeleton",class:o(t(r)("animate-pulse rounded-md bg-primary/10",e.class))},null,2))}});export{u as _};
import{d as a,h as n,n as o,u as t,v as r,o as l}from"./index-B0FmaMuw.js";const u=a({__name:"Skeleton",props:{class:{}},setup(s){const e=s;return(c,p)=>(l(),n("div",{"data-slot":"skeleton",class:o(t(r)("animate-pulse rounded-md bg-primary/10",e.class))},null,2))}});export{u as _};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{y as s,z as p,r as o,q as r}from"./index-B0FmaMuw.js";function m(e){const a=new URLSearchParams().toString();return s.get(`/api/accounts${a?`?${a}`:""}`)}function A(e){return s.post("/api/accounts/create",e)}function b(e){return s.post(`/api/accounts/refresh?id=${e}`)}function w(e){return s.delete(`/api/accounts/delete?id=${e}`)}function y(e){return s.delete("/api/accounts/batch/delete",{data:{ids:e}})}function S(e){return s.post("/api/accounts/batch/refresh",{ids:e})}const _=p("accounts",()=>{const e=o([]),n=o(!1),a=o(null),u=r(()=>e.value.length),i=r(()=>e.value.filter(t=>t.is_active).length),l=r(()=>e.value.filter(t=>!t.is_active).length),d=r(()=>e.value.reduce((t,c)=>t+Math.max(0,(c.seats_entitled||0)-(c.seats_in_use||0)),0));async function f(){n.value=!0,a.value=null;try{const t=await m();if(t.data.success)e.value=t.data.data||[];else throw new Error(t.data.message||"获取账号列表失败")}catch(t){throw a.value=t.message||"获取账号列表失败",t}finally{n.value=!1}}function h(t){const c=e.value.findIndex(v=>v.id===t.id);c!==-1&&(e.value[c]=t)}return{accounts:e,loading:n,error:a,totalTeams:u,validTeams:i,invalidTeams:l,totalAvailableSeats:d,fetchAccounts:f,updateAccount:h}});export{S as a,y as b,A as c,w as d,b as r,_ as u};

View File

@@ -1 +0,0 @@
import{y as r,z as m,r as o,q as c}from"./index-BIETROXK.js";function p(e){const a=new URLSearchParams().toString();return r.get(`/api/accounts${a?`?${a}`:""}`)}function A(e){return r.post("/api/accounts/create",e)}function w(e){return r.post(`/api/accounts/refresh?id=${e}`)}function y(e){return r.delete(`/api/accounts/delete?id=${e}`)}const S=m("accounts",()=>{const e=o([]),n=o(!1),a=o(null),u=c(()=>e.value.length),i=c(()=>e.value.filter(t=>t.is_active).length),l=c(()=>e.value.filter(t=>!t.is_active).length),d=c(()=>e.value.reduce((t,s)=>t+Math.max(0,(s.seats_entitled||0)-(s.seats_in_use||0)),0));async function f(){n.value=!0,a.value=null;try{const t=await p();if(t.data.success)e.value=t.data.data||[];else throw new Error(t.data.message||"获取账号列表失败")}catch(t){throw a.value=t.message||"获取账号列表失败",t}finally{n.value=!1}}function v(t){const s=e.value.findIndex(h=>h.id===t.id);s!==-1&&(e.value[s]=t)}return{accounts:e,loading:n,error:a,totalTeams:u,validTeams:i,invalidTeams:l,totalAvailableSeats:d,fetchAccounts:f,updateAccount:v}});export{A as c,y as d,w as r,S as u};

View File

@@ -1 +1 @@
import{c}from"./index-BIETROXK.js";const r=c("circle-x",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);export{r as C};
import{c}from"./index-B0FmaMuw.js";const r=c("circle-x",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);export{r as C};

View File

@@ -1 +1 @@
import{y as t}from"./index-BIETROXK.js";function e(i){return t.post("/api/invite/card",i)}function a(i){return t.get(`/api/invite?account_id=${i}`)}function r(i){return t.delete("/api/invite",{data:i})}function o(i){return t.post("/api/invite",i)}export{o as a,r as d,e as i,a as l};
import{y as t}from"./index-B0FmaMuw.js";function e(i){return t.post("/api/invite/card",i)}function a(i){return t.get(`/api/invite?account_id=${i}`)}function r(i){return t.delete("/api/invite",{data:i})}function o(i){return t.post("/api/invite",i)}export{o as a,r as d,e as i,a as l};

View File

@@ -1 +1 @@
import{c as e}from"./index-BIETROXK.js";const t=e("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);export{t as R};
import{c as e}from"./index-B0FmaMuw.js";const t=e("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);export{t as R};

View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>mygo Team</title>
<script type="module" crossorigin src="/assets/index-BIETROXK.js"></script>
<script type="module" crossorigin src="/assets/index-B0FmaMuw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C_xOPDav.css">
</head>
<body>

View File

@@ -7,6 +7,7 @@ export interface Account {
is_active: boolean
seats_in_use: number
seats_entitled: number
active_until?: string
created_at: string
updated_at: string
}
@@ -56,3 +57,18 @@ export function refreshAccount(id: number) {
export function deleteAccount(id: number) {
return request.delete(`/api/accounts/delete?id=${id}`)
}
export interface BatchOperationResponse {
success: boolean
message?: string
success_count?: number
failed_count?: number
}
export function batchDeleteAccounts(ids: number[]) {
return request.delete<BatchOperationResponse>('/api/accounts/batch/delete', { data: { ids } })
}
export function batchRefreshAccounts(ids: number[]) {
return request.post<BatchOperationResponse>('/api/accounts/batch/refresh', { ids })
}

View File

@@ -7,7 +7,7 @@ export interface InviteByCardRequest {
export interface Invitation {
id: number
email: string
invited_email: string
account_id: number
status: string
created_at: string

View File

@@ -114,7 +114,7 @@ async function handleDelete() {
try {
const response = await deleteInvite({
email: invitation.email,
email: invitation.invited_email,
account_id: accountId.value,
})
if (response.data.success) {
@@ -199,7 +199,7 @@ function handlePageSizeChange(value: any) {
<TableBody>
<TableRow v-for="invitation in paginatedInvitations" :key="invitation.id">
<TableCell class="font-medium">
{{ invitation.email }}
{{ invitation.invited_email }}
</TableCell>
<TableCell>
<Badge variant="outline">
@@ -277,7 +277,7 @@ function handlePageSizeChange(value: any) {
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除用户 <strong>{{ pendingDelete?.email }}</strong> 此操作将从 Team 中移除该用户
确定要删除用户 <strong>{{ pendingDelete?.invited_email }}</strong> 此操作将从 Team 中移除该用户
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -53,8 +53,9 @@ import {
PaginationPrevious,
} from '@/components/ui/pagination'
import { useAccountsStore } from '@/stores/accounts'
import { createAccount, refreshAccount, deleteAccount, type Account } from '@/api/accounts'
import { createAccount, refreshAccount, deleteAccount, batchDeleteAccounts, batchRefreshAccounts, type Account } from '@/api/accounts'
import { inviteByAdmin } from '@/api/invite'
import { Checkbox } from '@/components/ui/checkbox'
import { Plus, RefreshCw, Users, Loader2, Eye, EyeOff, Trash2, UserPlus, Shuffle } from 'lucide-vue-next'
const router = useRouter()
@@ -87,6 +88,12 @@ const currentPage = ref(1)
const pageSize = ref(10)
const pageSizeOptions = [5, 10, 20, 50]
// Selection for batch operations
const selectedIds = ref<number[]>([])
const batchDeleting = ref(false)
const batchRefreshing = ref(false)
const batchDeleteDialogOpen = ref(false)
const totalPages = computed(() => Math.ceil(accountsStore.accounts.length / pageSize.value))
const paginatedAccounts = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
@@ -278,6 +285,99 @@ function handlePageSizeChange(value: any) {
currentPage.value = 1
}
}
function formatDate(dateStr?: string) {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleDateString('zh-CN')
}
const isAllSelected = computed(() => {
if (paginatedAccounts.value.length === 0) return false
return paginatedAccounts.value.every(a => selectedIds.value.includes(a.id))
})
function toggleSelectAll() {
if (isAllSelected.value) {
// Deselect all in current page
const pageIds = paginatedAccounts.value.map(a => a.id)
selectedIds.value = selectedIds.value.filter(id => !pageIds.includes(id))
} else {
// Select all in current page
const pageIds = paginatedAccounts.value.map(a => a.id)
const newIds = pageIds.filter(id => !selectedIds.value.includes(id))
selectedIds.value = [...selectedIds.value, ...newIds]
}
}
function toggleSelect(id: number) {
if (selectedIds.value.includes(id)) {
selectedIds.value = selectedIds.value.filter(i => i !== id)
} else {
selectedIds.value = [...selectedIds.value, id]
}
}
function clearSelection() {
selectedIds.value = []
}
function confirmBatchDelete() {
if (selectedIds.value.length === 0) {
toast.error('请先选择要删除的 Team')
return
}
batchDeleteDialogOpen.value = true
}
async function handleBatchDelete() {
if (selectedIds.value.length === 0) return
batchDeleting.value = true
batchDeleteDialogOpen.value = false
try {
const ids = [...selectedIds.value]
const response = await batchDeleteAccounts(ids)
if (response.data.success) {
toast.success(`成功删除 ${response.data.success_count} 个 Team`)
} else {
toast.success(`删除完成: 成功 ${response.data.success_count} 个, 失败 ${response.data.failed_count}`)
}
clearSelection()
await accountsStore.fetchAccounts()
if (paginatedAccounts.value.length === 0 && currentPage.value > 1) {
currentPage.value--
}
} catch (e: any) {
toast.error(e.response?.data?.message || '批量删除失败')
} finally {
batchDeleting.value = false
}
}
async function handleBatchRefresh() {
if (selectedIds.value.length === 0) {
toast.error('请先选择要刷新的 Team')
return
}
batchRefreshing.value = true
try {
const ids = [...selectedIds.value]
const response = await batchRefreshAccounts(ids)
if (response.data.success) {
toast.success(`成功刷新 ${response.data.success_count} 个 Team`)
} else {
toast.success(`刷新完成: 成功 ${response.data.success_count} 个, 失败 ${response.data.failed_count}`)
}
await accountsStore.fetchAccounts()
} catch (e: any) {
toast.error(e.response?.data?.message || '批量刷新失败')
} finally {
batchRefreshing.value = false
}
}
</script>
<template>
@@ -285,6 +385,64 @@ function handlePageSizeChange(value: any) {
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">Team 管理</h1>
<div class="flex items-center gap-2">
<!-- Batch operations - always show when there's data -->
<template v-if="accountsStore.accounts.length > 0">
<Button
variant="outline"
@click="handleBatchRefresh"
:disabled="selectedIds.length === 0 || batchRefreshing"
>
<Loader2 v-if="batchRefreshing" class="h-4 w-4 mr-2 animate-spin" />
<RefreshCw v-else class="h-4 w-4 mr-2" />
刷新{{ selectedIds.length > 0 ? ` (${selectedIds.length})` : '' }}
</Button>
<Button
variant="destructive"
@click="confirmBatchDelete"
:disabled="selectedIds.length === 0 || batchDeleting"
>
<Loader2 v-if="batchDeleting" class="h-4 w-4 mr-2 animate-spin" />
<Trash2 v-else class="h-4 w-4 mr-2" />
删除{{ selectedIds.length > 0 ? ` (${selectedIds.length})` : '' }}
</Button>
</template>
<!-- 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>
<Dialog v-model:open="dialogOpen">
<DialogTrigger as-child>
<Button>
@@ -350,42 +508,6 @@ 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>
@@ -415,14 +537,27 @@ function handlePageSizeChange(value: any) {
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-12">
<Checkbox
:model-value="isAllSelected"
@update:model-value="toggleSelectAll"
/>
</TableHead>
<TableHead>名称</TableHead>
<TableHead>订阅状态</TableHead>
<TableHead>到期时间</TableHead>
<TableHead class="text-right">剩余席位</TableHead>
<TableHead class="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="account in paginatedAccounts" :key="account.id">
<TableRow v-for="account in paginatedAccounts" :key="account.id" :class="{ 'bg-muted/50': selectedIds.includes(account.id) }">
<TableCell>
<Checkbox
:model-value="selectedIds.includes(account.id)"
@update:model-value="() => toggleSelect(account.id)"
/>
</TableCell>
<TableCell class="font-medium">
{{ account.name || account.team_account_id }}
</TableCell>
@@ -431,6 +566,9 @@ function handlePageSizeChange(value: any) {
{{ account.is_active ? '有效' : '无效' }}
</Badge>
</TableCell>
<TableCell class="text-muted-foreground">
{{ formatDate(account.active_until) }}
</TableCell>
<TableCell class="text-right">
{{ (account.seats_entitled || 0) - (account.seats_in_use || 0) }}
</TableCell>
@@ -533,6 +671,24 @@ function handlePageSizeChange(value: any) {
</AlertDialogContent>
</AlertDialog>
<!-- Batch Delete confirmation dialog -->
<AlertDialog v-model:open="batchDeleteDialogOpen">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认批量删除</AlertDialogTitle>
<AlertDialogDescription>
确定要删除选中的 <strong>{{ selectedIds.length }}</strong> 个 Team 吗?此操作不可撤销,相关的邀请记录也会被删除。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction @click="handleBatchDelete" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- Invite dialog -->
<Dialog v-model:open="inviteDialogOpen">
<DialogContent>