Compare commits
8 Commits
2c70713339
...
322917982e
Author | SHA1 | Date | |
---|---|---|---|
322917982e | |||
7553091ada | |||
9e7cb8b6fa | |||
fef1100cd7 | |||
cd0238d6b9 | |||
47d8ed51fc | |||
c796c83626 | |||
013a4a4e90 |
@ -1,36 +1,44 @@
|
|||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
env: {
|
||||||
env: { browser: true, es2020: true },
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
extends: [
|
extends: [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:react/recommended",
|
||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
"prettier"
|
"eslint-config-prettier",
|
||||||
],
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
env: { node: true },
|
||||||
|
files: [".eslintrc.{js,cjs}"],
|
||||||
|
parserOptions: { sourceType: "script" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
settings: { react: { version: "detect" } },
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
project: true,
|
project: true,
|
||||||
tsconfigRootDir: __dirname
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
plugins: ["react-refresh"],
|
plugins: ["@typescript-eslint", "react", "react-refresh", "prettier"],
|
||||||
rules: {
|
rules: {
|
||||||
"react-refresh/only-export-components": [
|
"linebreak-style": ["error", "unix"],
|
||||||
"warn",
|
"quotes": ["error", "double"],
|
||||||
{ allowConstantExport: true }
|
"react/react-in-jsx-scope": ["off"],
|
||||||
],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
"argsIgnorePattern": "^_",
|
"argsIgnorePattern": "^_",
|
||||||
"varsIgnorePattern": "^_",
|
"varsIgnorePattern": "^_",
|
||||||
"caughtErrorsIgnorePattern": "^_"
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
"prettier/prettier": ["error"],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.eslintrc.cjs
|
||||||
|
pnpm-lock.yaml
|
@ -1,3 +1,9 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 4
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all"
|
||||||
}
|
}
|
||||||
|
13
package.json
13
package.json
@ -12,17 +12,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@ant-design/pro-components": "^2.6.43",
|
"@ant-design/pro-components": "^2.6.43",
|
||||||
"@sentry/react": "^7.90.0",
|
"@sentry/react": "^7.91.0",
|
||||||
"@sentry/vite-plugin": "^2.10.2",
|
"@sentry/vite-plugin": "^2.10.2",
|
||||||
"ace-builds": "^1.32.2",
|
"ace-builds": "^1.32.2",
|
||||||
"antd": "^5.12.4",
|
"antd": "^5.12.5",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"github-markdown-css": "^5.5.0",
|
"github-markdown-css": "^5.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-ace": "^10.1.0",
|
"react-ace": "^10.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.21.0",
|
"react-router-dom": "^6.21.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"rehype-mathjax": "^4.0.3",
|
"rehype-mathjax": "^4.0.3",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
@ -35,11 +36,13 @@
|
|||||||
"@types/react": "^18.2.45",
|
"@types/react": "^18.2.45",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^6.15.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.1",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"prettier": "3.0.0",
|
"prettier": "3.0.0",
|
||||||
|
3109
pnpm-lock.yaml
3109
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,6 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
axios.defaults.baseURL =
|
axios.defaults.baseURL = import.meta.env.MODE === "production" ? "/api/v1" : "http://127.0.0.1:8000/api/v1";
|
||||||
import.meta.env.MODE === "production"
|
|
||||||
? "/api/v1"
|
|
||||||
: "http://127.0.0.1:8000/api/v1";
|
|
||||||
|
|
||||||
export interface ResponseWrap<T> {
|
export interface ResponseWrap<T> {
|
||||||
code: number;
|
code: number;
|
||||||
@ -11,18 +8,19 @@ export interface ResponseWrap<T> {
|
|||||||
body: T;
|
body: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Meta {
|
export interface WithCount<T> {
|
||||||
ID: number;
|
count: number;
|
||||||
CreatedAt: number;
|
data: T[];
|
||||||
UpdatedAt: number;
|
|
||||||
DeletedAt: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function send<D, T>(
|
export interface Meta {
|
||||||
api: string,
|
ID: number;
|
||||||
data?: D,
|
CreatedAt: Date;
|
||||||
token?: string,
|
UpdatedAt: Date;
|
||||||
): Promise<T> {
|
DeletedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function send<D, T>(api: string, data?: D, token?: string): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const resp = await axios.post<ResponseWrap<T>>(api, data, {
|
const resp = await axios.post<ResponseWrap<T>>(api, data, {
|
||||||
headers: { Authorization: token },
|
headers: { Authorization: token },
|
||||||
|
@ -50,10 +50,7 @@ export interface UploadResp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ProblemApi {
|
export class ProblemApi {
|
||||||
static async CreateVersion(
|
static async CreateVersion(data: CreateVersionReq, token: string): Promise<void> {
|
||||||
data: CreateVersionReq,
|
|
||||||
token: string,
|
|
||||||
): Promise<void> {
|
|
||||||
return send("/problem/create_version", data, token);
|
return send("/problem/create_version", data, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Meta, send } from "./base.ts";
|
import { Meta, send, WithCount } from "./base.ts";
|
||||||
import { SubmissionInfo } from "./submission.ts";
|
import { SubmissionInfo } from "./submission.ts";
|
||||||
|
|
||||||
export enum Verdict {
|
export enum Verdict {
|
||||||
@ -37,6 +37,11 @@ export interface StatusInfo {
|
|||||||
is_enabled: boolean;
|
is_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SubmissionWithPoint {
|
||||||
|
submission: SubmissionInfo;
|
||||||
|
point: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueryReq {
|
export interface QueryReq {
|
||||||
pid?: number;
|
pid?: number;
|
||||||
uid?: number;
|
uid?: number;
|
||||||
@ -44,11 +49,6 @@ export interface QueryReq {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryResp {
|
|
||||||
submission: SubmissionInfo;
|
|
||||||
point: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryBySubmissionReq {
|
export interface QueryBySubmissionReq {
|
||||||
sid: number;
|
sid: number;
|
||||||
}
|
}
|
||||||
@ -59,21 +59,21 @@ export interface QueryByVersionReq {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QueryByVersionResp {
|
||||||
|
count: number;
|
||||||
|
status: StatusInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
export class StatusApi {
|
export class StatusApi {
|
||||||
static async Query(data: QueryReq): Promise<QueryResp[]> {
|
static async Query(data: QueryReq, token: string): Promise<WithCount<SubmissionWithPoint>> {
|
||||||
return send("/status/query", data);
|
return send("/status/query", data, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async QueryBySubmission(
|
static async QueryBySubmission(data: QueryBySubmissionReq, token: string): Promise<StatusInfo> {
|
||||||
data: QueryBySubmissionReq,
|
return send("/status/query/submission", data, token);
|
||||||
): Promise<StatusInfo> {
|
|
||||||
return send("/status/query/submission", data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async QueryByVersion(
|
static async QueryByVersion(data: QueryByVersionReq, token: string): Promise<WithCount<StatusInfo>> {
|
||||||
data: QueryByVersionReq,
|
|
||||||
token: string,
|
|
||||||
): Promise<StatusInfo[]> {
|
|
||||||
return send("/status/query/version", data, token);
|
return send("/status/query/version", data, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Meta, send } from "./base.ts";
|
import { Meta, send } from "./base.ts";
|
||||||
import { UserProfile } from "./user.ts";
|
import { UserProfile } from "./user.ts";
|
||||||
|
import { ProblemInfo } from "./problem.ts";
|
||||||
|
|
||||||
export interface SubmissionInfo {
|
export interface SubmissionInfo {
|
||||||
meta: Meta;
|
meta: Meta;
|
||||||
problem_id: number;
|
problem: ProblemInfo;
|
||||||
user: UserProfile;
|
user: UserProfile;
|
||||||
language: string;
|
language: string;
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { Meta, send } from "./base.ts";
|
import { Meta, send } from "./base.ts";
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
Admin = 30,
|
||||||
|
User = 20,
|
||||||
|
Guest = 10,
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserReq {
|
export interface UserReq {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
@ -1,35 +1,20 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { Navigate } from "react-router-dom";
|
|
||||||
import { UserApi } from "../api/user.ts";
|
import { UserApi } from "../api/user.ts";
|
||||||
|
import { useAuth } from "./hook.ts";
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
token: string;
|
token: string;
|
||||||
nickname: string;
|
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
login: (
|
login: (username: string, password: string, onSuccess?: VoidFunction, onFailed?: (error: string) => void) => void;
|
||||||
username: string,
|
logout: (onSuccess?: VoidFunction, onFailed?: (error: string) => void) => void;
|
||||||
password: string,
|
|
||||||
onSuccess?: VoidFunction,
|
|
||||||
onFailed?: (error: string) => void,
|
|
||||||
) => void;
|
|
||||||
logout: (
|
|
||||||
onSuccess?: VoidFunction,
|
|
||||||
onFailed?: (error: string) => void,
|
|
||||||
) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = React.createContext<AuthContextType>({
|
const AuthContext = React.createContext<AuthContextType>({
|
||||||
token: "",
|
token: "",
|
||||||
nickname: "guest",
|
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
login: (
|
login: (_username: string, _password: string, _onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
|
||||||
_username: string,
|
|
||||||
_password: string,
|
|
||||||
_onSuccess?: VoidFunction,
|
|
||||||
onFailed?: (error: string) => void,
|
|
||||||
) => {
|
|
||||||
onFailed && onFailed("not implemented");
|
onFailed && onFailed("not implemented");
|
||||||
},
|
},
|
||||||
logout: (_onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
|
logout: (_onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
|
||||||
@ -39,7 +24,6 @@ const AuthContext = React.createContext<AuthContextType>({
|
|||||||
|
|
||||||
function AuthProvider({ children }: { children: React.ReactNode }) {
|
function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [token, setToken] = useState("");
|
const [token, setToken] = useState("");
|
||||||
const [nickname, setNickname] = useState("guest");
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -60,7 +44,6 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
localStorage.setItem("token", resp.token);
|
localStorage.setItem("token", resp.token);
|
||||||
setToken(resp.token);
|
setToken(resp.token);
|
||||||
setNickname(resp.nickname);
|
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
onSuccess && onSuccess();
|
onSuccess && onSuccess();
|
||||||
})
|
})
|
||||||
@ -71,13 +54,9 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = (
|
const logout = (onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
|
||||||
onSuccess?: VoidFunction,
|
|
||||||
onFailed?: (error: string) => void,
|
|
||||||
) => {
|
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
setToken("");
|
setToken("");
|
||||||
setNickname("guest");
|
|
||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
@ -95,20 +74,19 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = { token, nickname, isLoggedIn, login, logout };
|
const value = { token, isLoggedIn, login, logout };
|
||||||
|
|
||||||
return (
|
return <AuthContext.Provider value={value}> {children} </AuthContext.Provider>;
|
||||||
<AuthContext.Provider value={value}> {children} </AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequireAuth({ children }: { children: React.ReactNode }) {
|
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
const auth = React.useContext(AuthContext);
|
const auth = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
if (!auth.token) {
|
useEffect(() => {
|
||||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
if (!auth.isLoggedIn) navigate("/login", { state: { from: location } });
|
||||||
}
|
}, [auth.isLoggedIn, location, navigate]);
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,7 @@ interface LoginFormProps<T> {
|
|||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MyLoginForm<T = LoginFormValues>(
|
export default function MyLoginForm<T = LoginFormValues>(props: LoginFormProps<T>) {
|
||||||
props: LoginFormProps<T>,
|
|
||||||
) {
|
|
||||||
const [msg, msgContextHolder] = message.useMessage();
|
const [msg, msgContextHolder] = message.useMessage();
|
||||||
const [errMsg, setErrMsg] = useState("");
|
const [errMsg, setErrMsg] = useState("");
|
||||||
|
|
||||||
|
@ -20,24 +20,20 @@ export default function Markdown(props: MarkdownProps) {
|
|||||||
const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg];
|
const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg];
|
||||||
|
|
||||||
const renderers: Components = {
|
const renderers: Components = {
|
||||||
code: function makeCodeBlock({
|
code: function makeCodeBlock({ inline, className, children, ...props }) {
|
||||||
inline,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
const codeBlock = (
|
const codeBlock = (
|
||||||
<PrismAsync
|
<PrismAsync
|
||||||
{...props}
|
{...props}
|
||||||
language={(match || ["", "text"])[1]}
|
language={(match || ["", "text"])[1]}
|
||||||
PreTag="div"
|
PreTag="div"
|
||||||
children={String(children).replace(/\n$/, "")}
|
|
||||||
wrapLines={true}
|
wrapLines={true}
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
wrapLongLines={true}
|
wrapLongLines={true}
|
||||||
style={oneLight}
|
style={oneLight}
|
||||||
/>
|
>
|
||||||
|
{String(children).replace(/\n$/, "")}
|
||||||
|
</PrismAsync>
|
||||||
);
|
);
|
||||||
const codeInline = <code {...props}>{children}</code>;
|
const codeInline = <code {...props}>{children}</code>;
|
||||||
return !inline && match ? codeBlock : codeInline;
|
return !inline && match ? codeBlock : codeInline;
|
||||||
@ -48,11 +44,12 @@ export default function Markdown(props: MarkdownProps) {
|
|||||||
<>
|
<>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
className={"markdown-body"}
|
className={"markdown-body"}
|
||||||
children={props.markdown}
|
|
||||||
remarkPlugins={remarkPlugins}
|
remarkPlugins={remarkPlugins}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
components={renderers}
|
components={renderers}
|
||||||
/>
|
>
|
||||||
|
{props.markdown}
|
||||||
|
</ReactMarkdown>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, useLoaderData } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Collapse, CollapseProps, Descriptions, Space, Tag } from "antd";
|
import { Collapse, CollapseProps, Descriptions, Space, Tag } from "antd";
|
||||||
|
|
||||||
import { DetailsResp } from "../api/problem.ts";
|
import { DetailsResp } from "../api/problem.ts";
|
||||||
|
|
||||||
interface ProblemDetailsProps {
|
interface ProblemDetailsProps {
|
||||||
action: React.ReactNode;
|
details: DetailsResp;
|
||||||
|
action?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProblemDetails(props: ProblemDetailsProps) {
|
export default function ProblemDetails(props: ProblemDetailsProps) {
|
||||||
const details = useLoaderData() as DetailsResp;
|
const details = props.details;
|
||||||
|
|
||||||
const problemInfo = (
|
const problemInfo = (
|
||||||
<Descriptions bordered column={1} size="small">
|
<Descriptions bordered column={1} size="small">
|
||||||
|
<Descriptions.Item label="Title">
|
||||||
|
<Link to={`/problem/${details.problem.meta.ID}`}>{details.problem.title}</Link>
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="Provider">
|
<Descriptions.Item label="Provider">
|
||||||
<Link to={`/user/${details.problem.provider.meta.ID}`}>
|
<Link to={`/profile/${details.problem.provider.meta.ID}`}>{details.problem.provider.nick_name}</Link>
|
||||||
{details.problem.provider.nick_name}
|
|
||||||
</Link>
|
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="Supported Languages">
|
<Descriptions.Item label="Supported Languages">
|
||||||
<Space size={[0, 8]} wrap>
|
<Space size={[0, 8]} wrap>
|
||||||
@ -25,23 +27,15 @@ export default function ProblemDetails(props: ProblemDetailsProps) {
|
|||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="Task Nums">
|
<Descriptions.Item label="Task Nums">{details.context.Tasks.length}</Descriptions.Item>
|
||||||
{details.context.Tasks.length}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
);
|
);
|
||||||
|
|
||||||
const runtimeLimit = (
|
const runtimeLimit = (
|
||||||
<Descriptions bordered column={1} size="small">
|
<Descriptions bordered column={1} size="small">
|
||||||
<Descriptions.Item label="Time Limit">
|
<Descriptions.Item label="Time Limit">{details.context.Runtime.TimeLimit} ms</Descriptions.Item>
|
||||||
{details.context.Runtime.TimeLimit} ms
|
<Descriptions.Item label="Memory Limit">{details.context.Runtime.MemoryLimit} MB</Descriptions.Item>
|
||||||
</Descriptions.Item>
|
<Descriptions.Item label="Process Limit">{details.context.Runtime.NProcLimit}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Memory Limit">
|
|
||||||
{details.context.Runtime.MemoryLimit} MB
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Process Limit">
|
|
||||||
{details.context.Runtime.NProcLimit}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -56,16 +50,25 @@ export default function ProblemDetails(props: ProblemDetailsProps) {
|
|||||||
label: "Runtime Limit",
|
label: "Runtime Limit",
|
||||||
children: runtimeLimit,
|
children: runtimeLimit,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "3",
|
|
||||||
label: "Action",
|
|
||||||
children: props.action,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const items: CollapseProps["items"] = [
|
||||||
|
...miscItems,
|
||||||
|
...(props.action
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "3",
|
||||||
|
label: "Action",
|
||||||
|
children: props.action,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
const defaultActiveKey = ["1", "2", ...(props.action ? ["3"] : [])];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Collapse items={miscItems} defaultActiveKey={["1", "2", "3"]} />
|
<Collapse items={items} defaultActiveKey={defaultActiveKey} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
128
src/components/status-table.tsx
Normal file
128
src/components/status-table.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ProColumns, ProTable } from "@ant-design/pro-components";
|
||||||
|
import { Button, Space } from "antd";
|
||||||
|
|
||||||
|
import { StatusApi } from "../api/status.ts";
|
||||||
|
|
||||||
|
interface SubmissionInfo {
|
||||||
|
id: number;
|
||||||
|
problem_title: string;
|
||||||
|
problem_id: number;
|
||||||
|
nick_name: string;
|
||||||
|
user_id: number;
|
||||||
|
point: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: ProColumns<SubmissionInfo>[] = [
|
||||||
|
{
|
||||||
|
title: "ID",
|
||||||
|
dataIndex: "id",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Problem",
|
||||||
|
dataIndex: "problem_title",
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "User",
|
||||||
|
dataIndex: "nick_name",
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Point",
|
||||||
|
dataIndex: "point",
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Date",
|
||||||
|
dataIndex: "date",
|
||||||
|
valueType: "dateTime",
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Action",
|
||||||
|
dataIndex: "action",
|
||||||
|
render: (_node, entity) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Button>
|
||||||
|
<Link to={`/problem/${entity.problem_id}`}>查看问题</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button>
|
||||||
|
<Link to={`/profile/${entity.user_id}`}>查看作者</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button>
|
||||||
|
<Link to={`/details/${entity.id}`}>查看详情</Link>
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface StatusTableProps {
|
||||||
|
title?: string;
|
||||||
|
pid?: number;
|
||||||
|
uid?: number;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatusTable(props: StatusTableProps) {
|
||||||
|
const request = async (
|
||||||
|
params: Record<string, string> & {
|
||||||
|
pageSize?: number;
|
||||||
|
current?: number;
|
||||||
|
keyword?: string | undefined;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const cur = params.current || 1;
|
||||||
|
const size = params.pageSize || 10;
|
||||||
|
|
||||||
|
const res = await StatusApi.Query(
|
||||||
|
{
|
||||||
|
pid: props.pid,
|
||||||
|
uid: props.uid,
|
||||||
|
offset: (cur - 1) * (size || 10),
|
||||||
|
limit: size,
|
||||||
|
},
|
||||||
|
props.token,
|
||||||
|
);
|
||||||
|
const data = res.data.map((i) => {
|
||||||
|
return {
|
||||||
|
id: i.submission.meta.ID,
|
||||||
|
problem_title: i.submission.problem.title,
|
||||||
|
problem_id: i.submission.problem.meta.ID,
|
||||||
|
nick_name: i.submission.user.nick_name,
|
||||||
|
user_id: i.submission.user.meta.ID,
|
||||||
|
point: i.point === -1 ? "Pending" : i.point.toString(),
|
||||||
|
date: i.submission.meta.CreatedAt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { success: true, data: data, total: res.count };
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return { success: false, data: [], total: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProTable<SubmissionInfo>
|
||||||
|
cardBordered
|
||||||
|
columns={columns}
|
||||||
|
request={request}
|
||||||
|
rowKey={(record) => record.id}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
}}
|
||||||
|
search={false}
|
||||||
|
dateFormatter="string"
|
||||||
|
headerTitle={props.title || "Judge Status"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
41
src/components/user.tsx
Normal file
41
src/components/user.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { Collapse, CollapseProps, Descriptions } from "antd";
|
||||||
|
|
||||||
|
import { UserApi, UserProfile, UserRole } from "../api/user.ts";
|
||||||
|
|
||||||
|
interface UserInfoProps {
|
||||||
|
uid: number;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserInfo(props: UserInfoProps) {
|
||||||
|
const [profile, setProfile] = useState<UserProfile>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void UserApi.Profile({ uid: props.uid }, props.token).then(setProfile);
|
||||||
|
}, [props.uid, props.token]);
|
||||||
|
|
||||||
|
const roleName = Object.entries(UserRole).filter(([, v]) => v === profile?.role);
|
||||||
|
|
||||||
|
const items: CollapseProps["items"] = [
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: "User Info",
|
||||||
|
children: (
|
||||||
|
<Descriptions bordered column={1} size="small">
|
||||||
|
<Descriptions.Item label="Nickname">{profile?.nick_name || "Not Found"}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Joined at">
|
||||||
|
{(profile?.meta.CreatedAt && dayjs(profile?.meta.CreatedAt).format("YYYY-MM-DD HH:mm:ss")) ||
|
||||||
|
"N/A"}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Role">
|
||||||
|
{(roleName.length > 0 && roleName[0][0]) || "N/A"}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Collapse items={items} defaultActiveKey={["1"]} />;
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
"Helvetica Neue", sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
@ -9,6 +8,5 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,7 @@ Sentry.init({
|
|||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sentryCreateBrowserRouter =
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter);
|
||||||
Sentry.wrapCreateBrowserRouter(createBrowserRouter);
|
|
||||||
|
|
||||||
const router = sentryCreateBrowserRouter([
|
const router = sentryCreateBrowserRouter([
|
||||||
{
|
{
|
||||||
|
124
src/pages/details.tsx
Normal file
124
src/pages/details.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Card, Col, Row, Tabs, TabsProps, Tooltip } from "antd";
|
||||||
|
|
||||||
|
import { PrismAsync } from "react-syntax-highlighter";
|
||||||
|
import { oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
|
|
||||||
|
import ProblemDetails from "../components/problem-details.tsx";
|
||||||
|
import { useAuth } from "../components/hook.ts";
|
||||||
|
import { DetailsResp } from "../api/problem.ts";
|
||||||
|
import { ProblemLoader } from "../api/loader.ts";
|
||||||
|
import { StatusApi, StatusInfo, Verdict } from "../api/status.ts";
|
||||||
|
|
||||||
|
const VerdictMap: {
|
||||||
|
[key in Verdict]: { color: string; tip: string; label: string; name: string };
|
||||||
|
} = {
|
||||||
|
[Verdict.Accepted]: { color: "#52C41A", tip: "#72E43A", label: "AC", name: "Accepted" },
|
||||||
|
[Verdict.WrongAnswer]: { color: "#E74C3C", tip: "#FF6C5C", label: "WA", name: "Wrong Answer" },
|
||||||
|
[Verdict.JuryFailed]: { color: "#0E1D69", tip: "#2E3D89", label: "JF", name: "Jury Failed" },
|
||||||
|
[Verdict.PartialCorrect]: { color: "#E67E22", tip: "#FF9E42", label: "PC", name: "Partial Correct" },
|
||||||
|
[Verdict.TimeLimitExceeded]: { color: "#052242", tip: "#254262", label: "TLE", name: "Time Limit Exceeded" },
|
||||||
|
[Verdict.MemoryLimitExceeded]: { color: "#052242", tip: "#254262", label: "MLE", name: "Memory Limit Exceeded" },
|
||||||
|
[Verdict.RuntimeError]: { color: "#9D3DCF", tip: "#BD5DEF", label: "RE", name: "Runtime Error" },
|
||||||
|
[Verdict.CompileError]: { color: "#FADB14", tip: "#FFFB34", label: "CE", name: "Compile Error" },
|
||||||
|
[Verdict.SystemError]: { color: "#0E1D69", tip: "#2E3D89", label: "UKE", name: "System Error" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const gridStyle: React.CSSProperties = {
|
||||||
|
width: "5.5em",
|
||||||
|
height: "5.5em",
|
||||||
|
margin: "0.2em",
|
||||||
|
fontSize: "1.5em",
|
||||||
|
padding: "0.5em",
|
||||||
|
textAlign: "center",
|
||||||
|
color: "white",
|
||||||
|
};
|
||||||
|
|
||||||
|
const topLeftStyle: React.CSSProperties = {
|
||||||
|
position: "absolute",
|
||||||
|
fontSize: "0.7em",
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyle: React.CSSProperties = {
|
||||||
|
fontSize: "1.2em",
|
||||||
|
paddingTop: "0.6em",
|
||||||
|
textAlign: "center",
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusStyle: React.CSSProperties = {
|
||||||
|
fontSize: "0.65em",
|
||||||
|
textAlign: "center",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DetailsPage() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const { id } = useParams();
|
||||||
|
const id_num = +(id || "0");
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<StatusInfo>();
|
||||||
|
const [details, setDetails] = useState<DetailsResp>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void StatusApi.QueryBySubmission({ sid: id_num }, auth.token).then(setStatus);
|
||||||
|
}, [id_num, auth.token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!status) return;
|
||||||
|
void ProblemLoader({
|
||||||
|
params: { id: status.submission.problem.meta.ID.toString() },
|
||||||
|
}).then(setDetails);
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
const taskInfo = (
|
||||||
|
<Card style={{ padding: "1em" }}>
|
||||||
|
{status?.context?.tasks?.map((task) => (
|
||||||
|
<Card.Grid key={task.id} style={{ ...gridStyle, background: VerdictMap[task.verdict].color }}>
|
||||||
|
<Tooltip title={task.message} color={VerdictMap[task.verdict].tip}>
|
||||||
|
<div style={topLeftStyle}>{`#${task.id}`}</div>
|
||||||
|
<div>
|
||||||
|
<div style={labelStyle}>{VerdictMap[task.verdict].label}</div>
|
||||||
|
<div style={statusStyle}>{`${task.real_time}ms/${task.memory}KB`}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Card.Grid>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceCode = (
|
||||||
|
<PrismAsync
|
||||||
|
language={status?.submission.code === "" ? "text" : status?.submission.language || "text"}
|
||||||
|
wrapLines={true}
|
||||||
|
showLineNumbers={true}
|
||||||
|
wrapLongLines={true}
|
||||||
|
style={oneLight}
|
||||||
|
>
|
||||||
|
{status?.submission.code || "Not Available"}
|
||||||
|
</PrismAsync>
|
||||||
|
);
|
||||||
|
|
||||||
|
const items: TabsProps["items"] = [
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: "Task Info",
|
||||||
|
children: taskInfo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2",
|
||||||
|
label: "Source Code",
|
||||||
|
children: sourceCode,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const body = <Tabs defaultActiveKey="1" items={items} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row justify="center" align="top" gutter={[16, 16]}>
|
||||||
|
<Col span={18}>{body}</Col>
|
||||||
|
<Col span={6}>{details && <ProblemDetails details={details} />}</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { useRouteError, isRouteErrorResponse, useNavigate } from "react-router-dom";
|
||||||
useRouteError,
|
|
||||||
isRouteErrorResponse,
|
|
||||||
useNavigate,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import { Button, Result, Space } from "antd";
|
import { Button, Result, Space } from "antd";
|
||||||
|
|
||||||
const ErrorPage = () => {
|
const ErrorPage = () => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useAuth } from "../components/hook.ts";
|
import { useAuth } from "../components/hook.ts";
|
||||||
@ -7,6 +8,22 @@ export default function LoginPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (isFirstRender.current) {
|
||||||
|
if (auth.isLoggedIn) {
|
||||||
|
if (window.history?.length) navigate(-1);
|
||||||
|
else navigate("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isFirstRender.current = false;
|
||||||
|
return () => void (isFirstRender.current = true);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const onFinish: CallbackType = (values, setErrMsg, msg) => {
|
const onFinish: CallbackType = (values, setErrMsg, msg) => {
|
||||||
auth.login(
|
auth.login(
|
||||||
values.username,
|
values.username,
|
||||||
|
@ -14,25 +14,18 @@ export default function ProblemPage() {
|
|||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlayCircleOutlined />}
|
||||||
onClick={() =>
|
onClick={() => navigate(`/problem/${details.problem.meta.ID}/submit`)}
|
||||||
navigate(`/problem/${details.problem.meta.ID}/submit`)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button icon={<SearchOutlined />} onClick={() => navigate(`/problem/${details.problem.meta.ID}/status`)}>
|
||||||
icon={<SearchOutlined />}
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/problem/${details.problem.meta.ID}/status`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Status
|
Status
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProblemStatement = <Markdown markdown={details.problem.statement} />;
|
const ProblemStatement = <Markdown markdown={details.problem.statement} />;
|
||||||
const MiscPanel = <ProblemDetails action={actionBtn} />;
|
const MiscPanel = <ProblemDetails details={details} action={actionBtn} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
25
src/pages/profile.tsx
Normal file
25
src/pages/profile.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Col, Row } from "antd";
|
||||||
|
|
||||||
|
import { useAuth } from "../components/hook.ts";
|
||||||
|
import StatusTable from "../components/status-table.tsx";
|
||||||
|
import UserInfo from "../components/user.tsx";
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const { id } = useParams();
|
||||||
|
const id_num = +(id || "0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row justify="center" align="top" gutter={[16, 16]}>
|
||||||
|
<Col span={18}>
|
||||||
|
<StatusTable uid={id_num} token={auth.token} />
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<UserInfo uid={id_num} token={auth.token} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -15,20 +15,14 @@ type RegisterFormValues = {
|
|||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onFinish: CallbackType<RegisterFormValues> = (
|
const onFinish: CallbackType<RegisterFormValues> = (values, setErrMsg, msg) => {
|
||||||
values,
|
|
||||||
setErrMsg,
|
|
||||||
msg,
|
|
||||||
) => {
|
|
||||||
UserApi.Create({
|
UserApi.Create({
|
||||||
username: values.username,
|
username: values.username,
|
||||||
nickname: values.nickname,
|
nickname: values.nickname,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
void msg
|
void msg.success("注册成功").then(() => navigate("/login", { replace: true }));
|
||||||
.success("注册成功")
|
|
||||||
.then(() => navigate("/login", { replace: true }));
|
|
||||||
})
|
})
|
||||||
.catch((err: string) => setErrMsg(err));
|
.catch((err: string) => setErrMsg(err));
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,8 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import { DefaultFooter, PageContainer, ProLayout, ProLayoutProps } from "@ant-design/pro-components";
|
||||||
DefaultFooter,
|
import { BlockOutlined, LoginOutlined, LogoutOutlined, PlusCircleOutlined, ProfileOutlined } from "@ant-design/icons";
|
||||||
PageContainer,
|
|
||||||
ProLayout,
|
|
||||||
ProLayoutProps,
|
|
||||||
} from "@ant-design/pro-components";
|
|
||||||
import {
|
|
||||||
BlockOutlined,
|
|
||||||
LoginOutlined,
|
|
||||||
LogoutOutlined,
|
|
||||||
PlusCircleOutlined,
|
|
||||||
ProfileOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { Dropdown, message, Skeleton } from "antd";
|
import { Dropdown, message, Skeleton } from "antd";
|
||||||
|
|
||||||
import { NavConfigs } from "../routes.tsx";
|
import { NavConfigs } from "../routes.tsx";
|
||||||
@ -36,9 +25,7 @@ const LayoutProps: ProLayoutProps = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
menuItemRender: (item, dom) => <Link to={item.path || "/"}>{dom}</Link>,
|
menuItemRender: (item, dom) => <Link to={item.path || "/"}>{dom}</Link>,
|
||||||
footerRender: () => (
|
footerRender: () => <DefaultFooter copyright="2023 WOJ Created by WHUPRJ" />,
|
||||||
<DefaultFooter copyright="2023 WOJ Created by WHUPRJ" />
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvatarUserItems = [
|
const AvatarUserItems = [
|
||||||
@ -115,9 +102,7 @@ export default function Root() {
|
|||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: auth.isLoggedIn
|
items: auth.isLoggedIn ? AvatarUserItems : AvatarGuestItems,
|
||||||
? AvatarUserItems
|
|
||||||
: AvatarGuestItems,
|
|
||||||
onClick: (key) => avatarActions[key.key](),
|
onClick: (key) => avatarActions[key.key](),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -128,11 +113,7 @@ export default function Root() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProLayout
|
<ProLayout {...LayoutProps} location={{ pathname: curTab }} avatarProps={avatarProps}>
|
||||||
{...LayoutProps}
|
|
||||||
location={{ pathname: curTab }}
|
|
||||||
avatarProps={avatarProps}
|
|
||||||
>
|
|
||||||
{msgContextHolder}
|
{msgContextHolder}
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<React.Suspense fallback={<Skeleton />}>
|
<React.Suspense fallback={<Skeleton />}>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { ProColumns, ProTable } from "@ant-design/pro-components";
|
import { ProColumns, ProTable } from "@ant-design/pro-components";
|
||||||
|
import { Button } from "antd";
|
||||||
|
|
||||||
import { ProblemApi } from "../api/problem.ts";
|
import { ProblemApi } from "../api/problem.ts";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
interface ProblemList {
|
interface ProblemList {
|
||||||
id: number;
|
id: number;
|
||||||
@ -32,7 +34,11 @@ const columns: ProColumns<ProblemList>[] = [
|
|||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
dataIndex: "action",
|
dataIndex: "action",
|
||||||
render: (_node, entity) => {
|
render: (_node, entity) => {
|
||||||
return <Link to={`/problem/${entity.id}`}>View</Link>;
|
return (
|
||||||
|
<Button>
|
||||||
|
<Link to={`/problem/${entity.id}`}>View</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
26
src/pages/status.tsx
Normal file
26
src/pages/status.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import StatusTable from "../components/status-table.tsx";
|
||||||
|
import { useLoaderData, useParams } from "react-router-dom";
|
||||||
|
import { useAuth } from "../components/hook.ts";
|
||||||
|
import { Col, Row } from "antd";
|
||||||
|
import ProblemDetails from "../components/problem-details.tsx";
|
||||||
|
import { DetailsResp } from "../api/problem.ts";
|
||||||
|
|
||||||
|
export default function ProblemStatusPage() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const details = useLoaderData() as DetailsResp;
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row justify="center" align="top" gutter={[16, 16]}>
|
||||||
|
<Col span={18}>
|
||||||
|
{" "}
|
||||||
|
<StatusTable pid={+(id || "0")} token={auth.token} />
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<ProblemDetails details={details} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -27,9 +27,7 @@ export default function SubmitPage() {
|
|||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [msg, msgContextHolder] = message.useMessage();
|
const [msg, msgContextHolder] = message.useMessage();
|
||||||
|
|
||||||
const langOptions = AvailLang.filter((l) =>
|
const langOptions = AvailLang.filter((l) => details.context.Languages.some((x) => x.Lang === l.value));
|
||||||
details.context.Languages.some((x) => x.Lang === l.value),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [lang, setLang] = useState(langOptions[0].value);
|
const [lang, setLang] = useState(langOptions[0].value);
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
@ -70,7 +68,7 @@ export default function SubmitPage() {
|
|||||||
onChange: setCode,
|
onChange: setCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const MiscPanel = <ProblemDetails action={funcMenu} />;
|
const MiscPanel = <ProblemDetails action={funcMenu} details={details} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -2,18 +2,17 @@ import React from "react";
|
|||||||
|
|
||||||
import { ProblemLoader } from "./api/loader.ts";
|
import { ProblemLoader } from "./api/loader.ts";
|
||||||
import { RequireAuth } from "./components/auth.tsx";
|
import { RequireAuth } from "./components/auth.tsx";
|
||||||
import {
|
import { HomeOutlined, ProfileOutlined, QuestionCircleOutlined } from "@ant-design/icons";
|
||||||
HomeOutlined,
|
|
||||||
ProfileOutlined,
|
|
||||||
QuestionCircleOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
|
|
||||||
|
const DetailsPage = React.lazy(() => import("./pages/details.tsx"));
|
||||||
const HomePage = React.lazy(() => import("./pages/home.tsx"));
|
const HomePage = React.lazy(() => import("./pages/home.tsx"));
|
||||||
const LoginPage = React.lazy(() => import("./pages/login.tsx"));
|
const LoginPage = React.lazy(() => import("./pages/login.tsx"));
|
||||||
const LogoutPage = React.lazy(() => import("./pages/logout.tsx"));
|
const LogoutPage = React.lazy(() => import("./pages/logout.tsx"));
|
||||||
|
const ProblemPage = React.lazy(() => import("./pages/problem.tsx"));
|
||||||
|
const ProblemStatusPage = React.lazy(() => import("./pages/status.tsx"));
|
||||||
|
const ProfilePage = React.lazy(() => import("./pages/profile.tsx"));
|
||||||
const RegisterPage = React.lazy(() => import("./pages/register.tsx"));
|
const RegisterPage = React.lazy(() => import("./pages/register.tsx"));
|
||||||
const SearchPage = React.lazy(() => import("./pages/search.tsx"));
|
const SearchPage = React.lazy(() => import("./pages/search.tsx"));
|
||||||
const ProblemPage = React.lazy(() => import("./pages/problem.tsx"));
|
|
||||||
const SubmitPage = React.lazy(() => import("./pages/submit.tsx"));
|
const SubmitPage = React.lazy(() => import("./pages/submit.tsx"));
|
||||||
|
|
||||||
const RouteConfigs = [
|
const RouteConfigs = [
|
||||||
@ -37,14 +36,6 @@ const RouteConfigs = [
|
|||||||
path: "register",
|
path: "register",
|
||||||
element: <RegisterPage />,
|
element: <RegisterPage />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "profile",
|
|
||||||
element: (
|
|
||||||
<RequireAuth>
|
|
||||||
<p>Profile</p>
|
|
||||||
</RequireAuth>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "search",
|
path: "search",
|
||||||
element: <SearchPage />,
|
element: <SearchPage />,
|
||||||
@ -63,6 +54,39 @@ const RouteConfigs = [
|
|||||||
),
|
),
|
||||||
loader: ProblemLoader,
|
loader: ProblemLoader,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "problem/:id/status",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ProblemStatusPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
loader: ProblemLoader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "profile",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ProfilePage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "profile/:id",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ProfilePage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "details/:id",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<DetailsPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const NavConfigs = [
|
const NavConfigs = [
|
||||||
|
Reference in New Issue
Block a user