use new frontend
35
admin-frontend/.eslintrc.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"airbnb"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-filename-extension": [ "warn", {"extensions": [".tsx"]} ],
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": ["error"],
|
||||
"import/extensions": ["error", "ignorePackages", {"ts": "never", "tsx": "never"}],
|
||||
"no-param-reassign": ["error", { "props": false }]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) TS template.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
|
@ -1,30 +1,26 @@
|
||||
{
|
||||
"name": "admin-frontend",
|
||||
"name": "nonebot-bison-admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "bison",
|
||||
"proxy": "http://127.0.0.1:8080",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.6.4",
|
||||
"@reduxjs/toolkit": "^1.7.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"antd": "^4.16.13",
|
||||
"axios": "^0.21.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.0.1",
|
||||
"@testing-library/user-event": "^14.1.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.25",
|
||||
"@types/react": "^18.0.6",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"eslint": "^7.32.0 || ^8.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.6.0",
|
||||
"web-vitals": "^2.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@ -51,10 +47,15 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.175",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"@types/react-router-dom": "^5.3.0",
|
||||
"react-app-rewired": "^2.1.8",
|
||||
"redux-devtools": "^3.7.0"
|
||||
"@typescript-eslint/eslint-plugin": "^5.31.0",
|
||||
"@typescript-eslint/parser": "^5.31.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.3.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-react-redux": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>React Redux App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 12 KiB |
@ -9,30 +9,31 @@
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
animation: App-logo-float infinite 3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
color: rgb(112, 76, 182);
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@keyframes App-logo-float {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
50% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './app/store';
|
||||
import App from './App';
|
||||
|
||||
test("renders learn react link", () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(getByText(/learn/i)).toBeInTheDocument();
|
||||
});
|
||||
|
@ -1,43 +1,31 @@
|
||||
import "antd/dist/antd.css";
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
|
||||
import "./App.css";
|
||||
import { Admin } from "./pages/admin";
|
||||
import { Auth } from "./pages/auth";
|
||||
import { getGlobalConf } from "./store/globalConfSlice";
|
||||
import { useAppSelector } from "./store/hooks";
|
||||
import { loadLoginState, loginSelector } from "./store/loginSlice";
|
||||
|
||||
function LoginSwitch() {
|
||||
const login = useSelector(loginSelector);
|
||||
if (login.login) {
|
||||
return <Admin />;
|
||||
} else {
|
||||
return <div>not login</div>;
|
||||
}
|
||||
}
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import { useAppDispatch, useAppSelector } from './app/hooks';
|
||||
import Auth from './features/auth/Auth';
|
||||
import { loadGlobalConf, selectGlobalConfLoaded } from './features/globalConf/globalConfSlice';
|
||||
import Home from './pages/Home';
|
||||
import Unauthed from './pages/Unauthed';
|
||||
|
||||
function App() {
|
||||
const dispatch = useDispatch();
|
||||
const globalConf = useAppSelector((state) => state.globalConf);
|
||||
const dispatch = useAppDispatch();
|
||||
const globalConfLoaded = useAppSelector(selectGlobalConfLoaded);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getGlobalConf());
|
||||
dispatch(loadLoginState());
|
||||
}, [dispatch]);
|
||||
if (!globalConfLoaded) {
|
||||
dispatch(loadGlobalConf());
|
||||
}
|
||||
}, [globalConfLoaded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{globalConf.loaded && (
|
||||
<Router basename="/bison">
|
||||
<Switch>
|
||||
<Route path="/auth/:code">
|
||||
<Auth />
|
||||
</Route>
|
||||
<Route path="/admin/">
|
||||
<LoginSwitch />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
{ globalConfLoaded
|
||||
&& (
|
||||
<Routes>
|
||||
<Route path="/auth/:code" element={<Auth />} />
|
||||
<Route path="/unauthed" element={<Unauthed />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
</Routes>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -1,63 +0,0 @@
|
||||
import axios from "axios";
|
||||
import {
|
||||
GlobalConf,
|
||||
TokenResp,
|
||||
SubscribeResp,
|
||||
TargetNameResp,
|
||||
SubscribeConfig,
|
||||
} from "../utils/type";
|
||||
import { baseUrl } from "./utils";
|
||||
|
||||
export async function getGlobalConf(): Promise<GlobalConf> {
|
||||
const res = await axios.get<GlobalConf>(`${baseUrl}global_conf`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function auth(token: string): Promise<TokenResp> {
|
||||
const res = await axios.get<TokenResp>(`${baseUrl}auth`, {
|
||||
params: { token },
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getSubscribe(): Promise<SubscribeResp> {
|
||||
const res = await axios.get(`${baseUrl}subs`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function getTargetName(
|
||||
platformName: string,
|
||||
target: string
|
||||
): Promise<TargetNameResp> {
|
||||
const res = await axios.get(`${baseUrl}target_name`, {
|
||||
params: { platformName, target },
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function addSubscribe(groupNumber: string, req: SubscribeConfig) {
|
||||
const res = await axios.post(`${baseUrl}subs`, req, {
|
||||
params: { groupNumber },
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function delSubscribe(
|
||||
groupNumber: string,
|
||||
platformName: string,
|
||||
target: string
|
||||
) {
|
||||
const res = await axios.delete(`${baseUrl}subs`, {
|
||||
params: { groupNumber, platformName, target },
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updateSubscribe(
|
||||
groupNumber: string,
|
||||
req: SubscribeConfig
|
||||
) {
|
||||
return axios
|
||||
.patch(`${baseUrl}subs`, req, { params: { groupNumber } })
|
||||
.then((res) => res.data);
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { Store } from "src/store";
|
||||
import { clearLoginStatus } from "src/store/loginSlice";
|
||||
// import { useContext } from 'react';
|
||||
// import { LoginContext } from "../utils/context";
|
||||
|
||||
export const baseUrl = "/bison/api/";
|
||||
let store: Store;
|
||||
export const injectStore = (_store: Store) => {
|
||||
store = _store;
|
||||
};
|
||||
|
||||
// const loginStatus = useContext(LoginContext);
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
if (
|
||||
config.url &&
|
||||
config.url.startsWith(baseUrl) &&
|
||||
config.url !== `${baseUrl}auth` &&
|
||||
config.url !== `${baseUrl}global_conf`
|
||||
) {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
} else {
|
||||
throw new axios.Cancel("User not login");
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
// const data = response.data;
|
||||
// const parseToMap = (item: any): any => {
|
||||
// if (item instanceof Array) {
|
||||
// return item.map(parseToMap);
|
||||
// } else if (item instanceof Object) {
|
||||
// let res = new Map();
|
||||
// for (const key of Object.keys(item)) {
|
||||
// res.set(key, parseToMap(item[key]));
|
||||
// }
|
||||
// return res;
|
||||
// } else {
|
||||
// return item;
|
||||
// }
|
||||
// }
|
||||
// response.data = parseToMap(data);
|
||||
return response;
|
||||
},
|
||||
function (error: AxiosError) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
store.dispatch(clearLoginStatus());
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
6
admin-frontend/src/app/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import type { RootState, AppDispatch } from './store';
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
22
admin-frontend/src/app/store.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
|
||||
import authReducer from '../features/auth/authSlice';
|
||||
import globalConfReducer from '../features/globalConf/globalConfSlice';
|
||||
import { subscribeApi } from '../features/subsribeConfigManager/subscribeConfigSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
globalConf: globalConfReducer,
|
||||
[subscribeApi.reducerPath]: subscribeApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(subscribeApi.middleware),
|
||||
});
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
Action<string>
|
||||
>;
|
@ -1,219 +0,0 @@
|
||||
import { Form, Input, Modal, Select, Tag } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { addSubscribe, getTargetName, updateSubscribe } from "src/api/config";
|
||||
import { InputTag } from "src/component/inputTag";
|
||||
import { platformConfSelector } from "src/store/globalConfSlice";
|
||||
import { CategoryConfig, SubscribeConfig } from "src/utils/type";
|
||||
|
||||
interface InputTagCustomProp {
|
||||
value?: Array<string>;
|
||||
onChange?: (value: Array<string>) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
function InputTagCustom(prop: InputTagCustomProp) {
|
||||
const [value, setValue] = useState(prop.value || []);
|
||||
const handleSetValue = (newVal: Array<string>) => {
|
||||
setValue(() => newVal);
|
||||
if (prop.onChange) {
|
||||
prop.onChange(newVal);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (prop.value) {
|
||||
setValue(prop.value);
|
||||
}
|
||||
}, [prop.value]);
|
||||
return (
|
||||
<>
|
||||
{prop.disabled ? (
|
||||
<Tag color="default">不支持标签</Tag>
|
||||
) : (
|
||||
<>
|
||||
{value.length === 0 && <Tag color="green">全部标签</Tag>}
|
||||
<InputTag
|
||||
color="blue"
|
||||
addText="添加标签"
|
||||
value={value}
|
||||
onChange={handleSetValue}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddModalProp {
|
||||
showModal: boolean;
|
||||
groupNumber: string;
|
||||
setShowModal: (s: boolean) => void;
|
||||
refresh: () => void;
|
||||
initVal?: SubscribeConfig;
|
||||
}
|
||||
export function AddModal({
|
||||
showModal,
|
||||
groupNumber,
|
||||
setShowModal,
|
||||
refresh,
|
||||
initVal,
|
||||
}: AddModalProp) {
|
||||
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||
const platformConf = useSelector(platformConfSelector);
|
||||
const [hasTarget, setHasTarget] = useState(false);
|
||||
const [categories, setCategories] = useState({} as CategoryConfig);
|
||||
const [enabledTag, setEnableTag] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [inited, setInited] = useState(false);
|
||||
const changePlatformSelect = (platform: string) => {
|
||||
setHasTarget((_) => platformConf[platform].hasTarget);
|
||||
setCategories((_) => platformConf[platform].categories);
|
||||
setEnableTag(platformConf[platform].enabledTag);
|
||||
if (!platformConf[platform].hasTarget) {
|
||||
getTargetName(platform, "default").then((res) => {
|
||||
console.log(res);
|
||||
form.setFieldsValue({
|
||||
targetName: res.targetName,
|
||||
target: "",
|
||||
});
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
targetName: "",
|
||||
target: "",
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleSubmit = (value: any) => {
|
||||
let newVal = Object.assign({}, value);
|
||||
if (typeof newVal.tags !== "object") {
|
||||
newVal.tags = [];
|
||||
}
|
||||
if (typeof newVal.cats !== "object") {
|
||||
newVal.cats = [];
|
||||
}
|
||||
if (newVal.target === "") {
|
||||
newVal.target = "default";
|
||||
}
|
||||
if (initVal) {
|
||||
// patch
|
||||
updateSubscribe(groupNumber, newVal).then(() => {
|
||||
setConfirmLoading(false);
|
||||
setShowModal(false);
|
||||
form.resetFields();
|
||||
refresh();
|
||||
});
|
||||
} else {
|
||||
addSubscribe(groupNumber, newVal).then(() => {
|
||||
setConfirmLoading(false);
|
||||
setShowModal(false);
|
||||
form.resetFields();
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleModleFinish = () => {
|
||||
form.submit();
|
||||
setConfirmLoading(() => true);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (initVal && !inited) {
|
||||
const platformName = initVal.platformName;
|
||||
setHasTarget(platformConf[platformName].hasTarget);
|
||||
setCategories(platformConf[platformName].categories);
|
||||
setEnableTag(platformConf[platformName].enabledTag);
|
||||
setInited(true);
|
||||
form.setFieldsValue(initVal);
|
||||
}
|
||||
}, [initVal, form, platformConf, inited]);
|
||||
return (
|
||||
<Modal
|
||||
title="添加订阅"
|
||||
visible={showModal}
|
||||
confirmLoading={confirmLoading}
|
||||
onCancel={() => setShowModal(false)}
|
||||
onOk={handleModleFinish}
|
||||
>
|
||||
<Form form={form} labelCol={{ span: 6 }} name="b" onFinish={handleSubmit}>
|
||||
<Form.Item label="平台" name="platformName" rules={[]}>
|
||||
<Select style={{ width: "80%" }} onChange={changePlatformSelect}>
|
||||
{Object.keys(platformConf).map((platformName) => (
|
||||
<Select.Option key={platformName} value={platformName}>
|
||||
{platformConf[platformName].name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="账号"
|
||||
name="target"
|
||||
rules={[
|
||||
{ required: hasTarget, message: "请输入账号" },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
try {
|
||||
const res = await getTargetName(
|
||||
form.getFieldValue("platformName"),
|
||||
value
|
||||
);
|
||||
if (res.targetName) {
|
||||
form.setFieldsValue({
|
||||
targetName: res.targetName,
|
||||
});
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
targetName: "",
|
||||
});
|
||||
return Promise.reject("账号不正确,请重新检查账号");
|
||||
}
|
||||
} catch {
|
||||
return Promise.reject("服务器错误,请稍后再试");
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={hasTarget ? "获取方式见文档" : "此平台不需要账号"}
|
||||
disabled={!hasTarget}
|
||||
style={{ width: "80%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="账号名称" name="targetName">
|
||||
<Input style={{ width: "80%" }} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="订阅分类"
|
||||
name="cats"
|
||||
rules={[
|
||||
{
|
||||
required: Object.keys(categories).length > 0,
|
||||
message: "请至少选择一个分类进行订阅",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
style={{ width: "80%" }}
|
||||
mode="multiple"
|
||||
disabled={Object.keys(categories).length === 0}
|
||||
placeholder={
|
||||
Object.keys(categories).length > 0
|
||||
? "请选择要订阅的分类"
|
||||
: "本平台不支持分类"
|
||||
}
|
||||
>
|
||||
{Object.keys(categories).length > 0 &&
|
||||
Object.keys(categories).map((indexStr) => (
|
||||
<Select.Option key={indexStr} value={parseInt(indexStr)}>
|
||||
{categories[parseInt(indexStr)]}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="订阅Tag" name="tags">
|
||||
<InputTagCustom disabled={!enabledTag} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
import { Input, Tag, Tooltip } from "antd";
|
||||
import { PresetColorType, PresetStatusColorType } from "antd/lib/_util/colors";
|
||||
import { LiteralUnion } from "antd/lib/_util/type";
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
|
||||
interface InputTagProp {
|
||||
value?: Array<string>;
|
||||
onChange?: (value: Array<string>) => void;
|
||||
color?: LiteralUnion<PresetColorType | PresetStatusColorType, string>;
|
||||
addText?: string;
|
||||
}
|
||||
export function InputTag(prop: InputTagProp) {
|
||||
const [value, setValue] = useState<Array<string>>(prop.value || []);
|
||||
const [inputVisible, setInputVisible] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [editInputIndex, setEditInputIndex] = useState(-1);
|
||||
const [editInputValue, setEditInputValue] = useState("");
|
||||
const inputRef = useRef(null as any);
|
||||
const editInputRef = useRef(null as any);
|
||||
useEffect(() => {
|
||||
if (prop.value) {
|
||||
setValue(prop.value);
|
||||
}
|
||||
}, [prop.value]);
|
||||
useEffect(() => {
|
||||
if (inputVisible) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputVisible]);
|
||||
useEffect(() => {
|
||||
if (editInputIndex !== -1) {
|
||||
editInputRef.current.focus();
|
||||
}
|
||||
}, [editInputIndex]);
|
||||
|
||||
const handleClose = (removedTag: string) => {
|
||||
const tags = value.filter((tag) => tag !== removedTag);
|
||||
setValue((_) => tags);
|
||||
if (prop.onChange) {
|
||||
prop.onChange(tags);
|
||||
}
|
||||
};
|
||||
|
||||
const showInput = () => {
|
||||
setInputVisible((_) => true);
|
||||
};
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue && value.indexOf(inputValue) === -1) {
|
||||
const newVal = [...value, inputValue];
|
||||
setValue((_) => newVal);
|
||||
if (prop.onChange) {
|
||||
prop.onChange(newVal);
|
||||
}
|
||||
}
|
||||
setInputVisible((_) => false);
|
||||
setInputValue((_) => "");
|
||||
};
|
||||
|
||||
const handleEditInputChange = (e: any) => {
|
||||
setEditInputValue((_) => e.target.value);
|
||||
};
|
||||
|
||||
const handleEditInputConfirm = () => {
|
||||
const newTags = value.slice();
|
||||
newTags[editInputIndex] = editInputValue;
|
||||
setValue((_) => newTags);
|
||||
if (prop.onChange) {
|
||||
prop.onChange(newTags);
|
||||
}
|
||||
setEditInputIndex((_) => -1);
|
||||
setEditInputValue((_) => "");
|
||||
};
|
||||
|
||||
const handleInputChange = (e: any) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value.map((tag, index) => {
|
||||
if (editInputIndex === index) {
|
||||
return (
|
||||
<Input
|
||||
ref={editInputRef}
|
||||
key={tag}
|
||||
size="small"
|
||||
value={editInputValue}
|
||||
onChange={handleEditInputChange}
|
||||
onBlur={handleEditInputConfirm}
|
||||
onPressEnter={handleInputConfirm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const isLongTag = tag.length > 20;
|
||||
const tagElem = (
|
||||
<Tag
|
||||
color={prop.color || "default"}
|
||||
style={{ userSelect: "none" }}
|
||||
key={tag}
|
||||
closable
|
||||
onClose={() => handleClose(tag)}
|
||||
>
|
||||
<span
|
||||
onDoubleClick={(e) => {
|
||||
setEditInputIndex((_) => index);
|
||||
setEditInputValue((_) => tag);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
return isLongTag ? (
|
||||
<Tooltip title={tag} key={tag}>
|
||||
{tagElem}
|
||||
</Tooltip>
|
||||
) : (
|
||||
tagElem
|
||||
);
|
||||
})}
|
||||
{inputVisible && (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
size="small"
|
||||
style={{ width: "78px", marginRight: "8px", verticalAlign: "top" }}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onPressEnter={handleInputConfirm}
|
||||
/>
|
||||
)}
|
||||
{!inputVisible && (
|
||||
<Tag
|
||||
className="site-tag-plus"
|
||||
onClick={showInput}
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "dashed thin",
|
||||
borderColor: "#bfbfbf",
|
||||
}}
|
||||
>
|
||||
<PlusOutlined /> {prop.addText || "Add Tag"}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
message,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import Modal from "antd/lib/modal/Modal";
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { addSubscribe, delSubscribe } from "src/api/config";
|
||||
import { platformConfSelector } from "src/store/globalConfSlice";
|
||||
import {
|
||||
groupConfigSelector,
|
||||
updateGroupSubs,
|
||||
} from "src/store/groupConfigSlice";
|
||||
import { PlatformConfig, SubscribeConfig, SubscribeResp } from "src/utils/type";
|
||||
import { AddModal } from "./addSubsModal";
|
||||
|
||||
interface CopyModalProp {
|
||||
setShowModal: (modalShow: boolean) => void;
|
||||
showModal: boolean;
|
||||
config: SubscribeConfig;
|
||||
groups: SubscribeResp;
|
||||
currentGroupNumber: string;
|
||||
reload: () => void;
|
||||
}
|
||||
function CopyModal({
|
||||
setShowModal,
|
||||
config,
|
||||
currentGroupNumber,
|
||||
groups,
|
||||
showModal,
|
||||
reload,
|
||||
}: CopyModalProp) {
|
||||
const [confirmLoading, setConfirmLoading] = useState(false);
|
||||
const [selectedGroups, setSelectGroups] = useState<Array<string>>([]);
|
||||
const postReqs = async (
|
||||
selectedGroups: Array<string>,
|
||||
config: SubscribeConfig
|
||||
) => {
|
||||
for (let selectedGroup of selectedGroups) {
|
||||
await addSubscribe(selectedGroup, config);
|
||||
}
|
||||
};
|
||||
const handleOk = () => {
|
||||
if (selectedGroups.length === 0) {
|
||||
message.error("请至少选择一个目标群");
|
||||
} else {
|
||||
setConfirmLoading(true);
|
||||
postReqs(selectedGroups, config).then(() => {
|
||||
setConfirmLoading(false);
|
||||
setShowModal(false);
|
||||
return reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title="复制订阅"
|
||||
visible={showModal}
|
||||
confirmLoading={confirmLoading}
|
||||
onCancel={() => setShowModal(false)}
|
||||
onOk={handleOk}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
onChange={(value: Array<string>) => setSelectGroups(value)}
|
||||
style={{ width: "80%" }}
|
||||
>
|
||||
{Object.keys(groups)
|
||||
.filter((groupNumber) => groupNumber !== currentGroupNumber)
|
||||
.map((groupNumber) => (
|
||||
<Select.Option value={groupNumber} key={groupNumber}>
|
||||
{`${groupNumber} - ${groups[groupNumber].name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
interface SubscribeCardProp {
|
||||
groupNumber: string;
|
||||
config: SubscribeConfig;
|
||||
}
|
||||
export function SubscribeCard({ groupNumber, config }: SubscribeCardProp) {
|
||||
const platformConfs = useSelector(platformConfSelector);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const platformConf = platformConfs[config.platformName] as PlatformConfig;
|
||||
const dispatcher = useDispatch();
|
||||
const groupSubscribes = useSelector(groupConfigSelector);
|
||||
const reload = () => dispatcher(updateGroupSubs());
|
||||
const handleDelete =
|
||||
(groupNumber: string, platformName: string, target: string) => () => {
|
||||
delSubscribe(groupNumber, platformName, target).then(() => {
|
||||
reload();
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Col span={8} key={`${config.platformName}-${config.target}`}>
|
||||
<Card
|
||||
title={`${platformConf.name} - ${config.targetName}`}
|
||||
actions={[
|
||||
<Tooltip title="编辑">
|
||||
<EditOutlined
|
||||
onClick={() => {
|
||||
setShowEditModal((state) => !state);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="添加到其他群">
|
||||
<CopyOutlined
|
||||
onClick={() => {
|
||||
setShowModal((state) => !state);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Popconfirm
|
||||
title={`确定要删除 ${platformConf.name} - ${config.targetName}`}
|
||||
onConfirm={handleDelete(
|
||||
groupNumber,
|
||||
config.platformName,
|
||||
config.target || "default"
|
||||
)}
|
||||
>
|
||||
<Tooltip title="删除">
|
||||
<DeleteOutlined />
|
||||
</Tooltip>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<Form labelCol={{ span: 4 }}>
|
||||
<Form.Item label="订阅帐号">
|
||||
{platformConf.hasTarget ? (
|
||||
config.target
|
||||
) : (
|
||||
<Tag color="default">无帐号</Tag>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="订阅类型">
|
||||
{Object.keys(platformConf.categories).length > 0 ? (
|
||||
config.cats.map((catKey: number) => (
|
||||
<Tag color="green" key={catKey}>
|
||||
{platformConf.categories[catKey]}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Tag color="default">不支持类型</Tag>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item label="订阅Tag">
|
||||
{platformConf.enabledTag ? (
|
||||
config.tags.length > 0 ? (
|
||||
config.tags.map((tag) => (
|
||||
<Tag color="green" key={tag}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Tag color="blue">全部标签</Tag>
|
||||
)
|
||||
) : (
|
||||
<Tag color="default">不支持Tag</Tag>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
<CopyModal
|
||||
setShowModal={setShowModal}
|
||||
reload={reload}
|
||||
currentGroupNumber={groupNumber}
|
||||
showModal={showModal}
|
||||
config={config}
|
||||
groups={groupSubscribes}
|
||||
/>
|
||||
<AddModal
|
||||
showModal={showEditModal}
|
||||
setShowModal={setShowEditModal}
|
||||
groupNumber={groupNumber}
|
||||
refresh={reload}
|
||||
initVal={config}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
24
admin-frontend/src/features/auth/Auth.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { login, selectIsFailed, selectIsLogin } from './authSlice';
|
||||
|
||||
export default function Auth() {
|
||||
const isLogin = useAppSelector(selectIsLogin);
|
||||
const dispatch = useAppDispatch();
|
||||
const { code } = useParams();
|
||||
const isFailed = useAppSelector(selectIsFailed);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLogin && code) {
|
||||
dispatch(login(code));
|
||||
}
|
||||
}, [isLogin, code]);
|
||||
return (
|
||||
{ isLogin
|
||||
? <Navigate to="/home" />
|
||||
: isFailed
|
||||
? <Navigate to="/unauthed" />
|
||||
: <div> login </div>}
|
||||
);
|
||||
}
|
33
admin-frontend/src/features/auth/authQuery.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {
|
||||
BaseQueryFn, FetchArgs, fetchBaseQuery, FetchBaseQueryError,
|
||||
} from '@reduxjs/toolkit/dist/query';
|
||||
import { RootState } from '../../app/store';
|
||||
import { baseUrl } from '../../utils/urls';
|
||||
import { setLogout } from './authSlice';
|
||||
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl,
|
||||
prepareHeaders: (headers, { getState }) => {
|
||||
const { token } = (getState() as RootState).auth;
|
||||
|
||||
if (token) {
|
||||
headers.set('authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
export const baseQueryWithAuth: BaseQueryFn<
|
||||
string | FetchArgs,
|
||||
unknown,
|
||||
FetchBaseQueryError
|
||||
> = async (args, api, extraOptions) => {
|
||||
const result = await baseQuery(args, api, extraOptions);
|
||||
if (result.error && result.error.status === 401) {
|
||||
api.dispatch(setLogout());
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default baseQueryWithAuth;
|
70
admin-frontend/src/features/auth/authSlice.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import {
|
||||
CaseReducer, createAsyncThunk, createSlice, PayloadAction,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../app/store';
|
||||
import { TokenResp } from '../../utils/type';
|
||||
import { authUrl } from '../../utils/urls';
|
||||
|
||||
export interface AuthStatus {
|
||||
login: boolean;
|
||||
token: string;
|
||||
failed: boolean;
|
||||
userType: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
login: false,
|
||||
failed: false,
|
||||
token: '',
|
||||
userType: '',
|
||||
id: 0,
|
||||
} as AuthStatus;
|
||||
|
||||
export const login = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (code: string) => {
|
||||
const res = await fetch(`${authUrl}?token=${code}`);
|
||||
return (await res.json()) as TokenResp;
|
||||
},
|
||||
);
|
||||
|
||||
const setLoginReducer: CaseReducer<AuthStatus, PayloadAction<TokenResp>> = (state, action) => {
|
||||
if (action.payload.status === 200) {
|
||||
state.login = true;
|
||||
state.id = action.payload.id;
|
||||
state.userType = action.payload.type;
|
||||
state.token = action.payload.token;
|
||||
} else {
|
||||
state.login = false;
|
||||
state.failed = true;
|
||||
}
|
||||
};
|
||||
|
||||
export const setLogoutReducer: CaseReducer<AuthStatus> = (state) => {
|
||||
state.login = false;
|
||||
};
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setLogin: setLoginReducer,
|
||||
setLogout: setLogoutReducer,
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(login.pending, (state) => {
|
||||
state.failed = false;
|
||||
})
|
||||
.addCase(login.fulfilled, setLoginReducer)
|
||||
.addCase(login.rejected, setLogoutReducer);
|
||||
},
|
||||
});
|
||||
|
||||
export const { setLogin, setLogout } = authSlice.actions;
|
||||
|
||||
export const selectIsLogin = (state: RootState) => state.auth.login;
|
||||
export const selectIsFailed = (state: RootState) => state.auth.failed;
|
||||
|
||||
export default authSlice.reducer;
|
35
admin-frontend/src/features/globalConf/globalConfSlice.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../app/store';
|
||||
import { GlobalConf } from '../../utils/type';
|
||||
import { globalConfUrl } from '../../utils/urls';
|
||||
|
||||
const initialState = {
|
||||
loaded: false,
|
||||
platformConf: {},
|
||||
} as GlobalConf;
|
||||
|
||||
export const loadGlobalConf = createAsyncThunk(
|
||||
'globalConf/load',
|
||||
async () => {
|
||||
const res = await fetch(globalConfUrl);
|
||||
return (await res.json()) as GlobalConf;
|
||||
},
|
||||
);
|
||||
|
||||
export const globalConfSlice = createSlice({
|
||||
name: 'globalConf',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(loadGlobalConf.fulfilled, (state, payload) => {
|
||||
state.platformConf = payload.payload.platformConf;
|
||||
state.loaded = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default globalConfSlice.reducer;
|
||||
|
||||
export const selectGlobalConfLoaded = (state: RootState) => state.globalConf.loaded;
|
||||
export const selectPlatformConf = (state: RootState) => state.globalConf.platformConf;
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { SubscribeResp } from '../../utils/type';
|
||||
import { useGetSubsQuery } from './subscribeConfigSlice';
|
||||
|
||||
export function SubscribeManager() {
|
||||
const {
|
||||
data: subs,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
} = useGetSubsQuery();
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { StatusResp, SubscribeResp, SubscribeConfig } from '../../utils/type';
|
||||
import { subsribeUrl } from '../../utils/urls';
|
||||
import { baseQueryWithAuth } from '../auth/authQuery';
|
||||
|
||||
export const subscribeApi = createApi({
|
||||
reducerPath: 'subscribe',
|
||||
baseQuery: baseQueryWithAuth,
|
||||
tagTypes: ['Subscribe'],
|
||||
endpoints: (builder) => ({
|
||||
getSubs: builder.query<SubscribeResp, void>({
|
||||
query: () => '/subs',
|
||||
providesTags: ['Subscribe'],
|
||||
}),
|
||||
newSub: builder.mutation<StatusResp, SubscribeConfig>({
|
||||
query: (config) => ({
|
||||
method: 'POST',
|
||||
url: '/subs',
|
||||
body: config,
|
||||
}),
|
||||
invalidatesTags: ['Subscribe'],
|
||||
}),
|
||||
updateSub: builder.mutation<StatusResp, SubscribeResp>({
|
||||
query: (config) => ({
|
||||
method: 'PATCH',
|
||||
url: '/subs',
|
||||
body: config,
|
||||
}),
|
||||
invalidatesTags: ['Subscribe'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetSubsQuery } = subscribeApi;
|
@ -1,20 +1,23 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import store from "./store";
|
||||
import { injectStore } from "src/api/utils";
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import { store } from './app/store';
|
||||
import './index.css';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
injectStore(store);
|
||||
ReactDOM.render(
|
||||
const container = document.getElementById('root')!;
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
<BrowserRouter basename="/bison">
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="#764ABC"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.1 KiB |
11
admin-frontend/src/pages/Home.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { SubscribeManager } from '../features/subsribeConfigManager/SubscribeManager';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<div>hoem</div>
|
||||
<SubscribeManager />
|
||||
</>
|
||||
);
|
||||
}
|
7
admin-frontend/src/pages/Unauthed.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Unauthed() {
|
||||
return (
|
||||
<div>not login</div>
|
||||
);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
.layout-side .user {
|
||||
height: 32px;
|
||||
margin: 16px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import { Button, Collapse, Empty, Row } from "antd";
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AddModal } from "src/component/addSubsModal";
|
||||
import { SubscribeCard } from "src/component/subscribeCard";
|
||||
import {
|
||||
groupConfigSelector,
|
||||
updateGroupSubs,
|
||||
} from "src/store/groupConfigSlice";
|
||||
|
||||
interface ConfigPageProp {
|
||||
tab: string;
|
||||
}
|
||||
export function ConfigPage(prop: ConfigPageProp) {
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
const [currentAddingGroupNumber, setCurrentAddingGroupNumber] = useState("");
|
||||
const configData = useSelector(groupConfigSelector);
|
||||
const dispatcher = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatcher(updateGroupSubs());
|
||||
}, [prop.tab, dispatcher]);
|
||||
const clickNew =
|
||||
(groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setShowModal((_) => true);
|
||||
setCurrentAddingGroupNumber(groupNumber);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
if (Object.keys(configData).length === 0) {
|
||||
return <Empty />;
|
||||
} else {
|
||||
let groups: Array<ReactElement> = [];
|
||||
for (let key of Object.keys(configData)) {
|
||||
let value = configData[key];
|
||||
groups.push(
|
||||
<Collapse.Panel
|
||||
key={key}
|
||||
header={
|
||||
<span>
|
||||
{`${key} - ${value.name}`}
|
||||
<Button style={{ float: "right" }} onClick={clickNew(key)}>
|
||||
添加
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Row
|
||||
gutter={[
|
||||
{ xs: 8, sm: 16, md: 24, lg: 32 },
|
||||
{ xs: 8, sm: 16, md: 24, lg: 32 },
|
||||
]}
|
||||
align="middle"
|
||||
>
|
||||
{value.subscribes.map((subs, idx) => (
|
||||
<SubscribeCard key={idx} groupNumber={key} config={subs} />
|
||||
))}
|
||||
</Row>
|
||||
</Collapse.Panel>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Collapse>{groups}</Collapse>
|
||||
<AddModal
|
||||
groupNumber={currentAddingGroupNumber}
|
||||
showModal={showModal}
|
||||
refresh={() => dispatcher(updateGroupSubs())}
|
||||
setShowModal={(s: boolean) => setShowModal((_) => s)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { BugOutlined, SettingOutlined } from "@ant-design/icons";
|
||||
import { Layout, Menu } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { loginSelector } from "src/store/loginSlice";
|
||||
import "./admin.css";
|
||||
import { ConfigPage } from "./configPage";
|
||||
|
||||
export function Admin() {
|
||||
const login = useSelector(loginSelector);
|
||||
const [tab, changeTab] = useState("manage");
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Layout.Sider className="layout-side">
|
||||
<div className="user"></div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
defaultSelectedKeys={[tab]}
|
||||
onClick={({ key }) => changeTab(key)}
|
||||
>
|
||||
<Menu.Item key="manage" icon={<SettingOutlined />}>
|
||||
订阅管理
|
||||
</Menu.Item>
|
||||
{login.type === "admin" && (
|
||||
<Menu.Item key="log" icon={<BugOutlined />}>
|
||||
查看日志
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
</Layout.Sider>
|
||||
<Layout.Content>
|
||||
<div style={{ margin: "24px", background: "#fff", minHeight: "640px" }}>
|
||||
{tab === "manage" ? <ConfigPage tab={tab} /> : null}
|
||||
</div>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
);
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useParams } from "react-router";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { login, loginSelector } from "src/store/loginSlice";
|
||||
|
||||
interface AuthParam {
|
||||
code: string;
|
||||
}
|
||||
export function Auth() {
|
||||
const { code } = useParams<AuthParam>();
|
||||
const dispatch = useDispatch();
|
||||
const loginState = useSelector(loginSelector);
|
||||
useEffect(() => {
|
||||
const loginFun = async () => {
|
||||
dispatch(login(code));
|
||||
};
|
||||
loginFun();
|
||||
}, [code, dispatch]);
|
||||
return (
|
||||
<>
|
||||
{loginState.login ? (
|
||||
<Redirect to={{ pathname: "/admin" }} />
|
||||
) : loginState.failed ? (
|
||||
<div>登录失败,请重新获取连接</div>
|
||||
) : (
|
||||
<div>Logining...</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
import { ReportHandler } from "web-vitals";
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
import('web-vitals').then(({
|
||||
getCLS, getFID, getFCP, getLCP, getTTFB,
|
||||
}) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
|
@ -2,4 +2,4 @@
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom";
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
|
@ -1,44 +0,0 @@
|
||||
import {
|
||||
CaseReducer,
|
||||
createAsyncThunk,
|
||||
createSlice,
|
||||
PayloadAction,
|
||||
} from "@reduxjs/toolkit";
|
||||
import { getGlobalConf as getGlobalConfApi } from "src/api/config";
|
||||
import { GlobalConf } from "src/utils/type";
|
||||
import { RootState } from ".";
|
||||
|
||||
const initialState: GlobalConf = {
|
||||
platformConf: {},
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
const setGlobalConf: CaseReducer<GlobalConf, PayloadAction<GlobalConf>> = (
|
||||
_,
|
||||
action
|
||||
) => {
|
||||
return { ...action.payload, loaded: true };
|
||||
};
|
||||
|
||||
export const getGlobalConf = createAsyncThunk(
|
||||
"globalConf/set",
|
||||
getGlobalConfApi,
|
||||
{
|
||||
condition: (_, { getState }) =>
|
||||
!(getState() as RootState).globalConf.loaded,
|
||||
}
|
||||
);
|
||||
|
||||
export const globalConfSlice = createSlice({
|
||||
name: "globalConf",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getGlobalConf.fulfilled, setGlobalConf);
|
||||
},
|
||||
});
|
||||
|
||||
export const platformConfSelector = (state: RootState) =>
|
||||
state.globalConf.platformConf;
|
||||
|
||||
export default globalConfSlice.reducer;
|
@ -1,36 +0,0 @@
|
||||
import {
|
||||
CaseReducer,
|
||||
createAsyncThunk,
|
||||
createSlice,
|
||||
PayloadAction,
|
||||
} from "@reduxjs/toolkit";
|
||||
import { SubscribeResp } from "src/utils/type";
|
||||
import { getSubscribe } from "src/api/config";
|
||||
import { RootState } from ".";
|
||||
const initialState: SubscribeResp = {};
|
||||
|
||||
const setSubs: CaseReducer<SubscribeResp, PayloadAction<SubscribeResp>> = (
|
||||
_,
|
||||
action
|
||||
) => {
|
||||
return action.payload;
|
||||
};
|
||||
|
||||
export const updateGroupSubs = createAsyncThunk(
|
||||
"groupConfig/update",
|
||||
getSubscribe
|
||||
);
|
||||
|
||||
export const groupConfigSlice = createSlice({
|
||||
name: "groupConfig",
|
||||
initialState,
|
||||
reducers: {
|
||||
setSubs,
|
||||
},
|
||||
extraReducers: (reducer) => {
|
||||
reducer.addCase(updateGroupSubs.fulfilled, setSubs);
|
||||
},
|
||||
});
|
||||
|
||||
export const groupConfigSelector = (state: RootState) => state.groupConfig;
|
||||
export default groupConfigSlice.reducer;
|
@ -1,5 +0,0 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from ".";
|
||||
|
||||
export const useAppDispacher = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
@ -1,18 +0,0 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import loginSlice from "./loginSlice";
|
||||
import globalConfSlice from "./globalConfSlice";
|
||||
import groupConfigSlice from "./groupConfigSlice";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
login: loginSlice,
|
||||
globalConf: globalConfSlice,
|
||||
groupConfig: groupConfigSlice,
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type Store = typeof store;
|
@ -1,121 +0,0 @@
|
||||
import {
|
||||
AnyAction,
|
||||
CaseReducer,
|
||||
createAsyncThunk,
|
||||
createSlice,
|
||||
PayloadAction,
|
||||
ThunkAction,
|
||||
} from "@reduxjs/toolkit";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import { LoginStatus, TokenResp } from "src/utils/type";
|
||||
import { auth } from "src/api/config";
|
||||
import { RootState } from ".";
|
||||
|
||||
const initialState: LoginStatus = {
|
||||
login: false,
|
||||
type: "",
|
||||
name: "",
|
||||
id: "123",
|
||||
// groups: [],
|
||||
token: "",
|
||||
failed: false,
|
||||
};
|
||||
|
||||
interface storedInfo {
|
||||
type: string;
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const loginAction: CaseReducer<LoginStatus, PayloadAction<TokenResp>> = (
|
||||
_,
|
||||
action
|
||||
) => {
|
||||
return {
|
||||
login: true,
|
||||
failed: false,
|
||||
type: action.payload.type,
|
||||
name: action.payload.name,
|
||||
id: action.payload.id,
|
||||
token: action.payload.token,
|
||||
};
|
||||
};
|
||||
|
||||
export const login = createAsyncThunk(
|
||||
"auth/login",
|
||||
async (code: string) => {
|
||||
let res = await auth(code);
|
||||
if (res.status !== 200) {
|
||||
throw Error("Login Error");
|
||||
} else {
|
||||
localStorage.setItem(
|
||||
"loginInfo",
|
||||
JSON.stringify({
|
||||
type: res.type,
|
||||
name: res.name,
|
||||
id: res.id,
|
||||
})
|
||||
);
|
||||
localStorage.setItem("token", res.token);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
{
|
||||
condition: (_: string, { getState }) => {
|
||||
const { login } = getState() as { login: LoginStatus };
|
||||
return !login.login;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const loginSlice = createSlice({
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
doLogin: loginAction,
|
||||
doClearLogin: (state) => {
|
||||
state.login = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(login.fulfilled, loginAction);
|
||||
builder.addCase(login.rejected, (stat) => {
|
||||
stat.failed = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { doLogin, doClearLogin } = loginSlice.actions;
|
||||
|
||||
export const loadLoginState =
|
||||
(): ThunkAction<void, RootState, unknown, AnyAction> =>
|
||||
(dispatch, getState) => {
|
||||
if (getState().login.login) {
|
||||
return;
|
||||
}
|
||||
const infoJson = localStorage.getItem("loginInfo");
|
||||
const jwtToken = localStorage.getItem("token");
|
||||
if (infoJson && jwtToken) {
|
||||
const decodedJwt = jwt_decode(jwtToken) as { exp: number };
|
||||
if (decodedJwt.exp < Date.now() / 1000) {
|
||||
return;
|
||||
}
|
||||
const info = JSON.parse(infoJson) as storedInfo;
|
||||
const payload: TokenResp = {
|
||||
...info,
|
||||
status: 200,
|
||||
token: jwtToken,
|
||||
};
|
||||
dispatch(doLogin(payload));
|
||||
}
|
||||
};
|
||||
|
||||
export const clearLoginStatus =
|
||||
(): ThunkAction<void, RootState, unknown, AnyAction> => (dispatch) => {
|
||||
localStorage.removeItem("loginInfo");
|
||||
localStorage.removeItem("token");
|
||||
dispatch(doClearLogin());
|
||||
};
|
||||
export const loginSelector = (state: RootState) => state.login;
|
||||
|
||||
export default loginSlice.reducer;
|
@ -1,31 +1,10 @@
|
||||
interface QQGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LoginStatus {
|
||||
login: boolean;
|
||||
type: string;
|
||||
name: string;
|
||||
id: string;
|
||||
// groups: Array<QQGroup>
|
||||
export interface TokenResp {
|
||||
status: number;
|
||||
token: string;
|
||||
failed: boolean;
|
||||
type: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type LoginContextType = {
|
||||
login: LoginStatus;
|
||||
save: (status: LoginStatus) => void;
|
||||
};
|
||||
|
||||
export interface SubscribeConfig {
|
||||
platformName: string;
|
||||
target: string;
|
||||
targetName: string;
|
||||
cats: Array<number>;
|
||||
tags: Array<string>;
|
||||
}
|
||||
|
||||
export interface GlobalConf {
|
||||
platformConf: AllPlatformConf;
|
||||
loaded: boolean;
|
||||
@ -47,12 +26,12 @@ export interface PlatformConfig {
|
||||
hasTarget: boolean;
|
||||
}
|
||||
|
||||
export interface TokenResp {
|
||||
status: number;
|
||||
token: string;
|
||||
type: string;
|
||||
id: string;
|
||||
name: string;
|
||||
export interface SubscribeConfig {
|
||||
platformName: string;
|
||||
target: string;
|
||||
targetName: string;
|
||||
cats: Array<number>;
|
||||
tags: Array<string>;
|
||||
}
|
||||
|
||||
export interface SubscribeGroupDetail {
|
||||
@ -64,6 +43,7 @@ export interface SubscribeResp {
|
||||
[idx: string]: SubscribeGroupDetail;
|
||||
}
|
||||
|
||||
export interface TargetNameResp {
|
||||
targetName: string;
|
||||
export interface StatusResp {
|
||||
status: number;
|
||||
msg: string;
|
||||
}
|
||||
|
4
admin-frontend/src/utils/urls.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const baseUrl = '/bison/api/';
|
||||
export const authUrl = `${baseUrl}auth`;
|
||||
export const globalConfUrl = `${baseUrl}global_conf`;
|
||||
export const subsribeUrl = `${baseUrl}subs`;
|
@ -18,8 +18,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "./"
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
@ -3,6 +3,7 @@ from nonebot.adapters.onebot.v11.bot import Bot
|
||||
|
||||
from ..config import NoSuchSubscribeException, NoSuchUserException, config
|
||||
from ..platform import check_sub_target, platform_manager
|
||||
from ..types import Target as T_Target
|
||||
from .jwt import pack_jwt
|
||||
from .token_manager import token_manager
|
||||
|
||||
@ -45,7 +46,7 @@ async def auth(token: str):
|
||||
groups = await bot.call_api("get_group_list")
|
||||
if str(qq) in nonebot.get_driver().config.superusers:
|
||||
jwt_obj = {
|
||||
"id": str(qq),
|
||||
"id": qq,
|
||||
"groups": list(
|
||||
map(
|
||||
lambda info: {
|
||||
@ -59,7 +60,7 @@ async def auth(token: str):
|
||||
ret_obj = {
|
||||
"type": "admin",
|
||||
"name": nickname,
|
||||
"id": str(qq),
|
||||
"id": qq,
|
||||
"token": pack_jwt(jwt_obj),
|
||||
}
|
||||
return {"status": 200, **ret_obj}
|
||||
@ -68,14 +69,14 @@ async def auth(token: str):
|
||||
ret_obj = {
|
||||
"type": "user",
|
||||
"name": nickname,
|
||||
"id": str(qq),
|
||||
"id": qq,
|
||||
"token": pack_jwt(jwt_obj),
|
||||
}
|
||||
return {"status": 200, **ret_obj}
|
||||
else:
|
||||
return {"status": 400, "type": "", "name": "", "id": "", "token": ""}
|
||||
return {"status": 400, "type": "", "name": "", "id": 0, "token": ""}
|
||||
else:
|
||||
return {"status": 400, "type": "", "name": "", "id": "", "token": ""}
|
||||
return {"status": 400, "type": "", "name": "", "id": 0, "token": ""}
|
||||
|
||||
|
||||
async def get_subs_info(jwt_obj: dict):
|
||||
@ -83,18 +84,16 @@ async def get_subs_info(jwt_obj: dict):
|
||||
res = {}
|
||||
for group in groups:
|
||||
group_id = group["id"]
|
||||
raw_subs = await config.list_subscribe(group_id, "group")
|
||||
subs = list(
|
||||
map(
|
||||
lambda sub: {
|
||||
"platformName": sub["target_type"],
|
||||
"target": sub["target"],
|
||||
"targetName": sub["target_name"],
|
||||
"cats": sub["cats"],
|
||||
"tags": sub["tags"],
|
||||
},
|
||||
config.list_subscribe(group_id, "group"),
|
||||
)
|
||||
)
|
||||
map(lambda sub: {
|
||||
"platformName": sub.target.platform_name,
|
||||
"targetName": sub.target.name,
|
||||
"cats": sub.categories,
|
||||
"tags": sub.tags
|
||||
},
|
||||
raw_subs)
|
||||
)
|
||||
res[group_id] = {"name": group["name"], "subscribes": subs}
|
||||
return res
|
||||
|
||||
@ -111,15 +110,15 @@ async def add_group_sub(
|
||||
cats: list[int],
|
||||
tags: list[str],
|
||||
):
|
||||
config.add_subscribe(
|
||||
int(group_number), "group", target, target_name, platform_name, cats, tags
|
||||
)
|
||||
await config.add_subscribe(
|
||||
int(group_number), "group", T_Target(target), target_name, platform_name, cats, tags
|
||||
)
|
||||
return {"status": 200, "msg": ""}
|
||||
|
||||
|
||||
async def del_group_sub(group_number: str, platform_name: str, target: str):
|
||||
try:
|
||||
config.del_subscribe(int(group_number), "group", target, platform_name)
|
||||
await config.del_subscribe(int(group_number), "group", target, platform_name)
|
||||
except (NoSuchUserException, NoSuchSubscribeException):
|
||||
return {"status": 400, "msg": "删除错误"}
|
||||
return {"status": 200, "msg": ""}
|
||||
@ -134,7 +133,7 @@ async def update_group_sub(
|
||||
tags: list[str],
|
||||
):
|
||||
try:
|
||||
config.update_subscribe(
|
||||
await config.update_subscribe(
|
||||
int(group_number), "group", target, target_name, platform_name, cats, tags
|
||||
)
|
||||
except (NoSuchUserException, NoSuchSubscribeException):
|
||||
|