mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-02 08:06:38 +08:00
commit
7c9c7f511c
@ -1,17 +1,17 @@
|
|||||||
import UsageByModel from "./usage-by-model-chart";
|
import UsageByModel from "./usage-by-model-chart";
|
||||||
import UserByMap from "./user-by-map";
|
import UserByMap from "./user-by-map";
|
||||||
import { getSession } from "@/lib/auth";
|
// import { getSession } from "@/lib/auth";
|
||||||
import { isName, ADMIN_LIST } from "@/lib/auth_list";
|
// import { isName, ADMIN_LIST } from "@/lib/auth_list";
|
||||||
import { redirect } from "next/navigation";
|
// import { redirect } from "next/navigation";
|
||||||
import { Flex } from "antd";
|
import { Flex } from "antd";
|
||||||
|
|
||||||
export default async function AdminPage() {
|
export default async function AdminPage() {
|
||||||
const session = await getSession();
|
// const session = await getSession();
|
||||||
const name = session?.user?.email || session?.user?.name;
|
// const name = session?.user?.email || session?.user?.name;
|
||||||
if (!(name && ADMIN_LIST.includes(name))) {
|
// if (!(name && ADMIN_LIST.includes(name))) {
|
||||||
// Replace '/dashboard' with the desired redirect path
|
// // Replace '/dashboard' with the desired redirect path
|
||||||
redirect("/");
|
// redirect("/");
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { ReactNode, useState } from "react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { ADMIN_LIST } from "@/lib/auth_list";
|
||||||
|
import React, { ReactNode, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
@ -15,12 +18,23 @@ const { Header, Sider, Content } = Layout;
|
|||||||
|
|
||||||
function MainLayout({ children }: { children: ReactNode }) {
|
function MainLayout({ children }: { children: ReactNode }) {
|
||||||
// const [theme, setTheme] = useState<ThemeConfig>('dark');
|
// const [theme, setTheme] = useState<ThemeConfig>('dark');
|
||||||
|
const { data, status } = useSession();
|
||||||
|
const name = data?.user?.email || data?.user?.name;
|
||||||
|
// console.log('name', name, data, status)
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const {
|
const {
|
||||||
token: { colorBgContainer, borderRadiusLG, colorBgLayout },
|
token: { colorBgContainer, borderRadiusLG, colorBgLayout },
|
||||||
} = theme.useToken();
|
} = theme.useToken();
|
||||||
|
|
||||||
|
// 客户端才执行
|
||||||
|
useEffect(() => {
|
||||||
|
// 用户已登录,且没设置密码
|
||||||
|
// if (status === "loading") return;
|
||||||
|
if (status === "authenticated" && !(name && ADMIN_LIST.includes(name))) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
// 状态变化时,重新判断
|
||||||
|
}, [name, status]);
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
|
22
app/app/(admin)/admin/users/page.tsx
Normal file
22
app/app/(admin)/admin/users/page.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Flex } from "antd";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import UsersTable from "../../components/users-table";
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
const users = await prisma.user.findMany();
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
export default async function UsersPage() {
|
||||||
|
const users: User[] = await getData();
|
||||||
|
|
||||||
|
// console.log("data", data);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex gap="middle" vertical>
|
||||||
|
<UsersTable users={users} />
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -68,33 +68,15 @@ const SideBar: React.FC = () => {
|
|||||||
setCurrent(e.key);
|
setCurrent(e.key);
|
||||||
router.push(e.key);
|
router.push(e.key);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
|
||||||
// 如果按钮和路径不相等,那其实应该跳转到按钮的网址
|
|
||||||
if (current != pathname) {
|
|
||||||
router.push(current);
|
|
||||||
}
|
|
||||||
}, [current, pathname, router]);
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const handleStart = () => setLoading(true)
|
// // 如果按钮和路径不相等,那其实应该跳转到按钮的网址
|
||||||
// const handleComplete = () => setLoading(false);
|
// if (current != pathname) {
|
||||||
// router.events.on('routeChangeStart', handleStart);
|
// router.push(current);
|
||||||
// router.events.on('routeChangeComplete', handleStop);
|
// }
|
||||||
// router.events.on('routeChangeError', handleStop);
|
// }, [current, pathname, router]);
|
||||||
//
|
|
||||||
// return () => {
|
|
||||||
// router.
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }, [router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*<Switch*/}
|
|
||||||
{/* // checked={theme === 'dark'}*/}
|
|
||||||
{/* // onChange={changeTheme}*/}
|
|
||||||
{/* checkedChildren="Dark"*/}
|
|
||||||
{/* unCheckedChildren="Light"*/}
|
|
||||||
{/*/>*/}
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<Menu
|
<Menu
|
||||||
|
164
app/app/(admin)/components/users-table.tsx
Normal file
164
app/app/(admin)/components/users-table.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
import { Space, Table, Tag, Input, Button } from "antd";
|
||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import type { FilterDropdownProps } from "antd/es/table/interface";
|
||||||
|
import type { GetRef, TableColumnsType, TableColumnType } from "antd";
|
||||||
|
import Highlighter from "react-highlight-words";
|
||||||
|
// 后期考虑删除该依赖
|
||||||
|
|
||||||
|
import { getCurrentTime } from "@/app/utils/custom";
|
||||||
|
|
||||||
|
interface UserInterface {
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
|
type DataIndex = keyof User;
|
||||||
|
type InputRef = GetRef<typeof Input>;
|
||||||
|
|
||||||
|
function UsersTable({ users }: UserInterface) {
|
||||||
|
// const data = {}
|
||||||
|
// console.log('[data]', users)
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [searchedColumn, setSearchedColumn] = useState("");
|
||||||
|
const searchInput = useRef<InputRef>(null);
|
||||||
|
const handleSearch = (
|
||||||
|
selectedKeys: string[],
|
||||||
|
confirm: FilterDropdownProps["confirm"],
|
||||||
|
dataIndex: DataIndex,
|
||||||
|
) => {
|
||||||
|
confirm();
|
||||||
|
setSearchText(selectedKeys[0]);
|
||||||
|
setSearchedColumn(dataIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = (clearFilters: () => void) => {
|
||||||
|
clearFilters();
|
||||||
|
setSearchText("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnSearchProps = (
|
||||||
|
dataIndex: DataIndex,
|
||||||
|
): TableColumnType<User> => ({
|
||||||
|
filterDropdown: ({
|
||||||
|
setSelectedKeys,
|
||||||
|
selectedKeys,
|
||||||
|
confirm,
|
||||||
|
clearFilters,
|
||||||
|
close,
|
||||||
|
}) => (
|
||||||
|
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
|
||||||
|
<Input
|
||||||
|
ref={searchInput}
|
||||||
|
placeholder={`Search ${dataIndex}`}
|
||||||
|
value={selectedKeys[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
|
}
|
||||||
|
onPressEnter={() =>
|
||||||
|
handleSearch(selectedKeys as string[], confirm, dataIndex)
|
||||||
|
}
|
||||||
|
style={{ marginBottom: 8, display: "block" }}
|
||||||
|
/>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() =>
|
||||||
|
handleSearch(selectedKeys as string[], confirm, dataIndex)
|
||||||
|
}
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 90 }}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => clearFilters && handleReset(clearFilters)}
|
||||||
|
size="small"
|
||||||
|
style={{ width: 90 }}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
confirm({ closeDropdown: false });
|
||||||
|
setSearchText((selectedKeys as string[])[0]);
|
||||||
|
setSearchedColumn(dataIndex);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
filterIcon: (filtered: boolean) => (
|
||||||
|
<SearchOutlined style={{ color: filtered ? "#1677ff" : undefined }} />
|
||||||
|
),
|
||||||
|
onFilter: (value, record: User) => {
|
||||||
|
let result = record?.[dataIndex];
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes((value as string).toLowerCase());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onFilterDropdownOpenChange: (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
setTimeout(() => searchInput.current?.select(), 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render: (text) =>
|
||||||
|
searchedColumn === dataIndex ? (
|
||||||
|
<Highlighter
|
||||||
|
highlightStyle={{ backgroundColor: "#ffc069", padding: 0 }}
|
||||||
|
searchWords={[searchText]}
|
||||||
|
autoEscape
|
||||||
|
textToHighlight={text ? text.toString() : ""}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: TableColumnsType<User> = [
|
||||||
|
{ title: "Name", dataIndex: "name", ...getColumnSearchProps("name") },
|
||||||
|
{
|
||||||
|
title: "UserName",
|
||||||
|
dataIndex: "username",
|
||||||
|
...getColumnSearchProps("username"),
|
||||||
|
},
|
||||||
|
{ title: "Email", dataIndex: "email", ...getColumnSearchProps("email") },
|
||||||
|
{
|
||||||
|
title: "createdAt",
|
||||||
|
dataIndex: "createdAt",
|
||||||
|
render: (value) => getCurrentTime(value),
|
||||||
|
sorter: (a, b) => {
|
||||||
|
if (a.createdAt < b.createdAt) return 1;
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "updatedAt",
|
||||||
|
dataIndex: "updatedAt",
|
||||||
|
render: (value) => getCurrentTime(value),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Table dataSource={users} rowKey="id" columns={columns} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersTable;
|
@ -1,5 +1,7 @@
|
|||||||
export function getCurrentTime(): string {
|
export function getCurrentTime(now?: Date): string {
|
||||||
const now = new Date();
|
if (!now) {
|
||||||
|
const now = new Date();
|
||||||
|
}
|
||||||
const formatter = new Intl.DateTimeFormat("zh-CN", {
|
const formatter = new Intl.DateTimeFormat("zh-CN", {
|
||||||
timeZone: "Asia/Shanghai", // 设置为中国标准时间
|
timeZone: "Asia/Shanghai", // 设置为中国标准时间
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
export const DENY_LIST: string[] = [
|
export const DENY_LIST: string[] = [
|
||||||
"suibian", "某某", "张三", "李四", "啊实打实", "官方回复电话", "笑死", "观化听风"
|
"suibian", "某某", "张三", "李四", "啊实打实", "官方回复电话", "笑死", "观化听风", "null", "undefined",
|
||||||
]
|
]
|
||||||
export const ADMIN_LIST: string[] = [
|
export const ADMIN_LIST: string[] = [
|
||||||
"司金辉", "sijinhui", "sijinhui@qq.com",
|
"司金辉", "sijinhui", "sijinhui@qq.com",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"@vercel/speed-insights": "^1.0.9",
|
"@vercel/speed-insights": "^1.0.9",
|
||||||
"antd": "^5.15.1",
|
"antd": "^5.15.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cron": "^3.1.6",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"emoji-picker-react": "^4.7.10",
|
"emoji-picker-react": "^4.7.10",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
@ -39,6 +40,7 @@
|
|||||||
"nodemailer": "^6.9.13",
|
"nodemailer": "^6.9.13",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-highlight-words": "^0.20.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.21.3",
|
"react-router-dom": "^6.21.3",
|
||||||
"rehype-highlight": "^7.0.0",
|
"rehype-highlight": "^7.0.0",
|
||||||
@ -60,6 +62,7 @@
|
|||||||
"@types/node": "^20.11.10",
|
"@types/node": "^20.11.10",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@types/react-highlight-words": "^0.16.7",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.4",
|
"@types/spark-md5": "^3.0.4",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
@ -74,7 +77,7 @@
|
|||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
"prisma": "^5.9.0",
|
"prisma": "^5.11.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
Loading…
Reference in New Issue
Block a user