mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
feat: better admin user subscription management ui
This commit is contained in:
parent
140bed53f9
commit
6e8c33f1a2
@ -2,10 +2,12 @@ package admin
|
||||
|
||||
import (
|
||||
"chat/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type GenerateInvitationForm struct {
|
||||
@ -45,26 +47,25 @@ type BanForm struct {
|
||||
|
||||
type QuotaOperationForm struct {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Quota float32 `json:"quota" binding:"required"`
|
||||
Override bool `json:"override"`
|
||||
}
|
||||
Quota *float32 `json:"quota" binding:"required"`
|
||||
Override bool `json:"override"`}
|
||||
|
||||
type SubscriptionOperationForm struct {
|
||||
Id int64 `json:"id"`
|
||||
Month int64 `json:"month"`
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Expired string `json:"expired" binding:"required"`
|
||||
}
|
||||
|
||||
type SubscriptionLevelForm struct {
|
||||
Id int64 `json:"id"`
|
||||
Level int64 `json:"level"`
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Level *int64 `json:"level" binding:"required"`
|
||||
}
|
||||
|
||||
type ReleaseUsageForm struct {
|
||||
Id int64 `json:"id"`
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type UpdateRootPasswordForm struct {
|
||||
Password string `json:"password"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func UpdateMarketAPI(c *gin.Context) {
|
||||
@ -337,7 +338,7 @@ func UserQuotaAPI(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := quotaMigration(db, form.Id, form.Quota, form.Override)
|
||||
err := quotaMigration(db, form.Id, *form.Quota, form.Override)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
@ -363,18 +364,26 @@ func UserSubscriptionAPI(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := subscriptionMigration(db, form.Id, form.Month)
|
||||
if err != nil {
|
||||
// convert to time
|
||||
if _, err := time.Parse("2006-01-02 15:04:05", form.Expired); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := subscriptionMigration(db, form.Id, form.Expired); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
"status": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": true,
|
||||
})
|
||||
}
|
||||
|
||||
func SubscriptionLevelAPI(c *gin.Context) {
|
||||
@ -389,7 +398,7 @@ func SubscriptionLevelAPI(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err := subscriptionLevelMigration(db, form.Id, form.Level)
|
||||
err := subscriptionLevelMigration(db, form.Id, *form.Level)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": false,
|
||||
|
@ -3,6 +3,7 @@ package admin
|
||||
import (
|
||||
"chat/globals"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@ -12,7 +13,7 @@ type MarketModel struct {
|
||||
Name string `json:"name" mapstructure:"name" required:"true"`
|
||||
Description string `json:"description" mapstructure:"description"`
|
||||
Default bool `json:"default" mapstructure:"default"`
|
||||
HighContext bool `json:"high_context" mapstructure:"high_context"`
|
||||
HighContext bool `json:"high_context" mapstructure:"highcontext"`
|
||||
Avatar string `json:"avatar" mapstructure:"avatar"`
|
||||
Tag ModelTag `json:"tag" mapstructure:"tag"`
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ type UserData struct {
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Quota float32 `json:"quota"`
|
||||
UsedQuota float32 `json:"used_quota"`
|
||||
ExpiredAt string `json:"expired_at"`
|
||||
IsSubscribed bool `json:"is_subscribed"`
|
||||
TotalMonth int64 `json:"total_month"`
|
||||
Enterprise bool `json:"enterprise"`
|
||||
|
@ -7,10 +7,11 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
// AuthLike is to solve the problem of import cycle
|
||||
@ -97,6 +98,7 @@ func getUsersForm(db *sql.DB, page int64, search string) PaginationForm {
|
||||
stamp := utils.ConvertTime(expired)
|
||||
if stamp != nil {
|
||||
user.IsSubscribed = stamp.After(time.Now())
|
||||
user.ExpiredAt = stamp.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
user.Enterprise = isEnterprise.Valid && isEnterprise.Bool
|
||||
user.IsBanned = isBanned.Valid && isBanned.Bool
|
||||
@ -171,17 +173,11 @@ func quotaMigration(db *sql.DB, id int64, quota float32, override bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func subscriptionMigration(db *sql.DB, id int64, month int64) error {
|
||||
// if month is negative, then decrease month
|
||||
// if month is positive, then increase month
|
||||
|
||||
expireAt := time.Now().AddDate(0, int(month), 0)
|
||||
|
||||
func subscriptionMigration(db *sql.DB, id int64, expired string) error {
|
||||
_, err := globals.ExecDb(db, `
|
||||
INSERT INTO subscription (user_id, total_month, expired_at) VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE total_month = total_month + ?, expired_at = DATE_ADD(expired_at, INTERVAL ? MONTH)
|
||||
`, id, month, expireAt, month, month)
|
||||
|
||||
INSERT INTO subscription (user_id, expired_at) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE expired_at = ?
|
||||
`, id, expired, expired)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,7 @@
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^13.2.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
|
8295
app/pnpm-lock.yaml
generated
8295
app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "chatnio",
|
||||
"version": "3.10.9"
|
||||
"version": "3.11.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -230,12 +230,12 @@ export async function quotaOperation(
|
||||
|
||||
export async function subscriptionOperation(
|
||||
id: number,
|
||||
month: number,
|
||||
expired: string,
|
||||
): Promise<CommonResponse> {
|
||||
try {
|
||||
const response = await axios.post("/admin/user/subscription", {
|
||||
id,
|
||||
month,
|
||||
expired,
|
||||
});
|
||||
return response.data as CommonResponse;
|
||||
} catch (e) {
|
||||
|
@ -104,6 +104,7 @@ export type UserData = {
|
||||
used_quota: number;
|
||||
is_subscribed: boolean;
|
||||
total_month: number;
|
||||
expired_at: string;
|
||||
level: number;
|
||||
enterprise: boolean;
|
||||
};
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||
import { Switch } from "@/components/ui/switch.tsx";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
@ -23,13 +23,22 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Calendar } from "@/components/ui/calendar.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { Combobox } from "@/components/ui/combo-box.tsx";
|
||||
|
||||
export enum popupTypes {
|
||||
Text = "text",
|
||||
Number = "number",
|
||||
Switch = "switch",
|
||||
Clock = "clock",
|
||||
List = "list",
|
||||
Empty = "empty",
|
||||
}
|
||||
type ParamProps = {
|
||||
dataList?: string[];
|
||||
dataListTranslated?: string;
|
||||
};
|
||||
|
||||
export type PopupDialogProps = {
|
||||
title: string;
|
||||
@ -38,6 +47,7 @@ export type PopupDialogProps = {
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
onValueChange?: (value: string) => string;
|
||||
params?: ParamProps;
|
||||
onSubmit?: (value: string) => Promise<boolean>;
|
||||
destructive?: boolean;
|
||||
disabled?: boolean;
|
||||
@ -63,6 +73,7 @@ function PopupField({
|
||||
type,
|
||||
setValue,
|
||||
onValueChange,
|
||||
params,
|
||||
value,
|
||||
placeholder,
|
||||
componentProps,
|
||||
@ -81,7 +92,18 @@ function PopupField({
|
||||
{...componentProps}
|
||||
/>
|
||||
);
|
||||
case popupTypes.Clock:
|
||||
return <CalendarComp value={value} onValueChange={(v) => setValue(v)} />;
|
||||
|
||||
case popupTypes.List:
|
||||
return (
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(v) => setValue(v)}
|
||||
list={params?.dataList || []}
|
||||
listTranslated={params?.dataListTranslated || ""}
|
||||
/>
|
||||
);
|
||||
case popupTypes.Number:
|
||||
return (
|
||||
<NumberInput
|
||||
@ -111,7 +133,150 @@ function PopupField({
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function fixedZero(val: number) {
|
||||
return val < 10 ? `0${val}` : val.toString();
|
||||
}
|
||||
|
||||
function CalendarComp(props: {
|
||||
value: string;
|
||||
onValueChange: (v: string) => void;
|
||||
}) {
|
||||
const { value, onValueChange } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const convertedDate = useMemo(() => {
|
||||
const date = new Date(value.split(" ")[0] || "1970-01-01");
|
||||
console.log(`[calendar] converted date:`, date);
|
||||
return date;
|
||||
}, [value]);
|
||||
|
||||
const onDateChange = (date: Date, overrideTime?: boolean) => {
|
||||
const v = `${date.getFullYear()}-${fixedZero(
|
||||
date.getMonth() + 1,
|
||||
)}-${fixedZero(date.getDate())}`;
|
||||
const t = !overrideTime
|
||||
? value.split(" ")[1] || "00:00:00"
|
||||
: `${fixedZero(date.getHours())}:${fixedZero(
|
||||
date.getMinutes(),
|
||||
)}:${fixedZero(date.getSeconds())}`;
|
||||
|
||||
console.log(`[calendar] clicked date: [${v} ${t}]`);
|
||||
onValueChange(`${v} ${t}`);
|
||||
};
|
||||
|
||||
const [month, setMonth] = useState(convertedDate);
|
||||
useEffect(() => {
|
||||
setMonth(convertedDate);
|
||||
}, [convertedDate]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-2 items-center justify-center px-2 w-full h-fit`}
|
||||
>
|
||||
<Calendar
|
||||
className={`scale-90 md:scale-100`}
|
||||
mode="single"
|
||||
month={month}
|
||||
onMonthChange={(date) => date && setMonth(date)}
|
||||
selected={convertedDate}
|
||||
onSelect={(date) => date && onDateChange(date)}
|
||||
/>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
placeholder={t("date.pick")}
|
||||
className={`w-full text-center`}
|
||||
/>
|
||||
<Separator />
|
||||
<div className={`flex flex-row w-full flex-wrap`}>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() => onDateChange(new Date("1970-01-01 00:00:00"), true)}
|
||||
>
|
||||
{t("date.clean")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() => onDateChange(new Date(), true)}
|
||||
>
|
||||
{t("date.today")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() =>
|
||||
onDateChange(
|
||||
new Date(convertedDate.setDate(convertedDate.getDate() + 1)),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("date.add-day")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() =>
|
||||
onDateChange(
|
||||
new Date(convertedDate.setDate(convertedDate.getDate() - 1)),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("date.sub-day")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() =>
|
||||
onDateChange(
|
||||
new Date(convertedDate.setMonth(convertedDate.getMonth() + 1)),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("date.add-month")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() =>
|
||||
onDateChange(
|
||||
new Date(convertedDate.setMonth(convertedDate.getMonth() - 1)),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("date.sub-month")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() =>
|
||||
onDateChange(
|
||||
new Date(
|
||||
convertedDate.setFullYear(convertedDate.getFullYear() + 1),
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("date.add-year")}
|
||||
</Button>
|
||||
<Button
|
||||
variant={`outline`}
|
||||
className={`m-0.5 shrink-0`}
|
||||
onClick={() =>
|
||||
onDateChange(
|
||||
new Date(
|
||||
convertedDate.setFullYear(convertedDate.getFullYear() - 1),
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("date.sub-year")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function PopupDialog(props: PopupDialogProps) {
|
||||
const {
|
||||
title,
|
||||
|
@ -57,7 +57,7 @@ import { getNumber, isEnter, parseNumber } from "@/utils/base.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectUsername } from "@/store/auth.ts";
|
||||
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
||||
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
type OperationMenuProps = {
|
||||
user: UserData;
|
||||
onRefresh?: () => void;
|
||||
@ -187,17 +187,16 @@ function OperationMenu({ user, onRefresh }: OperationMenuProps) {
|
||||
}}
|
||||
/>
|
||||
<PopupDialog
|
||||
type={popupTypes.Number}
|
||||
type={popupTypes.Clock}
|
||||
title={t("admin.subscription-action")}
|
||||
name={t("admin.month")}
|
||||
description={t("admin.subscription-action-desc")}
|
||||
defaultValue={"0"}
|
||||
onValueChange={getNumber}
|
||||
description={t("admin.subscription-action-desc", {
|
||||
username: user.username,
|
||||
})}
|
||||
defaultValue={user.expired_at || "1970-01-01 00:00:00"}
|
||||
open={subscriptionOpen}
|
||||
setOpen={setSubscriptionOpen}
|
||||
onSubmit={async (value) => {
|
||||
const month = parseNumber(value);
|
||||
const resp = await subscriptionOperation(user.id, month);
|
||||
const resp = await subscriptionOperation(user.id, value);
|
||||
doToast(t, toast, resp);
|
||||
|
||||
if (resp.status) onRefresh?.();
|
||||
@ -205,16 +204,20 @@ function OperationMenu({ user, onRefresh }: OperationMenuProps) {
|
||||
}}
|
||||
/>
|
||||
<PopupDialog
|
||||
type={popupTypes.Number}
|
||||
type={popupTypes.List}
|
||||
title={t("admin.subscription-level")}
|
||||
name={t("admin.level")}
|
||||
description={t("admin.subscription-level-desc")}
|
||||
defaultValue={user.level.toString()}
|
||||
onValueChange={getNumber}
|
||||
defaultValue={userTypeArray[user.level]}
|
||||
params={{
|
||||
dataList: userTypeArray,
|
||||
dataListTranslated: "admin.identity",
|
||||
}}
|
||||
open={subscriptionLevelOpen}
|
||||
setOpen={setSubscriptionLevelOpen}
|
||||
onSubmit={async (value) => {
|
||||
const level = parseNumber(value);
|
||||
const level = userTypeArray.indexOf(value as UserType);
|
||||
console.log(level);
|
||||
const resp = await subscriptionLevelOperation(user.id, level);
|
||||
doToast(t, toast, resp);
|
||||
|
||||
@ -331,14 +334,14 @@ function OperationMenu({ user, onRefresh }: OperationMenuProps) {
|
||||
<CalendarClock className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.subscription-action")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setReleaseOpen(true)}>
|
||||
<CalendarOff className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.release-subscription-action")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSubscriptionLevelOpen(true)}>
|
||||
<CalendarCheck2 className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.subscription-level")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setReleaseOpen(true)}>
|
||||
<CalendarOff className={`h-4 w-4 mr-2`} />
|
||||
{t("admin.release-subscription-action")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
@ -398,6 +401,7 @@ function UserTable() {
|
||||
<TableHead>{t("admin.is-subscribed")}</TableHead>
|
||||
<TableHead>{t("admin.level")}</TableHead>
|
||||
<TableHead>{t("admin.total-month")}</TableHead>
|
||||
<TableHead>{t("admin.expired-at")}</TableHead>
|
||||
<TableHead>{t("admin.is-banned")}</TableHead>
|
||||
<TableHead>{t("admin.is-admin")}</TableHead>
|
||||
<TableHead>{t("admin.action")}</TableHead>
|
||||
@ -415,11 +419,20 @@ function UserTable() {
|
||||
</TableCell>
|
||||
<TableCell>{user.quota}</TableCell>
|
||||
<TableCell>{user.used_quota}</TableCell>
|
||||
<TableCell>{t(user.is_subscribed.toString())}</TableCell>
|
||||
<TableCell>
|
||||
{t(user.is_subscribed.toString())}
|
||||
<Tips
|
||||
className={`inline-block`}
|
||||
content={t("admin.is-subscribed-tips")}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={`whitespace-nowrap`}>
|
||||
{t(`admin.identity.${userTypeArray[user.level]}`)}
|
||||
</TableCell>
|
||||
<TableCell>{user.total_month}</TableCell>
|
||||
<TableCell className={`whitespace-nowrap`}>
|
||||
{user.expired_at || "-"}
|
||||
</TableCell>
|
||||
<TableCell>{t(user.is_banned.toString())}</TableCell>
|
||||
<TableCell>{t(user.is_admin.toString())}</TableCell>
|
||||
<TableCell>
|
||||
|
73
app/src/components/ui/calendar.tsx
Normal file
73
app/src/components/ui/calendar.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/components/ui/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-6 w-6 bg-transparent p-1 opacity-50 hover:opacity-100",
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||
),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => (
|
||||
<ChevronLeft className="h-4 w-4" {...props} />
|
||||
),
|
||||
IconRight: ({ ...props }) => (
|
||||
<ChevronRight className="h-4 w-4" {...props} />
|
||||
),
|
||||
}}
|
||||
formatters={{
|
||||
formatCaption: (date) => `${date.getFullYear()}/${date.getMonth() + 1}`,
|
||||
formatWeekdayName: (date) =>
|
||||
date.toLocaleDateString(undefined, { weekday: "short" }),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Calendar.displayName = "Calendar";
|
||||
|
||||
export { Calendar };
|
@ -7,7 +7,7 @@ import {
|
||||
import { syncSiteInfo } from "@/admin/api/info.ts";
|
||||
import { setAxiosConfig } from "@/conf/api.ts";
|
||||
|
||||
export const version = "3.10.9"; // version of the current build
|
||||
export const version = "3.11.0"; // version of the current build
|
||||
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 tokenField = getTokenField(deploy); // token field name for storing token
|
||||
|
@ -49,7 +49,9 @@ function ShareContent({ data }: ShareTableProps) {
|
||||
const time = useMemo(() => {
|
||||
return data.map((row) => {
|
||||
const date = new Date(row.time);
|
||||
return `${date.getMonth()}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
|
||||
return `${
|
||||
date.getMonth() + 1
|
||||
}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
|
@ -462,7 +462,9 @@
|
||||
"is-banned": "封禁",
|
||||
"used-quota": "已用点数",
|
||||
"is-subscribed": "是否订阅",
|
||||
"is-subscribed-tips": "是否订阅评判逻辑: 有订阅等级且订阅时间未过期",
|
||||
"total-month": "总计订阅月数",
|
||||
"expired-at": "订阅到期时间",
|
||||
"enterprise": "企业版",
|
||||
"action": "操作",
|
||||
"search-username": "搜索用户名",
|
||||
@ -482,12 +484,12 @@
|
||||
"quota-action-desc": "请输入点数变更值(正数为增加,负数为减少)",
|
||||
"quota-set-action": "点数设置",
|
||||
"quota-set-action-desc": "设置用户的点数",
|
||||
"subscription-action": "订阅管理",
|
||||
"subscription-action-desc": "请输入赠送的订阅月数",
|
||||
"subscription-action": "订阅时间管理",
|
||||
"subscription-action-desc": "请设置用户 {{username}} 的订阅到期时间",
|
||||
"release-subscription-action": "释放订阅用量",
|
||||
"release-subscription-action-desc": "是否释放用户的订阅用量?",
|
||||
"subscription-level": "设置订阅等级",
|
||||
"subscription-level-desc": "设置用户的订阅等级 (0 为普通用户, 1 为基础版订阅, 2 为标准版订阅, 3 为专业版订阅)",
|
||||
"subscription-level-desc": "设置用户的订阅等级",
|
||||
"operate-success": "操作成功",
|
||||
"operate-success-prompt": "您的操作已成功执行。",
|
||||
"operate-failed": "操作失败",
|
||||
@ -800,5 +802,16 @@
|
||||
"edit": "编辑预设",
|
||||
"delete": "删除预设"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"pick": "选择一个日期",
|
||||
"today": "今天",
|
||||
"clean": "归零",
|
||||
"add-day": "增加一天",
|
||||
"sub-day": "减少一天",
|
||||
"add-month": "增加一个月",
|
||||
"sub-month": "减少一个月",
|
||||
"add-year": "增加一年",
|
||||
"sub-year": "减少一年"
|
||||
}
|
||||
}
|
||||
|
@ -721,7 +721,9 @@
|
||||
"broadcast-tip": "Notifications will only show the most recent one and will only be notified once. Site announcements can be set in the system settings. The pop-up window will be displayed on the homepage for the first time and subsequent viewing will be supported.",
|
||||
"created-at": "Creation Time",
|
||||
"used-at": "Collection time",
|
||||
"used-username": "Claim User"
|
||||
"used-username": "Claim User",
|
||||
"is-subscribed-tips": "Subscription judgment logic: There is a subscription tier and the subscription period has not expired",
|
||||
"expired-at": "Subscription Expiration Time"
|
||||
},
|
||||
"mask": {
|
||||
"title": "Mask Settings",
|
||||
@ -801,5 +803,16 @@
|
||||
"min-quota": "Minimum Balance",
|
||||
"your-quota": "Your balance",
|
||||
"title": "Title",
|
||||
"my-account": "My Account"
|
||||
"my-account": "My Account",
|
||||
"date": {
|
||||
"pick": "Select date",
|
||||
"today": "Today",
|
||||
"clean": "Return to zero",
|
||||
"add-day": "Add one day",
|
||||
"sub-day": "Decrease by one day",
|
||||
"add-month": "Add one month",
|
||||
"sub-month": "Decrease by one month",
|
||||
"add-year": "Add one year",
|
||||
"sub-year": "Decrease by one year"
|
||||
}
|
||||
}
|
@ -722,7 +722,9 @@
|
||||
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。",
|
||||
"created-at": "作成日時",
|
||||
"used-at": "乗車時間",
|
||||
"used-username": "ユーザーを請求する"
|
||||
"used-username": "ユーザーを請求する",
|
||||
"is-subscribed-tips": "サブスクリプション判断ロジック:サブスクリプションティアがあり、サブスクリプション期間が満了していません",
|
||||
"expired-at": "サブスクリプションの有効期限"
|
||||
},
|
||||
"mask": {
|
||||
"title": "プリセット設定",
|
||||
@ -802,5 +804,16 @@
|
||||
"min-quota": "最低残高",
|
||||
"your-quota": "残高",
|
||||
"title": "タイトル",
|
||||
"my-account": "マイアカウント"
|
||||
"my-account": "マイアカウント",
|
||||
"date": {
|
||||
"pick": "日付を選択",
|
||||
"today": "今日",
|
||||
"clean": "ゼロに戻る",
|
||||
"add-day": "1日追加",
|
||||
"sub-day": "1日短縮",
|
||||
"add-month": "1か月追加",
|
||||
"sub-month": "1か月短縮",
|
||||
"add-year": "1年追加",
|
||||
"sub-year": "1年減"
|
||||
}
|
||||
}
|
@ -722,7 +722,9 @@
|
||||
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.",
|
||||
"created-at": "Время создания",
|
||||
"used-at": "Время награждения",
|
||||
"used-username": "Получить пользователя"
|
||||
"used-username": "Получить пользователя",
|
||||
"is-subscribed-tips": "Логика суждения о подписке: существует уровень подписки, и период подписки не истек",
|
||||
"expired-at": "Срок действия подписки"
|
||||
},
|
||||
"mask": {
|
||||
"title": "Настройки маски",
|
||||
@ -802,5 +804,16 @@
|
||||
"min-quota": "Минимальный баланс",
|
||||
"your-quota": "Ваш баланс",
|
||||
"title": "заглавие",
|
||||
"my-account": "Моя учетная запись"
|
||||
"my-account": "Моя учетная запись",
|
||||
"date": {
|
||||
"pick": "Выберите дату",
|
||||
"today": "сегодня",
|
||||
"clean": "Вернуться к нулю",
|
||||
"add-day": "Добавить один день",
|
||||
"sub-day": "Уменьшить на один день",
|
||||
"add-month": "Добавить один месяц",
|
||||
"sub-month": "Уменьшить на один месяц",
|
||||
"add-year": "Добавить один год",
|
||||
"sub-year": "Уменьшение на один год"
|
||||
}
|
||||
}
|
@ -354,7 +354,11 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
>
|
||||
<SelectTrigger className={`select`}>
|
||||
<SelectValue
|
||||
placeholder={data.protocol ? t("admin.system.mailProtocolTLS") : t("admin.system.mailProtocolSSL")}
|
||||
placeholder={
|
||||
data.protocol
|
||||
? t("admin.system.mailProtocolTLS")
|
||||
: t("admin.system.mailProtocolSSL")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -389,9 +393,7 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"transition-all duration-300",
|
||||
)}
|
||||
className={cn("transition-all duration-300")}
|
||||
placeholder={t("admin.system.mailUser")}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
@ -423,9 +425,7 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
})
|
||||
}
|
||||
placeholder={`${data.username}@${location.hostname}`}
|
||||
className={cn(
|
||||
"transition-all duration-300",
|
||||
)}
|
||||
className={cn("transition-all duration-300")}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphSpace />
|
||||
|
@ -8,7 +8,9 @@ import {
|
||||
import { RootState } from "@/store/index.ts";
|
||||
import { isMobile } from "@/utils/device";
|
||||
|
||||
export const sendKeys = isMobile() ? ["Ctrl + Enter", "Enter"] : ["Enter", "Ctrl + Enter"];
|
||||
export const sendKeys = isMobile()
|
||||
? ["Ctrl + Enter", "Enter"]
|
||||
: ["Enter", "Ctrl + Enter"];
|
||||
export const initialSettings = {
|
||||
context: true,
|
||||
align: false,
|
||||
|
218
app/vite.config.ts.timestamp-1720242630247-6ccb25948ef1f.mjs
Normal file
218
app/vite.config.ts.timestamp-1720242630247-6ccb25948ef1f.mjs
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user