Compare commits

...

3 Commits

27 changed files with 170 additions and 3 deletions

37
backend/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download all dependencies
RUN go mod download
# Copy the source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main ./cmd/main.go
# Final stage
FROM alpine:latest
WORKDIR /app
# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates tzdata
# Copy binary from builder
COPY --from=builder /app/main .
COPY --from=builder /app/.env.example .
# Expose port
EXPOSE 8080
# Command to run
CMD ["./main"]

View File

@@ -8,6 +8,7 @@ import (
"gpt-manager-go/internal/middleware"
"gpt-manager-go/internal/repository"
"gpt-manager-go/internal/service"
"gpt-manager-go/internal/static"
)
// SetupRoutes 设置路由
@@ -86,6 +87,9 @@ func SetupRoutes(db *sql.DB) http.Handler {
mux.Handle("/api/cardkeys", middleware.AuthMiddleware(protectedMux))
mux.Handle("/api/cardkeys/", middleware.AuthMiddleware(protectedMux))
// 静态文件服务(前端)
mux.Handle("/", static.Handler())
// CORS 中间件包装
return corsMiddleware(mux)
}

View File

@@ -0,0 +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 _};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +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};

View File

@@ -0,0 +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};

View File

@@ -0,0 +1 @@
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

@@ -0,0 +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};

View File

@@ -0,0 +1 @@
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 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};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +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 _};

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 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

@@ -0,0 +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};

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 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};

View File

@@ -0,0 +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};

14
backend/internal/static/dist/index.html vendored Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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>
<link rel="stylesheet" crossorigin href="/assets/index-C_xOPDav.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
backend/internal/static/dist/vite.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,56 @@
package static
import (
"embed"
"io/fs"
"net/http"
"strings"
)
//go:embed dist/*
var StaticFiles embed.FS
// Handler 返回静态文件处理器
// 用于服务嵌入的前端静态文件
func Handler() http.Handler {
// 提取 dist 子目录
distFS, err := fs.Sub(StaticFiles, "dist")
if err != nil {
// 如果 dist 目录不存在,返回空处理器
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Static files not available", http.StatusNotFound)
})
}
fileServer := http.FileServer(http.FS(distFS))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 对于 API 路径,不处理
if strings.HasPrefix(r.URL.Path, "/api/") {
http.NotFound(w, r)
return
}
// 尝试服务静态文件
// 对于 SPA如果文件不存在返回 index.html
path := r.URL.Path
if path == "/" {
path = "/index.html"
}
// 检查文件是否存在
_, err := fs.Stat(distFS, strings.TrimPrefix(path, "/"))
if err != nil {
// 文件不存在,返回 index.html支持 SPA 路由)
r.URL.Path = "/"
}
fileServer.ServeHTTP(w, r)
})
}
// IsAvailable 检查静态文件是否可用
func IsAvailable() bool {
_, err := fs.Sub(StaticFiles, "dist")
return err == nil
}

27
docker-compose.yml Normal file
View File

@@ -0,0 +1,27 @@
services:
app:
container_name: gpt-manager
build: ./backend
ports:
- "${EXPOSE_PORT}:8080"
depends_on:
- db
env_file:
- .env
environment:
- PORT=${PORT}
restart: unless-stopped
db:
container_name: gpt-manager-db
image: postgres:16-alpine
restart: always
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<title>mygo Team</title>
</head>
<body>
<div id="app"></div>

View File

@@ -77,8 +77,8 @@ async function loadInvitations() {
loading.value = true
try {
const response = await listInvitations(accountId.value)
if (response.data.success && response.data.data) {
invitations.value = response.data.data
if (response.data.success) {
invitations.value = response.data.data || []
} else {
toast.error(response.data.message || '获取邀请列表失败')
}