mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 13:30:13 +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 (
|
import (
|
||||||
"chat/utils"
|
"chat/utils"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GenerateInvitationForm struct {
|
type GenerateInvitationForm struct {
|
||||||
@ -44,27 +46,26 @@ type BanForm struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QuotaOperationForm struct {
|
type QuotaOperationForm struct {
|
||||||
Id int64 `json:"id" binding:"required"`
|
Id int64 `json:"id" binding:"required"`
|
||||||
Quota float32 `json:"quota" binding:"required"`
|
Quota *float32 `json:"quota" binding:"required"`
|
||||||
Override bool `json:"override"`
|
Override bool `json:"override"`}
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionOperationForm struct {
|
type SubscriptionOperationForm struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id" binding:"required"`
|
||||||
Month int64 `json:"month"`
|
Expired string `json:"expired" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscriptionLevelForm struct {
|
type SubscriptionLevelForm struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id" binding:"required"`
|
||||||
Level int64 `json:"level"`
|
Level *int64 `json:"level" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReleaseUsageForm struct {
|
type ReleaseUsageForm struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRootPasswordForm struct {
|
type UpdateRootPasswordForm struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateMarketAPI(c *gin.Context) {
|
func UpdateMarketAPI(c *gin.Context) {
|
||||||
@ -337,7 +338,7 @@ func UserQuotaAPI(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := quotaMigration(db, form.Id, form.Quota, form.Override)
|
err := quotaMigration(db, form.Id, *form.Quota, form.Override)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": false,
|
"status": false,
|
||||||
@ -363,18 +364,26 @@ func UserSubscriptionAPI(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := subscriptionMigration(db, form.Id, form.Month)
|
// convert to time
|
||||||
if err != nil {
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": true,
|
"status": false,
|
||||||
|
"message": err.Error(),
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := subscriptionMigration(db, form.Id, form.Expired); err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubscriptionLevelAPI(c *gin.Context) {
|
func SubscriptionLevelAPI(c *gin.Context) {
|
||||||
@ -389,7 +398,7 @@ func SubscriptionLevelAPI(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := subscriptionLevelMigration(db, form.Id, form.Level)
|
err := subscriptionLevelMigration(db, form.Id, *form.Level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": false,
|
"status": false,
|
||||||
|
@ -3,6 +3,7 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"chat/globals"
|
"chat/globals"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ type MarketModel struct {
|
|||||||
Name string `json:"name" mapstructure:"name" required:"true"`
|
Name string `json:"name" mapstructure:"name" required:"true"`
|
||||||
Description string `json:"description" mapstructure:"description"`
|
Description string `json:"description" mapstructure:"description"`
|
||||||
Default bool `json:"default" mapstructure:"default"`
|
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"`
|
Avatar string `json:"avatar" mapstructure:"avatar"`
|
||||||
Tag ModelTag `json:"tag" mapstructure:"tag"`
|
Tag ModelTag `json:"tag" mapstructure:"tag"`
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ type UserData struct {
|
|||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Quota float32 `json:"quota"`
|
Quota float32 `json:"quota"`
|
||||||
UsedQuota float32 `json:"used_quota"`
|
UsedQuota float32 `json:"used_quota"`
|
||||||
|
ExpiredAt string `json:"expired_at"`
|
||||||
IsSubscribed bool `json:"is_subscribed"`
|
IsSubscribed bool `json:"is_subscribed"`
|
||||||
TotalMonth int64 `json:"total_month"`
|
TotalMonth int64 `json:"total_month"`
|
||||||
Enterprise bool `json:"enterprise"`
|
Enterprise bool `json:"enterprise"`
|
||||||
|
@ -7,10 +7,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthLike is to solve the problem of import cycle
|
// 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)
|
stamp := utils.ConvertTime(expired)
|
||||||
if stamp != nil {
|
if stamp != nil {
|
||||||
user.IsSubscribed = stamp.After(time.Now())
|
user.IsSubscribed = stamp.After(time.Now())
|
||||||
|
user.ExpiredAt = stamp.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
user.Enterprise = isEnterprise.Valid && isEnterprise.Bool
|
user.Enterprise = isEnterprise.Valid && isEnterprise.Bool
|
||||||
user.IsBanned = isBanned.Valid && isBanned.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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscriptionMigration(db *sql.DB, id int64, month int64) error {
|
func subscriptionMigration(db *sql.DB, id int64, expired string) error {
|
||||||
// if month is negative, then decrease month
|
|
||||||
// if month is positive, then increase month
|
|
||||||
|
|
||||||
expireAt := time.Now().AddDate(0, int(month), 0)
|
|
||||||
|
|
||||||
_, err := globals.ExecDb(db, `
|
_, err := globals.ExecDb(db, `
|
||||||
INSERT INTO subscription (user_id, total_month, expired_at) VALUES (?, ?, ?)
|
INSERT INTO subscription (user_id, expired_at) VALUES (?, ?)
|
||||||
ON DUPLICATE KEY UPDATE total_month = total_month + ?, expired_at = DATE_ADD(expired_at, INTERVAL ? MONTH)
|
ON DUPLICATE KEY UPDATE expired_at = ?
|
||||||
`, id, month, expireAt, month, month)
|
`, id, expired, expired)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^13.2.2",
|
"react-i18next": "^13.2.2",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
|
8701
app/pnpm-lock.yaml
generated
8701
app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "chatnio",
|
"productName": "chatnio",
|
||||||
"version": "3.10.9"
|
"version": "3.11.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
@ -230,12 +230,12 @@ export async function quotaOperation(
|
|||||||
|
|
||||||
export async function subscriptionOperation(
|
export async function subscriptionOperation(
|
||||||
id: number,
|
id: number,
|
||||||
month: number,
|
expired: string,
|
||||||
): Promise<CommonResponse> {
|
): Promise<CommonResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/admin/user/subscription", {
|
const response = await axios.post("/admin/user/subscription", {
|
||||||
id,
|
id,
|
||||||
month,
|
expired,
|
||||||
});
|
});
|
||||||
return response.data as CommonResponse;
|
return response.data as CommonResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -104,6 +104,7 @@ export type UserData = {
|
|||||||
used_quota: number;
|
used_quota: number;
|
||||||
is_subscribed: boolean;
|
is_subscribed: boolean;
|
||||||
total_month: number;
|
total_month: number;
|
||||||
|
expired_at: string;
|
||||||
level: number;
|
level: number;
|
||||||
enterprise: boolean;
|
enterprise: boolean;
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { Input } from "@/components/ui/input.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 { NumberInput } from "@/components/ui/number-input.tsx";
|
||||||
import { Switch } from "@/components/ui/switch.tsx";
|
import { Switch } from "@/components/ui/switch.tsx";
|
||||||
import { Alert, AlertDescription } from "./ui/alert";
|
import { Alert, AlertDescription } from "./ui/alert";
|
||||||
@ -23,13 +23,22 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} 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 {
|
export enum popupTypes {
|
||||||
Text = "text",
|
Text = "text",
|
||||||
Number = "number",
|
Number = "number",
|
||||||
Switch = "switch",
|
Switch = "switch",
|
||||||
|
Clock = "clock",
|
||||||
|
List = "list",
|
||||||
Empty = "empty",
|
Empty = "empty",
|
||||||
}
|
}
|
||||||
|
type ParamProps = {
|
||||||
|
dataList?: string[];
|
||||||
|
dataListTranslated?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PopupDialogProps = {
|
export type PopupDialogProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -38,6 +47,7 @@ export type PopupDialogProps = {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onValueChange?: (value: string) => string;
|
onValueChange?: (value: string) => string;
|
||||||
|
params?: ParamProps;
|
||||||
onSubmit?: (value: string) => Promise<boolean>;
|
onSubmit?: (value: string) => Promise<boolean>;
|
||||||
destructive?: boolean;
|
destructive?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -63,6 +73,7 @@ function PopupField({
|
|||||||
type,
|
type,
|
||||||
setValue,
|
setValue,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
|
params,
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
componentProps,
|
componentProps,
|
||||||
@ -81,7 +92,18 @@ function PopupField({
|
|||||||
{...componentProps}
|
{...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:
|
case popupTypes.Number:
|
||||||
return (
|
return (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@ -111,7 +133,150 @@ function PopupField({
|
|||||||
return null;
|
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) {
|
function PopupDialog(props: PopupDialogProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
|
@ -57,7 +57,7 @@ import { getNumber, isEnter, parseNumber } from "@/utils/base.ts";
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { selectUsername } from "@/store/auth.ts";
|
import { selectUsername } from "@/store/auth.ts";
|
||||||
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
import { PaginationAction } from "@/components/ui/pagination.tsx";
|
||||||
|
import Tips from "@/components/Tips.tsx";
|
||||||
type OperationMenuProps = {
|
type OperationMenuProps = {
|
||||||
user: UserData;
|
user: UserData;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
@ -187,17 +187,16 @@ function OperationMenu({ user, onRefresh }: OperationMenuProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PopupDialog
|
<PopupDialog
|
||||||
type={popupTypes.Number}
|
type={popupTypes.Clock}
|
||||||
title={t("admin.subscription-action")}
|
title={t("admin.subscription-action")}
|
||||||
name={t("admin.month")}
|
description={t("admin.subscription-action-desc", {
|
||||||
description={t("admin.subscription-action-desc")}
|
username: user.username,
|
||||||
defaultValue={"0"}
|
})}
|
||||||
onValueChange={getNumber}
|
defaultValue={user.expired_at || "1970-01-01 00:00:00"}
|
||||||
open={subscriptionOpen}
|
open={subscriptionOpen}
|
||||||
setOpen={setSubscriptionOpen}
|
setOpen={setSubscriptionOpen}
|
||||||
onSubmit={async (value) => {
|
onSubmit={async (value) => {
|
||||||
const month = parseNumber(value);
|
const resp = await subscriptionOperation(user.id, value);
|
||||||
const resp = await subscriptionOperation(user.id, month);
|
|
||||||
doToast(t, toast, resp);
|
doToast(t, toast, resp);
|
||||||
|
|
||||||
if (resp.status) onRefresh?.();
|
if (resp.status) onRefresh?.();
|
||||||
@ -205,16 +204,20 @@ function OperationMenu({ user, onRefresh }: OperationMenuProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PopupDialog
|
<PopupDialog
|
||||||
type={popupTypes.Number}
|
type={popupTypes.List}
|
||||||
title={t("admin.subscription-level")}
|
title={t("admin.subscription-level")}
|
||||||
name={t("admin.level")}
|
name={t("admin.level")}
|
||||||
description={t("admin.subscription-level-desc")}
|
description={t("admin.subscription-level-desc")}
|
||||||
defaultValue={user.level.toString()}
|
defaultValue={userTypeArray[user.level]}
|
||||||
onValueChange={getNumber}
|
params={{
|
||||||
|
dataList: userTypeArray,
|
||||||
|
dataListTranslated: "admin.identity",
|
||||||
|
}}
|
||||||
open={subscriptionLevelOpen}
|
open={subscriptionLevelOpen}
|
||||||
setOpen={setSubscriptionLevelOpen}
|
setOpen={setSubscriptionLevelOpen}
|
||||||
onSubmit={async (value) => {
|
onSubmit={async (value) => {
|
||||||
const level = parseNumber(value);
|
const level = userTypeArray.indexOf(value as UserType);
|
||||||
|
console.log(level);
|
||||||
const resp = await subscriptionLevelOperation(user.id, level);
|
const resp = await subscriptionLevelOperation(user.id, level);
|
||||||
doToast(t, toast, resp);
|
doToast(t, toast, resp);
|
||||||
|
|
||||||
@ -331,14 +334,14 @@ function OperationMenu({ user, onRefresh }: OperationMenuProps) {
|
|||||||
<CalendarClock className={`h-4 w-4 mr-2`} />
|
<CalendarClock className={`h-4 w-4 mr-2`} />
|
||||||
{t("admin.subscription-action")}
|
{t("admin.subscription-action")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setReleaseOpen(true)}>
|
|
||||||
<CalendarOff className={`h-4 w-4 mr-2`} />
|
|
||||||
{t("admin.release-subscription-action")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => setSubscriptionLevelOpen(true)}>
|
<DropdownMenuItem onClick={() => setSubscriptionLevelOpen(true)}>
|
||||||
<CalendarCheck2 className={`h-4 w-4 mr-2`} />
|
<CalendarCheck2 className={`h-4 w-4 mr-2`} />
|
||||||
{t("admin.subscription-level")}
|
{t("admin.subscription-level")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setReleaseOpen(true)}>
|
||||||
|
<CalendarOff className={`h-4 w-4 mr-2`} />
|
||||||
|
{t("admin.release-subscription-action")}
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
@ -398,6 +401,7 @@ function UserTable() {
|
|||||||
<TableHead>{t("admin.is-subscribed")}</TableHead>
|
<TableHead>{t("admin.is-subscribed")}</TableHead>
|
||||||
<TableHead>{t("admin.level")}</TableHead>
|
<TableHead>{t("admin.level")}</TableHead>
|
||||||
<TableHead>{t("admin.total-month")}</TableHead>
|
<TableHead>{t("admin.total-month")}</TableHead>
|
||||||
|
<TableHead>{t("admin.expired-at")}</TableHead>
|
||||||
<TableHead>{t("admin.is-banned")}</TableHead>
|
<TableHead>{t("admin.is-banned")}</TableHead>
|
||||||
<TableHead>{t("admin.is-admin")}</TableHead>
|
<TableHead>{t("admin.is-admin")}</TableHead>
|
||||||
<TableHead>{t("admin.action")}</TableHead>
|
<TableHead>{t("admin.action")}</TableHead>
|
||||||
@ -415,11 +419,20 @@ function UserTable() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{user.quota}</TableCell>
|
<TableCell>{user.quota}</TableCell>
|
||||||
<TableCell>{user.used_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`}>
|
<TableCell className={`whitespace-nowrap`}>
|
||||||
{t(`admin.identity.${userTypeArray[user.level]}`)}
|
{t(`admin.identity.${userTypeArray[user.level]}`)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{user.total_month}</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_banned.toString())}</TableCell>
|
||||||
<TableCell>{t(user.is_admin.toString())}</TableCell>
|
<TableCell>{t(user.is_admin.toString())}</TableCell>
|
||||||
<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 { syncSiteInfo } from "@/admin/api/info.ts";
|
||||||
import { setAxiosConfig } from "@/conf/api.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 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
|
||||||
|
@ -49,7 +49,9 @@ function ShareContent({ data }: ShareTableProps) {
|
|||||||
const time = useMemo(() => {
|
const time = useMemo(() => {
|
||||||
return data.map((row) => {
|
return data.map((row) => {
|
||||||
const date = new Date(row.time);
|
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]);
|
}, [data]);
|
||||||
|
|
||||||
|
@ -462,7 +462,9 @@
|
|||||||
"is-banned": "封禁",
|
"is-banned": "封禁",
|
||||||
"used-quota": "已用点数",
|
"used-quota": "已用点数",
|
||||||
"is-subscribed": "是否订阅",
|
"is-subscribed": "是否订阅",
|
||||||
|
"is-subscribed-tips": "是否订阅评判逻辑: 有订阅等级且订阅时间未过期",
|
||||||
"total-month": "总计订阅月数",
|
"total-month": "总计订阅月数",
|
||||||
|
"expired-at": "订阅到期时间",
|
||||||
"enterprise": "企业版",
|
"enterprise": "企业版",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"search-username": "搜索用户名",
|
"search-username": "搜索用户名",
|
||||||
@ -482,12 +484,12 @@
|
|||||||
"quota-action-desc": "请输入点数变更值(正数为增加,负数为减少)",
|
"quota-action-desc": "请输入点数变更值(正数为增加,负数为减少)",
|
||||||
"quota-set-action": "点数设置",
|
"quota-set-action": "点数设置",
|
||||||
"quota-set-action-desc": "设置用户的点数",
|
"quota-set-action-desc": "设置用户的点数",
|
||||||
"subscription-action": "订阅管理",
|
"subscription-action": "订阅时间管理",
|
||||||
"subscription-action-desc": "请输入赠送的订阅月数",
|
"subscription-action-desc": "请设置用户 {{username}} 的订阅到期时间",
|
||||||
"release-subscription-action": "释放订阅用量",
|
"release-subscription-action": "释放订阅用量",
|
||||||
"release-subscription-action-desc": "是否释放用户的订阅用量?",
|
"release-subscription-action-desc": "是否释放用户的订阅用量?",
|
||||||
"subscription-level": "设置订阅等级",
|
"subscription-level": "设置订阅等级",
|
||||||
"subscription-level-desc": "设置用户的订阅等级 (0 为普通用户, 1 为基础版订阅, 2 为标准版订阅, 3 为专业版订阅)",
|
"subscription-level-desc": "设置用户的订阅等级",
|
||||||
"operate-success": "操作成功",
|
"operate-success": "操作成功",
|
||||||
"operate-success-prompt": "您的操作已成功执行。",
|
"operate-success-prompt": "您的操作已成功执行。",
|
||||||
"operate-failed": "操作失败",
|
"operate-failed": "操作失败",
|
||||||
@ -640,8 +642,8 @@
|
|||||||
"proxy-endpoint-placeholder": "请输入正向代理地址,如:socks5://example.com:1080",
|
"proxy-endpoint-placeholder": "请输入正向代理地址,如:socks5://example.com:1080",
|
||||||
"proxy-username": "代理用户名",
|
"proxy-username": "代理用户名",
|
||||||
"proxy-username-placeholder": "请输入代理的鉴权用户名 (可选)",
|
"proxy-username-placeholder": "请输入代理的鉴权用户名 (可选)",
|
||||||
"proxy-password": "代理密码",
|
"proxy-password": "代理密码",
|
||||||
"proxy-password-placeholder": "请输入代理的鉴权密码 (可选)",
|
"proxy-password-placeholder": "请输入代理的鉴权密码 (可选)",
|
||||||
"proxy-desc": "正向代理,支持 HTTP/HTTPS/SOCKS5 代理 (反向代理请填写接入点, 非特殊情况无需设置正向代理)"
|
"proxy-desc": "正向代理,支持 HTTP/HTTPS/SOCKS5 代理 (反向代理请填写接入点, 非特殊情况无需设置正向代理)"
|
||||||
},
|
},
|
||||||
"charge": {
|
"charge": {
|
||||||
@ -800,5 +802,16 @@
|
|||||||
"edit": "编辑预设",
|
"edit": "编辑预设",
|
||||||
"delete": "删除预设"
|
"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.",
|
"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",
|
"created-at": "Creation Time",
|
||||||
"used-at": "Collection 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": {
|
"mask": {
|
||||||
"title": "Mask Settings",
|
"title": "Mask Settings",
|
||||||
@ -801,5 +803,16 @@
|
|||||||
"min-quota": "Minimum Balance",
|
"min-quota": "Minimum Balance",
|
||||||
"your-quota": "Your balance",
|
"your-quota": "Your balance",
|
||||||
"title": "Title",
|
"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": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。",
|
"broadcast-tip": "通知には最新の通知のみが表示され、一度だけ通知されます。サイトのお知らせは、システム設定で設定できます。ポップアップウィンドウがホームページに初めて表示され、その後の表示がサポートされます。",
|
||||||
"created-at": "作成日時",
|
"created-at": "作成日時",
|
||||||
"used-at": "乗車時間",
|
"used-at": "乗車時間",
|
||||||
"used-username": "ユーザーを請求する"
|
"used-username": "ユーザーを請求する",
|
||||||
|
"is-subscribed-tips": "サブスクリプション判断ロジック:サブスクリプションティアがあり、サブスクリプション期間が満了していません",
|
||||||
|
"expired-at": "サブスクリプションの有効期限"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
"title": "プリセット設定",
|
"title": "プリセット設定",
|
||||||
@ -802,5 +804,16 @@
|
|||||||
"min-quota": "最低残高",
|
"min-quota": "最低残高",
|
||||||
"your-quota": "残高",
|
"your-quota": "残高",
|
||||||
"title": "タイトル",
|
"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": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.",
|
"broadcast-tip": "Уведомления будут отображаться только самые последние и будут уведомлены только один раз. Объявления сайта можно задать в системных настройках. Всплывающее окно будет отображаться на главной странице в первый раз и будет поддерживаться последующий просмотр.",
|
||||||
"created-at": "Время создания",
|
"created-at": "Время создания",
|
||||||
"used-at": "Время награждения",
|
"used-at": "Время награждения",
|
||||||
"used-username": "Получить пользователя"
|
"used-username": "Получить пользователя",
|
||||||
|
"is-subscribed-tips": "Логика суждения о подписке: существует уровень подписки, и период подписки не истек",
|
||||||
|
"expired-at": "Срок действия подписки"
|
||||||
},
|
},
|
||||||
"mask": {
|
"mask": {
|
||||||
"title": "Настройки маски",
|
"title": "Настройки маски",
|
||||||
@ -802,5 +804,16 @@
|
|||||||
"min-quota": "Минимальный баланс",
|
"min-quota": "Минимальный баланс",
|
||||||
"your-quota": "Ваш баланс",
|
"your-quota": "Ваш баланс",
|
||||||
"title": "заглавие",
|
"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`}>
|
<SelectTrigger className={`select`}>
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={data.protocol ? t("admin.system.mailProtocolTLS") : t("admin.system.mailProtocolSSL")}
|
placeholder={
|
||||||
|
data.protocol
|
||||||
|
? t("admin.system.mailProtocolTLS")
|
||||||
|
: t("admin.system.mailProtocolSSL")
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -389,9 +393,7 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
|||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn("transition-all duration-300")}
|
||||||
"transition-all duration-300",
|
|
||||||
)}
|
|
||||||
placeholder={t("admin.system.mailUser")}
|
placeholder={t("admin.system.mailUser")}
|
||||||
/>
|
/>
|
||||||
</ParagraphItem>
|
</ParagraphItem>
|
||||||
@ -423,9 +425,7 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder={`${data.username}@${location.hostname}`}
|
placeholder={`${data.username}@${location.hostname}`}
|
||||||
className={cn(
|
className={cn("transition-all duration-300")}
|
||||||
"transition-all duration-300",
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</ParagraphItem>
|
</ParagraphItem>
|
||||||
<ParagraphSpace />
|
<ParagraphSpace />
|
||||||
@ -863,7 +863,7 @@ function Search({ data, dispatch, onChange }: CompProps<SearchState>) {
|
|||||||
/>
|
/>
|
||||||
</ParagraphItem>
|
</ParagraphItem>
|
||||||
<ParagraphItem>
|
<ParagraphItem>
|
||||||
<Label>{t("admin.system.searchEngines")}</Label>
|
<Label>{t("admin.system.searchEngines")}</Label>
|
||||||
<MultiCombobox
|
<MultiCombobox
|
||||||
value={data.engines}
|
value={data.engines}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
@ -8,7 +8,9 @@ import {
|
|||||||
import { RootState } from "@/store/index.ts";
|
import { RootState } from "@/store/index.ts";
|
||||||
import { isMobile } from "@/utils/device";
|
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 = {
|
export const initialSettings = {
|
||||||
context: true,
|
context: true,
|
||||||
align: false,
|
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