format frontend code

This commit is contained in:
felinae98 2022-02-12 10:35:35 +08:00
parent 9055a039a8
commit 649c1cf8f2
No known key found for this signature in database
GPG Key ID: 00C8B010587FF610
20 changed files with 825 additions and 592 deletions

View File

@ -20,4 +20,4 @@ repos:
rev: v2.5.1 rev: v2.5.1
hooks: hooks:
- id: prettier - id: prettier
types_or: [markdown] types_or: [markdown, ts, tsx]

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from "react";
import { render, screen } from '@testing-library/react'; import { render, screen } from "@testing-library/react";
import App from './App'; import App from "./App";
test('renders learn react link', () => { test("renders learn react link", () => {
render(<App />); render(<App />);
const linkElement = screen.getByText(/learn react/i); const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();

View File

@ -1,49 +1,46 @@
import 'antd/dist/antd.css'; import "antd/dist/antd.css";
import React, {useEffect} from 'react'; import React, { useEffect } from "react";
import {useDispatch, useSelector} from 'react-redux'; import { useDispatch, useSelector } from "react-redux";
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'; import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import './App.css'; import "./App.css";
import {Admin} from './pages/admin'; import { Admin } from "./pages/admin";
import {Auth} from './pages/auth'; import { Auth } from "./pages/auth";
import {getGlobalConf} from './store/globalConfSlice'; import { getGlobalConf } from "./store/globalConfSlice";
import {useAppSelector} from './store/hooks'; import { useAppSelector } from "./store/hooks";
import {loadLoginState, loginSelector} from './store/loginSlice'; import { loadLoginState, loginSelector } from "./store/loginSlice";
function LoginSwitch() { function LoginSwitch() {
const login = useSelector(loginSelector) const login = useSelector(loginSelector);
if (login.login) { if (login.login) {
return <Admin />; return <Admin />;
} else { } else {
return ( return <div>not login</div>;
<div>
not login
</div>
)
} }
} }
function App() { function App() {
const dispatch = useDispatch() const dispatch = useDispatch();
const globalConf = useAppSelector(state => state.globalConf) const globalConf = useAppSelector((state) => state.globalConf);
useEffect(() => { useEffect(() => {
dispatch(getGlobalConf()); dispatch(getGlobalConf());
dispatch(loadLoginState()) dispatch(loadLoginState());
}, [dispatch]); }, [dispatch]);
return <> return (
{ globalConf.loaded && <>
{globalConf.loaded && (
<Router basename="/bison"> <Router basename="/bison">
<Switch> <Switch>
<Route path="/auth/:code"> <Route path="/auth/:code">
<Auth /> <Auth />
</Route> </Route>
<Route path="/admin/"> <Route path="/admin/">
<LoginSwitch /> <LoginSwitch />
</Route> </Route>
</Switch> </Switch>
</Router> </Router>
} )}
</>; </>
);
} }
export default App; export default App;

View File

@ -1,6 +1,12 @@
import axios from "axios"; import axios from "axios";
import { GlobalConf, TokenResp, SubscribeResp, TargetNameResp, SubscribeConfig } from "../utils/type"; import {
import { baseUrl } from './utils'; GlobalConf,
TokenResp,
SubscribeResp,
TargetNameResp,
SubscribeConfig,
} from "../utils/type";
import { baseUrl } from "./utils";
export async function getGlobalConf(): Promise<GlobalConf> { export async function getGlobalConf(): Promise<GlobalConf> {
const res = await axios.get<GlobalConf>(`${baseUrl}global_conf`); const res = await axios.get<GlobalConf>(`${baseUrl}global_conf`);
@ -8,7 +14,9 @@ export async function getGlobalConf(): Promise<GlobalConf> {
} }
export async function auth(token: string): Promise<TokenResp> { export async function auth(token: string): Promise<TokenResp> {
const res = await axios.get<TokenResp>(`${baseUrl}auth`, {params: {token}}); const res = await axios.get<TokenResp>(`${baseUrl}auth`, {
params: { token },
});
return res.data; return res.data;
} }
@ -17,22 +25,39 @@ export async function getSubscribe(): Promise<SubscribeResp> {
return res.data; return res.data;
} }
export async function getTargetName(platformName: string, target: string): Promise<TargetNameResp> { export async function getTargetName(
const res = await axios.get(`${baseUrl}target_name`, {params: {platformName, target}}); platformName: string,
target: string
): Promise<TargetNameResp> {
const res = await axios.get(`${baseUrl}target_name`, {
params: { platformName, target },
});
return res.data; return res.data;
} }
export async function addSubscribe(groupNumber: string, req: SubscribeConfig) { export async function addSubscribe(groupNumber: string, req: SubscribeConfig) {
const res = await axios.post(`${baseUrl}subs`, req, {params: {groupNumber}}) const res = await axios.post(`${baseUrl}subs`, req, {
params: { groupNumber },
});
return res.data; return res.data;
} }
export async function delSubscribe(groupNumber: string, platformName: string, target: string) { export async function delSubscribe(
const res = await axios.delete(`${baseUrl}subs`, {params: {groupNumber, platformName, target}}); groupNumber: string,
platformName: string,
target: string
) {
const res = await axios.delete(`${baseUrl}subs`, {
params: { groupNumber, platformName, target },
});
return res.data; return res.data;
} }
export async function updateSubscribe(groupNumber: string, req: SubscribeConfig) { export async function updateSubscribe(
return axios.patch(`${baseUrl}subs`, req, {params: {groupNumber}}) groupNumber: string,
.then(res => res.data); req: SubscribeConfig
) {
return axios
.patch(`${baseUrl}subs`, req, { params: { groupNumber } })
.then((res) => res.data);
} }

View File

@ -1,51 +1,61 @@
import axios, {AxiosError} from "axios"; import axios, { AxiosError } from "axios";
import {Store} from "src/store"; import { Store } from "src/store";
import { clearLoginStatus } from 'src/store/loginSlice'; import { clearLoginStatus } from "src/store/loginSlice";
// import { useContext } from 'react'; // import { useContext } from 'react';
// import { LoginContext } from "../utils/context"; // import { LoginContext } from "../utils/context";
export const baseUrl = '/bison/api/' export const baseUrl = "/bison/api/";
let store: Store let store: Store;
export const injectStore = (_store: Store) => { export const injectStore = (_store: Store) => {
store = _store store = _store;
} };
// const loginStatus = useContext(LoginContext); // const loginStatus = useContext(LoginContext);
axios.interceptors.request.use(function (config) { axios.interceptors.request.use(
if (config.url && config.url.startsWith(baseUrl) && config.url !== `${baseUrl}auth` function (config) {
&& config.url !== `${baseUrl}global_conf`) { if (
const token = localStorage.getItem('token'); config.url &&
if (token) { config.url.startsWith(baseUrl) &&
config.headers['Authorization'] = `Bearer ${token}`; config.url !== `${baseUrl}auth` &&
} else { config.url !== `${baseUrl}global_conf`
throw new axios.Cancel('User not login'); ) {
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);
} }
return config; );
}, function (error) {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) { axios.interceptors.response.use(
// const data = response.data; function (response) {
// const parseToMap = (item: any): any => { // const data = response.data;
// if (item instanceof Array) { // const parseToMap = (item: any): any => {
// return item.map(parseToMap); // if (item instanceof Array) {
// } else if (item instanceof Object) { // return item.map(parseToMap);
// let res = new Map(); // } else if (item instanceof Object) {
// for (const key of Object.keys(item)) { // let res = new Map();
// res.set(key, parseToMap(item[key])); // for (const key of Object.keys(item)) {
// } // res.set(key, parseToMap(item[key]));
// return res; // }
// } else { // return res;
// return item; // } else {
// } // return item;
// } // }
// response.data = parseToMap(data); // }
return response; // response.data = parseToMap(data);
}, function(error: AxiosError) { return response;
if(error.response && error.response.status === 401) { },
store.dispatch(clearLoginStatus()); function (error: AxiosError) {
if (error.response && error.response.status === 401) {
store.dispatch(clearLoginStatus());
}
return Promise.reject(error);
} }
return Promise.reject(error); );
});

View File

@ -1,15 +1,15 @@
import {Form, Input, Modal, Select, Tag} from 'antd'; import { Form, Input, Modal, Select, Tag } from "antd";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useSelector} from 'react-redux'; import { useSelector } from "react-redux";
import {addSubscribe, getTargetName, updateSubscribe} from 'src/api/config'; import { addSubscribe, getTargetName, updateSubscribe } from "src/api/config";
import {InputTag} from 'src/component/inputTag'; import { InputTag } from "src/component/inputTag";
import {platformConfSelector} from 'src/store/globalConfSlice'; import { platformConfSelector } from "src/store/globalConfSlice";
import {CategoryConfig, SubscribeConfig} from 'src/utils/type'; import { CategoryConfig, SubscribeConfig } from "src/utils/type";
interface InputTagCustomProp { interface InputTagCustomProp {
value?: Array<string>, value?: Array<string>;
onChange?: (value: Array<string>) => void, onChange?: (value: Array<string>) => void;
disabled?: boolean disabled?: boolean;
} }
function InputTagCustom(prop: InputTagCustomProp) { function InputTagCustom(prop: InputTagCustomProp) {
const [value, setValue] = useState(prop.value || []); const [value, setValue] = useState(prop.value || []);
@ -18,165 +18,202 @@ function InputTagCustom(prop: InputTagCustomProp) {
if (prop.onChange) { if (prop.onChange) {
prop.onChange(newVal); prop.onChange(newVal);
} }
} };
useEffect(() => { useEffect(() => {
if (prop.value) { if (prop.value) {
setValue(prop.value); setValue(prop.value);
} }
}, [prop.value]) }, [prop.value]);
return ( return (
<> <>
{ {prop.disabled ? (
prop.disabled ? <Tag color="default"></Tag>: <Tag color="default"></Tag>
) : (
<> <>
{value.length === 0 && {value.length === 0 && <Tag color="green"></Tag>}
<Tag color="green"></Tag> <InputTag
} color="blue"
<InputTag color="blue" addText="添加标签" value={value} onChange={handleSetValue} /> addText="添加标签"
</> value={value}
} onChange={handleSetValue}
/>
</>
)}
</> </>
) );
} }
interface AddModalProp { interface AddModalProp {
showModal: boolean, showModal: boolean;
groupNumber: string, groupNumber: string;
setShowModal: (s: boolean) => void, setShowModal: (s: boolean) => void;
refresh: () => void refresh: () => void;
initVal?: SubscribeConfig initVal?: SubscribeConfig;
} }
export function AddModal({ export function AddModal({
showModal, groupNumber, setShowModal, refresh, initVal showModal,
groupNumber,
setShowModal,
refresh,
initVal,
}: AddModalProp) { }: AddModalProp) {
const [ confirmLoading, setConfirmLoading ] = useState<boolean>(false); const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
const platformConf = useSelector(platformConfSelector) const platformConf = useSelector(platformConfSelector);
const [ hasTarget, setHasTarget ] = useState(false); const [hasTarget, setHasTarget] = useState(false);
const [ categories, setCategories ] = useState({} as CategoryConfig); const [categories, setCategories] = useState({} as CategoryConfig);
const [ enabledTag, setEnableTag ] = useState(false); const [enabledTag, setEnableTag] = useState(false);
const [ form ] = Form.useForm(); const [form] = Form.useForm();
const [ inited, setInited ] = useState(false); const [inited, setInited] = useState(false);
const changePlatformSelect = (platform: string) => { const changePlatformSelect = (platform: string) => {
setHasTarget(_ => platformConf[platform].hasTarget); setHasTarget((_) => platformConf[platform].hasTarget);
setCategories(_ => platformConf[platform].categories); setCategories((_) => platformConf[platform].categories);
setEnableTag(platformConf[platform].enabledTag) setEnableTag(platformConf[platform].enabledTag);
if (! platformConf[platform].hasTarget) { if (!platformConf[platform].hasTarget) {
getTargetName(platform, 'default') getTargetName(platform, "default").then((res) => {
.then(res => { console.log(res);
console.log(res) form.setFieldsValue({
form.setFieldsValue({ targetName: res.targetName,
targetName: res.targetName, target: "",
target: '' });
}) });
})
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
targetName: '', targetName: "",
target: '' target: "",
}) });
} }
} };
const handleSubmit = (value: any) => { const handleSubmit = (value: any) => {
let newVal = Object.assign({}, value) let newVal = Object.assign({}, value);
if (typeof newVal.tags !== 'object') { if (typeof newVal.tags !== "object") {
newVal.tags = [] newVal.tags = [];
} }
if (typeof newVal.cats !== 'object') { if (typeof newVal.cats !== "object") {
newVal.cats = [] newVal.cats = [];
} }
if (newVal.target === '') { if (newVal.target === "") {
newVal.target = 'default' newVal.target = "default";
} }
if (initVal) { // patch if (initVal) {
updateSubscribe(groupNumber, newVal) // patch
.then(() => { updateSubscribe(groupNumber, newVal).then(() => {
setConfirmLoading(false);
setShowModal(false);
form.resetFields();
refresh();
});
} else {
addSubscribe(groupNumber, newVal)
.then(() => {
setConfirmLoading(false); setConfirmLoading(false);
setShowModal(false); setShowModal(false);
form.resetFields(); form.resetFields();
refresh(); refresh();
}); });
} else {
addSubscribe(groupNumber, newVal).then(() => {
setConfirmLoading(false);
setShowModal(false);
form.resetFields();
refresh();
});
} }
} };
const handleModleFinish = () => { const handleModleFinish = () => {
form.submit(); form.submit();
setConfirmLoading(() => true); setConfirmLoading(() => true);
} };
useEffect(() => { useEffect(() => {
if (initVal && !inited) { if (initVal && !inited) {
const platformName = initVal.platformName; const platformName = initVal.platformName;
setHasTarget(platformConf[platformName].hasTarget); setHasTarget(platformConf[platformName].hasTarget);
setCategories(platformConf[platformName].categories); setCategories(platformConf[platformName].categories);
setEnableTag(platformConf[platformName].enabledTag); setEnableTag(platformConf[platformName].enabledTag);
setInited(true) setInited(true);
form.setFieldsValue(initVal) form.setFieldsValue(initVal);
} }
}, [initVal, form, platformConf, inited]) }, [initVal, form, platformConf, inited]);
return <Modal title="添加订阅" visible={showModal} return (
confirmLoading={confirmLoading} onCancel={() => setShowModal(false)} <Modal
onOk={handleModleFinish}> title="添加订阅"
<Form form={form} labelCol={{ span: 6 }} name="b" onFinish={handleSubmit} > visible={showModal}
<Form.Item label="平台" name="platformName" rules={[]}> confirmLoading={confirmLoading}
<Select style={{ width: '80%' }} onChange={changePlatformSelect}> onCancel={() => setShowModal(false)}
{Object.keys(platformConf).map(platformName => onOk={handleModleFinish}
<Select.Option key={platformName} value={platformName}>{platformConf[platformName].name}</Select.Option> >
)} <Form form={form} labelCol={{ span: 6 }} name="b" onFinish={handleSubmit}>
</Select> <Form.Item label="平台" name="platformName" rules={[]}>
</Form.Item> <Select style={{ width: "80%" }} onChange={changePlatformSelect}>
<Form.Item label="账号" name="target" rules={[ {Object.keys(platformConf).map((platformName) => (
{required: hasTarget, message: "请输入账号"}, <Select.Option key={platformName} value={platformName}>
{validator: async (_, value) => { {platformConf[platformName].name}
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.Option>
) ))}
} </Select>
</Select> </Form.Item>
</Form.Item> <Form.Item
<Form.Item label="订阅Tag" name="tags"> label="账号"
<InputTagCustom disabled={!enabledTag}/> name="target"
</Form.Item> rules={[
</Form> { 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> </Modal>
);
} }

View File

@ -1,122 +1,151 @@
import {Input, Tag, Tooltip} from "antd"; import { Input, Tag, Tooltip } from "antd";
import {PresetColorType, PresetStatusColorType} from 'antd/lib/_util/colors' import { PresetColorType, PresetStatusColorType } from "antd/lib/_util/colors";
import {LiteralUnion} from 'antd/lib/_util/type' import { LiteralUnion } from "antd/lib/_util/type";
import React, {useRef, useState, useEffect} from "react"; import React, { useRef, useState, useEffect } from "react";
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from "@ant-design/icons";
interface InputTagProp { interface InputTagProp {
value?: Array<string>, value?: Array<string>;
onChange?: (value: Array<string>) => void onChange?: (value: Array<string>) => void;
color?: LiteralUnion<PresetColorType | PresetStatusColorType, string>; color?: LiteralUnion<PresetColorType | PresetStatusColorType, string>;
addText?: string addText?: string;
} }
export function InputTag(prop: InputTagProp) { export function InputTag(prop: InputTagProp) {
const [ value, setValue ] = useState<Array<string>>(prop.value || []); const [value, setValue] = useState<Array<string>>(prop.value || []);
const [ inputVisible, setInputVisible ] = useState(false); const [inputVisible, setInputVisible] = useState(false);
const [ inputValue, setInputValue ] = useState(''); const [inputValue, setInputValue] = useState("");
const [ editInputIndex, setEditInputIndex ] = useState(-1); const [editInputIndex, setEditInputIndex] = useState(-1);
const [ editInputValue, setEditInputValue ] = useState(''); const [editInputValue, setEditInputValue] = useState("");
const inputRef = useRef(null as any); const inputRef = useRef(null as any);
const editInputRef = useRef(null as any); const editInputRef = useRef(null as any);
useEffect(() => { useEffect(() => {
if (prop.value) { if (prop.value) {
setValue(prop.value); setValue(prop.value);
} }
}, [prop.value]) }, [prop.value]);
useEffect(() => { useEffect(() => {
if (inputVisible) { if (inputVisible) {
inputRef.current.focus() inputRef.current.focus();
} }
}, [inputVisible]); }, [inputVisible]);
useEffect(() => { useEffect(() => {
if (editInputIndex !== -1) { if (editInputIndex !== -1) {
editInputRef.current.focus(); editInputRef.current.focus();
} }
}, [editInputIndex]); }, [editInputIndex]);
const handleClose = (removedTag: string) => { const handleClose = (removedTag: string) => {
const tags = value.filter(tag => tag !== removedTag); const tags = value.filter((tag) => tag !== removedTag);
setValue(_ => tags); setValue((_) => tags);
if (prop.onChange) { if (prop.onChange) {
prop.onChange(tags); prop.onChange(tags);
} }
} };
const showInput = () => { const showInput = () => {
setInputVisible(_ => true); setInputVisible((_) => true);
} };
const handleInputConfirm = () => { const handleInputConfirm = () => {
if (inputValue && value.indexOf(inputValue) === -1) { if (inputValue && value.indexOf(inputValue) === -1) {
const newVal = [...value, inputValue]; const newVal = [...value, inputValue];
setValue(_ => newVal); setValue((_) => newVal);
if (prop.onChange) { if (prop.onChange) {
prop.onChange(newVal); prop.onChange(newVal);
} }
} }
setInputVisible(_ => false); setInputVisible((_) => false);
setInputValue(_ => ''); setInputValue((_) => "");
} };
const handleEditInputChange = (e: any) => { const handleEditInputChange = (e: any) => {
setEditInputValue(_ => e.target.value); setEditInputValue((_) => e.target.value);
} };
const handleEditInputConfirm = () => { const handleEditInputConfirm = () => {
const newTags = value.slice(); const newTags = value.slice();
newTags[editInputIndex] = editInputValue; newTags[editInputIndex] = editInputValue;
setValue(_ => newTags); setValue((_) => newTags);
if (prop.onChange) { if (prop.onChange) {
prop.onChange(newTags); prop.onChange(newTags);
} }
setEditInputIndex(_ => -1); setEditInputIndex((_) => -1);
setEditInputValue(_ => ''); setEditInputValue((_) => "");
} };
const handleInputChange = (e: any) => { const handleInputChange = (e: any) => {
setInputValue(e.target.value); setInputValue(e.target.value);
} };
return ( return (
<> <>
{ value.map((tag, index) => { {value.map((tag, index) => {
if (editInputIndex === index) { if (editInputIndex === index) {
return ( return (
<Input ref={editInputRef} key={tag} size="small" <Input
value={editInputValue} onChange={handleEditInputChange} ref={editInputRef}
onBlur={handleEditInputConfirm} onPressEnter={handleInputConfirm} /> 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 ? (
const isLongTag = tag.length > 20; <Tooltip title={tag} key={tag}>
const tagElem = ( {tagElem}
<Tag color={prop.color || "default"} style={{userSelect: 'none'}} key={tag} closable onClose={() => handleClose(tag)}> </Tooltip>
<span onDoubleClick={e => { ) : (
setEditInputIndex(_ => index); tagElem
setEditInputValue(_ => tag);
e.preventDefault();
}}>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
); );
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : ( tagElem );
})} })}
{inputVisible && ( {inputVisible && (
<Input ref={inputRef} type="text" size="small" <Input
style={{width: '78px', marginRight: '8px', verticalAlign: 'top'}} value={inputValue} ref={inputRef}
onChange={handleInputChange} onBlur={handleInputConfirm} type="text"
onPressEnter={handleInputConfirm} /> size="small"
style={{ width: "78px", marginRight: "8px", verticalAlign: "top" }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)} )}
{!inputVisible && ( {!inputVisible && (
<Tag className="site-tag-plus" onClick={showInput} style={{background: '#fff', border: 'dashed thin', borderColor: '#bfbfbf' }}> <Tag
<PlusOutlined/> {prop.addText || "Add Tag"} className="site-tag-plus"
onClick={showInput}
style={{
background: "#fff",
border: "dashed thin",
borderColor: "#bfbfbf",
}}
>
<PlusOutlined /> {prop.addText || "Add Tag"}
</Tag> </Tag>
)} )}
</> </>
); );
} }

View File

@ -1,108 +1,190 @@
import {CopyOutlined, DeleteOutlined, EditOutlined} from '@ant-design/icons'; import { CopyOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons";
import {Card, Col, Form, message, Popconfirm, Select, Tag, Tooltip} from 'antd'; import {
import Modal from 'antd/lib/modal/Modal'; Card,
import React, {useState} from "react"; Col,
import {useDispatch, useSelector} from 'react-redux'; Form,
import {addSubscribe, delSubscribe} from 'src/api/config'; message,
import {platformConfSelector} from 'src/store/globalConfSlice'; Popconfirm,
import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice'; Select,
import {PlatformConfig, SubscribeConfig, SubscribeResp} from 'src/utils/type'; Tag,
import {AddModal} from './addSubsModal'; 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 { interface CopyModalProp {
setShowModal: (modalShow: boolean) => void setShowModal: (modalShow: boolean) => void;
showModal: boolean showModal: boolean;
config: SubscribeConfig, config: SubscribeConfig;
groups: SubscribeResp groups: SubscribeResp;
currentGroupNumber: string currentGroupNumber: string;
reload: () => void reload: () => void;
} }
function CopyModal({setShowModal,config, function CopyModal({
currentGroupNumber,groups,showModal,reload}: CopyModalProp) { setShowModal,
const [confirmLoading, setConfirmLoading] = useState(false) config,
const [ selectedGroups, setSelectGroups ] = useState<Array<string>>([]); currentGroupNumber,
const postReqs = async (selectedGroups: Array<string>, config: SubscribeConfig) => { groups,
for(let selectedGroup of selectedGroups) { 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); await addSubscribe(selectedGroup, config);
} }
} };
const handleOk = () => { const handleOk = () => {
if (selectedGroups.length === 0) { if (selectedGroups.length === 0) {
message.error("请至少选择一个目标群"); message.error("请至少选择一个目标群");
} else{ } else {
setConfirmLoading(true) setConfirmLoading(true);
postReqs(selectedGroups, config).then(() => { postReqs(selectedGroups, config).then(() => {
setConfirmLoading(false) setConfirmLoading(false);
setShowModal(false) setShowModal(false);
return reload() return reload();
}) });
} }
} };
return <Modal title="复制订阅" visible={showModal} confirmLoading={confirmLoading} return (
onCancel={() => setShowModal(false)} onOk={handleOk}> <Modal
<Select mode="multiple" onChange={(value: Array<string>) => setSelectGroups(value)} title="复制订阅"
style={{width: '80%'}}> visible={showModal}
{ confirmLoading={confirmLoading}
Object.keys(groups).filter(groupNumber => groupNumber !== currentGroupNumber) onCancel={() => setShowModal(false)}
.map((groupNumber) => onOk={handleOk}
<Select.Option value={groupNumber} key={groupNumber}> >
{`${groupNumber} - ${groups[groupNumber].name}`} <Select
</Select.Option>) mode="multiple"
} onChange={(value: Array<string>) => setSelectGroups(value)}
</Select> 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> </Modal>
);
} }
interface SubscribeCardProp { interface SubscribeCardProp {
groupNumber: string groupNumber: string;
config: SubscribeConfig config: SubscribeConfig;
} }
export function SubscribeCard({groupNumber, config}: SubscribeCardProp) { export function SubscribeCard({ groupNumber, config }: SubscribeCardProp) {
const platformConfs = useSelector(platformConfSelector) const platformConfs = useSelector(platformConfSelector);
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false);
const platformConf = platformConfs[config.platformName] as PlatformConfig; const platformConf = platformConfs[config.platformName] as PlatformConfig;
const dispatcher = useDispatch(); const dispatcher = useDispatch();
const groupSubscribes = useSelector(groupConfigSelector); const groupSubscribes = useSelector(groupConfigSelector);
const reload = () => dispatcher(updateGroupSubs()) const reload = () => dispatcher(updateGroupSubs());
const handleDelete = (groupNumber: string, platformName: string, target: string) => () => { const handleDelete =
delSubscribe(groupNumber, platformName, target).then(() => { (groupNumber: string, platformName: string, target: string) => () => {
reload() delSubscribe(groupNumber, platformName, target).then(() => {
}) reload();
} });
};
return ( return (
<Col span={8} key={`${config.platformName}-${config.target}`}> <Col span={8} key={`${config.platformName}-${config.target}`}>
<Card title={`${platformConf.name} - ${config.targetName}`} <Card
actions={[ title={`${platformConf.name} - ${config.targetName}`}
<Tooltip title="编辑"> actions={[
<EditOutlined onClick={()=>{setShowEditModal(state => !state)}}/> <Tooltip title="编辑">
</Tooltip>, <EditOutlined
<Tooltip title="添加到其他群"> onClick={() => {
<CopyOutlined onClick={()=>{setShowModal(state => !state)}}/> setShowEditModal((state) => !state);
</Tooltip>, }}
<Popconfirm title={`确定要删除 ${platformConf.name} - ${config.targetName}`} />
onConfirm={handleDelete(groupNumber, config.platformName, config.target || 'default')}> </Tooltip>,
<Tooltip title="删除" ><DeleteOutlined /></Tooltip> <Tooltip title="添加到其他群">
</Popconfirm>, <CopyOutlined
]}> onClick={() => {
<Form labelCol={{ span: 4 }}> setShowModal((state) => !state);
<Form.Item label="订阅帐号"> }}
{ platformConf.hasTarget ? config.target : <Tag color="default"></Tag> } />
</Form.Item> </Tooltip>,
<Form.Item label="订阅类型"> <Popconfirm
{Object.keys(platformConf.categories).length > 0 ? title={`确定要删除 ${platformConf.name} - ${config.targetName}`}
config.cats.map((catKey: number) => (<Tag color="green" key={catKey}>{platformConf.categories[catKey]}</Tag>)) : onConfirm={handleDelete(
<Tag color="default"></Tag>} groupNumber,
</Form.Item> config.platformName,
<Form.Item label="订阅Tag"> config.target || "default"
{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> <Tooltip title="删除">
</Form> <DeleteOutlined />
</Card> </Tooltip>
<CopyModal setShowModal={setShowModal} reload={reload} currentGroupNumber={groupNumber} </Popconfirm>,
showModal={showModal} config={config} groups={groupSubscribes}/> ]}
<AddModal showModal={showEditModal} setShowModal={setShowEditModal} >
groupNumber={groupNumber} refresh={reload} initVal={config}/> <Form labelCol={{ span: 4 }}>
</Col> <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>
);
} }

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import {Provider} from 'react-redux'; import { Provider } from "react-redux";
import App from './App'; import App from "./App";
import './index.css'; import "./index.css";
import reportWebVitals from './reportWebVitals'; import reportWebVitals from "./reportWebVitals";
import store from './store'; import store from "./store";
import {injectStore} from 'src/api/utils'; import { injectStore } from "src/api/utils";
injectStore(store); injectStore(store);
ReactDOM.render( ReactDOM.render(
@ -14,7 +14,7 @@ ReactDOM.render(
<App /> <App />
</Provider> </Provider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -1,56 +1,73 @@
import {Button, Collapse, Empty, Row} from 'antd'; import { Button, Collapse, Empty, Row } from "antd";
import React, {ReactElement, useEffect, useState} from "react"; import React, { ReactElement, useEffect, useState } from "react";
import {useDispatch, useSelector} from 'react-redux'; import { useDispatch, useSelector } from "react-redux";
import {AddModal} from 'src/component/addSubsModal'; import { AddModal } from "src/component/addSubsModal";
import {SubscribeCard} from 'src/component/subscribeCard'; import { SubscribeCard } from "src/component/subscribeCard";
import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice'; import {
groupConfigSelector,
updateGroupSubs,
} from "src/store/groupConfigSlice";
interface ConfigPageProp { interface ConfigPageProp {
tab: string tab: string;
} }
export function ConfigPage(prop: ConfigPageProp) { export function ConfigPage(prop: ConfigPageProp) {
const [ showModal, setShowModal ] = useState<boolean>(false); const [showModal, setShowModal] = useState<boolean>(false);
const [ currentAddingGroupNumber, setCurrentAddingGroupNumber ] = useState(''); const [currentAddingGroupNumber, setCurrentAddingGroupNumber] = useState("");
const configData = useSelector(groupConfigSelector); const configData = useSelector(groupConfigSelector);
const dispatcher = useDispatch(); const dispatcher = useDispatch();
useEffect(() => { useEffect(() => {
dispatcher(updateGroupSubs()) dispatcher(updateGroupSubs());
}, [prop.tab, dispatcher]); }, [prop.tab, dispatcher]);
const clickNew = (groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => { const clickNew =
setShowModal(_ => true); (groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
setCurrentAddingGroupNumber(groupNumber); setShowModal((_) => true);
e.stopPropagation(); setCurrentAddingGroupNumber(groupNumber);
} e.stopPropagation();
};
if (Object.keys(configData).length === 0) { if (Object.keys(configData).length === 0) {
return <Empty /> return <Empty />;
} else { } else {
let groups: Array<ReactElement> = []; let groups: Array<ReactElement> = [];
for (let key of Object.keys(configData)) { for (let key of Object.keys(configData)) {
let value = configData[key]; let value = configData[key];
groups.push( groups.push(
<Collapse.Panel key={key} header={ <Collapse.Panel
<span>{`${key} - ${value.name}`}<Button style={{float: "right"}} onClick={clickNew(key)}></Button></span> key={key}
}> header={
<Row gutter={[{ xs: 8, sm: 16, md: 24, lg: 32}, <span>
{ xs: 8, sm: 16, md: 24, lg: 32}]} align="middle"> {`${key} - ${value.name}`}
{value.subscribes.map((subs, idx) => <SubscribeCard key={idx} <Button style={{ float: "right" }} onClick={clickNew(key)}>
groupNumber={key} config={subs} />)}
</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> </Row>
</Collapse.Panel> </Collapse.Panel>
) );
} }
return ( return (
<div> <div>
<Collapse> <Collapse>{groups}</Collapse>
{groups} <AddModal
</Collapse> groupNumber={currentAddingGroupNumber}
<AddModal groupNumber={currentAddingGroupNumber} showModal={showModal} showModal={showModal}
refresh={() => dispatcher(updateGroupSubs())} refresh={() => dispatcher(updateGroupSubs())}
setShowModal={(s: boolean) => setShowModal(_ => s)} /> setShowModal={(s: boolean) => setShowModal((_) => s)}
</div> />
) </div>
);
} }
} }

View File

@ -1,37 +1,39 @@
import {BugOutlined, SettingOutlined} from '@ant-design/icons'; import { BugOutlined, SettingOutlined } from "@ant-design/icons";
import {Layout, Menu} from 'antd'; import { Layout, Menu } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useSelector} from 'react-redux'; import { useSelector } from "react-redux";
import {loginSelector} from 'src/store/loginSlice'; import { loginSelector } from "src/store/loginSlice";
import './admin.css'; import "./admin.css";
import {ConfigPage} from './configPage'; import { ConfigPage } from "./configPage";
export function Admin() { export function Admin() {
const login = useSelector(loginSelector) const login = useSelector(loginSelector);
const [ tab, changeTab ] = useState("manage"); const [tab, changeTab] = useState("manage");
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: "100vh" }}>
<Layout.Sider className="layout-side"> <Layout.Sider className="layout-side">
<div className="user"> <div className="user"></div>
</div> <Menu
<Menu mode="inline" theme="dark" defaultSelectedKeys={[tab]} mode="inline"
onClick={({key}) => changeTab(key)}> theme="dark"
<Menu.Item key="manage" icon={<SettingOutlined />}></Menu.Item> defaultSelectedKeys={[tab]}
{ login.type === 'admin' && onClick={({ key }) => changeTab(key)}
<Menu.Item key="log" icon={<BugOutlined />}></Menu.Item> >
} <Menu.Item key="manage" icon={<SettingOutlined />}>
</Menu>
</Layout.Sider> </Menu.Item>
<Layout.Content> {login.type === "admin" && (
<div style={{margin: '24px', background: '#fff', minHeight: '640px'}}> <Menu.Item key="log" icon={<BugOutlined />}>
{
tab === 'manage' ? </Menu.Item>
<ConfigPage tab={tab}/> )}
: null </Menu>
} </Layout.Sider>
</div> <Layout.Content>
</Layout.Content> <div style={{ margin: "24px", background: "#fff", minHeight: "640px" }}>
</Layout> {tab === "manage" ? <ConfigPage tab={tab} /> : null}
) </div>
</Layout.Content>
</Layout>
);
} }

View File

@ -1,28 +1,31 @@
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useDispatch, useSelector} from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {useParams} from "react-router"; import { useParams } from "react-router";
import {Redirect} from 'react-router-dom'; import { Redirect } from "react-router-dom";
import {login, loginSelector} from 'src/store/loginSlice'; import { login, loginSelector } from "src/store/loginSlice";
interface AuthParam { interface AuthParam {
code: string code: string;
} }
export function Auth() { export function Auth() {
const { code } = useParams<AuthParam>(); const { code } = useParams<AuthParam>();
const dispatch = useDispatch(); const dispatch = useDispatch();
const loginState = useSelector(loginSelector) const loginState = useSelector(loginSelector);
useEffect(() => { useEffect(() => {
const loginFun = async () => { const loginFun = async () => {
dispatch(login(code)); dispatch(login(code));
} };
loginFun(); loginFun();
}, [code, dispatch]) }, [code, dispatch]);
return <> return (
{ loginState.login ? <>
<Redirect to={{pathname: '/admin'}} /> : {loginState.login ? (
loginState.failed ? <Redirect to={{ pathname: "/admin" }} />
<div></div> : ) : loginState.failed ? (
<div>Logining...</div> <div></div>
} ) : (
</>; <div>Logining...</div>
)}
</>
);
} }

View File

@ -1,8 +1,8 @@
import { ReportHandler } from 'web-vitals'; import { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { 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); getCLS(onPerfEntry);
getFID(onPerfEntry); getFID(onPerfEntry);
getFCP(onPerfEntry); getFCP(onPerfEntry);

View File

@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'; import "@testing-library/jest-dom";

View File

@ -1,23 +1,31 @@
import {CaseReducer, createAsyncThunk, createSlice, PayloadAction} from "@reduxjs/toolkit"; import {
import {getGlobalConf as getGlobalConfApi} from "src/api/config"; CaseReducer,
import {GlobalConf} from "src/utils/type"; createAsyncThunk,
import {RootState} from "."; 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 = { const initialState: GlobalConf = {
platformConf: {}, platformConf: {},
loaded: false loaded: false,
} };
const setGlobalConf: CaseReducer<GlobalConf, PayloadAction<GlobalConf>> = (_, action) => { const setGlobalConf: CaseReducer<GlobalConf, PayloadAction<GlobalConf>> = (
return {...action.payload, loaded: true} _,
} action
) => {
return { ...action.payload, loaded: true };
};
export const getGlobalConf = createAsyncThunk( export const getGlobalConf = createAsyncThunk(
"globalConf/set", "globalConf/set",
getGlobalConfApi, getGlobalConfApi,
{ {
condition: (_, { getState }) => !(getState() as RootState).globalConf.loaded condition: (_, { getState }) =>
!(getState() as RootState).globalConf.loaded,
} }
); );
@ -26,10 +34,11 @@ export const globalConfSlice = createSlice({
initialState, initialState,
reducers: {}, reducers: {},
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getGlobalConf.fulfilled, setGlobalConf) builder.addCase(getGlobalConf.fulfilled, setGlobalConf);
} },
}) });
export const platformConfSelector = (state: RootState) => state.globalConf.platformConf export const platformConfSelector = (state: RootState) =>
state.globalConf.platformConf;
export default globalConfSlice.reducer export default globalConfSlice.reducer;

View File

@ -1,27 +1,36 @@
import {CaseReducer, createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit'; import {
import {SubscribeResp} from 'src/utils/type'; CaseReducer,
import {getSubscribe} from 'src/api/config'; createAsyncThunk,
import {RootState} from '.'; createSlice,
const initialState: SubscribeResp = {} 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) => { const setSubs: CaseReducer<SubscribeResp, PayloadAction<SubscribeResp>> = (
return action.payload _,
} action
) => {
return action.payload;
};
export const updateGroupSubs = createAsyncThunk( export const updateGroupSubs = createAsyncThunk(
"groupConfig/update", getSubscribe "groupConfig/update",
) getSubscribe
);
export const groupConfigSlice = createSlice({ export const groupConfigSlice = createSlice({
name: "groupConfig", name: "groupConfig",
initialState, initialState,
reducers: { reducers: {
setSubs setSubs,
}, },
extraReducers: (reducer) => { extraReducers: (reducer) => {
reducer.addCase(updateGroupSubs.fulfilled, setSubs) reducer.addCase(updateGroupSubs.fulfilled, setSubs);
} },
}) });
export const groupConfigSelector = (state: RootState) => state.groupConfig; export const groupConfigSelector = (state: RootState) => state.groupConfig;
export default groupConfigSlice.reducer; export default groupConfigSlice.reducer;

View File

@ -1,5 +1,5 @@
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import {AppDispatch, RootState} from "."; import { AppDispatch, RootState } from ".";
export const useAppDispacher = () => useDispatch<AppDispatch>() export const useAppDispacher = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -1,15 +1,15 @@
import {configureStore} from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import loginSlice from "./loginSlice"; import loginSlice from "./loginSlice";
import globalConfSlice from "./globalConfSlice"; import globalConfSlice from "./globalConfSlice";
import groupConfigSlice from './groupConfigSlice'; import groupConfigSlice from "./groupConfigSlice";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
login: loginSlice, login: loginSlice,
globalConf: globalConfSlice, globalConf: globalConfSlice,
groupConfig: groupConfigSlice, groupConfig: groupConfigSlice,
} },
}) });
export default store; export default store;

