mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 04:50:14 +09:00
feat: support copy/delete actions on gift code management
This commit is contained in:
parent
d4015c33b2
commit
6d92ff790f
@ -14,6 +14,10 @@ type GenerateInvitationForm struct {
|
||||
Number int `json:"number"`
|
||||
}
|
||||
|
||||
type DeleteInvitationForm struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type GenerateRedeemForm struct {
|
||||
Quota float32 `json:"quota"`
|
||||
Number int `json:"number"`
|
||||
@ -139,6 +143,24 @@ func InvitationPaginationAPI(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, GetInvitationPagination(db, int64(page)))
|
||||
}
|
||||
|
||||
func DeleteInvitationAPI(c *gin.Context) {
|
||||
db := utils.GetDBFromContext(c)
|
||||
|
||||
var form DeleteInvitationForm
|
||||
if err := c.ShouldBindJSON(&form); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := DeleteInvitationCode(db, form.Code)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": err == nil,
|
||||
"error": err,
|
||||
})
|
||||
}
|
||||
func GenerateInvitationAPI(c *gin.Context) {
|
||||
db := utils.GetDBFromContext(c)
|
||||
|
||||
|
@ -53,6 +53,13 @@ func GetInvitationPagination(db *sql.DB, page int64) PaginationForm {
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteInvitationCode(db *sql.DB, code string) error {
|
||||
_, err := globals.ExecDb(db, `
|
||||
DELETE FROM invitation WHERE code = ?
|
||||
`, code)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewInvitationCode(db *sql.DB, code string, quota float32, t string) error {
|
||||
_, err := globals.ExecDb(db, `
|
||||
INSERT INTO invitation (code, quota, type)
|
||||
|
@ -17,6 +17,7 @@ func Register(app *gin.RouterGroup) {
|
||||
|
||||
app.GET("/admin/invitation/list", InvitationPaginationAPI)
|
||||
app.POST("/admin/invitation/generate", GenerateInvitationAPI)
|
||||
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
|
||||
|
||||
app.GET("/admin/redeem/list", RedeemListAPI)
|
||||
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
|
||||
|
@ -102,6 +102,15 @@ export async function getInvitationList(
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteInvitation(code: string): Promise<CommonResponse> {
|
||||
try {
|
||||
const response = await axios.post("/admin/invitation/delete", { code });
|
||||
return response.data as CommonResponse;
|
||||
} catch (e) {
|
||||
return { status: false, message: getErrorMessage(e) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateInvitation(
|
||||
type: string,
|
||||
quota: number,
|
||||
|
@ -11,11 +11,13 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
|
||||
type ActionProps = {
|
||||
tooltip?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => any;
|
||||
native?: boolean;
|
||||
variant?:
|
||||
| "secondary"
|
||||
| "default"
|
||||
@ -26,7 +28,13 @@ type ActionProps = {
|
||||
| null
|
||||
| undefined;
|
||||
};
|
||||
function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
||||
function OperationAction({
|
||||
tooltip,
|
||||
children,
|
||||
onClick,
|
||||
variant,
|
||||
native,
|
||||
}: ActionProps) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@ -34,7 +42,11 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
||||
{variant === "destructive" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button size={`icon`} className={`w-8 h-8`} variant={variant}>
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={cn(!native && `w-8 h-8`)}
|
||||
variant={variant}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@ -52,7 +64,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
||||
) : (
|
||||
<Button
|
||||
size={`icon`}
|
||||
className={`w-8 h-8`}
|
||||
className={cn(!native && `w-8 h-8`)}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
>
|
||||
|
@ -18,17 +18,24 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { InvitationForm, InvitationResponse } from "@/admin/types.ts";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Download, Loader2, RotateCw } from "lucide-react";
|
||||
import { Button, TemporaryButton } from "@/components/ui/button.tsx";
|
||||
import { Copy, Download, Loader2, RotateCw, Trash } from "lucide-react";
|
||||
import { useEffectAsync } from "@/utils/hook.ts";
|
||||
import { generateInvitation, getInvitationList } from "@/admin/api/chart.ts";
|
||||
import {
|
||||
deleteInvitation,
|
||||
generateInvitation,
|
||||
getInvitationList,
|
||||
} from "@/admin/api/chart.ts";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { useToast } from "@/components/ui/use-toast.ts";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { saveAsFile } from "@/utils/dom.ts";
|
||||
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
|
||||
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
import OperationAction from "@/components/OperationAction.tsx";
|
||||
import { toastState } from "@/api/common.ts";
|
||||
|
||||
function GenerateDialog() {
|
||||
function GenerateDialog({ update }: { update: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
@ -43,8 +50,10 @@ function GenerateDialog() {
|
||||
|
||||
async function generateCode() {
|
||||
const data = await generateInvitation(type, Number(quota), Number(number));
|
||||
if (data.status) setData(data.data.join("\n"));
|
||||
else
|
||||
if (data.status) {
|
||||
setData(data.data.join("\n"));
|
||||
update();
|
||||
} else
|
||||
toast({
|
||||
title: t("admin.error"),
|
||||
description: data.message,
|
||||
@ -167,6 +176,7 @@ function InvitationTable() {
|
||||
<TableHead>{t("admin.type")}</TableHead>
|
||||
<TableHead>{t("admin.used")}</TableHead>
|
||||
<TableHead>{t("admin.updated-at")}</TableHead>
|
||||
<TableHead>{t("admin.action")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -174,9 +184,33 @@ function InvitationTable() {
|
||||
<TableRow key={idx} className={`whitespace-nowrap`}>
|
||||
<TableCell>{invitation.code}</TableCell>
|
||||
<TableCell>{invitation.quota}</TableCell>
|
||||
<TableCell>{invitation.type}</TableCell>
|
||||
<TableCell>
|
||||
<Badge>{invitation.type}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{t(`admin.used-${invitation.used}`)}</TableCell>
|
||||
<TableCell>{invitation.updated_at}</TableCell>
|
||||
<TableCell className={`flex gap-2`}>
|
||||
<TemporaryButton
|
||||
size={`icon`}
|
||||
variant={`outline`}
|
||||
onClick={() => copyClipboard(invitation.code)}
|
||||
>
|
||||
<Copy className={`h-4 w-4`} />
|
||||
</TemporaryButton>
|
||||
<OperationAction
|
||||
native
|
||||
tooltip={t("delete")}
|
||||
variant={`destructive`}
|
||||
onClick={async () => {
|
||||
const resp = await deleteInvitation(invitation.code);
|
||||
toastState(toast, t, resp, true);
|
||||
|
||||
resp.status && (await update());
|
||||
}}
|
||||
>
|
||||
<Trash className={`h-4 w-4`} />
|
||||
</OperationAction>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@ -202,7 +236,7 @@ function InvitationTable() {
|
||||
<Button variant={`outline`} size={`icon`} onClick={update}>
|
||||
<RotateCw className={`h-4 w-4`} />
|
||||
</Button>
|
||||
<GenerateDialog />
|
||||
<GenerateDialog update={update} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -4,7 +4,8 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "./lib/utils";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Check, Loader2 } from "lucide-react";
|
||||
import { useTemporaryState } from "@/utils/hook.ts";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
@ -119,4 +120,26 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
type TemporaryButtonProps = ButtonProps & {
|
||||
interval?: number;
|
||||
};
|
||||
|
||||
const TemporaryButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
TemporaryButtonProps
|
||||
>(({ interval, children, onClick, ...props }, ref) => {
|
||||
const { state, triggerState } = useTemporaryState(interval);
|
||||
|
||||
const event = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (onClick) onClick(e);
|
||||
triggerState();
|
||||
};
|
||||
|
||||
return (
|
||||
<Button ref={ref} onClick={event} {...props}>
|
||||
{state ? <Check className={`h-4 w-4`} /> : children}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
export { Button, TemporaryButton, buttonVariants };
|
||||
|
@ -482,7 +482,7 @@
|
||||
"operate-success-prompt": "您的操作已成功执行。",
|
||||
"operate-failed": "操作失败",
|
||||
"operate-failed-prompt": "操作失败,原因:{{reason}}",
|
||||
"updated-at": "领取时间",
|
||||
"updated-at": "更新时间",
|
||||
"used-true": "已使用",
|
||||
"used-false": "未使用",
|
||||
"generate": "批量生成",
|
||||
|
@ -391,7 +391,7 @@
|
||||
"operate-success-prompt": "Your operation has been successfully executed.",
|
||||
"operate-failed": "Operate Failed",
|
||||
"operate-failed-prompt": "Operation failed, reason: {{reason}}",
|
||||
"updated-at": "Updated At",
|
||||
"updated-at": "Updated on ",
|
||||
"used-true": "Used",
|
||||
"used-false": "Unused",
|
||||
"generate": "Generate",
|
||||
|
@ -391,7 +391,7 @@
|
||||
"operate-success-prompt": "アクションは正常に実行されました。",
|
||||
"operate-failed": "操作に失敗しました",
|
||||
"operate-failed-prompt": "{{reason}}の操作が失敗しました",
|
||||
"updated-at": "乗車時間",
|
||||
"updated-at": "アップデート時間",
|
||||
"used-true": "使用済み",
|
||||
"used-false": "活用していない",
|
||||
"generate": "バッチ生成",
|
||||
|
@ -391,7 +391,7 @@
|
||||
"operate-success-prompt": "Ваша операция была успешно выполнена.",
|
||||
"operate-failed": "Не удалось",
|
||||
"operate-failed-prompt": "Не удалось выполнить операцию, причина: {{reason}}",
|
||||
"updated-at": "Обновлено",
|
||||
"updated-at": "Время обновления",
|
||||
"used-true": "Использовано",
|
||||
"used-false": "Не использовано",
|
||||
"generate": "Генерировать",
|
||||
|
72
nginx.conf
72
nginx.conf
@ -1,40 +1,38 @@
|
||||
worker_processes 1;
|
||||
server
|
||||
{
|
||||
# this is a sample configuration for nginx
|
||||
listen 80;
|
||||
|
||||
events {
|
||||
worker_connections 8192;
|
||||
multi_accept on;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 8000 default_server;
|
||||
listen [::]:8000 default_server;
|
||||
server_name _;
|
||||
|
||||
root /app/dist;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8094/;
|
||||
proxy_set_header Host 127.0.0.1:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header X-Host $host:$server_port;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
error_page 404 =200 /index.html;
|
||||
}
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md|package.json|package-lock.json|\.env) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
location ~ /purge(/.*) {
|
||||
proxy_cache_purge cache_one 127.0.0.1$request_uri$is_args$args;
|
||||
}
|
||||
|
||||
location / {
|
||||
# if you are using compile deployment mode, please use the http://localhost:8094 instead
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host 127.0.0.1:$server_port;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
proxy_set_header X-Host $host:$server_port;
|
||||
proxy_set_header X-Scheme $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/chatnio.log;
|
||||
error_log /www/wwwlogs/chatnio.error.log;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user