mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 21:10:18 +09:00
update api
This commit is contained in:
parent
610464c493
commit
b8076e12a9
53
app/src/admin/api.ts
Normal file
53
app/src/admin/api.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
BillingChartResponse,
|
||||||
|
ErrorChartResponse,
|
||||||
|
InfoResponse,
|
||||||
|
ModelChartResponse,
|
||||||
|
RequestChartResponse,
|
||||||
|
} from "@/admin/types.ts";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export async function getAdminInfo(): Promise<InfoResponse> {
|
||||||
|
const response = await axios.get("/admin/analytics/info");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return { subscription_count: 0, billing_today: 0, billing_month: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as InfoResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getModelChart(): Promise<ModelChartResponse> {
|
||||||
|
const response = await axios.get("/admin/analytics/model");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return { date: [], value: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as ModelChartResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRequestChart(): Promise<RequestChartResponse> {
|
||||||
|
const response = await axios.get("/admin/analytics/request");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return { date: [], value: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as RequestChartResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBillingChart(): Promise<BillingChartResponse> {
|
||||||
|
const response = await axios.get("/admin/analytics/billing");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return { date: [], value: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as BillingChartResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getErrorChart(): Promise<ErrorChartResponse> {
|
||||||
|
const response = await axios.get("/admin/analytics/error");
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return { date: [], value: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as ErrorChartResponse;
|
||||||
|
}
|
@ -1,3 +1,9 @@
|
|||||||
|
export type InfoResponse = {
|
||||||
|
billing_today: number;
|
||||||
|
billing_month: number;
|
||||||
|
subscription_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ModelChartResponse = {
|
export type ModelChartResponse = {
|
||||||
date: string[];
|
date: string[];
|
||||||
value: {
|
value: {
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import ModelChart from "@/components/admin/assemblies/ModelChart.tsx";
|
import ModelChart from "@/components/admin/assemblies/ModelChart.tsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {BillingChartResponse, ErrorChartResponse, ModelChartResponse, RequestChartResponse} from "@/admin/types.ts";
|
import {
|
||||||
|
BillingChartResponse,
|
||||||
|
ErrorChartResponse,
|
||||||
|
ModelChartResponse,
|
||||||
|
RequestChartResponse,
|
||||||
|
} from "@/admin/types.ts";
|
||||||
|
|
||||||
import {ArcElement, Chart, Filler, LineElement, PointElement} from "chart.js";
|
import { ArcElement, Chart, Filler, LineElement, PointElement } from "chart.js";
|
||||||
import {
|
import {
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
@ -18,6 +23,13 @@ import { themeEvent } from "@/events/theme.ts";
|
|||||||
import RequestChart from "@/components/admin/assemblies/RequestChart.tsx";
|
import RequestChart from "@/components/admin/assemblies/RequestChart.tsx";
|
||||||
import BillingChart from "@/components/admin/assemblies/BillingChart.tsx";
|
import BillingChart from "@/components/admin/assemblies/BillingChart.tsx";
|
||||||
import ErrorChart from "@/components/admin/assemblies/ErrorChart.tsx";
|
import ErrorChart from "@/components/admin/assemblies/ErrorChart.tsx";
|
||||||
|
import { useEffectAsync } from "@/utils/hook.ts";
|
||||||
|
import {
|
||||||
|
getBillingChart,
|
||||||
|
getErrorChart,
|
||||||
|
getModelChart,
|
||||||
|
getRequestChart,
|
||||||
|
} from "@/admin/api.ts";
|
||||||
|
|
||||||
Chart.register(
|
Chart.register(
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@ -61,39 +73,54 @@ function ChartBox() {
|
|||||||
};
|
};
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [dark, setDark] = useState<boolean>(getMemory("theme") === "dark");
|
const [dark, setDark] = useState<boolean>(getMemory("theme") === "dark");
|
||||||
themeEvent.bind((theme: string) => setDark(theme === "dark"));
|
themeEvent.bind((theme: string) => setDark(theme === "dark"));
|
||||||
|
|
||||||
const [model, setModel] = useState<ModelChartResponse>({
|
const [model, setModel] = useState<ModelChartResponse>({
|
||||||
date: [], value: [],
|
date: [],
|
||||||
|
value: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [request, setRequest] = useState<RequestChartResponse>({
|
const [request, setRequest] = useState<RequestChartResponse>({
|
||||||
date: [], value: [],
|
date: [],
|
||||||
|
value: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [billing, setBilling] = useState<BillingChartResponse>({
|
const [billing, setBilling] = useState<BillingChartResponse>({
|
||||||
date: [], value: [],
|
date: [],
|
||||||
|
value: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [error, setError] = useState<ErrorChartResponse>({
|
const [error, setError] = useState<ErrorChartResponse>({
|
||||||
date: [], value: [],
|
date: [],
|
||||||
|
value: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffectAsync(async () => {
|
||||||
|
setModel(await getModelChart());
|
||||||
|
setRequest(await getRequestChart());
|
||||||
|
setBilling(await getBillingChart());
|
||||||
|
setError(await getErrorChart());
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`chart-boxes`}>
|
<div className={`chart-boxes`}>
|
||||||
<div className={`chart-box`}>
|
<div className={`chart-box`}>
|
||||||
<ModelChart labels={model.date} datasets={model.value} dark={dark} />
|
<ModelChart labels={model.date} datasets={model.value} dark={dark} />
|
||||||
</div>
|
</div>
|
||||||
<div className={`chart-box`}>
|
<div className={`chart-box`}>
|
||||||
<RequestChart labels={request.date} datasets={request.value} dark={dark} />
|
<RequestChart
|
||||||
|
labels={request.date}
|
||||||
|
datasets={request.value}
|
||||||
|
dark={dark}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`chart-box`}>
|
<div className={`chart-box`}>
|
||||||
<BillingChart labels={billing.date} datasets={billing.value} dark={dark} />
|
<BillingChart
|
||||||
|
labels={billing.date}
|
||||||
|
datasets={billing.value}
|
||||||
|
dark={dark}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`chart-box`}>
|
<div className={`chart-box`}>
|
||||||
<ErrorChart labels={error.date} datasets={error.value} dark={dark} />
|
<ErrorChart labels={error.date} datasets={error.value} dark={dark} />
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CircleDollarSign, Users2, Wallet } from "lucide-react";
|
import { CircleDollarSign, Users2, Wallet } from "lucide-react";
|
||||||
|
import { useEffectAsync } from "@/utils/hook.ts";
|
||||||
|
import { getAdminInfo } from "@/admin/api.ts";
|
||||||
|
import { InfoResponse } from "@/admin/types.ts";
|
||||||
|
|
||||||
function InfoBox() {
|
function InfoBox() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState<InfoResponse>({
|
||||||
today: 0,
|
billing_today: 0,
|
||||||
month: 0,
|
billing_month: 0,
|
||||||
users: 0,
|
subscription_count: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffectAsync(async () => {
|
||||||
|
setForm(await getAdminInfo());
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`info-boxes`}>
|
<div className={`info-boxes`}>
|
||||||
<div className={`info-box`}>
|
<div className={`info-box`}>
|
||||||
<div className={`box-wrapper`}>
|
<div className={`box-wrapper`}>
|
||||||
<div className={`box-title`}>{t("admin.billing-today")}</div>
|
<div className={`box-title`}>{t("admin.billing-today")}</div>
|
||||||
<div className={`box-value money`}>{form.today}</div>
|
<div className={`box-value money`}>{form.billing_today}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`box-icon`}>
|
<div className={`box-icon`}>
|
||||||
<CircleDollarSign />
|
<CircleDollarSign />
|
||||||
@ -25,7 +32,7 @@ function InfoBox() {
|
|||||||
<div className={`info-box`}>
|
<div className={`info-box`}>
|
||||||
<div className={`box-wrapper`}>
|
<div className={`box-wrapper`}>
|
||||||
<div className={`box-title`}>{t("admin.billing-month")}</div>
|
<div className={`box-title`}>{t("admin.billing-month")}</div>
|
||||||
<div className={`box-value money`}>{form.month}</div>
|
<div className={`box-value money`}>{form.billing_month}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`box-icon`}>
|
<div className={`box-icon`}>
|
||||||
<Wallet />
|
<Wallet />
|
||||||
@ -36,7 +43,7 @@ function InfoBox() {
|
|||||||
<div className={`box-wrapper`}>
|
<div className={`box-wrapper`}>
|
||||||
<div className={`box-title`}>{t("admin.subscription-users")}</div>
|
<div className={`box-title`}>{t("admin.subscription-users")}</div>
|
||||||
<div className={`box-value`}>
|
<div className={`box-value`}>
|
||||||
{form.users}
|
{form.subscription_count}
|
||||||
<span className={`box-subvalue`}>{t("admin.seat")}</span>
|
<span className={`box-subvalue`}>{t("admin.seat")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { selectMenu } from "@/store/menu.ts";
|
import { selectMenu } from "@/store/menu.ts";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { LayoutDashboard, Settings } from "lucide-react";
|
import { LayoutDashboard, Settings, Users } from "lucide-react";
|
||||||
import router from "@/router.tsx";
|
import router from "@/router.tsx";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -42,10 +42,11 @@ function MenuBar() {
|
|||||||
icon={<LayoutDashboard />}
|
icon={<LayoutDashboard />}
|
||||||
path={"/"}
|
path={"/"}
|
||||||
/>
|
/>
|
||||||
|
<MenuItem title={t("admin.users")} icon={<Users />} path={"/users"} />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title={t("admin.settings")}
|
title={t("admin.settings")}
|
||||||
icon={<Settings />}
|
icon={<Settings />}
|
||||||
path={"/config"}
|
path={"/settings"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
|
|
||||||
type BillingChartProps = {
|
type BillingChartProps = {
|
||||||
@ -14,7 +14,7 @@ function BillingChart({ labels, datasets, dark }: BillingChartProps) {
|
|||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'CNY',
|
label: "CNY",
|
||||||
fill: true,
|
fill: true,
|
||||||
data: datasets,
|
data: datasets,
|
||||||
backgroundColor: "rgba(255,205,111,0.78)",
|
backgroundColor: "rgba(255,205,111,0.78)",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
|
|
||||||
type ErrorChartProps = {
|
type ErrorChartProps = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
|
|
||||||
type RequestChartProps = {
|
type RequestChartProps = {
|
||||||
@ -14,7 +14,7 @@ function RequestChart({ labels, datasets, dark }: RequestChartProps) {
|
|||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: t('admin.requests'),
|
label: t("admin.requests"),
|
||||||
fill: true,
|
fill: true,
|
||||||
data: datasets,
|
data: datasets,
|
||||||
borderColor: "rgba(109,179,255,1)",
|
borderColor: "rgba(109,179,255,1)",
|
||||||
|
@ -258,6 +258,7 @@ const resources = {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
dashboard: "Dashboard",
|
dashboard: "Dashboard",
|
||||||
|
users: "User Management",
|
||||||
settings: "Settings",
|
settings: "Settings",
|
||||||
"billing-today": "Billing Today",
|
"billing-today": "Billing Today",
|
||||||
"billing-month": "Billing Month",
|
"billing-month": "Billing Month",
|
||||||
@ -509,6 +510,7 @@ const resources = {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
dashboard: "仪表盘",
|
dashboard: "仪表盘",
|
||||||
|
users: "用户管理",
|
||||||
settings: "设置",
|
settings: "设置",
|
||||||
"billing-today": "今日入账",
|
"billing-today": "今日入账",
|
||||||
"billing-month": "本月入账",
|
"billing-month": "本月入账",
|
||||||
@ -776,6 +778,7 @@ const resources = {
|
|||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
dashboard: "Панель управления",
|
dashboard: "Панель управления",
|
||||||
|
users: "Управление пользователями",
|
||||||
settings: "Настройки",
|
settings: "Настройки",
|
||||||
"billing-today": "Сегодняшний доход",
|
"billing-today": "Сегодняшний доход",
|
||||||
"billing-month": "Доход за месяц",
|
"billing-month": "Доход за месяц",
|
||||||
|
@ -10,6 +10,8 @@ const Article = lazy(() => import("@/routes/Article.tsx"));
|
|||||||
|
|
||||||
const Admin = lazy(() => import("@/routes/Admin.tsx"));
|
const Admin = lazy(() => import("@/routes/Admin.tsx"));
|
||||||
const Dashboard = lazy(() => import("@/routes/admin/DashBoard.tsx"));
|
const Dashboard = lazy(() => import("@/routes/admin/DashBoard.tsx"));
|
||||||
|
const Settings = lazy(() => import("@/routes/admin/Settings.tsx"));
|
||||||
|
const Users = lazy(() => import("@/routes/admin/Users.tsx"));
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -72,6 +74,24 @@ const router = createBrowserRouter([
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "admin-users",
|
||||||
|
path: "users",
|
||||||
|
element: (
|
||||||
|
<Suspense>
|
||||||
|
<Users />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "admin-settings",
|
||||||
|
path: "settings",
|
||||||
|
element: (
|
||||||
|
<Suspense>
|
||||||
|
<Settings />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
ErrorBoundary: NotFound,
|
ErrorBoundary: NotFound,
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
import "@/assets/admin/all.less";
|
import "@/assets/admin/all.less";
|
||||||
import MenuBar from "@/components/admin/MenuBar.tsx";
|
import MenuBar from "@/components/admin/MenuBar.tsx";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { selectAdmin, selectInit } from "@/store/auth.ts";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import router from "@/router.tsx";
|
||||||
|
|
||||||
function Admin() {
|
function Admin() {
|
||||||
|
const init = useSelector(selectInit);
|
||||||
|
const admin = useSelector(selectAdmin);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (init && !admin) router.navigate("/");
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`admin-page`}>
|
<div className={`admin-page`}>
|
||||||
<MenuBar />
|
<MenuBar />
|
||||||
|
5
app/src/routes/admin/Settings.tsx
Normal file
5
app/src/routes/admin/Settings.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function Settings() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings;
|
5
app/src/routes/admin/Users.tsx
Normal file
5
app/src/routes/admin/Users.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function Users() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Users;
|
@ -1,7 +1,7 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { tokenField } from "@/conf.ts";
|
import { tokenField } from "@/conf.ts";
|
||||||
import { AppDispatch } from "./index.ts";
|
import { AppDispatch, RootState } from "./index.ts";
|
||||||
import { forgetMemory, setMemory } from "@/utils/memory.ts";
|
import { forgetMemory, setMemory } from "@/utils/memory.ts";
|
||||||
|
|
||||||
export const authSlice = createSlice({
|
export const authSlice = createSlice({
|
||||||
@ -10,6 +10,7 @@ export const authSlice = createSlice({
|
|||||||
token: "",
|
token: "",
|
||||||
init: false,
|
init: false,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
|
admin: false,
|
||||||
username: "",
|
username: "",
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
@ -28,6 +29,9 @@ export const authSlice = createSlice({
|
|||||||
setInit: (state, action) => {
|
setInit: (state, action) => {
|
||||||
state.init = action.payload as boolean;
|
state.init = action.payload as boolean;
|
||||||
},
|
},
|
||||||
|
setAdmin: (state, action) => {
|
||||||
|
state.admin = action.payload as boolean;
|
||||||
|
},
|
||||||
logout: (state) => {
|
logout: (state) => {
|
||||||
state.token = "";
|
state.token = "";
|
||||||
state.authenticated = false;
|
state.authenticated = false;
|
||||||
@ -60,6 +64,7 @@ export function validateToken(
|
|||||||
dispatch(setAuthenticated(res.data.status));
|
dispatch(setAuthenticated(res.data.status));
|
||||||
dispatch(setUsername(res.data.user));
|
dispatch(setUsername(res.data.user));
|
||||||
dispatch(setInit(true));
|
dispatch(setInit(true));
|
||||||
|
dispatch(setAdmin(res.data.admin));
|
||||||
hook && hook();
|
hook && hook();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -68,10 +73,18 @@ export function validateToken(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectAuthenticated = (state: any) => state.auth.authenticated;
|
export const selectAuthenticated = (state: RootState) =>
|
||||||
export const selectUsername = (state: any) => state.auth.username;
|
state.auth.authenticated;
|
||||||
export const selectInit = (state: any) => state.auth.init;
|
export const selectUsername = (state: RootState) => state.auth.username;
|
||||||
|
export const selectInit = (state: RootState) => state.auth.init;
|
||||||
|
export const selectAdmin = (state: RootState) => state.auth.admin;
|
||||||
|
|
||||||
export const { setToken, setAuthenticated, setUsername, logout, setInit } =
|
export const {
|
||||||
authSlice.actions;
|
setToken,
|
||||||
|
setAuthenticated,
|
||||||
|
setUsername,
|
||||||
|
logout,
|
||||||
|
setInit,
|
||||||
|
setAdmin,
|
||||||
|
} = authSlice.actions;
|
||||||
export default authSlice.reducer;
|
export default authSlice.reducer;
|
||||||
|
Loading…
Reference in New Issue
Block a user