mirror of
https://github.com/coaidev/coai.git
synced 2025-06-05 13:20:15 +09:00
feat: support initial user quota (#41)
This commit is contained in:
parent
8dfb1185b8
commit
8e678637fb
@ -23,8 +23,14 @@ export type SearchState = {
|
||||
query: number;
|
||||
};
|
||||
|
||||
export type SiteState = {
|
||||
quota: number;
|
||||
announcement: string;
|
||||
};
|
||||
|
||||
export type SystemProps = {
|
||||
general: GeneralState;
|
||||
site: SiteState;
|
||||
mail: MailState;
|
||||
search: SearchState;
|
||||
};
|
||||
@ -70,6 +76,10 @@ export const initialSystemState: SystemProps = {
|
||||
docs: "",
|
||||
file: "",
|
||||
},
|
||||
site: {
|
||||
quota: 0,
|
||||
announcement: "",
|
||||
},
|
||||
mail: {
|
||||
host: "",
|
||||
port: 465,
|
||||
|
@ -218,6 +218,20 @@ input[type="number"] {
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&.row-layout {
|
||||
align-items: flex-start !important;
|
||||
flex-direction: column !important;
|
||||
|
||||
& > * {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: normal;
|
||||
|
@ -71,11 +71,17 @@ function Paragraph({
|
||||
function ParagraphItem({
|
||||
children,
|
||||
className,
|
||||
rowLayout,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
rowLayout?: boolean;
|
||||
}) {
|
||||
return <div className={cn("paragraph-item", className)}>{children}</div>;
|
||||
return (
|
||||
<div className={cn("paragraph-item", className, rowLayout && "row-layout")}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ParagraphDescription({ children }: { children: string }) {
|
||||
|
@ -507,6 +507,7 @@
|
||||
"system": {
|
||||
"general": "常规设置",
|
||||
"search": "联网搜索",
|
||||
"site": "站点设置",
|
||||
"mail": "SMTP 发件设置",
|
||||
"save": "保存",
|
||||
"updateRoot": "修改 Root 密码",
|
||||
@ -533,7 +534,11 @@
|
||||
"mailFrom": "发件人",
|
||||
"searchEndpoint": "搜索接入点",
|
||||
"searchQuery": "最大搜索结果数",
|
||||
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。"
|
||||
"searchTip": "DuckDuckGo 搜索接入点,如不填写自动使用 WebPilot 和 New Bing 逆向进行搜索功能(速度较慢)。\nDuckDuckGo API 项目搭建:[duckduckgo-api](https://github.com/binjie09/duckduckgo-api)。",
|
||||
"quota": "用户初始点数",
|
||||
"quotaTip": "用户注册后赠送的点数",
|
||||
"announcement": "站点公告",
|
||||
"announcementPlaceholder": "请输入站点公告 (支持 Markdown / HTML 格式)"
|
||||
},
|
||||
"logger": {
|
||||
"title": "服务日志",
|
||||
|
@ -433,7 +433,12 @@
|
||||
"docsTip": "Document link, leave blank for default https://docs.chatnio.net",
|
||||
"file": "File Parsing Service",
|
||||
"filePlaceholder": "File parsing service, leave blank for default https://blob.chatnio.net (stability not guaranteed)",
|
||||
"fileTip": "For file parsing services, please refer to the [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) project to build."
|
||||
"fileTip": "For file parsing services, please refer to the [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) project to build.",
|
||||
"site": "Site Ayarları",
|
||||
"quota": "User Initial Points",
|
||||
"quotaTip": "Credits given after user registration",
|
||||
"announcement": "Site Announcement",
|
||||
"announcementPlaceholder": "Please enter a site announcement (Markdown/HTML format supported)"
|
||||
},
|
||||
"user": "User Management",
|
||||
"invitation-code": "Invitation Code",
|
||||
|
@ -433,7 +433,12 @@
|
||||
"docsTip": "ドキュメントリンク、デフォルトの場合は空白のままhttps://docs.chatnio.net",
|
||||
"file": "ファイル解析サービス",
|
||||
"filePlaceholder": "ファイル解析サービス、デフォルトの場合は空白のままhttps://blob.chatnio.net (安定性は保証されていません)",
|
||||
"fileTip": "ファイル解析サービスについては、[chatnio-blob-service ]( https://github.com/Deeptrain-Community/chatnio-blob-service)プロジェクトを参照して構築してください。"
|
||||
"fileTip": "ファイル解析サービスについては、[chatnio-blob-service ]( https://github.com/Deeptrain-Community/chatnio-blob-service)プロジェクトを参照して構築してください。",
|
||||
"site": "サイト設定",
|
||||
"quota": "ユーザーの初期ポイント",
|
||||
"quotaTip": "ユーザー登録後に付与されたクレジット",
|
||||
"announcement": "サイトのお知らせ",
|
||||
"announcementPlaceholder": "サイトのお知らせを入力してください( Markdown/HTML形式に対応)"
|
||||
},
|
||||
"user": "ユーザー管理",
|
||||
"invitation-code": "招待コード",
|
||||
|
@ -433,7 +433,12 @@
|
||||
"docsTip": "Ссылка на документ, оставьте пустым для по умолчанию https://docs.chatnio.net",
|
||||
"file": "Служба разбора файлов",
|
||||
"filePlaceholder": "Служба разбора файлов, оставьте пустым для по умолчанию https://blob.chatnio.net (стабильность не гарантируется)",
|
||||
"fileTip": "Для получения услуг по разбору файлов обратитесь к проекту [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) для сборки."
|
||||
"fileTip": "Для получения услуг по разбору файлов обратитесь к проекту [chatnio-blob-service] (https://github.com/Deeptrain-Community/chatnio-blob-service) для сборки.",
|
||||
"site": "Настройки сайта",
|
||||
"quota": "Начальные точки пользователя",
|
||||
"quotaTip": "Кредиты, предоставленные после регистрации пользователя",
|
||||
"announcement": "Объявление о площадке",
|
||||
"announcementPlaceholder": "Введите объявление сайта (поддерживается формат Markdown/HTML)"
|
||||
},
|
||||
"user": "Управление пользователями",
|
||||
"invitation-code": "Код приглашения",
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
MailState,
|
||||
SearchState,
|
||||
setConfig,
|
||||
SiteState,
|
||||
SystemProps,
|
||||
updateRootPassword,
|
||||
} from "@/admin/api/system.ts";
|
||||
@ -41,6 +42,8 @@ import {
|
||||
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||
import Require from "@/components/Require.tsx";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
|
||||
type CompProps<T> = {
|
||||
data: T;
|
||||
@ -366,6 +369,64 @@ function Mail({ data, dispatch, onChange }: CompProps<MailState>) {
|
||||
);
|
||||
}
|
||||
|
||||
function Site({ data, dispatch, onChange }: CompProps<SiteState>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// export type SiteState = {
|
||||
// quota: number;
|
||||
// announcement: string;
|
||||
// }
|
||||
|
||||
return (
|
||||
<Paragraph
|
||||
title={t("admin.system.site")}
|
||||
configParagraph={true}
|
||||
isCollapsed={true}
|
||||
>
|
||||
<ParagraphItem>
|
||||
<Label>
|
||||
{t("admin.system.quota")}
|
||||
<Tips
|
||||
className={`inline-block`}
|
||||
content={t("admin.system.quotaTip")}
|
||||
/>
|
||||
</Label>
|
||||
<NumberInput
|
||||
value={data.quota}
|
||||
onValueChange={(value) =>
|
||||
dispatch({ type: "update:site.quota", value })
|
||||
}
|
||||
placeholder={`5`}
|
||||
min={0}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphItem rowLayout={true}>
|
||||
<Label>{t("admin.system.announcement")}</Label>
|
||||
<Textarea
|
||||
value={data.announcement}
|
||||
onChange={(e) =>
|
||||
dispatch({
|
||||
type: "update:site.announcement",
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("admin.system.announcementPlaceholder")}
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphFooter>
|
||||
<div className={`grow`} />
|
||||
<Button
|
||||
size={`sm`}
|
||||
loading={true}
|
||||
onClick={async () => await onChange()}
|
||||
>
|
||||
{t("admin.system.save")}
|
||||
</Button>
|
||||
</ParagraphFooter>
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
function Search({ data, dispatch, onChange }: CompProps<SearchState>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -452,6 +513,7 @@ function System() {
|
||||
</CardHeader>
|
||||
<CardContent className={`flex flex-col gap-1`}>
|
||||
<General data={data.general} dispatch={setData} onChange={doSaving} />
|
||||
<Site data={data.site} dispatch={setData} onChange={doSaving} />
|
||||
<Mail data={data.mail} dispatch={setData} onChange={doSaving} />
|
||||
<Search data={data.search} dispatch={setData} onChange={doSaving} />
|
||||
</CardContent>
|
||||
|
@ -12,21 +12,23 @@ export function setKey<T>(state: T, key: string, value: any): T {
|
||||
}
|
||||
|
||||
export const formReducer = <T>() => {
|
||||
return (state: T, action: any) => {
|
||||
return (state: T, action: any): T => {
|
||||
action.payload = action.payload ?? action.value;
|
||||
|
||||
switch (action.type) {
|
||||
case "update":
|
||||
return { ...state, ...action.payload };
|
||||
return { ...state, ...action.payload } as T;
|
||||
case "reset":
|
||||
return { ...action.payload };
|
||||
return { ...action.payload } as T;
|
||||
case "set":
|
||||
return action.payload;
|
||||
return action.payload as T;
|
||||
default:
|
||||
if (action.type.startsWith("update:")) {
|
||||
const key = action.type.slice(7);
|
||||
return setKey(state, key, action.payload);
|
||||
return setKey(state, key, action.payload) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -144,6 +144,7 @@ func SignUp(c *gin.Context, form RegisterForm) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
user.CreateInitialQuota(db)
|
||||
return user.GenerateToken()
|
||||
}
|
||||
|
||||
@ -193,6 +194,8 @@ func DeepLogin(c *gin.Context, token string) (string, error) {
|
||||
Username: user.Username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
u.CreateInitialQuota(db)
|
||||
return u.GenerateToken()
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,16 @@
|
||||
package auth
|
||||
|
||||
import "database/sql"
|
||||
import (
|
||||
"chat/channel"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func (u *User) CreateInitialQuota(db *sql.DB) bool {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)
|
||||
`, u.GetID(db), channel.SystemInstance.GetInitialQuota(), 0.)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (u *User) GetQuota(db *sql.DB) float32 {
|
||||
var quota float32
|
||||
|
@ -9,10 +9,11 @@ import (
|
||||
)
|
||||
|
||||
type ApiInfo struct {
|
||||
Title string `json:"title"`
|
||||
Logo string `json:"logo"`
|
||||
File string `json:"file"`
|
||||
Docs string `json:"docs"`
|
||||
Title string `json:"title"`
|
||||
Logo string `json:"logo"`
|
||||
File string `json:"file"`
|
||||
Docs string `json:"docs"`
|
||||
Announcement string `json:"announcement"`
|
||||
}
|
||||
|
||||
type generalState struct {
|
||||
@ -23,6 +24,11 @@ type generalState struct {
|
||||
Docs string `json:"docs" mapstructure:"docs"`
|
||||
}
|
||||
|
||||
type siteState struct {
|
||||
Quota float64 `json:"quota" mapstructure:"quota"`
|
||||
Announcement string `json:"announcement" mapstructure:"announcement"`
|
||||
}
|
||||
|
||||
type mailState struct {
|
||||
Host string `json:"host" mapstructure:"host"`
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
@ -38,6 +44,7 @@ type searchState struct {
|
||||
|
||||
type SystemConfig struct {
|
||||
General generalState `json:"general" mapstructure:"general"`
|
||||
Site siteState `json:"site" mapstructure:"site"`
|
||||
Mail mailState `json:"mail" mapstructure:"mail"`
|
||||
Search searchState `json:"search" mapstructure:"search"`
|
||||
}
|
||||
@ -65,21 +72,27 @@ func (c *SystemConfig) SaveConfig() error {
|
||||
|
||||
func (c *SystemConfig) AsInfo() ApiInfo {
|
||||
return ApiInfo{
|
||||
Title: c.General.Title,
|
||||
Logo: c.General.Logo,
|
||||
File: c.General.File,
|
||||
Docs: c.General.Docs,
|
||||
Title: c.General.Title,
|
||||
Logo: c.General.Logo,
|
||||
File: c.General.File,
|
||||
Docs: c.General.Docs,
|
||||
Announcement: c.Site.Announcement,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SystemConfig) UpdateConfig(data *SystemConfig) error {
|
||||
c.General = data.General
|
||||
c.Site = data.Site
|
||||
c.Mail = data.Mail
|
||||
c.Search = data.Search
|
||||
|
||||
return c.SaveConfig()
|
||||
}
|
||||
|
||||
func (c *SystemConfig) GetInitialQuota() float64 {
|
||||
return c.Site.Quota
|
||||
}
|
||||
|
||||
func (c *SystemConfig) GetBackend() string {
|
||||
return c.General.Backend
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user