feat: admin

This commit is contained in:
archer
2023-06-10 15:23:35 +08:00
parent 7f9899f7f3
commit 7dd8e7bea1
9 changed files with 185 additions and 195 deletions

View File

@@ -11,8 +11,10 @@
"start:api": "nodemon server.js" "start:api": "nodemon server.js"
}, },
"dependencies": { "dependencies": {
"@arco-design/web-react": "^2.49.1",
"concurrently": "^8.1.0", "concurrently": "^8.1.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1",
"dayjs": "^1.11.8", "dayjs": "^1.11.8",
"dotenv": "^16.1.4", "dotenv": "^16.1.4",
"express": "^4.18.2", "express": "^4.18.2",
@@ -21,6 +23,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-admin": "^4.11.0", "react-admin": "^4.11.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^12.3.1",
"tushan": "^0.2.22" "tushan": "^0.2.22"
}, },
"devDependencies": { "devDependencies": {

16
admin/pnpm-lock.yaml generated
View File

@@ -5,12 +5,18 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: 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: concurrently:
specifier: ^8.1.0 specifier: ^8.1.0
version: registry.npmmirror.com/concurrently@8.1.0 version: registry.npmmirror.com/concurrently@8.1.0
cors: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: registry.npmmirror.com/cors@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: dayjs:
specifier: ^1.11.8 specifier: ^1.11.8
version: registry.npmmirror.com/dayjs@1.11.8 version: registry.npmmirror.com/dayjs@1.11.8
@@ -35,6 +41,9 @@ dependencies:
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: registry.npmmirror.com/react-dom@18.2.0(react@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: tushan:
specifier: ^0.2.22 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) 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 - encoding
dev: false 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: 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} 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 name: css-color-keywords

View File

@@ -1,8 +1,9 @@
import { User, Model, Kb } from '../schema.js'; import { User, Model, Kb } from '../schema.js';
import { auth } from './system.js';
export const useAppRoute = (app) => { export const useAppRoute = (app) => {
// 获取AI助手列表 // 获取AI助手列表
app.get('/models', async (req, res) => { app.get('/models', auth(), async (req, res) => {
try { try {
const start = parseInt(req.query._start) || 0; const start = parseInt(req.query._start) || 0;
const end = parseInt(req.query._end) || 20; const end = parseInt(req.query._end) || 20;

View File

@@ -1,8 +1,9 @@
import { Kb } from '../schema.js'; import { Kb } from '../schema.js';
import { auth } from './system.js';
export const useKbRoute = (app) => { export const useKbRoute = (app) => {
// 获取用户知识库列表 // 获取用户知识库列表
app.get('/kbs', async (req, res) => { app.get('/kbs', auth(), async (req, res) => {
try { try {
const start = parseInt(req.query._start) || 0; const start = parseInt(req.query._start) || 0;
const end = parseInt(req.query._end) || 20; const end = parseInt(req.query._end) || 20;

View File

@@ -31,7 +31,8 @@ export const useUserRoute = (app) => {
return { return {
...obj, ...obj,
id: obj._id, 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) => { app.post('/users', auth(), async (req, res) => {
try { try {
const { const { username, password, balance } = req.body;
username,
password,
balance,
promotion,
openaiKey = '',
avatar = '/icon/human.png'
} = req.body;
if (!username || !password || !balance) { if (!username || !password || !balance) {
return res.status(400).json({ error: 'Invalid user information' }); return res.status(400).json({ error: 'Invalid user information' });
} }
@@ -64,19 +58,12 @@ export const useUserRoute = (app) => {
if (existingUser) { if (existingUser) {
return res.status(400).json({ error: 'Username already exists' }); return res.status(400).json({ error: 'Username already exists' });
} }
const user = new User({
_id: new mongoose.Types.ObjectId(), const result = await User.create({
username, username,
password, password,
balance, balance
promotion: {
rate: promotion?.rate || 0
},
openaiKey,
avatar,
createTime: new Date()
}); });
const result = await user.save();
res.json(result); res.json(result);
} catch (err) { } catch (err) {
console.log(`Error creating user: ${err}`); console.log(`Error creating user: ${err}`);
@@ -88,15 +75,13 @@ export const useUserRoute = (app) => {
app.put('/users/:id', auth(), async (req, res) => { app.put('/users/:id', auth(), async (req, res) => {
try { try {
const _id = req.params.id; const _id = req.params.id;
// Check if a new password is provided in the request body let { password, balance = 0 } = req.body;
if (req.body.password) {
// Hash the new password const result = await User.findByIdAndUpdate(_id, {
const hashedPassword = hashPassword(req.body.password); ...(password && { password: hashPassword(hashPassword(password)) }),
req.body.password = hashedPassword; ...(balance && { balance })
} });
const result = await User.updateOne({ _id: _id }, { $set: req.body });
res.json(result); res.json(result);
} catch (err) { } catch (err) {
console.log(`Error updating user: ${err}`); console.log(`Error updating user: ${err}`);

View File

@@ -8,6 +8,7 @@ import {
} from 'tushan'; } from 'tushan';
import { authProvider } from './auth'; import { authProvider } from './auth';
import { userFields, payFields, kbFields, ModelFields } from './fields'; import { userFields, payFields, kbFields, ModelFields } from './fields';
import { Dashboard } from './Dashboard';
const authStorageKey = 'tushan:auth'; const authStorageKey = 'tushan:auth';
@@ -34,6 +35,7 @@ function App() {
header={'FastGpt-Admin'} header={'FastGpt-Admin'}
dataProvider={dataProvider} dataProvider={dataProvider}
authProvider={authProvider} authProvider={authProvider}
dashboard={<Dashboard />}
> >
<Resource <Resource
name="users" name="users"

View File

@@ -1,159 +1,143 @@
import { import { Card, Link, Space, Grid, Divider, Typography } from '@arco-design/web-react';
Card, import { IconApps, IconUser, IconUserGroup } from 'tushan/icon';
Link, import React, { useState, useEffect } from 'react';
Space, import { useTranslation } from 'react-i18next';
Grid,
Divider, const authStorageKey = 'tushan:auth';
Typography,
} from '@arco-design/web-react'; export const Dashboard: React.FC = React.memo(() => {
import { IconApps, IconUser, IconUserGroup } from 'tushan/icon'; const [userCount, setUserCount] = useState(0); //用户数量
import React, { useState, useEffect } from 'react'; const [kbCount, setkbCount] = useState(0);
import { useTranslation } from 'react-i18next'; const [modelCount, setmodelCount] = useState(0);
useEffect(() => {
const fetchCounts = async () => {
const baseUrl = import.meta.env.VITE_PUBLIC_SERVER_URL;
export const Dashboard: React.FC = React.memo(() => { const { token } = JSON.parse(window.localStorage.getItem(authStorageKey) ?? '{}');
const [userCount, setUserCount] = useState(0); //用户数量 const headers = {
const [kbCount, setkbCount] = useState(0); 'Content-Type': 'application/json',
const [modelCount, setmodelCount] = useState(0); Authorization: `Bearer ${token}`
useEffect(() => { };
const fetchCounts = async () => { const userResponse = await fetch(`${baseUrl}/users?_end=1`, {
const userResponse = await fetch('http://localhost:3001/users', { headers
headers: { 'Content-Type': 'application/json' }, });
}); const kbResponse = await fetch(`${baseUrl}/kbs?_end=1`, {
const kbResponse = await fetch('http://localhost:3001/kbs', { headers
headers: { 'Content-Type': 'application/json' }, });
}); const modelResponse = await fetch(`${baseUrl}/models?_end=1`, {
const modelResponse = await fetch('http://localhost:3001/models', { headers
headers: { 'Content-Type': 'application/json' }, });
});
const userTotalCount = userResponse.headers.get('X-Total-Count');
const userTotalCount = userResponse.headers.get('X-Total-Count'); const kbTotalCount = kbResponse.headers.get('X-Total-Count');
const kbTotalCount = kbResponse.headers.get('X-Total-Count'); const modelTotalCount = modelResponse.headers.get('X-Total-Count');
const modelTotalCount = modelResponse.headers.get('X-Total-Count'); console.log(userTotalCount);
if (userTotalCount) { if (userTotalCount) {
setUserCount(Number(userTotalCount)); setUserCount(Number(userTotalCount));
} }
if (kbTotalCount) { if (kbTotalCount) {
setkbCount(Number(kbTotalCount)); setkbCount(Number(kbTotalCount));
} }
if (modelTotalCount) { if (modelTotalCount) {
setmodelCount(Number(modelTotalCount)); setmodelCount(Number(modelTotalCount));
} }
}; };
fetchCounts(); fetchCounts();
}, []); }, []);
return ( return (
<div> <div>
<div> <div>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<Card bordered={false}> <Card bordered={false}>
<Typography.Title heading={5}> <Typography.Title heading={5}>FastGpt Admin</Typography.Title>
{'你好,管理员'}
</Typography.Title> <Divider />
<Divider /> <Grid.Row justify="center">
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
<Grid.Row justify="center"> {/* 把 userCount 传递给 DataItem 组件 */}
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}> <DataItem icon={<IconUser />} title={'用户'} count={userCount} />
{/* 把 userCount 传递给 DataItem 组件 */} </Grid.Col>
<DataItem
icon={<IconUser />} <Divider type="vertical" style={{ height: 40 }} />
title={'用户'}
count={userCount} <Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
/> <DataItem icon={<IconUserGroup />} title={'知识库'} count={kbCount} />
</Grid.Col> </Grid.Col>
<Divider type="vertical" style={{ height: 40 }} /> <Divider type="vertical" style={{ height: 40 }} />
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}> <Grid.Col flex={1} style={{ paddingLeft: '1rem' }}>
<DataItem <DataItem icon={<IconApps />} title={'AI模型'} count={modelCount} />
icon={<IconUserGroup />} </Grid.Col>
title={'知识库'} </Grid.Row>
count={kbCount}
/> <Divider />
</Grid.Col> </Card>
</Space>
<Divider type="vertical" style={{ height: 40 }} /> </div>
</div>
<Grid.Col flex={1} style={{ paddingLeft: '1rem' }}> );
<DataItem });
icon={<IconApps />} Dashboard.displayName = 'Dashboard';
title={'AI模型'}
count={modelCount} const DashboardItem: React.FC<
/> React.PropsWithChildren<{
</Grid.Col> title: string;
</Grid.Row> href?: string;
}>
<Divider /> > = React.memo((props) => {
const { t } = useTranslation();
</Card> return (
</Space> <Card
</div> title={props.title}
</div> extra={
); props.href && (
}); <Link target="_blank" href={props.href}>
Dashboard.displayName = 'Dashboard'; {t('tushan.dashboard.more')}
</Link>
const DashboardItem: React.FC< )
React.PropsWithChildren<{ }
title: string; bordered={false}
href?: string; style={{ overflow: 'hidden' }}
}> >
> = React.memo((props) => { {props.children}
const { t } = useTranslation(); </Card>
);
return ( });
<Card DashboardItem.displayName = 'DashboardItem';
title={props.title}
extra={ const DataItem: React.FC<{
props.href && ( icon: React.ReactElement;
<Link target="_blank" href={props.href}> title: string;
{t('tushan.dashboard.more')} count: number;
</Link> }> = React.memo((props) => {
) return (
} <Space>
bordered={false} <div
style={{ overflow: 'hidden' }} style={{
> fontSize: 20,
{props.children} padding: '0.5rem',
</Card> borderRadius: '9999px',
); border: '1px solid #ccc',
}); width: 24,
DashboardItem.displayName = 'DashboardItem'; height: 24,
display: 'flex',
const DataItem: React.FC<{ justifyContent: 'center',
icon: React.ReactElement; alignItems: 'center'
title: string; }}
count: number; >
}> = React.memo((props) => { {props.icon}
return ( </div>
<Space> <div>
<div <div style={{ fontWeight: 700 }}>{props.title}</div>
style={{ <div>{props.count}</div>
fontSize: 20, </div>
padding: '0.5rem', </Space>
borderRadius: '9999px', );
border: '1px solid #ccc', });
width: 24, DataItem.displayName = 'DataItem';
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';

View File

@@ -1,5 +1,5 @@
import { createAuthProvider, type AuthProvider } from 'tushan'; import { createAuthProvider, type AuthProvider } from 'tushan';
export const authProvider: AuthProvider = createAuthProvider({ 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`
}); });

View File

@@ -1,13 +1,11 @@
import { import { createTextField, createNumberField } from 'tushan';
createTextField,
createNumberField,
} from 'tushan';
export const userFields = [ export const userFields = [
createTextField('id', { label: 'ID' }), createTextField('id', { label: 'ID' }),
createTextField('username', { label: '用户名' }), createTextField('username', { label: '用户名' }),
createNumberField('balance', { label: '余额', list: { sort: true } }), 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 = [ export const payFields = [