View File

@ -1,108 +1,121 @@
import { AnyAction, CaseReducer, createAsyncThunk, createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit"; import {
import jwt_decode from 'jwt-decode'; AnyAction,
CaseReducer,
createAsyncThunk,
createSlice,
PayloadAction,
ThunkAction,
} from "@reduxjs/toolkit";
import jwt_decode from "jwt-decode";
import { LoginStatus, TokenResp } from "src/utils/type"; import { LoginStatus, TokenResp } from "src/utils/type";
import { auth } from "src/api/config"; import { auth } from "src/api/config";
import {RootState} from "."; import { RootState } from ".";
const initialState: LoginStatus = { const initialState: LoginStatus = {
login: false, login: false,
type: '', type: "",
name: '', name: "",
id: '123', id: "123",
// groups: [], // groups: [],
token: '', token: "",
failed: false failed: false,
} };
interface storedInfo { interface storedInfo {
type: string type: string;
name: string name: string;
id: string id: string;
} }
const loginAction: CaseReducer<LoginStatus, PayloadAction<TokenResp>> = (_, action) => { const loginAction: CaseReducer<LoginStatus, PayloadAction<TokenResp>> = (
_,
action
) => {
return { return {
login: true, login: true,
failed: false, failed: false,
type: action.payload.type, type: action.payload.type,
name: action.payload.name, name: action.payload.name,
id: action.payload.id, id: action.payload.id,
token: action.payload.token token: action.payload.token,
} };
} };
export const login = createAsyncThunk( export const login = createAsyncThunk(
"auth/login", "auth/login",
async (code: string) => { async (code: string) => {
let res = await auth(code); let res = await auth(code);
if (res.status !== 200) { if (res.status !== 200) {
throw Error("Login Error") throw Error("Login Error");
} else { } else {
localStorage.setItem('loginInfo', JSON.stringify({ localStorage.setItem(
'type': res.type, "loginInfo",
'name': res.name, JSON.stringify({
id: res.id, type: res.type,
})) name: res.name,
localStorage.setItem('token', res.token) id: res.id,
})
);
localStorage.setItem("token", res.token);
} }
return res return res;
}, },
{ {
condition: (_: string, { getState }) => { condition: (_: string, { getState }) => {
const { login } = getState() as { login: LoginStatus } const { login } = getState() as { login: LoginStatus };
return !login.login; return !login.login;
} },
} }
) );
export const loginSlice = createSlice({ export const loginSlice = createSlice({
name: 'auth', name: "auth",
initialState, initialState,
reducers: { reducers: {
doLogin: loginAction, doLogin: loginAction,
doClearLogin: (state) => { doClearLogin: (state) => {
state.login = false state.login = false;
} },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(login.fulfilled, loginAction); builder.addCase(login.fulfilled, loginAction);
builder.addCase(login.rejected, (stat) => { builder.addCase(login.rejected, (stat) => {
stat.failed = true stat.failed = true;
}) });
} },
}) });
export const { doLogin, doClearLogin } = loginSlice.actions export const { doLogin, doClearLogin } = loginSlice.actions;
export const loadLoginState = (): ThunkAction<void, RootState, unknown, AnyAction> => export const loadLoginState =
(): ThunkAction<void, RootState, unknown, AnyAction> =>
(dispatch, getState) => { (dispatch, getState) => {
if (getState().login.login) { if (getState().login.login) {
return return;
} }
const infoJson = localStorage.getItem('loginInfo') const infoJson = localStorage.getItem("loginInfo");
const jwtToken = localStorage.getItem('token'); const jwtToken = localStorage.getItem("token");
if (infoJson && jwtToken) { if (infoJson && jwtToken) {
const decodedJwt = jwt_decode(jwtToken) as { exp: number }; const decodedJwt = jwt_decode(jwtToken) as { exp: number };
if (decodedJwt.exp < Date.now() / 1000) { if (decodedJwt.exp < Date.now() / 1000) {
return return;
} }
const info = JSON.parse(infoJson) as storedInfo const info = JSON.parse(infoJson) as storedInfo;
const payload: TokenResp = { const payload: TokenResp = {
...info, ...info,
status: 200, status: 200,
token: jwtToken, token: jwtToken,
} };
dispatch(doLogin(payload)) dispatch(doLogin(payload));
} }
} };
export const clearLoginStatus = (): ThunkAction<void, RootState, unknown, AnyAction> => export const clearLoginStatus =
(dispatch) => { (): ThunkAction<void, RootState, unknown, AnyAction> => (dispatch) => {
localStorage.removeItem('loginInfo') localStorage.removeItem("loginInfo");
localStorage.removeItem('token') localStorage.removeItem("token");
dispatch(doClearLogin()) dispatch(doClearLogin());
} };
export const loginSelector = (state: RootState) => state.login export const loginSelector = (state: RootState) => state.login;
export default loginSlice.reducer export default loginSlice.reducer;

