Compare commits

...

6 Commits

Author SHA1 Message Date
shanshanzhong147 7bc1e58bc1 chore: 调整 dashboard 续费英文文案 2026-05-24 17:48:31 -07:00
shanshanzhong147 057cafd9a3 feat: add admin order refund action 2026-05-24 09:11:02 -07:00
sepakeloudest ce6ab4f05b 增加工单关闭按钮
Build and Release / Build (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled
Issue Check Inactive / issue-check-inactive (push) Has been cancelled
2026-05-10 12:58:03 +03:00
sepakeloudest 750cab5cc9 增加备注功能
Build and Release / Build (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled
2026-05-09 06:11:07 +03:00
sepakeloudest 3e2873fb90 取消工单功能
Build and Release / Build (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled
2026-05-08 14:26:46 +03:00
sepakeloudest 644b20ebef 工单组件
Build and Release / Build (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled
2026-05-01 17:09:24 +03:00
11 changed files with 459 additions and 45 deletions
@@ -15,7 +15,7 @@
"pending": "Pending", "pending": "Pending",
"pendingTickets": "Pending Tickets", "pendingTickets": "Pending Tickets",
"register": "Register", "register": "Register",
"repurchase": "Repurchase", "repurchase": "Renewal",
"revenueTitle": "Revenue Statistics", "revenueTitle": "Revenue Statistics",
"selectTypePlaceholder": "Select Type", "selectTypePlaceholder": "Select Type",
"today": "Today", "today": "Today",
@@ -1,17 +1,25 @@
{ {
"amount": "Amount", "amount": "Amount",
"couponDiscount": "Coupon Discount", "couponDiscount": "Coupon Discount",
"confirmRefund": "Confirm refund",
"discount": "Discount Amount", "discount": "Discount Amount",
"feeAmount": "Fee Amount", "feeAmount": "Fee Amount",
"method": "Payment Method", "method": "Payment Method",
"orderNumber": "Order Number", "orderNumber": "Order Number",
"refund": "Refund",
"refundConfirmDescription": "This refund will immediately invalidate the user's subscription and deduct the related agent commission.",
"refundConfirmTitle": "Confirm refund",
"refundConfirmWarning": "This action cannot be repeated after the order is refunded.",
"refundSubmitting": "Refunding...",
"refundSuccess": "Refund completed.",
"status": { "status": {
"0": "Status", "0": "Status",
"1": "Pending", "1": "Pending",
"2": "Paid", "2": "Paid",
"3": "Cancelled", "3": "Cancelled",
"4": "Closed", "4": "Closed",
"5": "Completed" "5": "Completed",
"6": "Refunded"
}, },
"subscribe": "Subscribe", "subscribe": "Subscribe",
"subscribePrice": "Subscription Price", "subscribePrice": "Subscription Price",
@@ -1,17 +1,25 @@
{ {
"amount": "金额", "amount": "金额",
"couponDiscount": "优惠券折扣", "couponDiscount": "优惠券折扣",
"confirmRefund": "确认退费",
"discount": "折扣金额", "discount": "折扣金额",
"feeAmount": "手续费", "feeAmount": "手续费",
"method": "支付方式", "method": "支付方式",
"orderNumber": "订单编号", "orderNumber": "订单编号",
"refund": "退费",
"refundConfirmDescription": "该退费操作会使用户订阅立即失效,并扣减关联代理佣金。",
"refundConfirmTitle": "确认退费",
"refundConfirmWarning": "订单退费后不可再次重复执行,请谨慎操作。",
"refundSubmitting": "退费中...",
"refundSuccess": "退费成功。",
"status": { "status": {
"0": "状态", "0": "状态",
"1": "待支付", "1": "待支付",
"2": "已支付", "2": "已支付",
"3": "已取消", "3": "已取消",
"4": "已关闭", "4": "已关闭",
"5": "已完成" "5": "已完成",
"6": "已退费"
}, },
"subscribe": "订阅", "subscribe": "订阅",
"subscribePrice": "订阅价格", "subscribePrice": "订阅价格",
@@ -12,7 +12,7 @@
"0": "状态", "0": "状态",
"1": "待跟进", "1": "待跟进",
"2": "待回复", "2": "待回复",
"3": "已解决", "3": "已取消",
"4": "已关闭" "4": "已关闭"
}, },
"ticketList": "工单列表", "ticketList": "工单列表",
+125 -3
View File
@@ -1,4 +1,16 @@
import { useMutation } from "@tanstack/react-query";
import { useSearch } from "@tanstack/react-router"; import { useSearch } from "@tanstack/react-router";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@workspace/ui/components/alert-dialog";
import { Badge } from "@workspace/ui/components/badge"; import { Badge } from "@workspace/ui/components/badge";
import { Button } from "@workspace/ui/components/button"; import { Button } from "@workspace/ui/components/button";
import { import {
@@ -16,18 +28,27 @@ import { cn } from "@workspace/ui/lib/utils";
import { import {
activateOrder, activateOrder,
getOrderList, getOrderList,
refundOrder,
updateOrderStatus, updateOrderStatus,
} from "@workspace/ui/services/admin/order"; } from "@workspace/ui/services/admin/order";
import { useRef } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Display } from "@/components/display"; import { Display } from "@/components/display";
import { useGlobalStore } from "@/stores/global";
import { useSubscribe } from "@/stores/subscribe"; import { useSubscribe } from "@/stores/subscribe";
import { formatDate } from "@/utils/common"; import { formatDate } from "@/utils/common";
import { UserDetail } from "../user/user-detail"; import { UserDetail } from "../user/user-detail";
const REFUNDED_ORDER_STATUS = 6;
export default function Order() { export default function Order() {
const { t } = useTranslation("order"); const { t } = useTranslation("order");
const sp = useSearch({ strict: false }) as Record<string, string | undefined>; const sp = useSearch({ strict: false }) as Record<string, string | undefined>;
const user = useGlobalStore((state) => state.user);
const [confirmingOrderId, setConfirmingOrderId] = useState<number | null>(
null
);
const initialFilters = { const initialFilters = {
user_id: sp.user_id ? Number(sp.user_id) : undefined, user_id: sp.user_id ? Number(sp.user_id) : undefined,
search: sp.search || undefined, search: sp.search || undefined,
@@ -53,6 +74,11 @@ export default function Order() {
label: t("status.5", "Completed"), label: t("status.5", "Completed"),
className: "bg-green-500", className: "bg-green-500",
}, },
{
value: REFUNDED_ORDER_STATUS,
label: t("status.6", "Refunded"),
className: "bg-red-500",
},
]; ];
const typeOptions = [ const typeOptions = [
@@ -65,10 +91,101 @@ export default function Order() {
const ref = useRef<ProTableActions>(null); const ref = useRef<ProTableActions>(null);
const { subscribes, getSubscribeName } = useSubscribe(); const { subscribes, getSubscribeName } = useSubscribe();
const canRefundOrders = Boolean(user?.is_admin);
const refundMutation = useMutation({
mutationFn: async (order: API.Order) => {
await refundOrder({ id: order.id });
},
onSuccess: () => {
toast.success(t("refundSuccess", "Refund completed."));
setConfirmingOrderId(null);
ref.current?.refresh();
},
onError: (error) => {
console.error("Refund order failed", error);
setConfirmingOrderId(null);
},
});
const isRefundedOrder = (order: API.Order) =>
order.status === REFUNDED_ORDER_STATUS ||
order.status_name === t("status.6", "Refunded") ||
order.status_name?.toLowerCase() === "refunded";
const canRefundOrder = (order: API.Order) =>
canRefundOrders && !isRefundedOrder(order);
return ( return (
<ProTable<API.Order, any> <ProTable<API.Order, any>
action={ref} action={ref}
actions={{
render: (order) => {
if (!canRefundOrder(order)) return [];
const isPending =
refundMutation.isPending && confirmingOrderId === order.id;
return [
<AlertDialog
key={`refund-${order.id}`}
onOpenChange={(open) => {
setConfirmingOrderId(open ? order.id : null);
}}
open={confirmingOrderId === order.id}
>
<AlertDialogTrigger asChild>
<Button disabled={isPending} size="sm" variant="destructive">
{isPending
? t("refundSubmitting", "Refunding...")
: t("refund", "Refund")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("refundConfirmTitle", "Confirm refund")}
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2">
<p>
{t(
"refundConfirmDescription",
"This refund will immediately invalidate the user's subscription and deduct the related agent commission."
)}
</p>
<p className="font-medium text-foreground">
{t(
"refundConfirmWarning",
"This action cannot be repeated after the order is refunded."
)}
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>
{t("cancel", "Cancel")}
</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isPending}
onClick={async (event) => {
event.preventDefault();
if (isPending) return;
await refundMutation.mutateAsync(order);
}}
>
{isPending
? t("refundSubmitting", "Refunding...")
: t("confirmRefund", "Confirm refund")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>,
];
},
}}
columns={[ columns={[
{ {
accessorKey: "order_no", accessorKey: "order_no",
@@ -253,8 +370,13 @@ export default function Order() {
); );
} }
return ( return (
<Badge> <Badge
{option?.label || t(`status.${row.getValue("status")}`)} variant={isRefundedOrder(order) ? "destructive" : "default"}
>
{option?.label ||
(order.status_name
? t(`status.${row.getValue("status")}`, order.status_name)
: t(`status.${row.getValue("status")}`))}
</Badge> </Badge>
); );
}, },
+190 -16
View File
@@ -23,6 +23,10 @@ import {
getTicketList, getTicketList,
updateTicketStatus, updateTicketStatus,
} from "@workspace/ui/services/admin/ticket"; } from "@workspace/ui/services/admin/ticket";
import {
getUserDetail,
updateUserBasicInfo,
} from "@workspace/ui/services/admin/user";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -42,6 +46,7 @@ export default function Page() {
const [ticketId, setTicketId] = useState<any>(null); const [ticketId, setTicketId] = useState<any>(null);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const [loadingId, setLoadingId] = useState<any>(null);
const { data: ticket, refetch: refetchTicket } = useQuery({ const { data: ticket, refetch: refetchTicket } = useQuery({
queryKey: ["getTicket", ticketId], queryKey: ["getTicket", ticketId],
@@ -74,45 +79,214 @@ export default function Page() {
action={ref} action={ref}
actions={{ actions={{
render(row) { render(row) {
if (row.status !== 4) { if (row.status === 1) {
return [ return [
<Button key="reply" onClick={() => setTicketId(row.id)}>
{t("reply", "Reply")}
</Button>,
<ConfirmButton <ConfirmButton
cancelText={t("cancel", "Cancel")} cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")} confirmText={t("confirm", "Confirm")}
description={t( description="您确定要取消此次打款吗?取消后系统将记录此操作并关闭工单。"
"closeWarning", key="cancel-payout"
"Once closed, the ticket cannot be operated on. Please proceed with caution." onConfirm={async () => {
setLoadingId(row.id);
try {
await updateTicketStatus({
id: row.id,
status: 3,
});
toast.success("已取消打款并关闭工单");
ref.current?.refresh();
} catch (e) {
console.error("Cancel payout failed", e);
toast.error("取消打款失败");
}
setLoadingId(null);
}}
title="确认取消打款?"
trigger={
<Button
className="mr-2"
disabled={loadingId === row.id}
variant="outline"
>
{loadingId === row.id && (
<Icon
className="mr-2 animate-spin"
icon="lucide:loader-2"
/>
)} )}
</Button>
}
/>,
<ConfirmButton
cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")}
description={
<div className="flex flex-col gap-2">
<p>
线
</p>
<p className="font-bold text-rose-500 text-sm">
USDT 1 USDT
</p>
<p className="font-bold text-rose-500 text-sm">
/ 5%
</p>
</div>
}
key="colse" key="colse"
onConfirm={async () => { onConfirm={async () => {
setLoadingId(row.id);
try {
const titleStr = row.title || "";
const parts = titleStr.split("-");
if (parts.length > 1) {
const deductAmount = Number.parseFloat(
parts?.[1] ?? "0"
);
if (!Number.isNaN(deductAmount)) {
const res = await getUserDetail({ id: row.user_id });
const user = res?.data?.data;
if (user && typeof user.commission !== "undefined") {
const deductAmountCent = deductAmount * 100;
if (Number(user.commission) < deductAmountCent) {
toast.error("用户佣金余额不足,无法完成打款扣费");
setLoadingId(null);
return;
}
const newCommission =
Number(user.commission) - deductAmountCent;
await updateUserBasicInfo({
user_id: row.user_id,
commission: newCommission,
} as any);
}
}
}
} catch (e) {
console.error("Update commission failed", e);
}
await updateTicketStatus({ await updateTicketStatus({
id: row.id, id: row.id,
status: 4, status: 4,
}); });
toast.success(t("closeSuccess", "Closed successfully")); toast.success(t("closeSuccess", "Closed successfully"));
ref.current?.refresh(); ref.current?.refresh();
setLoadingId(null);
}} }}
title={t("confirmClose", "Are you sure you want to close?")} title="确认已完成打款?"
trigger={ trigger={
<Button variant="destructive">{t("close", "Close")}</Button> <Button
disabled={loadingId === row.id}
variant="destructive"
>
{loadingId === row.id && (
<Icon
className="mr-2 animate-spin"
icon="lucide:loader-2"
/>
)}
</Button>
}
/>,
<ConfirmButton
cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")}
description="检查佣金是否扣除,如已经扣除才可进行关闭,此操作不扣除佣金仅关闭工单。"
key="cancel-payout-1"
onConfirm={async () => {
setLoadingId(row.id);
try {
await updateTicketStatus({
id: row.id,
status: 4,
});
toast.success("已关闭工单");
ref.current?.refresh();
} catch (e) {
console.error("Cancel payout failed", e);
toast.error("关闭工单失败");
}
setLoadingId(null);
}}
title="确认关闭订单?"
trigger={
<Button
className="mr-2"
disabled={loadingId === row.id}
variant="secondary"
>
{loadingId === row.id && (
<Icon
className="mr-2 animate-spin"
icon="lucide:loader-2"
/>
)}
</Button>
} }
/>, />,
]; ];
} }
return [ return [];
<Button key="check" onClick={() => setTicketId(row.id)} size="sm">
{t("check", "Check")}
</Button>,
];
}, },
}} }}
columns={[ columns={[
{ {
accessorKey: "title", id: "withdrawal_type",
header: t("title", "Title"), header: "提现类型",
cell: ({ row }) => {
const title = row.original.title || "";
return title.split("-")[0] || title;
},
},
{
id: "withdrawal_amount",
header: "提现金额",
cell: ({ row }) => {
const title = row.original.title || "";
const parts = title.split("-");
if (parts.length < 2) return "";
return (
<span className="font-bold text-rose-500">${parts[1]}</span>
);
},
},
{
id: "withdrawal_method",
header: "提现方式",
cell: ({ row }) => {
const desc = row.original.description || "";
return desc.split("-")[0] || "";
},
},
{
id: "withdrawal_content",
header: "提现内容",
cell: ({ row }) => {
const desc = row.original.description || "";
const parts = desc.split("-");
if (parts.length < 2) return "";
const content = parts[1];
if (content?.startsWith("data:image/")) {
return (
<img
alt="withdrawal"
className="h-10 w-10 cursor-zoom-in rounded border object-cover"
height={40} // 这里的数值根据你的 UI 设计调整
onClick={() => {
const win = window.open();
win?.document.write(`<img src="${content}" />`);
}}
src={content}
width={40}
/>
);
}
return <span className="font-semibold">{content}</span>;
},
}, },
{ {
accessorKey: "user_id", accessorKey: "user_id",
+84 -15
View File
@@ -31,6 +31,13 @@ import {
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from "@workspace/ui/components/sheet"; } from "@workspace/ui/components/sheet";
import { FilePenLine } from 'lucide-react';
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from '@workspace/ui/components/popover';
import { Switch } from "@workspace/ui/components/switch"; import { Switch } from "@workspace/ui/components/switch";
import { import {
Tabs, Tabs,
@@ -39,7 +46,7 @@ import {
TabsTrigger, TabsTrigger,
} from "@workspace/ui/components/tabs"; } from "@workspace/ui/components/tabs";
import { Combobox } from "@workspace/ui/composed/combobox"; import { Combobox } from "@workspace/ui/composed/combobox";
import { ConfirmButton } from "@workspace/ui/composed/confirm-button"; // import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
import { import {
ProTable, ProTable,
type ProTableActions, type ProTableActions,
@@ -50,13 +57,13 @@ import {
} from "@workspace/ui/services/admin/group"; } from "@workspace/ui/services/admin/group";
import { import {
createUser, createUser,
deleteUser, // deleteUser,
getUserDetail, getUserDetail,
getUserList, getUserList,
updateUserBasicInfo, updateUserBasicInfo,
} from "@workspace/ui/services/admin/user"; } from "@workspace/ui/services/admin/user";
import { parseDeviceType } from "@workspace/ui/utils/device"; import { parseDeviceType } from "@workspace/ui/utils/device";
import { useCallback, useRef, useState } from "react"; import React, { useRef, useState, useCallback } from 'react';
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
import { Display } from "@/components/display"; import { Display } from "@/components/display";
@@ -72,6 +79,44 @@ import { NotifySettingsForm } from "./user-profile/notify-settings-form";
import UserSubscription from "./user-subscription"; import UserSubscription from "./user-subscription";
// import EditUserGroupDialog from "./edit-user-group-dialog"; // import EditUserGroupDialog from "./edit-user-group-dialog";
// 为 RemarkForm 组件定义 props 类型
interface RemarkFormProps {
initialRemark?: string | null;
onSave: (remark: string) => void;
CloseComponent: React.ComponentType<{ asChild?: boolean; children: React.ReactNode }>;
}
// 新的子组件,在管理它自己的备注状态
const RemarkForm: React.FC<RemarkFormProps> = ({ onSave, initialRemark, CloseComponent }) => {
const [remark, setRemark] = useState<string>(initialRemark ?? '');
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRemark(event.target.value);
};
const handleSaveClick = () => {
onSave(remark);
};
return (
<>
<div className='mb-2 text-sm font-semibold'></div>
<Input
type='text'
value={remark}
onChange={handleInputChange}
placeholder='在此输入备注...'
className='w-full'
/>
<CloseComponent asChild>
<Button onClick={handleSaveClick} variant='default' size={'sm'} className={'mt-2'}>
</Button>
</CloseComponent>
</>
);
};
export default function User() { export default function User() {
const { t } = useTranslation("user"); const { t } = useTranslation("user");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -139,7 +184,7 @@ export default function User() {
userId={row.id} userId={row.id}
/>, />,
<PreviewNodesDialog key="preview-nodes" userId={row.id} />, <PreviewNodesDialog key="preview-nodes" userId={row.id} />,
<ConfirmButton /* <ConfirmButton
cancelText={t("cancel", "Cancel")} cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")} confirmText={t("confirm", "Confirm")}
description={t( description={t(
@@ -156,7 +201,7 @@ export default function User() {
trigger={ trigger={
<Button variant="destructive">{t("delete", "Delete")}</Button> <Button variant="destructive">{t("delete", "Delete")}</Button>
} }
/>, />,*/
<DropdownMenu key="more" modal={false}> <DropdownMenu key="more" modal={false}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline">{t("more", "More")}</Button> <Button variant="outline">{t("more", "More")}</Button>
@@ -232,7 +277,7 @@ export default function User() {
accessorKey: "id", accessorKey: "id",
header: "ID", header: "ID",
}, },
{ /*{
id: "deleted_at", id: "deleted_at",
accessorKey: "deleted_at", accessorKey: "deleted_at",
header: t("isDeleted", "Deleted"), header: t("isDeleted", "Deleted"),
@@ -244,11 +289,11 @@ export default function User() {
<Badge variant="outline">{t("normal", "Normal")}</Badge> <Badge variant="outline">{t("normal", "Normal")}</Badge>
); );
}, },
}, },*/
{ {
id: "auth_methods", id: "auth_methods",
accessorKey: "auth_methods", accessorKey: "auth_methods",
header: t("userName", "Username"), header: '设备码/邮箱',
cell: ({ row }) => { cell: ({ row }) => {
const method = row.original.auth_methods?.[0]; const method = row.original.auth_methods?.[0];
const identifier = method?.auth_identifier || ""; const identifier = method?.auth_identifier || "";
@@ -259,23 +304,48 @@ export default function User() {
const display = isDevice ? deviceNo || identifier : identifier; const display = isDevice ? deviceNo || identifier : identifier;
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<Badge {/* <Badge
className="mr-1 uppercase" className="mr-1 uppercase"
title={method?.verified ? t("verified", "Verified") : ""} title={method?.verified ? t("verified", "Verified") : ""}
> >
{method?.auth_type} {method?.auth_type}
</Badge> </Badge>*/}
{deviceType && ( {deviceType && (
<Badge className="mr-1" variant="secondary"> <Badge className="mr-1" variant="secondary">
{deviceType} {deviceType}
</Badge> </Badge>
)} )}
<span title={isDevice ? display : undefined}>{display}</span> <span title={isDevice ? display : undefined}>{display}</span>
<Popover>
<PopoverTrigger>
<div className={'flex items-center'}>
{row.original?.remark ? `${row.original.remark}` : ''}
<FilePenLine size={14} className={'text-primary ml-2'} />
</div>
</PopoverTrigger>
<PopoverContent className={'w-64'}>
<RemarkForm
initialRemark={row.original.remark}
CloseComponent={PopoverClose}
onSave={async (remark) => {
const {
id,
} = row.original;
await updateUserBasicInfo({
user_id: id,
remark,
} as unknown as API.UpdateUserBasiceInfoRequest);
toast.success(t('updateSuccess'));
ref.current?.refresh();
}}
/>
</PopoverContent>
</Popover>
</div> </div>
); );
}, },
}, },
{ /* {
id: "balance", id: "balance",
accessorKey: "balance", accessorKey: "balance",
header: t("balance", "Balance"), header: t("balance", "Balance"),
@@ -290,7 +360,7 @@ export default function User() {
cell: ({ row }) => ( cell: ({ row }) => (
<Display type="currency" value={row.getValue("gift_amount")} /> <Display type="currency" value={row.getValue("gift_amount")} />
), ),
}, },*/
{ {
id: "commission", id: "commission",
accessorKey: "commission", accessorKey: "commission",
@@ -531,12 +601,11 @@ function UserSearchBar({
}} }}
value={searchType} value={searchType}
> >
<SelectTrigger className="w-24"> <SelectTrigger className="w-48">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="email">{t("email", "Email")}</SelectItem> <SelectItem value="email">/</SelectItem>
<SelectItem value="device">{t("deviceSearch", "Device")}</SelectItem>
<SelectItem value="user_id">{t("userId", "User ID")}</SelectItem> <SelectItem value="user_id">{t("userId", "User ID")}</SelectItem>
<SelectItem value="subscribe_id"> <SelectItem value="subscribe_id">
{t("subscription", "Subscription")} {t("subscription", "Subscription")}
+2 -1
View File
@@ -43,5 +43,6 @@ function PopoverAnchor({
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
} }
const PopoverClose = PopoverPrimitive.Close;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverClose };
+8 -2
View File
@@ -17,7 +17,7 @@ import type { ReactNode } from "react";
interface ConfirmationButtonProps { interface ConfirmationButtonProps {
trigger: ReactNode; trigger: ReactNode;
title: string; title: string;
description: string; description: ReactNode;
onConfirm: () => void | Promise<void>; onConfirm: () => void | Promise<void>;
cancelText?: string; cancelText?: string;
confirmText?: string; confirmText?: string;
@@ -36,7 +36,13 @@ export const ConfirmButton: React.FC<ConfirmationButtonProps> = ({
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle> <AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription> <AlertDialogDescription asChild>
{typeof description === "string" ? (
<span>{description}</span>
) : (
<div>{description}</div>
)}
</AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{cancelText}</AlertDialogCancel> <AlertDialogCancel>{cancelText}</AlertDialogCancel>
+18
View File
@@ -78,3 +78,21 @@ export async function activateOrder(
} }
); );
} }
/** Refund order POST /v1/admin/order/refund */
export async function refundOrder(
body: API.RefundOrderRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/order/refund`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
+8
View File
@@ -1423,6 +1423,7 @@ declare namespace API {
fee_amount: number; fee_amount: number;
trade_no: string; trade_no: string;
status: number; status: number;
status_name?: string;
subscribe_id: number; subscribe_id: number;
created_at: number; created_at: number;
updated_at: number; updated_at: number;
@@ -1446,6 +1447,7 @@ declare namespace API {
fee_amount: number; fee_amount: number;
trade_no: string; trade_no: string;
status: number; status: number;
status_name?: string;
subscribe_id: number; subscribe_id: number;
subscribe: Subscribe; subscribe: Subscribe;
created_at: number; created_at: number;
@@ -2438,6 +2440,7 @@ declare namespace API {
group_locked: boolean; group_locked: boolean;
auth_methods: UserAuthMethod[]; auth_methods: UserAuthMethod[];
user_devices: UserDevice[]; user_devices: UserDevice[];
remark: string;
rules: string[]; rules: string[];
created_at: number; created_at: number;
updated_at: number; updated_at: number;
@@ -2726,6 +2729,11 @@ declare namespace API {
order_no: string; order_no: string;
}; };
type RefundOrderRequest = {
id: number;
reason?: string;
};
type RedemptionCode = { type RedemptionCode = {
id: number; id: number;
code: string; code: string;