update api

This commit is contained in:
Zhang Minghan 2023-11-07 09:04:15 +08:00
parent 610464c493
commit b8076e12a9
14 changed files with 186 additions and 35 deletions

53
app/src/admin/api.ts Normal file
View 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;
}

View File

@ -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: {

View File

@ -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} />

View File

@ -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>

View File

@ -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>
); );

View File

@ -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)",

View File

@ -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 = {

View File

@ -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)",

View File

@ -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": "Доход за месяц",

View File

@ -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,
}, },

View File

@ -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 />

View File

@ -0,0 +1,5 @@
function Settings() {
return <></>;
}
export default Settings;

View File

@ -0,0 +1,5 @@
function Users() {
return <></>;
}
export default Users;

View File

@ -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;