feat: support copy/delete actions on gift code management

This commit is contained in:
Zhang Minghan 2024-03-10 23:38:31 +08:00
parent d4015c33b2
commit 6d92ff790f
12 changed files with 161 additions and 55 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
);

View File

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

View File

@ -482,7 +482,7 @@
"operate-success-prompt": "您的操作已成功执行。",
"operate-failed": "操作失败",
"operate-failed-prompt": "操作失败,原因:{{reason}}",
"updated-at": "领取时间",
"updated-at": "更新时间",
"used-true": "已使用",
"used-false": "未使用",
"generate": "批量生成",

View File

@ -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",

View File

@ -391,7 +391,7 @@
"operate-success-prompt": "アクションは正常に実行されました。",
"operate-failed": "操作に失敗しました",
"operate-failed-prompt": "{{reason}}の操作が失敗しました",
"updated-at": "乗車時間",
"updated-at": "アップデート時間",
"used-true": "使用済み",
"used-false": "活用していない",
"generate": "バッチ生成",

View File

@ -391,7 +391,7 @@
"operate-success-prompt": "Ваша операция была успешно выполнена.",
"operate-failed": "Не удалось",
"operate-failed-prompt": "Не удалось выполнить операцию, причина: {{reason}}",
"updated-at": "Обновлено",
"updated-at": "Время обновления",
"used-true": "Использовано",
"used-false": "Не использовано",
"generate": "Генерировать",

View File

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