View File

@ -1,34 +1,34 @@
interface QQGroup { interface QQGroup {
id: string, id: string;
name: string, name: string;
} }
export interface LoginStatus { export interface LoginStatus {
login: boolean login: boolean;
type: string type: string;
name: string name: string;
id: string id: string;
// groups: Array<QQGroup> // groups: Array<QQGroup>
token: string, token: string;
failed: boolean, failed: boolean;
} }
export type LoginContextType = { export type LoginContextType = {
login: LoginStatus login: LoginStatus;
save: (status: LoginStatus) => void save: (status: LoginStatus) => void;
} };
export interface SubscribeConfig { export interface SubscribeConfig {
platformName: string platformName: string;
target: string target: string;
targetName: string targetName: string;
cats: Array<number> cats: Array<number>;
tags: Array<string> tags: Array<string>;
} }
export interface GlobalConf { export interface GlobalConf {
platformConf: AllPlatformConf, platformConf: AllPlatformConf;
loaded: boolean loaded: boolean;
} }
export interface AllPlatformConf { export interface AllPlatformConf {
@ -36,34 +36,34 @@ export interface AllPlatformConf {
} }
export interface CategoryConfig { export interface CategoryConfig {
[idx: number]: string [idx: number]: string;
} }
export interface PlatformConfig { export interface PlatformConfig {
name: string name: string;
categories: CategoryConfig categories: CategoryConfig;
enabledTag: boolean, enabledTag: boolean;
platformName: string, platformName: string;
hasTarget: boolean hasTarget: boolean;
} }
export interface TokenResp { export interface TokenResp {
status: number, status: number;
token: string, token: string;
type: string, type: string;
id: string id: string;
name: string name: string;
} }
export interface SubscribeGroupDetail { export interface SubscribeGroupDetail {
name: string, name: string;
subscribes: Array<SubscribeConfig> subscribes: Array<SubscribeConfig>;
} }
export interface SubscribeResp { export interface SubscribeResp {
[idx: string]: SubscribeGroupDetail [idx: string]: SubscribeGroupDetail;
} }
export interface TargetNameResp { export interface TargetNameResp {
targetName: string targetName: string;
} }