mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 13:00:14 +09:00
feat: support redeem code operation (#90)
This commit is contained in:
parent
296c1f8781
commit
6f5dae5ca9
@ -133,27 +133,27 @@ func UserTypeAnalysisAPI(c *gin.Context) {
|
|||||||
|
|
||||||
func RedeemListAPI(c *gin.Context) {
|
func RedeemListAPI(c *gin.Context) {
|
||||||
db := utils.GetDBFromContext(c)
|
db := utils.GetDBFromContext(c)
|
||||||
c.JSON(http.StatusOK, GetRedeemData(db))
|
|
||||||
|
page, _ := strconv.Atoi(c.Query("page"))
|
||||||
|
c.JSON(http.StatusOK, GetRedeemData(db, int64(page)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func RedeemSegmentAPI(c *gin.Context) {
|
func DeleteRedeemAPI(c *gin.Context) {
|
||||||
quota := utils.ParseFloat32(c.Query("quota"))
|
|
||||||
onlyUnused := utils.ParseBool(c.Query("unused"))
|
|
||||||
|
|
||||||
db := utils.GetDBFromContext(c)
|
db := utils.GetDBFromContext(c)
|
||||||
|
|
||||||
data, err := GetRedeemSegment(db, quota, onlyUnused)
|
var form DeleteInvitationForm
|
||||||
if err != nil {
|
if err := c.ShouldBindJSON(&form); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": false,
|
"status": false,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := DeleteRedeemCode(db, form.Code)
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": true,
|
"status": err == nil,
|
||||||
"data": data,
|
"error": err,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,60 +5,64 @@ import (
|
|||||||
"chat/utils"
|
"chat/utils"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRedeemData(db *sql.DB) []RedeemData {
|
func GetRedeemData(db *sql.DB, page int64) PaginationForm {
|
||||||
var data []RedeemData
|
var data []interface{}
|
||||||
|
var total int64
|
||||||
|
if err := globals.QueryRowDb(db, `
|
||||||
|
SELECT COUNT(*) FROM redeem
|
||||||
|
`).Scan(&total); err != nil {
|
||||||
|
return PaginationForm{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := globals.QueryDb(db, `
|
rows, err := globals.QueryDb(db, `
|
||||||
SELECT quota, COUNT(*) AS total, SUM(IF(used = 0, 0, 1)) AS used
|
SELECT code, quota, used, created_at, updated_at
|
||||||
FROM redeem
|
FROM redeem
|
||||||
GROUP BY quota
|
ORDER BY id DESC LIMIT ? OFFSET ?
|
||||||
`)
|
`, pagination, page*pagination)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data
|
return PaginationForm{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var d RedeemData
|
var redeem RedeemData
|
||||||
if err := rows.Scan(&d.Quota, &d.Total, &d.Used); err != nil {
|
var createdAt []uint8
|
||||||
return data
|
var updatedAt []uint8
|
||||||
|
if err := rows.Scan(&redeem.Code, &redeem.Quota, &redeem.Used, &createdAt, &updatedAt); err != nil {
|
||||||
|
return PaginationForm{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data = append(data, d)
|
|
||||||
|
redeem.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05")
|
||||||
|
redeem.UpdatedAt = utils.ConvertTime(updatedAt).Format("2006-01-02 15:04:05")
|
||||||
|
data = append(data, redeem)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return PaginationForm{
|
||||||
|
Status: true,
|
||||||
|
Total: int(math.Ceil(float64(total) / float64(pagination))),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedeemSegment(db *sql.DB, quota float32, onlyUnused bool) ([]string, error) {
|
func DeleteRedeemCode(db *sql.DB, code string) error {
|
||||||
var codes []string
|
_, err := globals.ExecDb(db, `
|
||||||
var rows *sql.Rows
|
DELETE FROM redeem WHERE code = ?
|
||||||
var err error
|
`, code)
|
||||||
|
|
||||||
if onlyUnused {
|
return err
|
||||||
rows, err = globals.QueryDb(db, `
|
|
||||||
SELECT code FROM redeem WHERE quota = ? AND used = 0
|
|
||||||
`, quota)
|
|
||||||
} else {
|
|
||||||
rows, err = globals.QueryDb(db, `
|
|
||||||
SELECT code FROM redeem WHERE quota = ?
|
|
||||||
`, quota)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return codes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var code string
|
|
||||||
if err := rows.Scan(&code); err != nil {
|
|
||||||
return codes, err
|
|
||||||
}
|
|
||||||
codes = append(codes, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
return codes, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse {
|
func GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse {
|
||||||
|
@ -20,8 +20,8 @@ func Register(app *gin.RouterGroup) {
|
|||||||
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
|
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
|
||||||
|
|
||||||
app.GET("/admin/redeem/list", RedeemListAPI)
|
app.GET("/admin/redeem/list", RedeemListAPI)
|
||||||
app.GET("/admin/redeem/segment", RedeemSegmentAPI)
|
|
||||||
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
|
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
|
||||||
|
app.POST("/admin/redeem/delete", DeleteRedeemAPI)
|
||||||
|
|
||||||
app.GET("/admin/user/list", UserPaginationAPI)
|
app.GET("/admin/user/list", UserPaginationAPI)
|
||||||
app.POST("/admin/user/quota", UserQuotaAPI)
|
app.POST("/admin/user/quota", UserQuotaAPI)
|
||||||
|
@ -51,9 +51,11 @@ type InvitationData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RedeemData struct {
|
type RedeemData struct {
|
||||||
Quota float32 `json:"quota"`
|
Code string `json:"code"`
|
||||||
Used float32 `json:"used"`
|
Quota float32 `json:"quota"`
|
||||||
Total float32 `json:"total"`
|
Used bool `json:"used"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InvitationGenerateResponse struct {
|
type InvitationGenerateResponse struct {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "chatnio",
|
"productName": "chatnio",
|
||||||
"version": "3.10.5"
|
"version": "3.10.6"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -128,25 +128,22 @@ export async function generateInvitation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRedeemList(): Promise<RedeemResponse> {
|
export async function getRedeemList(page: number): Promise<RedeemResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/admin/redeem/list");
|
const response = await axios.get(`/admin/redeem/list?page=${page}`);
|
||||||
return response.data as RedeemResponse;
|
return response.data as RedeemResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
return [];
|
return { status: false, message: getErrorMessage(e), data: [], total: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRedeemSegment(quota: number, only_unused: boolean) {
|
export async function deleteRedeem(code: string): Promise<CommonResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.post("/admin/redeem/delete", { code });
|
||||||
`/admin/redeem/segment?quota=${quota}&unused=${only_unused}`,
|
return response.data as CommonResponse;
|
||||||
);
|
|
||||||
return response.data as RedeemResponse;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
return { status: false, message: getErrorMessage(e) };
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,14 +65,21 @@ export type InvitationResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Redeem = {
|
export type Redeem = {
|
||||||
|
code: string;
|
||||||
quota: number;
|
quota: number;
|
||||||
used: boolean;
|
used: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RedeemForm = {
|
||||||
|
data: Redeem[];
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RedeemResponse = Redeem[];
|
export type RedeemResponse = CommonResponse & {
|
||||||
export type RedeemSegmentResponse = CommonResponse & {
|
data: Redeem[];
|
||||||
data: string[];
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InvitationGenerateResponse = {
|
export type InvitationGenerateResponse = {
|
||||||
|
@ -35,6 +35,12 @@
|
|||||||
--destructive: 0 67.22% 50.59%;
|
--destructive: 0 67.22% 50.59%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--success: 120 100% 50%;
|
||||||
|
--success-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--failure: 0 67.22% 50.59%;
|
||||||
|
--failure-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--border: 240 5.9% 90%;
|
--border: 240 5.9% 90%;
|
||||||
--border-hover: 240 5.9% 85%;
|
--border-hover: 240 5.9% 85%;
|
||||||
--border-active: 240 5.9% 80%;
|
--border-active: 240 5.9% 80%;
|
||||||
|
@ -34,6 +34,7 @@ import { PaginationAction } from "@/components/ui/pagination.tsx";
|
|||||||
import { Badge } from "@/components/ui/badge.tsx";
|
import { Badge } from "@/components/ui/badge.tsx";
|
||||||
import OperationAction from "@/components/OperationAction.tsx";
|
import OperationAction from "@/components/OperationAction.tsx";
|
||||||
import { toastState } from "@/api/common.ts";
|
import { toastState } from "@/api/common.ts";
|
||||||
|
import StateBadge from "@/components/admin/common/StateBadge.tsx";
|
||||||
|
|
||||||
function GenerateDialog({ update }: { update: () => void }) {
|
function GenerateDialog({ update }: { update: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -191,7 +192,9 @@ function InvitationTable() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge>{invitation.type}</Badge>
|
<Badge>{invitation.type}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{t(`admin.used-${invitation.used}`)}</TableCell>
|
<TableCell>
|
||||||
|
<StateBadge state={invitation.used} />
|
||||||
|
</TableCell>
|
||||||
<TableCell>{invitation.username || "-"}</TableCell>
|
<TableCell>{invitation.username || "-"}</TableCell>
|
||||||
<TableCell>{invitation.created_at}</TableCell>
|
<TableCell>{invitation.created_at}</TableCell>
|
||||||
<TableCell>{invitation.updated_at}</TableCell>
|
<TableCell>{invitation.updated_at}</TableCell>
|
||||||
|
@ -17,18 +17,26 @@ import {
|
|||||||
} from "@/components/ui/dialog.tsx";
|
} from "@/components/ui/dialog.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { RedeemResponse } from "@/admin/types.ts";
|
import { RedeemForm, RedeemResponse } from "@/admin/types.ts";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button, TemporaryButton } from "@/components/ui/button.tsx";
|
||||||
import { Download, Loader2, RotateCw } from "lucide-react";
|
import { Copy, Download, Loader2, RotateCw, Trash } from "lucide-react";
|
||||||
import { generateRedeem, getRedeemList } from "@/admin/api/chart.ts";
|
import {
|
||||||
|
deleteRedeem,
|
||||||
|
generateRedeem,
|
||||||
|
getRedeemList,
|
||||||
|
} from "@/admin/api/chart.ts";
|
||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { useToast } from "@/components/ui/use-toast.ts";
|
import { useToast } from "@/components/ui/use-toast.ts";
|
||||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||||
import { saveAsFile } from "@/utils/dom.ts";
|
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
|
||||||
import { useEffectAsync } from "@/utils/hook.ts";
|
import { useEffectAsync } from "@/utils/hook.ts";
|
||||||
import { Badge } from "@/components/ui/badge.tsx";
|
import { Badge } from "@/components/ui/badge.tsx";
|
||||||
|
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
||||||
|
import OperationAction from "@/components/OperationAction.tsx";
|
||||||
|
import { toastState } from "@/api/common.ts";
|
||||||
|
import StateBadge from "@/components/admin/common/StateBadge.tsx";
|
||||||
|
|
||||||
function GenerateDialog({ sync }: { sync: () => void }) {
|
function GenerateDialog({ update }: { update: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
@ -44,7 +52,7 @@ function GenerateDialog({ sync }: { sync: () => void }) {
|
|||||||
const data = await generateRedeem(Number(quota), Number(number));
|
const data = await generateRedeem(Number(quota), Number(number));
|
||||||
if (data.status) {
|
if (data.status) {
|
||||||
setData(data.data.join("\n"));
|
setData(data.data.join("\n"));
|
||||||
sync();
|
update();
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: t("admin.error"),
|
title: t("admin.error"),
|
||||||
@ -75,14 +83,14 @@ function GenerateDialog({ sync }: { sync: () => void }) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("admin.generate")}</DialogTitle>
|
<DialogTitle>{t("admin.generate")}</DialogTitle>
|
||||||
<DialogDescription className={`pt-2`}>
|
<DialogDescription className={`pt-2`}>
|
||||||
<div className={`invitation-row`}>
|
<div className={`redeem-row`}>
|
||||||
<p className={`mr-4`}>{t("admin.quota")}</p>
|
<p className={`mr-4`}>{t("admin.quota")}</p>
|
||||||
<Input
|
<Input
|
||||||
value={quota}
|
value={quota}
|
||||||
onChange={(e) => setQuota(getNumber(e.target.value))}
|
onChange={(e) => setQuota(getNumber(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`invitation-row`}>
|
<div className={`redeem-row`}>
|
||||||
<p className={`mr-4`}>{t("admin.number")}</p>
|
<p className={`mr-4`}>{t("admin.number")}</p>
|
||||||
<Input
|
<Input
|
||||||
value={number}
|
value={number}
|
||||||
@ -131,42 +139,86 @@ function GenerateDialog({ sync }: { sync: () => void }) {
|
|||||||
|
|
||||||
function RedeemTable() {
|
function RedeemTable() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [data, setData] = useState<RedeemResponse>([]);
|
const { toast } = useToast();
|
||||||
|
const [data, setData] = useState<RedeemForm>({
|
||||||
|
total: 0,
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [page, setPage] = useState<number>(0);
|
||||||
|
|
||||||
const sync = async () => {
|
async function update() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const resp = await getRedeemList();
|
const resp = await getRedeemList(page);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setData(resp ?? []);
|
if (resp.status) setData(resp as RedeemResponse);
|
||||||
};
|
else
|
||||||
|
toast({
|
||||||
useEffectAsync(sync, []);
|
title: t("admin.error"),
|
||||||
|
description: resp.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
useEffectAsync(update, [page]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`redeem-table`}>
|
<div className={`redeem-table`}>
|
||||||
{data.length > 0 ? (
|
{(data.data && data.data.length > 0) || page > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className={`select-none whitespace-nowrap`}>
|
<TableRow className={`select-none whitespace-nowrap`}>
|
||||||
|
<TableHead>{t("admin.redeem.code")}</TableHead>
|
||||||
<TableHead>{t("admin.redeem.quota")}</TableHead>
|
<TableHead>{t("admin.redeem.quota")}</TableHead>
|
||||||
<TableHead>{t("admin.redeem.total")}</TableHead>
|
<TableHead>{t("admin.used")}</TableHead>
|
||||||
<TableHead>{t("admin.redeem.used")}</TableHead>
|
<TableHead>{t("admin.created-at")}</TableHead>
|
||||||
|
<TableHead>{t("admin.used-at")}</TableHead>
|
||||||
|
<TableHead>{t("admin.action")}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((redeem, idx) => (
|
{(data.data || []).map((redeem, idx) => (
|
||||||
<TableRow key={idx} className={`whitespace-nowrap`}>
|
<TableRow key={idx} className={`whitespace-nowrap`}>
|
||||||
|
<TableCell>{redeem.code}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={`outline`}>{redeem.quota}</Badge>
|
<Badge variant={`outline`}>{redeem.quota}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{redeem.total}</TableCell>
|
<TableCell>
|
||||||
<TableCell>{redeem.used}</TableCell>
|
<StateBadge state={redeem.used} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{redeem.created_at}</TableCell>
|
||||||
|
<TableCell>{redeem.updated_at}</TableCell>
|
||||||
|
<TableCell className={`flex gap-2`}>
|
||||||
|
<TemporaryButton
|
||||||
|
size={`icon`}
|
||||||
|
variant={`outline`}
|
||||||
|
onClick={() => copyClipboard(redeem.code)}
|
||||||
|
>
|
||||||
|
<Copy className={`h-4 w-4`} />
|
||||||
|
</TemporaryButton>
|
||||||
|
<OperationAction
|
||||||
|
native
|
||||||
|
tooltip={t("delete")}
|
||||||
|
variant={`destructive`}
|
||||||
|
onClick={async () => {
|
||||||
|
const resp = await deleteRedeem(redeem.code);
|
||||||
|
toastState(toast, t, resp, true);
|
||||||
|
|
||||||
|
resp.status && (await update());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash className={`h-4 w-4`} />
|
||||||
|
</OperationAction>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<PaginationAction
|
||||||
|
current={page}
|
||||||
|
total={data.total}
|
||||||
|
onPageChange={setPage}
|
||||||
|
offset
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : loading ? (
|
) : loading ? (
|
||||||
<div className={`flex flex-col my-4 items-center`}>
|
<div className={`flex flex-col my-4 items-center`}>
|
||||||
@ -177,10 +229,10 @@ function RedeemTable() {
|
|||||||
)}
|
)}
|
||||||
<div className={`redeem-action`}>
|
<div className={`redeem-action`}>
|
||||||
<div className={`grow`} />
|
<div className={`grow`} />
|
||||||
<Button variant={`outline`} size={`icon`} onClick={sync}>
|
<Button variant={`outline`} size={`icon`} onClick={update}>
|
||||||
<RotateCw className={`h-4 w-4`} />
|
<RotateCw className={`h-4 w-4`} />
|
||||||
</Button>
|
</Button>
|
||||||
<GenerateDialog sync={sync} />
|
<GenerateDialog update={update} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
12
app/src/components/admin/common/StateBadge.tsx
Normal file
12
app/src/components/admin/common/StateBadge.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export type StateBadgeProps = {
|
||||||
|
state: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StateBadge({ state }: StateBadgeProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <Badge variant="outline">{t(`admin.used-${state}`)}</Badge>;
|
||||||
|
}
|
@ -7,7 +7,7 @@ import {
|
|||||||
import { syncSiteInfo } from "@/admin/api/info.ts";
|
import { syncSiteInfo } from "@/admin/api/info.ts";
|
||||||
import { setAxiosConfig } from "@/conf/api.ts";
|
import { setAxiosConfig } from "@/conf/api.ts";
|
||||||
|
|
||||||
export const version = "3.10.5"; // version of the current build
|
export const version = "3.10.6"; // version of the current build
|
||||||
export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin)
|
export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin)
|
||||||
export const deploy: boolean = true; // is production environment (for api endpoint)
|
export const deploy: boolean = true; // is production environment (for api endpoint)
|
||||||
export const tokenField = getTokenField(deploy); // token field name for storing token
|
export const tokenField = getTokenField(deploy); // token field name for storing token
|
||||||
|
@ -547,7 +547,8 @@
|
|||||||
"redeem": {
|
"redeem": {
|
||||||
"quota": "点数",
|
"quota": "点数",
|
||||||
"used": "已用个数",
|
"used": "已用个数",
|
||||||
"total": "总个数"
|
"total": "总个数",
|
||||||
|
"code": "兑换码"
|
||||||
},
|
},
|
||||||
"plan": {
|
"plan": {
|
||||||
"enable": "启用订阅",
|
"enable": "启用订阅",
|
||||||
|
@ -571,7 +571,8 @@
|
|||||||
"redeem": {
|
"redeem": {
|
||||||
"quota": "Number of points",
|
"quota": "Number of points",
|
||||||
"used": "Used Count",
|
"used": "Used Count",
|
||||||
"total": "Total"
|
"total": "Total",
|
||||||
|
"code": "Code"
|
||||||
},
|
},
|
||||||
"market": {
|
"market": {
|
||||||
"title": "Marketplace",
|
"title": "Marketplace",
|
||||||
|
@ -571,7 +571,8 @@
|
|||||||
"redeem": {
|
"redeem": {
|
||||||
"quota": "Whirlies",
|
"quota": "Whirlies",
|
||||||
"used": "使用数",
|
"used": "使用数",
|
||||||
"total": "合計"
|
"total": "合計",
|
||||||
|
"code": "コードを利用する"
|
||||||
},
|
},
|
||||||
"market": {
|
"market": {
|
||||||
"title": "モデルマーケット",
|
"title": "モデルマーケット",
|
||||||
|
@ -571,7 +571,8 @@
|
|||||||
"redeem": {
|
"redeem": {
|
||||||
"quota": "Вихри",
|
"quota": "Вихри",
|
||||||
"used": "Количество использованных",
|
"used": "Количество использованных",
|
||||||
"total": "Итого"
|
"total": "Итого",
|
||||||
|
"code": "Промокод"
|
||||||
},
|
},
|
||||||
"market": {
|
"market": {
|
||||||
"title": "Модельный рынок",
|
"title": "Модельный рынок",
|
||||||
|
@ -46,6 +46,14 @@ module.exports = {
|
|||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
|
success: {
|
||||||
|
DEFAULT: "hsl(var(--success))",
|
||||||
|
foreground: "hsl(var(--success-foreground))",
|
||||||
|
},
|
||||||
|
failure: {
|
||||||
|
DEFAULT: "hsl(var(--failure))",
|
||||||
|
foreground: "hsl(var(--failure-foreground))",
|
||||||
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
@ -22,8 +22,8 @@ func (l *Limiter) RateLimit(client *redis.Client, ip string, path string) (bool,
|
|||||||
|
|
||||||
var limits = map[string]Limiter{
|
var limits = map[string]Limiter{
|
||||||
"/login": {Duration: 10, Count: 20},
|
"/login": {Duration: 10, Count: 20},
|
||||||
"/register": {Duration: 600, Count: 5},
|
"/register": {Duration: 120, Count: 10},
|
||||||
"/verify": {Duration: 120, Count: 5},
|
"/verify": {Duration: 120, Count: 10},
|
||||||
"/reset": {Duration: 120, Count: 10},
|
"/reset": {Duration: 120, Count: 10},
|
||||||
"/apikey": {Duration: 1, Count: 2},
|
"/apikey": {Duration: 1, Count: 2},
|
||||||
"/resetkey": {Duration: 3600, Count: 3},
|
"/resetkey": {Duration: 3600, Count: 3},
|
||||||
@ -36,7 +36,6 @@ var limits = map[string]Limiter{
|
|||||||
"/conversation": {Duration: 1, Count: 5},
|
"/conversation": {Duration: 1, Count: 5},
|
||||||
"/invite": {Duration: 7200, Count: 20},
|
"/invite": {Duration: 7200, Count: 20},
|
||||||
"/redeem": {Duration: 1200, Count: 60},
|
"/redeem": {Duration: 1200, Count: 60},
|
||||||
"/v1": {Duration: 1, Count: 600},
|
|
||||||
"/dashboard": {Duration: 1, Count: 5},
|
"/dashboard": {Duration: 1, Count: 5},
|
||||||
"/card": {Duration: 1, Count: 5},
|
"/card": {Duration: 1, Count: 5},
|
||||||
"/generation": {Duration: 1, Count: 5},
|
"/generation": {Duration: 1, Count: 5},
|
||||||
|
Loading…
Reference in New Issue
Block a user