mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-21 11:30:06 +00:00
feat: admin
This commit is contained in:
@@ -11,8 +11,10 @@
|
||||
"start:api": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-react": "^2.49.1",
|
||||
"concurrently": "^8.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.8",
|
||||
"dotenv": "^16.1.4",
|
||||
"express": "^4.18.2",
|
||||
@@ -21,6 +23,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-admin": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"tushan": "^0.2.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
16
admin/pnpm-lock.yaml
generated
16
admin/pnpm-lock.yaml
generated
@@ -5,12 +5,18 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@arco-design/web-react':
|
||||
specifier: ^2.49.1
|
||||
version: registry.npmmirror.com/@arco-design/web-react@2.49.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
|
||||
concurrently:
|
||||
specifier: ^8.1.0
|
||||
version: registry.npmmirror.com/concurrently@8.1.0
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: registry.npmmirror.com/cors@2.8.5
|
||||
crypto:
|
||||
specifier: ^1.0.1
|
||||
version: registry.npmmirror.com/crypto@1.0.1
|
||||
dayjs:
|
||||
specifier: ^1.11.8
|
||||
version: registry.npmmirror.com/dayjs@1.11.8
|
||||
@@ -35,6 +41,9 @@ dependencies:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: registry.npmmirror.com/react-dom@18.2.0(react@18.2.0)
|
||||
react-i18next:
|
||||
specifier: ^12.3.1
|
||||
version: registry.npmmirror.com/react-i18next@12.3.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
tushan:
|
||||
specifier: ^0.2.22
|
||||
version: registry.npmmirror.com/tushan@0.2.22(history@5.3.0)(prop-types@15.8.1)(react-hook-form@7.44.3)
|
||||
@@ -1896,6 +1905,13 @@ packages:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/crypto@1.0.1:
|
||||
resolution: {integrity: sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz}
|
||||
name: crypto
|
||||
version: 1.0.1
|
||||
deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/css-color-keywords@1.0.0:
|
||||
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz}
|
||||
name: css-color-keywords
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { User, Model, Kb } from '../schema.js';
|
||||
import { auth } from './system.js';
|
||||
|
||||
export const useAppRoute = (app) => {
|
||||
// 获取AI助手列表
|
||||
app.get('/models', async (req, res) => {
|
||||
app.get('/models', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { Kb } from '../schema.js';
|
||||
import { auth } from './system.js';
|
||||
|
||||
export const useKbRoute = (app) => {
|
||||
// 获取用户知识库列表
|
||||
app.get('/kbs', async (req, res) => {
|
||||
app.get('/kbs', auth(), async (req, res) => {
|
||||
try {
|
||||
const start = parseInt(req.query._start) || 0;
|
||||
const end = parseInt(req.query._end) || 20;
|
||||
|
@@ -31,7 +31,8 @@ export const useUserRoute = (app) => {
|
||||
return {
|
||||
...obj,
|
||||
id: obj._id,
|
||||
createTime: dayjs(obj.createTime).format('YYYY/MM/DD HH:mm')
|
||||
createTime: dayjs(obj.createTime).format('YYYY/MM/DD HH:mm'),
|
||||
password: ''
|
||||
};
|
||||
});
|
||||
|
||||
@@ -49,14 +50,7 @@ export const useUserRoute = (app) => {
|
||||
// 创建用户
|
||||
app.post('/users', auth(), async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
username,
|
||||
password,
|
||||
balance,
|
||||
promotion,
|
||||
openaiKey = '',
|
||||
avatar = '/icon/human.png'
|
||||
} = req.body;
|
||||
const { username, password, balance } = req.body;
|
||||
if (!username || !password || !balance) {
|
||||
return res.status(400).json({ error: 'Invalid user information' });
|
||||
}
|
||||
@@ -64,19 +58,12 @@ export const useUserRoute = (app) => {
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Username already exists' });
|
||||
}
|
||||
const user = new User({
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
|
||||
const result = await User.create({
|
||||
username,
|
||||
password,
|
||||
balance,
|
||||
promotion: {
|
||||
rate: promotion?.rate || 0
|
||||
},
|
||||
openaiKey,
|
||||
avatar,
|
||||
createTime: new Date()
|
||||
balance
|
||||
});
|
||||
const result = await user.save();
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error creating user: ${err}`);
|
||||
@@ -88,15 +75,13 @@ export const useUserRoute = (app) => {
|
||||
app.put('/users/:id', auth(), async (req, res) => {
|
||||
try {
|
||||
const _id = req.params.id;
|
||||
|
||||
// Check if a new password is provided in the request body
|
||||
if (req.body.password) {
|
||||
// Hash the new password
|
||||
const hashedPassword = hashPassword(req.body.password);
|
||||
req.body.password = hashedPassword;
|
||||
}
|
||||
|
||||
const result = await User.updateOne({ _id: _id }, { $set: req.body });
|
||||
|
||||
let { password, balance = 0 } = req.body;
|
||||
|
||||
const result = await User.findByIdAndUpdate(_id, {
|
||||
...(password && { password: hashPassword(hashPassword(password)) }),
|
||||
...(balance && { balance })
|
||||
});
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.log(`Error updating user: ${err}`);
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
} from 'tushan';
|
||||
import { authProvider } from './auth';
|
||||
import { userFields, payFields, kbFields, ModelFields } from './fields';
|
||||
import { Dashboard } from './Dashboard';
|
||||
|
||||
const authStorageKey = 'tushan:auth';
|
||||
|
||||
@@ -34,6 +35,7 @@ function App() {
|
||||
header={'FastGpt-Admin'}
|
||||
dataProvider={dataProvider}
|
||||
authProvider={authProvider}
|
||||
dashboard={<Dashboard />}
|
||||
>
|
||||
<Resource
|
||||
name="users"
|
||||
|
@@ -1,159 +1,143 @@
|
||||
import {
|
||||
Card,
|
||||
Link,
|
||||
Space,
|
||||
Grid,
|
||||
Divider,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import { IconApps, IconUser, IconUserGroup } from 'tushan/icon';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
|
||||
export const Dashboard: React.FC = React.memo(() => {
|
||||
|
||||
const [userCount, setUserCount] = useState(0); //用户数量
|
||||
const [kbCount, setkbCount] = useState(0);
|
||||
const [modelCount, setmodelCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const fetchCounts = async () => {
|
||||
const userResponse = await fetch('http://localhost:3001/users', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const kbResponse = await fetch('http://localhost:3001/kbs', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const modelResponse = await fetch('http://localhost:3001/models', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const userTotalCount = userResponse.headers.get('X-Total-Count');
|
||||
const kbTotalCount = kbResponse.headers.get('X-Total-Count');
|
||||
const modelTotalCount = modelResponse.headers.get('X-Total-Count');
|
||||
|
||||
if (userTotalCount) {
|
||||
setUserCount(Number(userTotalCount));
|
||||
}
|
||||
if (kbTotalCount) {
|
||||
setkbCount(Number(kbTotalCount));
|
||||
}
|
||||
if (modelTotalCount) {
|
||||
setmodelCount(Number(modelTotalCount));
|
||||
}
|
||||
};
|
||||
|
||||
fetchCounts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Card bordered={false}>
|
||||
<Typography.Title heading={5}>
|
||||
{'你好,管理员'}
|
||||
</Typography.Title>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Grid.Row justify="center">
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
{/* 把 userCount 传递给 DataItem 组件 */}
|
||||
<DataItem
|
||||
icon={<IconUser />}
|
||||
title={'用户'}
|
||||
count={userCount}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Divider type="vertical" style={{ height: 40 }} />
|
||||
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
<DataItem
|
||||
icon={<IconUserGroup />}
|
||||
title={'知识库'}
|
||||
count={kbCount}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Divider type="vertical" style={{ height: 40 }} />
|
||||
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
<DataItem
|
||||
icon={<IconApps />}
|
||||
title={'AI模型'}
|
||||
count={modelCount}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Dashboard.displayName = 'Dashboard';
|
||||
|
||||
const DashboardItem: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
title: string;
|
||||
href?: string;
|
||||
}>
|
||||
> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={props.title}
|
||||
extra={
|
||||
props.href && (
|
||||
<Link target="_blank" href={props.href}>
|
||||
{t('tushan.dashboard.more')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
bordered={false}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
DashboardItem.displayName = 'DashboardItem';
|
||||
|
||||
const DataItem: React.FC<{
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
count: number;
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<Space>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
padding: '0.5rem',
|
||||
borderRadius: '9999px',
|
||||
border: '1px solid #ccc',
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{props.title}</div>
|
||||
<div>{props.count}</div>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
});
|
||||
DataItem.displayName = 'DataItem';
|
||||
import { Card, Link, Space, Grid, Divider, Typography } from '@arco-design/web-react';
|
||||
import { IconApps, IconUser, IconUserGroup } from 'tushan/icon';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const authStorageKey = 'tushan:auth';
|
||||
|
||||
export const Dashboard: React.FC = React.memo(() => {
|
||||
const [userCount, setUserCount] = useState(0); //用户数量
|
||||
const [kbCount, setkbCount] = useState(0);
|
||||
const [modelCount, setmodelCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const fetchCounts = async () => {
|
||||
const baseUrl = import.meta.env.VITE_PUBLIC_SERVER_URL;
|
||||
const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
const userResponse = await fetch(`${baseUrl}/users?_end=1`, {
|
||||
headers
|
||||
});
|
||||
const kbResponse = await fetch(`${baseUrl}/kbs?_end=1`, {
|
||||
headers
|
||||
});
|
||||
const modelResponse = await fetch(`${baseUrl}/models?_end=1`, {
|
||||
headers
|
||||
});
|
||||
|
||||
const userTotalCount = userResponse.headers.get('X-Total-Count');
|
||||
const kbTotalCount = kbResponse.headers.get('X-Total-Count');
|
||||
const modelTotalCount = modelResponse.headers.get('X-Total-Count');
|
||||
console.log(userTotalCount);
|
||||
|
||||
if (userTotalCount) {
|
||||
setUserCount(Number(userTotalCount));
|
||||
}
|
||||
if (kbTotalCount) {
|
||||
setkbCount(Number(kbTotalCount));
|
||||
}
|
||||
if (modelTotalCount) {
|
||||
setmodelCount(Number(modelTotalCount));
|
||||
}
|
||||
};
|
||||
|
||||
fetchCounts();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Card bordered={false}>
|
||||
<Typography.Title heading={5}>FastGpt Admin</Typography.Title>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Grid.Row justify="center">
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
{/* 把 userCount 传递给 DataItem 组件 */}
|
||||
<DataItem icon={<IconUser />} title={'用户'} count={userCount} />
|
||||
</Grid.Col>
|
||||
|
||||
<Divider type="vertical" style={{ height: 40 }} />
|
||||
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
<DataItem icon={<IconUserGroup />} title={'知识库'} count={kbCount} />
|
||||
</Grid.Col>
|
||||
|
||||
<Divider type="vertical" style={{ height: 40 }} />
|
||||
|
||||
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
|
||||
<DataItem icon={<IconApps />} title={'AI模型'} count={modelCount} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
|
||||
<Divider />
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Dashboard.displayName = 'Dashboard';
|
||||
|
||||
const DashboardItem: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
title: string;
|
||||
href?: string;
|
||||
}>
|
||||
> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={props.title}
|
||||
extra={
|
||||
props.href && (
|
||||
<Link target="_blank" href={props.href}>
|
||||
{t('tushan.dashboard.more')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
bordered={false}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
DashboardItem.displayName = 'DashboardItem';
|
||||
|
||||
const DataItem: React.FC<{
|
||||
icon: React.ReactElement;
|
||||
title: string;
|
||||
count: number;
|
||||
}> = React.memo((props) => {
|
||||
return (
|
||||
<Space>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
padding: '0.5rem',
|
||||
borderRadius: '9999px',
|
||||
border: '1px solid #ccc',
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700 }}>{props.title}</div>
|
||||
<div>{props.count}</div>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
});
|
||||
DataItem.displayName = 'DataItem';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { createAuthProvider, type AuthProvider } from 'tushan';
|
||||
|
||||
export const authProvider: AuthProvider = createAuthProvider({
|
||||
loginUrl: `${import.meta.env.VITE_PUBLIC_SERVER_URL}api/login`
|
||||
loginUrl: `${import.meta.env.VITE_PUBLIC_SERVER_URL}/api/login`
|
||||
});
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
createTextField,
|
||||
createNumberField,
|
||||
} from 'tushan';
|
||||
import { createTextField, createNumberField } from 'tushan';
|
||||
|
||||
export const userFields = [
|
||||
createTextField('id', { label: 'ID' }),
|
||||
createTextField('username', { label: '用户名' }),
|
||||
createNumberField('balance', { label: '余额', list: { sort: true } }),
|
||||
createTextField('createTime', { label: 'Create Time', list: { sort: true } })
|
||||
createTextField('createTime', { label: 'Create Time', list: { sort: true } }),
|
||||
createTextField('password', { label: '密码', list: { hidden: true } })
|
||||
];
|
||||
|
||||
export const payFields = [
|
||||
|
Reference in New Issue
Block a user