first
This commit is contained in:
commit
a65ce00766
1
homework/README.md
Normal file
1
homework/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
此目录存放本周课后作业,可以在此文件添加作业设计思路和流程图等
|
||||||
26
homework/client/.eslintrc.json
Normal file
26
homework/client/.eslintrc.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2021,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": ["react", "prettier", "react-hooks"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"react/react-in-jsx-scope": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
homework/client/.gitignore
vendored
Normal file
23
homework/client/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
46
homework/client/README.md
Normal file
46
homework/client/README.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
13
homework/client/craco.config.js
Normal file
13
homework/client/craco.config.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
devServer: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://127.0.0.1:3001/",
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: {
|
||||||
|
"^/api": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
25999
homework/client/package-lock.json
generated
Normal file
25999
homework/client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
89
homework/client/package.json
Normal file
89
homework/client/package.json
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "./",
|
||||||
|
"dependencies": {
|
||||||
|
"@bytemd/plugin-gemoji": "^1.21.0",
|
||||||
|
"@bytemd/plugin-gfm": "^1.21.0",
|
||||||
|
"@bytemd/plugin-highlight-ssr": "^1.21.0",
|
||||||
|
"@bytemd/plugin-medium-zoom": "^1.21.0",
|
||||||
|
"@bytemd/react": "^1.21.0",
|
||||||
|
"@craco/craco": "^7.1.0",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/node": "^16.18.121",
|
||||||
|
"@types/react": "^18.3.16",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"antd": "^5.22.4",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-qr-code": "^2.0.15",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"react-router-dom": "^7.0.2",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"rehype-highlight": "^7.0.1",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "craco start",
|
||||||
|
"build": "craco build",
|
||||||
|
"test": "craco test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"prepare": "husky install"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"double"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.25.9",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
|
"@types/react-redux": "^7.1.34",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||||
|
"@typescript-eslint/parser": "^8.18.0",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"ajv-keywords": "^5.1.0",
|
||||||
|
"eslint": "^9.16.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-jest": "^28.9.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"eslint-plugin-react": "^7.37.2",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^15.2.11",
|
||||||
|
"prettier": "^3.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
homework/client/public/favicon.ico
Normal file
BIN
homework/client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
homework/client/public/index.html
Normal file
43
homework/client/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
homework/client/public/logo192.png
Normal file
BIN
homework/client/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
homework/client/public/logo512.png
Normal file
BIN
homework/client/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
homework/client/public/manifest.json
Normal file
25
homework/client/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
homework/client/public/robots.txt
Normal file
3
homework/client/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
195
homework/client/src/api/settings.ts
Normal file
195
homework/client/src/api/settings.ts
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import {
|
||||||
|
SubmitSettingsPayload,
|
||||||
|
SubmitSettingsResponse,
|
||||||
|
GetSettingsResponse,
|
||||||
|
CodeState,
|
||||||
|
GetCodeDetailsResponse,
|
||||||
|
UpdateSettingsPayload,
|
||||||
|
UpdateSettingsResponse,
|
||||||
|
DeleteSettingsResponse,
|
||||||
|
} from "../types/codeTypes";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const submitSettings = async (
|
||||||
|
payload: SubmitSettingsPayload,
|
||||||
|
): Promise<SubmitSettingsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post<SubmitSettingsResponse>(
|
||||||
|
"/api/tab",
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
data: response.data.data,
|
||||||
|
success: true,
|
||||||
|
message: response.data.message,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: error.response.data.message || "提交失败",
|
||||||
|
};
|
||||||
|
} else if (error.request) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: "未收到服务器响应",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: "提交过程中发生错误",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const updateCodeDetails = async (
|
||||||
|
id: string,
|
||||||
|
payload: UpdateSettingsPayload,
|
||||||
|
): Promise<UpdateSettingsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.put<UpdateSettingsResponse>(
|
||||||
|
`/api/tab/${id}`,
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
data: response.data.data,
|
||||||
|
success: true,
|
||||||
|
message: response.data.message,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: error.response.data.message || "更新失败",
|
||||||
|
};
|
||||||
|
} else if (error.request) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: "未收到服务器响应",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: "提交过程中发生错误",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const getSettings = async (): Promise<GetSettingsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get<GetSettingsResponse>("api/tab");
|
||||||
|
const data = {
|
||||||
|
tabs: response.data.tabs,
|
||||||
|
success: true,
|
||||||
|
message: response.data.message,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
tabs: [],
|
||||||
|
activeTabKey: "",
|
||||||
|
editorSettings: {
|
||||||
|
theme: "vs-dark",
|
||||||
|
},
|
||||||
|
title: "",
|
||||||
|
label: [],
|
||||||
|
content: "",
|
||||||
|
isEncrypted: false,
|
||||||
|
password: "",
|
||||||
|
date: "",
|
||||||
|
submitTime: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
message: "获取代码详情失败。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCodeDetails = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<GetCodeDetailsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get<GetCodeDetailsResponse>(
|
||||||
|
`/api/tab/${id}`,
|
||||||
|
);
|
||||||
|
const code: CodeState = response.data.data;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: code,
|
||||||
|
message: "获取成功",
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("获取代码详情时出错:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: {
|
||||||
|
id: "",
|
||||||
|
tabs: [],
|
||||||
|
activeTabKey: "",
|
||||||
|
editorSettings: {
|
||||||
|
theme: "vs-dark",
|
||||||
|
},
|
||||||
|
title: "",
|
||||||
|
label: [],
|
||||||
|
content: "",
|
||||||
|
isEncrypted: false,
|
||||||
|
password: "",
|
||||||
|
date: "",
|
||||||
|
submitTime: "",
|
||||||
|
},
|
||||||
|
message: "获取代码详情失败。", // 可选的错误消息
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const deleteSettings = async (
|
||||||
|
id: string,
|
||||||
|
): Promise<DeleteSettingsResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.delete<DeleteSettingsResponse>(
|
||||||
|
`/api/tab/${id}`,
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
data: response.data.data,
|
||||||
|
success: true,
|
||||||
|
message: response.data.message,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: error.response.data.message || "删除失败",
|
||||||
|
};
|
||||||
|
} else if (error.request) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: "未收到服务器响应",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: "",
|
||||||
|
message: "删除过程中发生错误",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
homework/client/src/assets/header.jpg
Normal file
BIN
homework/client/src/assets/header.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
homework/client/src/assets/threepoint.png
Normal file
BIN
homework/client/src/assets/threepoint.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
133
homework/client/src/components/CodeCard.tsx
Normal file
133
homework/client/src/components/CodeCard.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// src/components/CodeCard.tsx
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { LockOutlined, ShareAltOutlined } from "@ant-design/icons";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import MoreActionsPopover from "../components/MoreOutline";
|
||||||
|
import { Tag } from "antd";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import CodeEditor from "./EditorWrapper"; // 确保路径正确
|
||||||
|
import ShareButton from "./ShareButton"; // 引入 ShareButton 组件
|
||||||
|
|
||||||
|
interface CodeCardProps {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
submitTime: string;
|
||||||
|
tabContent: string; // 代码内容
|
||||||
|
label: string[]; // 代码语言
|
||||||
|
language: string;
|
||||||
|
password: string;
|
||||||
|
date: string;
|
||||||
|
onDeleteSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeCard: React.FC<CodeCardProps> = ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
isEncrypted,
|
||||||
|
submitTime,
|
||||||
|
tabContent,
|
||||||
|
label,
|
||||||
|
language,
|
||||||
|
password,
|
||||||
|
date,
|
||||||
|
onDeleteSuccess,
|
||||||
|
}) => {
|
||||||
|
const [isEncrypteds, setIsEncrypted] = useState(isEncrypted); // 是否加密
|
||||||
|
const [password1, setPassword] = useState<string>(password);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleEncryptionChange = (
|
||||||
|
newEncryptionState: boolean,
|
||||||
|
newPassword: string,
|
||||||
|
) => {
|
||||||
|
setIsEncrypted(newEncryptionState);
|
||||||
|
setPassword(newPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
// 导航到详情页面
|
||||||
|
navigate(`/details/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="code-card"
|
||||||
|
onClick={handleCardClick}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<div className="code-card-header">
|
||||||
|
<div className="code-card-title">
|
||||||
|
{title}
|
||||||
|
{isEncrypteds && (
|
||||||
|
<LockOutlined
|
||||||
|
style={{ marginLeft: 8, color: "rgb(127,127,127)" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="code-card-more" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<MoreActionsPopover
|
||||||
|
codeId={String(id)}
|
||||||
|
isEncrypted={isEncrypteds}
|
||||||
|
onCancelEncryption={() => {
|
||||||
|
setIsEncrypted(false);
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
onDeleteSuccess && onDeleteSuccess();
|
||||||
|
}}
|
||||||
|
onEncrypt={() => {
|
||||||
|
setIsEncrypted(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="code-card-time">
|
||||||
|
创建于 {dayjs(submitTime).format("YYYY-MM-DD HH:mm:ss")}
|
||||||
|
</div>
|
||||||
|
<div className="code-card-content">
|
||||||
|
<CodeEditor
|
||||||
|
language={language}
|
||||||
|
content={tabContent}
|
||||||
|
theme="vs-dark" // 你可以根据需要传递主题
|
||||||
|
readOnly={true} // 设置为只读模式
|
||||||
|
height="200px"
|
||||||
|
className="point"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="code-card-footer">
|
||||||
|
{isEncrypteds && (
|
||||||
|
<div className="encryption-box">
|
||||||
|
<span>加密</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="tags-box">
|
||||||
|
{label?.map((tag: string, index: number) => (
|
||||||
|
<Tag key={index} className="tag-item">
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="code-card-actions" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<ShareButton
|
||||||
|
id={id || ""}
|
||||||
|
isEncrypted={isEncrypteds}
|
||||||
|
password={password1}
|
||||||
|
expirationTime={date}
|
||||||
|
onEncryptionChange={handleEncryptionChange}
|
||||||
|
onExpirationChange={() => {}}
|
||||||
|
triggerContent={
|
||||||
|
<button>
|
||||||
|
<ShareAltOutlined />
|
||||||
|
<span style={{ marginLeft: "8px" }}>分享</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
className="share-page"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeCard;
|
||||||
88
homework/client/src/components/CodePage.tsx
Normal file
88
homework/client/src/components/CodePage.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// CodePageLayout.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Button, message, Spin } from "antd"; // 引入 Spin 组件
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import "../css/Layout.css";
|
||||||
|
import ContentTop from "./ContentTop";
|
||||||
|
import MoreSettings from "./MoreSettings";
|
||||||
|
import EncryptionSettings from "./EncryptionSettings";
|
||||||
|
import { submitSettings, updateCodeDetails } from "../api/settings";
|
||||||
|
import { RootState } from "../redux/store";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { SubmitSettingsResponse } from "../types/codeTypes";
|
||||||
|
import { clearCodeDetails } from "../redux/slice";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
interface CodePageLayoutProps {
|
||||||
|
isEditMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodePage: React.FC<CodePageLayoutProps> = ({ isEditMode }) => {
|
||||||
|
const codeDetails = useSelector((state: RootState) => state.code);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!codeDetails) return;
|
||||||
|
const currentTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
const settingsWithTime = { ...codeDetails, submitTime: currentTime };
|
||||||
|
try {
|
||||||
|
let result: SubmitSettingsResponse;
|
||||||
|
|
||||||
|
if (isEditMode) {
|
||||||
|
// 编辑模式,调用更新函数
|
||||||
|
result = await updateCodeDetails(codeDetails.id, codeDetails);
|
||||||
|
} else {
|
||||||
|
// 创建模式,调用提交函数
|
||||||
|
result = await submitSettings(settingsWithTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success("设置已成功提交!");
|
||||||
|
const newCodeId = result.data;
|
||||||
|
dispatch(clearCodeDetails());
|
||||||
|
navigate(`/details/${newCodeId}`);
|
||||||
|
} else {
|
||||||
|
message.error(`提交失败:${result.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error("提交过程中发生未知错误。");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!codeDetails) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: "center", marginTop: "50px" }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="content-body-top" style={{ background: "black" }}>
|
||||||
|
<ContentTop />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="content-body-bottom"
|
||||||
|
style={{
|
||||||
|
background: "black",
|
||||||
|
marginTop: "20px",
|
||||||
|
padding: "0px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreSettings />
|
||||||
|
</div>
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<EncryptionSettings />
|
||||||
|
<div style={{ marginTop: "30px", textAlign: "center" }}>
|
||||||
|
<Button type="primary" onClick={handleSubmit} className="submit">
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodePage;
|
||||||
36
homework/client/src/components/ContentTop.tsx
Normal file
36
homework/client/src/components/ContentTop.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// components/ContentTop.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Layout } from "antd"; // 引入 Spin 组件
|
||||||
|
import CreateTab from "./CreateTab";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../redux/store";
|
||||||
|
|
||||||
|
const { Header, Content } = Layout;
|
||||||
|
|
||||||
|
const ContentTop: React.FC = () => {
|
||||||
|
const codeDetails = useSelector((state: RootState) => state.code);
|
||||||
|
const title = codeDetails?.title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ height: "625px" }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Header className="content-header">
|
||||||
|
<div className="logo" />
|
||||||
|
<div className="title">{title}</div>
|
||||||
|
</Header>
|
||||||
|
<Layout className="content-layout">
|
||||||
|
{/* 内容区 */}
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
minHeight: 280,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CreateTab />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContentTop;
|
||||||
182
homework/client/src/components/CreateTab.tsx
Normal file
182
homework/client/src/components/CreateTab.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
// src/components/CreateTab.tsx
|
||||||
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Tabs } from "antd";
|
||||||
|
import { RootState } from "../redux/store"; // 导入 Redux 状态类型
|
||||||
|
import {
|
||||||
|
addTab,
|
||||||
|
setActiveTab,
|
||||||
|
removeTab,
|
||||||
|
updateTabContent,
|
||||||
|
setEditorTheme,
|
||||||
|
setEditorLanguage,
|
||||||
|
} from "../redux/slice"; // 导入 Redux actions
|
||||||
|
import FilterSelect from "./FilterSelectProps"; // 引入通用选择器
|
||||||
|
import CodeEditor from "./EditorWrapper";
|
||||||
|
|
||||||
|
const CreateTab: React.FC = () => {
|
||||||
|
const languageMapping: { [key: string]: string } = {
|
||||||
|
JavaScript: "javascript",
|
||||||
|
TypeScript: "typescript",
|
||||||
|
Python: "python",
|
||||||
|
};
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { editorSettings, tabs, activeTabKey } = useSelector(
|
||||||
|
(state: RootState) => state.code,
|
||||||
|
);
|
||||||
|
const newTabIndex = useRef<number>(1);
|
||||||
|
const [tabKey, setTabKey] = useState<string>("1");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabs.length > 0) {
|
||||||
|
// 找到当前所有标签页的最大键值
|
||||||
|
const maxKey = Math.max(...tabs.map((tab) => Number(tab.key)));
|
||||||
|
// 设置 newTabIndex 为最大键值加一
|
||||||
|
newTabIndex.current = maxKey + 1;
|
||||||
|
} else {
|
||||||
|
newTabIndex.current = 1;
|
||||||
|
}
|
||||||
|
}, [tabs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTabKey) {
|
||||||
|
setTabKey(activeTabKey);
|
||||||
|
} else if (tabs.length > 0) {
|
||||||
|
setTabKey(tabs[tabs.length - 1].key); // 设置为最后一个标签页
|
||||||
|
dispatch(setActiveTab(tabs[tabs.length - 1].key));
|
||||||
|
}
|
||||||
|
}, [activeTabKey, tabs, dispatch]);
|
||||||
|
|
||||||
|
// Tab 状态变化
|
||||||
|
const onTabChange = (key: string) => {
|
||||||
|
dispatch(setActiveTab(key));
|
||||||
|
setTabKey(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建新 Tab
|
||||||
|
const addNewTab = () => {
|
||||||
|
const newTabKey = `${newTabIndex.current}`;
|
||||||
|
const newTab = {
|
||||||
|
key: newTabKey,
|
||||||
|
title: `代码 ${newTabIndex.current}`,
|
||||||
|
content: "",
|
||||||
|
language: "JavaScript",
|
||||||
|
};
|
||||||
|
dispatch(addTab(newTab)); // 调用 Redux action 来添加新标签页
|
||||||
|
dispatch(setActiveTab(newTabKey)); // 切换到新创建的标签页
|
||||||
|
setTabKey(newTabKey);
|
||||||
|
newTabIndex.current += 1; // 更新 newTabIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除 Tab
|
||||||
|
const removeTabHandler = (targetKey: string) => {
|
||||||
|
dispatch(removeTab(targetKey));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditorChange = (value: string | undefined) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
dispatch(updateTabContent({ key: activeTabKey, content: value }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改后的 handleThemeChange
|
||||||
|
const handleThemeChange = (value: string | string[] | undefined) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
dispatch(setEditorTheme(value)); // 派发更新主题的 action
|
||||||
|
}
|
||||||
|
// 如果是多选或 undefined,不做处理
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改后的 onLanguageChange
|
||||||
|
const onLanguageChange = (value: string | string[] | undefined) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
dispatch(setEditorLanguage({ key: activeTabKey, language: value }));
|
||||||
|
}
|
||||||
|
// 如果是多选或 undefined,不做处理
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMonacoLanguage = (language: string): string => {
|
||||||
|
if (language && typeof language === "string") {
|
||||||
|
language = languageMapping[language] || language.toLowerCase();
|
||||||
|
}
|
||||||
|
return language; // 如果找不到映射则返回小写形式
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义主题选项
|
||||||
|
const themeOptions = [
|
||||||
|
{ value: "vs-dark", label: "Dark" },
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
// 根据需要添加更多主题
|
||||||
|
];
|
||||||
|
|
||||||
|
// 定义语言选项
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: "JavaScript", label: "JavaScript" },
|
||||||
|
{ value: "TypeScript", label: "TypeScript" },
|
||||||
|
{ value: "Python", label: "Python" },
|
||||||
|
// 根据需要添加更多语言
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }} className="tab-create">
|
||||||
|
{/* 主题选择器,使用 FilterSelect */}
|
||||||
|
<div className="theme">
|
||||||
|
<FilterSelect
|
||||||
|
placeholder="选择主题"
|
||||||
|
options={themeOptions}
|
||||||
|
value={editorSettings.theme}
|
||||||
|
onChange={handleThemeChange}
|
||||||
|
allowClear={false} // 不允许清除,确保主题始终有值
|
||||||
|
className="custom-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
activeKey={tabKey}
|
||||||
|
onChange={onTabChange}
|
||||||
|
type="editable-card"
|
||||||
|
onEdit={(targetKey, action) => {
|
||||||
|
if (action === "remove" && targetKey) {
|
||||||
|
removeTabHandler(targetKey as string);
|
||||||
|
}
|
||||||
|
if (action === "add") {
|
||||||
|
addNewTab();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabBarExtraContent={
|
||||||
|
<div className="lang">
|
||||||
|
<FilterSelect
|
||||||
|
placeholder="选择语言"
|
||||||
|
options={languageOptions}
|
||||||
|
value={
|
||||||
|
tabs.find((tab) => tab.key === activeTabKey)?.language ||
|
||||||
|
"JavaScript"
|
||||||
|
}
|
||||||
|
onChange={onLanguageChange}
|
||||||
|
allowClear={false} // 不允许清除,确保语言始终有值
|
||||||
|
className="custom-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
items={tabs.map((tab) => ({
|
||||||
|
key: tab.key,
|
||||||
|
label: tab.title,
|
||||||
|
children: (
|
||||||
|
<CodeEditor
|
||||||
|
language={getMonacoLanguage(tab.language)}
|
||||||
|
theme={editorSettings.theme}
|
||||||
|
content={tab.content}
|
||||||
|
onChange={onEditorChange}
|
||||||
|
readOnly={false} // 可编辑模式
|
||||||
|
height="500px"
|
||||||
|
className="code-editor"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
closable: true,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateTab;
|
||||||
48
homework/client/src/components/EditorWrapper.tsx
Normal file
48
homework/client/src/components/EditorWrapper.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// src/components/CodeEditor.tsx
|
||||||
|
import React from "react";
|
||||||
|
import MonacoEditor from "@monaco-editor/react";
|
||||||
|
|
||||||
|
interface CodeEditorProps {
|
||||||
|
language: string;
|
||||||
|
content: string;
|
||||||
|
theme: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
onChange?: (value: string | undefined) => void; // eslint-disable-line no-unused-vars
|
||||||
|
height?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
|
language,
|
||||||
|
content,
|
||||||
|
theme,
|
||||||
|
readOnly = false,
|
||||||
|
onChange,
|
||||||
|
height = "200px",
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<MonacoEditor
|
||||||
|
language={language}
|
||||||
|
value={content}
|
||||||
|
theme={theme}
|
||||||
|
onChange={onChange}
|
||||||
|
options={{
|
||||||
|
readOnly: readOnly,
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollbar: {
|
||||||
|
vertical: "auto",
|
||||||
|
horizontal: "auto",
|
||||||
|
},
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
overviewRulerBorder: false,
|
||||||
|
wordWrap: "on",
|
||||||
|
}}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeEditor;
|
||||||
131
homework/client/src/components/EncryptionSettings.tsx
Normal file
131
homework/client/src/components/EncryptionSettings.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// components/EncryptionSettings.tsx
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { toggleEncryption, setPassword } from "../redux/slice";
|
||||||
|
import { RootState } from "../redux/store";
|
||||||
|
import { Input, Button, Tooltip } from "antd";
|
||||||
|
import { ReloadOutlined, CloseCircleFilled } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const EncryptionSettings: React.FC = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isEncrypted, password } = useSelector(
|
||||||
|
(state: RootState) => state.code,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 生成随机密码
|
||||||
|
const generatePassword = (): string => {
|
||||||
|
const length = Math.floor(Math.random() * (8 - 4 + 1)) + 4; // 生成 4-8 位长度
|
||||||
|
const chars =
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let generatedPassword = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
generatedPassword += chars.charAt(
|
||||||
|
Math.floor(Math.random() * chars.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return generatedPassword;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证密码长度
|
||||||
|
const isValidLength = password.length >= 4 && password.length <= 8;
|
||||||
|
|
||||||
|
// 切换加密状态
|
||||||
|
const handleToggleEncryption = () => {
|
||||||
|
const newEncryptionState = !isEncrypted;
|
||||||
|
dispatch(toggleEncryption(newEncryptionState));
|
||||||
|
|
||||||
|
if (!newEncryptionState) {
|
||||||
|
// 如果禁用加密,清空密码
|
||||||
|
dispatch(setPassword(""));
|
||||||
|
}
|
||||||
|
// 不在这里调用 handleRefresh,让 useEffect 处理密码生成
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成新密码并设置
|
||||||
|
const handleRefresh = () => {
|
||||||
|
const newPassword = generatePassword();
|
||||||
|
dispatch(setPassword(newPassword));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空密码
|
||||||
|
const handleClearPassword = () => {
|
||||||
|
dispatch(setPassword(""));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理密码输入变化
|
||||||
|
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(setPassword(e.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在加密开启时自动生成密码(如果密码为空)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEncrypted && (!password || password.trim() === "")) {
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isEncrypted]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ marginTop: "25px", marginBottom: "40px" }}
|
||||||
|
className="encryption"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type={!isEncrypted ? "primary" : "default"}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isEncrypted) return; // 如果已经是公开状态,点击不做任何反应
|
||||||
|
handleToggleEncryption(); // 切换为公开状态
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
公开
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type={isEncrypted ? "primary" : "default"}
|
||||||
|
onClick={() => {
|
||||||
|
if (isEncrypted) return; // 如果已经是加密状态,点击不做任何反应
|
||||||
|
handleToggleEncryption(); // 切换加密状态
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
加密
|
||||||
|
</Button>
|
||||||
|
{isEncrypted && (
|
||||||
|
<div style={{ marginTop: "25px" }} className="password">
|
||||||
|
<label>密码</label>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入密码 (4-8 位)"
|
||||||
|
value={password}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
suffix={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
marginLeft: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title="清除密码">
|
||||||
|
<CloseCircleFilled
|
||||||
|
onClick={handleClearPassword}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tooltip title="生成新密码" className="refresh">
|
||||||
|
<ReloadOutlined onClick={handleRefresh} />
|
||||||
|
</Tooltip>
|
||||||
|
{/* 显示密码长度验证提示 */}
|
||||||
|
{!isValidLength && password.length > 0 && (
|
||||||
|
<div style={{ color: "red", marginTop: "5px" }}>
|
||||||
|
密码长度必须在 4 到 8 位之间
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EncryptionSettings;
|
||||||
101
homework/client/src/components/ExpirationTime.tsx
Normal file
101
homework/client/src/components/ExpirationTime.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// components/ExpirationTimePicker.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { DatePicker, Button } from "antd";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setDate } from "../redux/slice";
|
||||||
|
|
||||||
|
interface ExpirationTimePickerProps {
|
||||||
|
newExpiration: Dayjs | null;
|
||||||
|
setNewExpiration: (date: Dayjs | null) => void; // eslint-disable-line no-unused-vars
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Time {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
adjust: (current: Dayjs) => Dayjs; // eslint-disable-line no-unused-vars
|
||||||
|
}
|
||||||
|
|
||||||
|
const time: Time[] = [
|
||||||
|
{
|
||||||
|
label: "今天",
|
||||||
|
value: "今天",
|
||||||
|
adjust: (current) => current.startOf("day"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "15分钟",
|
||||||
|
value: "15分钟",
|
||||||
|
adjust: (current) => current.add(15, "minute"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "1小时",
|
||||||
|
value: "1小时",
|
||||||
|
adjust: (current) => current.add(1, "hour"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "6小时",
|
||||||
|
value: "6小时",
|
||||||
|
adjust: (current) => current.add(6, "hour"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "1周",
|
||||||
|
value: "1周",
|
||||||
|
adjust: (current) => current.add(1, "week"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "1个月",
|
||||||
|
value: "1个月",
|
||||||
|
adjust: (current) => current.add(1, "month"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ExpirationTimePicker: React.FC<ExpirationTimePickerProps> = ({
|
||||||
|
newExpiration,
|
||||||
|
setNewExpiration,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleExpirationChange = (value: Dayjs | null) => {
|
||||||
|
setNewExpiration(value);
|
||||||
|
if (value) {
|
||||||
|
dispatch(setDate(value.format("YYYY-MM-DD HH:mm:ss")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickSelect = (option: Time) => {
|
||||||
|
const selectedDate = option.adjust(dayjs());
|
||||||
|
setNewExpiration(selectedDate);
|
||||||
|
dispatch(setDate(selectedDate.toISOString()));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>过期时间</label>
|
||||||
|
<DatePicker
|
||||||
|
className="date-picker"
|
||||||
|
showTime
|
||||||
|
value={newExpiration}
|
||||||
|
onChange={handleExpirationChange}
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
placeholder="不填,永久有效"
|
||||||
|
showNow={false}
|
||||||
|
renderExtraFooter={() => (
|
||||||
|
<div style={{ textAlign: "center" }} className="date">
|
||||||
|
{time.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleQuickSelect(option)}
|
||||||
|
className="date-button"
|
||||||
|
style={{ margin: "0 4px" }}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpirationTimePicker;
|
||||||
42
homework/client/src/components/FilterSelectProps.tsx
Normal file
42
homework/client/src/components/FilterSelectProps.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// src/components/FilterSelect.tsx
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Select } from "antd";
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface FilterSelectProps {
|
||||||
|
placeholder: string;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
value: string | string[] | undefined;
|
||||||
|
onChange: (value: string | string[] | undefined) => void; // eslint-disable-line no-unused-vars
|
||||||
|
allowClear?: boolean;
|
||||||
|
className?: string;
|
||||||
|
mode?: "multiple" | "tags" | undefined; // 添加 mode 属性以支持多选
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterSelect: FC<FilterSelectProps> = ({
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
allowClear = false,
|
||||||
|
className = "",
|
||||||
|
mode = undefined,
|
||||||
|
}) => (
|
||||||
|
<Select
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(value: string | string[] | undefined) => onChange(value)}
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
allowClear={allowClear}
|
||||||
|
mode={mode} // 支持多选
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<Option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FilterSelect;
|
||||||
74
homework/client/src/components/Header.tsx
Normal file
74
homework/client/src/components/Header.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// components/HeaderComponent.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Layout, Menu } from "antd";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import "../css/Layout.css";
|
||||||
|
|
||||||
|
const { Header } = Layout;
|
||||||
|
|
||||||
|
// 定义菜单项的类型
|
||||||
|
interface MenuItem {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
path: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置所有菜单项
|
||||||
|
const MENU_ITEMS: MenuItem[] = [
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: "+ 创建",
|
||||||
|
path: "/create",
|
||||||
|
className: "menu-item-left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2",
|
||||||
|
label: "代码列表",
|
||||||
|
path: "/list",
|
||||||
|
className: "menu-item-right",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const HeaderComponent: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname;
|
||||||
|
|
||||||
|
// 根据当前路径设置选中的菜单项
|
||||||
|
let selectedKey: string | null = null;
|
||||||
|
|
||||||
|
if (path === "/list") {
|
||||||
|
selectedKey = "2";
|
||||||
|
} else if (path.startsWith("/create")) {
|
||||||
|
selectedKey = "1";
|
||||||
|
}
|
||||||
|
// 其它路径下 selectedKey 保持为 null,不选中任何菜单项
|
||||||
|
|
||||||
|
// 转换 MENU_ITEMS 以适应 'items' 属性,同时保持 className
|
||||||
|
const menuItems = MENU_ITEMS.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
<Link to={item.path} className={item.className}>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
<div className="menu-item-underline"></div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header className="header">
|
||||||
|
<div className="layout-logo">Code Manager</div>
|
||||||
|
<Menu
|
||||||
|
theme="dark"
|
||||||
|
mode="horizontal"
|
||||||
|
selectedKeys={selectedKey ? [selectedKey] : []} // 如果 selectedKey 为 null,则不选中任何菜单项
|
||||||
|
className="navbar-menu"
|
||||||
|
items={menuItems} // 使用 'items' prop 代替 children
|
||||||
|
/>
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderComponent;
|
||||||
57
homework/client/src/components/LabeledInput.tsx
Normal file
57
homework/client/src/components/LabeledInput.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// src/components/LabeledInput.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Input, Button } from "antd";
|
||||||
|
import { CloseCircleFilled } from "@ant-design/icons";
|
||||||
|
|
||||||
|
interface LabeledInputProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; // eslint-disable-line no-unused-vars
|
||||||
|
onClear: () => void;
|
||||||
|
maxLength?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LabeledInput: React.FC<LabeledInputProps> = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onClear,
|
||||||
|
maxLength,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: "10px", position: "relative" }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<label>{label}</label>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={() => {}} // 保留 onBlur 以防需要后续扩展
|
||||||
|
maxLength={maxLength}
|
||||||
|
placeholder={placeholder}
|
||||||
|
style={{ paddingRight: "25px" }}
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<CloseCircleFilled />}
|
||||||
|
onClick={onClear}
|
||||||
|
className="title-button"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "8px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LabeledInput;
|
||||||
63
homework/client/src/components/ModalContents.tsx
Normal file
63
homework/client/src/components/ModalContents.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// src/components/ModalContents.tsx
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
LockOutlined,
|
||||||
|
UnlockOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Input } from "antd";
|
||||||
|
|
||||||
|
interface EncryptModalContentProps {
|
||||||
|
password: string;
|
||||||
|
setPassword: (value: string) => void; // eslint-disable-line no-unused-vars
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EncryptModalContent: React.FC<EncryptModalContentProps> = ({
|
||||||
|
password,
|
||||||
|
setPassword,
|
||||||
|
}) => {
|
||||||
|
const isValidLength = password.length >= 4 && password.length <= 8;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="modal-header">
|
||||||
|
<LockOutlined />
|
||||||
|
<span className="modal-header-title">加密</span>
|
||||||
|
</div>
|
||||||
|
<Input.Password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
{!isValidLength && password.length > 0 && (
|
||||||
|
<div style={{ color: "red", marginLeft: "43px" }}>
|
||||||
|
密码长度必须在 4 到 8 位之间
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CancelEncryptModalContent: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<div className="modal-header">
|
||||||
|
<UnlockOutlined />
|
||||||
|
<span className="modal-header-title">取消加密</span>
|
||||||
|
</div>
|
||||||
|
<div className="modal-content-text">取消加密,代码将会公开</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DeleteModalContent: React.FC = () => (
|
||||||
|
<div>
|
||||||
|
<div className="modal-header">
|
||||||
|
<ExclamationCircleOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: "24px",
|
||||||
|
marginRight: "8px",
|
||||||
|
color: "#FFD700",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="modal-header-title">是否确认删除?不可找回</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
171
homework/client/src/components/MoreOutline.tsx
Normal file
171
homework/client/src/components/MoreOutline.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// src/components/MoreActionsPopover.tsx
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Popover, Modal, message } from "antd";
|
||||||
|
import {
|
||||||
|
LockOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
UnlockOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { updateCodeDetails, deleteSettings } from "../api/settings";
|
||||||
|
import {
|
||||||
|
EncryptModalContent,
|
||||||
|
CancelEncryptModalContent,
|
||||||
|
DeleteModalContent,
|
||||||
|
} from "./ModalContents";
|
||||||
|
import { ModalMode } from "../types/codeTypes";
|
||||||
|
|
||||||
|
interface MoreActionsPopoverProps {
|
||||||
|
codeId: string;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
onCancelEncryption: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onEncrypt: (password: string) => void; // eslint-disable-line no-unused-vars
|
||||||
|
}
|
||||||
|
|
||||||
|
const MoreActionsPopover: React.FC<MoreActionsPopoverProps> = ({
|
||||||
|
codeId,
|
||||||
|
isEncrypted,
|
||||||
|
onCancelEncryption,
|
||||||
|
onDelete,
|
||||||
|
onEncrypt,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [mode, setMode] = useState<ModalMode | null>(null);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
// 打开 Modal 的统一函数
|
||||||
|
const openModal = (newMode: ModalMode) => {
|
||||||
|
setMode(newMode);
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 Modal 确定按钮点击事件
|
||||||
|
const handleOk = async () => {
|
||||||
|
try {
|
||||||
|
if (mode === ModalMode.Encrypt) {
|
||||||
|
if (!password.trim()) {
|
||||||
|
message.warning("请输入密码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onEncrypt(password);
|
||||||
|
await updateCodeDetails(codeId, {
|
||||||
|
id: codeId,
|
||||||
|
isEncrypted: true,
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
message.success("加密成功");
|
||||||
|
} else if (mode === ModalMode.CancelEncrypt) {
|
||||||
|
await onCancelEncryption();
|
||||||
|
await updateCodeDetails(codeId, {
|
||||||
|
id: codeId,
|
||||||
|
isEncrypted: false,
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
message.success("取消加密成功");
|
||||||
|
} else if (mode === ModalMode.Delete) {
|
||||||
|
await deleteSettings(codeId);
|
||||||
|
await onDelete();
|
||||||
|
message.success("删除成功");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error("操作失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setVisible(false);
|
||||||
|
setMode(null);
|
||||||
|
setPassword("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 Modal 取消按钮点击事件
|
||||||
|
const handleCancel = () => {
|
||||||
|
setVisible(false);
|
||||||
|
setMode(null);
|
||||||
|
setPassword("");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Popover 内容
|
||||||
|
const content = (
|
||||||
|
<div className="custom-dropdown-menu-content">
|
||||||
|
{isEncrypted ? (
|
||||||
|
<div
|
||||||
|
className="custom-dropdown-menu-item"
|
||||||
|
onClick={() => openModal(ModalMode.CancelEncrypt)}
|
||||||
|
>
|
||||||
|
<UnlockOutlined className="custom-dropdown-menu-icon" />
|
||||||
|
<span>取消加密</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="custom-dropdown-menu-item"
|
||||||
|
onClick={() => openModal(ModalMode.Encrypt)}
|
||||||
|
>
|
||||||
|
<LockOutlined className="custom-dropdown-menu-icon" />
|
||||||
|
<span>加密</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="custom-dropdown-menu-item"
|
||||||
|
onClick={() => navigate(`/edit/${codeId}`)}
|
||||||
|
>
|
||||||
|
<EditOutlined className="custom-dropdown-menu-icon" />
|
||||||
|
<span>编辑</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="custom-dropdown-menu-item delete"
|
||||||
|
onClick={() => openModal(ModalMode.Delete)}
|
||||||
|
>
|
||||||
|
<DeleteOutlined className="custom-dropdown-menu-icon" />
|
||||||
|
<span>删除</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 动态渲染 Modal 内容
|
||||||
|
const renderModalContent = () => {
|
||||||
|
switch (mode) {
|
||||||
|
case ModalMode.Encrypt:
|
||||||
|
return (
|
||||||
|
<EncryptModalContent password={password} setPassword={setPassword} />
|
||||||
|
);
|
||||||
|
case ModalMode.CancelEncrypt:
|
||||||
|
return <CancelEncryptModalContent />;
|
||||||
|
case ModalMode.Delete:
|
||||||
|
return <DeleteModalContent />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover
|
||||||
|
content={content}
|
||||||
|
trigger="click"
|
||||||
|
placement="bottomRight"
|
||||||
|
overlayClassName="custom-dropdown-menu"
|
||||||
|
>
|
||||||
|
<MoreOutlined style={{ fontSize: "24px", cursor: "pointer" }} />
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
closable={false}
|
||||||
|
centered
|
||||||
|
className="more-actions-modal"
|
||||||
|
>
|
||||||
|
{renderModalContent()}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoreActionsPopover;
|
||||||
217
homework/client/src/components/MoreSettings.tsx
Normal file
217
homework/client/src/components/MoreSettings.tsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
// src/components/MoreSettings.tsx
|
||||||
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
addLabel,
|
||||||
|
removeLabel,
|
||||||
|
setTitle,
|
||||||
|
setContent,
|
||||||
|
setDate,
|
||||||
|
} from "../redux/slice";
|
||||||
|
import { RootState } from "../redux/store";
|
||||||
|
import { Collapse } from "antd";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import { Editor } from "@bytemd/react";
|
||||||
|
import "bytemd/dist/index.min.css"; // Import styles
|
||||||
|
import "highlight.js/styles/vs.css"; // Import code highlighting styles
|
||||||
|
// Import plugins
|
||||||
|
import gfm from "@bytemd/plugin-gfm";
|
||||||
|
import gemoji from "@bytemd/plugin-gemoji";
|
||||||
|
import highlight from "@bytemd/plugin-highlight-ssr";
|
||||||
|
import mediumZoom from "@bytemd/plugin-medium-zoom";
|
||||||
|
|
||||||
|
// Import the new ExpirationTimePicker component
|
||||||
|
import ExpirationTimePicker from "./ExpirationTime";
|
||||||
|
import LabeledInput from "./LabeledInput";
|
||||||
|
import FilterSelect from "./FilterSelectProps"; // 使用 FilterSelect 直接替代 LabeledSelect
|
||||||
|
// import EncryptionSelector from "./EncryptionSelector"; // 引入 EncryptionSelector
|
||||||
|
|
||||||
|
const MoreSettings: React.FC = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { title, label, date, content } = useSelector(
|
||||||
|
(state: RootState) => state.code,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [newTitle, setNewTitle] = useState<string>(title || "");
|
||||||
|
const [newExpiration, setNewExpiration] = useState<Dayjs | null>(
|
||||||
|
date ? dayjs(date) : null,
|
||||||
|
);
|
||||||
|
const [markdownContent, setInputMarkdown] = useState<string>(content || "");
|
||||||
|
|
||||||
|
// 同步 Redux 状态到本地状态
|
||||||
|
useEffect(() => {
|
||||||
|
setNewTitle(title || "");
|
||||||
|
setNewExpiration(date ? dayjs(date) : null);
|
||||||
|
setInputMarkdown(content || "");
|
||||||
|
}, [title, date, content]);
|
||||||
|
|
||||||
|
// 处理 Markdown 内容变化
|
||||||
|
const handleMarkdownChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setInputMarkdown(value);
|
||||||
|
dispatch(setContent(value)); // Update Redux store
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理标题变化
|
||||||
|
const handleTitleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setNewTitle(value);
|
||||||
|
dispatch(setTitle(value)); // Update title in Redux
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清除标题
|
||||||
|
const handleClearTitle = useCallback(() => {
|
||||||
|
setNewTitle("");
|
||||||
|
dispatch(setTitle("")); // Clear title in Redux store
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
const handleAddLabel = useCallback(
|
||||||
|
(values: string[]) => {
|
||||||
|
values.forEach((tag) => {
|
||||||
|
if (!label.includes(tag)) {
|
||||||
|
dispatch(addLabel(tag));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch, label],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 移除标签
|
||||||
|
const handleRemoveLabel = useCallback(
|
||||||
|
(removedLabel: string) => {
|
||||||
|
dispatch(removeLabel(removedLabel));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理过期时间变化
|
||||||
|
const handleExpirationChange = useCallback(
|
||||||
|
(newExpiration: Dayjs | null) => {
|
||||||
|
setNewExpiration(newExpiration);
|
||||||
|
const formattedDate = newExpiration
|
||||||
|
? dayjs(newExpiration).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
: "";
|
||||||
|
dispatch(setDate(formattedDate));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 验证标题长度
|
||||||
|
const isValidLength = newTitle.length > 0 && newTitle.length <= 40;
|
||||||
|
|
||||||
|
// 定义可用的标签
|
||||||
|
const availableLabels = [
|
||||||
|
"Java",
|
||||||
|
"JavaScript",
|
||||||
|
"C++",
|
||||||
|
"Python",
|
||||||
|
"作业",
|
||||||
|
"HTML",
|
||||||
|
"设计模式",
|
||||||
|
"Go",
|
||||||
|
];
|
||||||
|
|
||||||
|
// 格式化标签选项
|
||||||
|
const labelOptions = availableLabels.map((label) => ({
|
||||||
|
value: label,
|
||||||
|
label: label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 处理标签选择变化
|
||||||
|
const handleLabelChange = (value: string | string[] | undefined) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
// 新选择的标签
|
||||||
|
const added = value.filter((val) => !label.includes(val));
|
||||||
|
const removed = label.filter((val) => !value.includes(val));
|
||||||
|
|
||||||
|
if (added.length > 0) {
|
||||||
|
handleAddLabel(added);
|
||||||
|
}
|
||||||
|
if (removed.length > 0) {
|
||||||
|
handleRemoveLabel(removed[0]); // 假设一次只移除一个
|
||||||
|
}
|
||||||
|
} else if (value === undefined) {
|
||||||
|
// 如果选择被清除,移除所有标签
|
||||||
|
label.forEach((existingLabel) => dispatch(removeLabel(existingLabel)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义 Collapse 的 items
|
||||||
|
const collapseItems = [
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: "更多设置",
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<div className="flex-container">
|
||||||
|
{/* 标题输入框 */}
|
||||||
|
<LabeledInput
|
||||||
|
label="标题"
|
||||||
|
value={newTitle}
|
||||||
|
onChange={handleTitleChange}
|
||||||
|
onClear={handleClearTitle}
|
||||||
|
placeholder="请输入标题"
|
||||||
|
className="flex-item"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 标签选择器,直接使用 FilterSelect */}
|
||||||
|
<div className="flex-item labels">
|
||||||
|
<span className="select-label">标签</span>
|
||||||
|
<FilterSelect
|
||||||
|
placeholder="选择标签"
|
||||||
|
options={labelOptions}
|
||||||
|
value={label}
|
||||||
|
onChange={handleLabelChange}
|
||||||
|
allowClear
|
||||||
|
mode="multiple" // 启用多选
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 过期时间选择器 */}
|
||||||
|
<div className="flex-item sdate">
|
||||||
|
<ExpirationTimePicker
|
||||||
|
newExpiration={newExpiration}
|
||||||
|
setNewExpiration={handleExpirationChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题长度错误提示 */}
|
||||||
|
{!isValidLength && (
|
||||||
|
<div style={{ color: "red", marginTop: "5px" }}>
|
||||||
|
标题长度必须在 1 到 40 位之间
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Markdown 编辑器 */}
|
||||||
|
<div style={{ marginTop: "15px", marginBottom: "20px" }}>
|
||||||
|
<Editor
|
||||||
|
value={markdownContent}
|
||||||
|
onChange={handleMarkdownChange}
|
||||||
|
placeholder="在此输入 Markdown 内容"
|
||||||
|
plugins={[gfm(), gemoji(), highlight(), mediumZoom()]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="more-settings">
|
||||||
|
<Collapse
|
||||||
|
defaultActiveKey={[]} // 设置为 [] 以确保默认关闭
|
||||||
|
className="collapse-settings"
|
||||||
|
items={collapseItems} // 使用 'items' prop 代替 children
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoreSettings;
|
||||||
28
homework/client/src/components/PasswordDisplay.tsx
Normal file
28
homework/client/src/components/PasswordDisplay.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// src/components/PasswordDisplay.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Input, Space } from "antd";
|
||||||
|
|
||||||
|
interface PasswordDisplayProps {
|
||||||
|
isEncrypted: boolean;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PasswordDisplay: React.FC<PasswordDisplayProps> = ({
|
||||||
|
isEncrypted,
|
||||||
|
password,
|
||||||
|
}) => {
|
||||||
|
if (!isEncrypted) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="password-share">
|
||||||
|
<div className="pas-share">
|
||||||
|
<span>密码</span>
|
||||||
|
<Space style={{ width: "100%" }}>
|
||||||
|
<Input value={password || "无密码"} disabled />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PasswordDisplay;
|
||||||
158
homework/client/src/components/Share.tsx
Normal file
158
homework/client/src/components/Share.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
// src/components/SharePopover.tsx
|
||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Space, message } from "antd";
|
||||||
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
|
import ExpirationTimePicker from "../components/ExpirationTime";
|
||||||
|
import ShareLink from "../components/ShareLink";
|
||||||
|
import PasswordDisplay from "../components/PasswordDisplay";
|
||||||
|
import { updateCodeDetails } from "../api/settings";
|
||||||
|
import FilterSelect from "./FilterSelectProps"; // 引入通用选择器
|
||||||
|
|
||||||
|
interface SharePopoverProps {
|
||||||
|
isEncrypted: boolean;
|
||||||
|
password: string;
|
||||||
|
expirationTime: string; // ISO 格式的过期时间字符串
|
||||||
|
id: string;
|
||||||
|
onEncryptionChange: (
|
||||||
|
newEncryptionState: boolean, // eslint-disable-line no-unused-vars
|
||||||
|
newPassword: string, // eslint-disable-line no-unused-vars
|
||||||
|
) => void;
|
||||||
|
onExpirationChange: (newExpirationTime: string) => void; // eslint-disable-line no-unused-vars
|
||||||
|
}
|
||||||
|
|
||||||
|
const SharePopover: React.FC<SharePopoverProps> = ({
|
||||||
|
isEncrypted,
|
||||||
|
password,
|
||||||
|
expirationTime,
|
||||||
|
id,
|
||||||
|
onEncryptionChange,
|
||||||
|
onExpirationChange,
|
||||||
|
}) => {
|
||||||
|
const [generatedPassword, setGeneratedPassword] = useState<string>("");
|
||||||
|
const [selectedExpiration, setSelectedExpiration] = useState<Dayjs | null>(
|
||||||
|
expirationTime ? dayjs(expirationTime) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const generatePassword = useCallback((): string => {
|
||||||
|
const length = Math.floor(Math.random() * (8 - 4 + 1)) + 4; // 随机密码长度在4到8之间
|
||||||
|
const charset =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
return Array.from({ length }, () =>
|
||||||
|
charset.charAt(Math.floor(Math.random() * charset.length)),
|
||||||
|
).join("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 定义加密选择器的选项
|
||||||
|
const encryptionOptions = [
|
||||||
|
{ value: "公开", label: "公开" },
|
||||||
|
{ value: "加密", label: "加密" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleEncryptionChangeInternal = async (
|
||||||
|
value: string | string[] | undefined,
|
||||||
|
) => {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
// 如果不是字符串,忽略处理
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = value === "加密";
|
||||||
|
const newGeneratedPassword = newState ? generatePassword() : "";
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
setGeneratedPassword(newGeneratedPassword);
|
||||||
|
onEncryptionChange(newState, newGeneratedPassword);
|
||||||
|
|
||||||
|
// 更新后端
|
||||||
|
try {
|
||||||
|
await updateCodeDetails(id, {
|
||||||
|
id,
|
||||||
|
isEncrypted: newState,
|
||||||
|
password: newGeneratedPassword,
|
||||||
|
});
|
||||||
|
message.success("加密状态已更新!");
|
||||||
|
} catch (error) {
|
||||||
|
message.error("更新加密状态失败,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpirationChangeInternal = async (
|
||||||
|
newExpiration: Dayjs | null,
|
||||||
|
) => {
|
||||||
|
setSelectedExpiration(newExpiration);
|
||||||
|
const formattedDate = newExpiration
|
||||||
|
? dayjs(newExpiration).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
: "";
|
||||||
|
onExpirationChange(formattedDate);
|
||||||
|
|
||||||
|
// 更新后端
|
||||||
|
try {
|
||||||
|
await updateCodeDetails(id, { id, date: formattedDate });
|
||||||
|
message.success("过期时间已更新!");
|
||||||
|
} catch (error) {
|
||||||
|
message.error("更新过期时间失败,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setGeneratedPassword("");
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEncrypted) {
|
||||||
|
setGeneratedPassword("");
|
||||||
|
onEncryptionChange(false, "");
|
||||||
|
}
|
||||||
|
}, [isEncrypted, onEncryptionChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="share-popover">
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
|
{/* 分享链接部分 */}
|
||||||
|
<ShareLink id={id} isEncrypted={isEncrypted} password={password} />
|
||||||
|
|
||||||
|
{/* 分享范围选择器,使用 FilterSelect 替代 EncryptionSelector */}
|
||||||
|
<div className="encryption-selector">
|
||||||
|
<Space
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginTop: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>分享范围</span>
|
||||||
|
<FilterSelect
|
||||||
|
placeholder="选择分享范围"
|
||||||
|
options={encryptionOptions}
|
||||||
|
value={isEncrypted ? "加密" : "公开"}
|
||||||
|
onChange={handleEncryptionChangeInternal}
|
||||||
|
allowClear={false} // 不允许清除,确保始终选择一个选项
|
||||||
|
mode={undefined} // 单选模式
|
||||||
|
className="ispassword-share" // 保持原有的类名
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 密码显示 */}
|
||||||
|
<PasswordDisplay
|
||||||
|
isEncrypted={isEncrypted}
|
||||||
|
password={isEncrypted ? password : generatedPassword}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="share-date">
|
||||||
|
{/* 过期时间选择器 */}
|
||||||
|
<ExpirationTimePicker
|
||||||
|
newExpiration={selectedExpiration}
|
||||||
|
setNewExpiration={handleExpirationChangeInternal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SharePopover;
|
||||||
62
homework/client/src/components/ShareButton.tsx
Normal file
62
homework/client/src/components/ShareButton.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// src/components/ShareButton.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Popover } from "antd";
|
||||||
|
import SharePopover from "./Share";
|
||||||
|
|
||||||
|
interface ShareButtonProps {
|
||||||
|
id: string;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
password: string;
|
||||||
|
expirationTime: string;
|
||||||
|
onEncryptionChange: (
|
||||||
|
newEncryptionState: boolean, // eslint-disable-line no-unused-vars
|
||||||
|
newPassword: string, // eslint-disable-line no-unused-vars
|
||||||
|
) => void;
|
||||||
|
onExpirationChange: (newExpirationTime: string) => void; // eslint-disable-line no-unused-vars
|
||||||
|
triggerContent: React.ReactNode;
|
||||||
|
popoverTitle?: React.ReactNode;
|
||||||
|
trigger?: "hover" | "click" | "focus";
|
||||||
|
placement?: "top" | "left" | "right" | "bottom";
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShareButton: React.FC<ShareButtonProps> = ({
|
||||||
|
id,
|
||||||
|
isEncrypted,
|
||||||
|
password,
|
||||||
|
expirationTime,
|
||||||
|
onEncryptionChange,
|
||||||
|
onExpirationChange,
|
||||||
|
triggerContent,
|
||||||
|
popoverTitle = (
|
||||||
|
<div style={{ textAlign: "center", width: "100%", color: "#dcdcdc" }}>
|
||||||
|
分享代码片段
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
trigger = "hover",
|
||||||
|
placement = "bottom",
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={
|
||||||
|
<SharePopover
|
||||||
|
isEncrypted={isEncrypted}
|
||||||
|
password={password}
|
||||||
|
expirationTime={expirationTime}
|
||||||
|
id={id}
|
||||||
|
onEncryptionChange={onEncryptionChange}
|
||||||
|
onExpirationChange={onExpirationChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={popoverTitle}
|
||||||
|
trigger={trigger}
|
||||||
|
placement={placement}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{triggerContent}
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareButton;
|
||||||
56
homework/client/src/components/ShareLink.tsx
Normal file
56
homework/client/src/components/ShareLink.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// src/components/ShareLink.tsx
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Button, Input, Popover, message } from "antd";
|
||||||
|
import QRCode from "react-qr-code";
|
||||||
|
import { QrcodeOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
interface ShareLinkProps {
|
||||||
|
id: string;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShareLink: React.FC<ShareLinkProps> = ({ id, isEncrypted, password }) => {
|
||||||
|
const shareableUrl = useMemo(() => {
|
||||||
|
try {
|
||||||
|
const url = new URL(`${window.location.origin}/details/${id}`);
|
||||||
|
if (isEncrypted && password) {
|
||||||
|
url.searchParams.set("pw", password);
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete("pw");
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
} catch (error) {
|
||||||
|
return window.location.href;
|
||||||
|
}
|
||||||
|
}, [id, isEncrypted, password]);
|
||||||
|
|
||||||
|
const handleCopyLink = () => {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(shareableUrl)
|
||||||
|
.then(() => {
|
||||||
|
message.success("链接已复制!");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error("复制失败,请手动复制链接");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="share-link">
|
||||||
|
<Input value={shareableUrl} disabled style={{ width: "80%" }} />
|
||||||
|
<Button onClick={handleCopyLink} size="small" className="copy-share">
|
||||||
|
复制链接
|
||||||
|
</Button>
|
||||||
|
<Popover
|
||||||
|
content={<QRCode value={shareableUrl} size={128} />}
|
||||||
|
trigger="hover"
|
||||||
|
placement="topRight"
|
||||||
|
>
|
||||||
|
<Button icon={<QrcodeOutlined />} className="qrc-share" />
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareLink;
|
||||||
1233
homework/client/src/css/Layout.css
Normal file
1233
homework/client/src/css/Layout.css
Normal file
File diff suppressed because it is too large
Load Diff
20
homework/client/src/index.tsx
Normal file
20
homework/client/src/index.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Provider } from "react-redux";
|
||||||
|
import store, { persistor } from "./redux/store";
|
||||||
|
import RouterConfig from "./router";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { HashRouter } from "react-router-dom";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById("root") as HTMLElement,
|
||||||
|
);
|
||||||
|
root.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={<div>加载中...</div>} persistor={persistor}>
|
||||||
|
<HashRouter>
|
||||||
|
{" "}
|
||||||
|
{/* 确保 RouterConfig 被包裹在 BrowserRouter 中 */}
|
||||||
|
<RouterConfig />
|
||||||
|
</HashRouter>
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
1
homework/client/src/logo.svg
Normal file
1
homework/client/src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
459
homework/client/src/pages/CodeDetailsPage.tsx
Normal file
459
homework/client/src/pages/CodeDetailsPage.tsx
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
// src/pages/CodeDetailsPage.tsx
|
||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { Button, message, Tag, Spin } from "antd";
|
||||||
|
import {
|
||||||
|
ShareAltOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { getCodeDetails } from "../api/settings"; // 需要实现的 API 调用
|
||||||
|
import { CodeState } from "../types/codeTypes";
|
||||||
|
import "../css/Layout.css";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import "highlight.js/styles/vs.css";
|
||||||
|
import rehypeHighlight from "rehype-highlight"; // 用于高亮显示代码
|
||||||
|
import { Tabs } from "antd";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
import ShareButton from "../components/ShareButton"; // 引入新创建的 ShareButton 组件
|
||||||
|
import MoreActionsPopover from "../components/MoreOutline";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import CodeEditor from "../components/EditorWrapper"; // 引入自定义的 CodeEditor 组件
|
||||||
|
import rehypeSanitize from "rehype-sanitize";
|
||||||
|
|
||||||
|
const languageFullNames: { [key: string]: string } = {
|
||||||
|
js: "JavaScript",
|
||||||
|
py: "Python",
|
||||||
|
java: "Java",
|
||||||
|
rb: "Ruby",
|
||||||
|
html: "HTML",
|
||||||
|
css: "CSS",
|
||||||
|
ts: "TypeScript",
|
||||||
|
c: "C",
|
||||||
|
cpp: "C++",
|
||||||
|
go: "Go",
|
||||||
|
bash: "Bash",
|
||||||
|
php: "PHP",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeDetailsPage: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const codeRef = useRef<HTMLDivElement | null>(null); // 引用 code 块
|
||||||
|
const [codeDetails, setCodeDetail] = useState<CodeState | null>(null);
|
||||||
|
const [language, setLanguage] = useState<string | null>(null);
|
||||||
|
const [activeTabKey, setActiveTabKey] = useState<string | undefined>(
|
||||||
|
codeDetails?.tabs[0]?.key,
|
||||||
|
);
|
||||||
|
const [expirationTime, setExpirationTime] = useState<string>("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const languageMapping: { [key: string]: string } = {
|
||||||
|
JavaScript: "javascript",
|
||||||
|
TypeScript: "typescript",
|
||||||
|
Python: "python",
|
||||||
|
};
|
||||||
|
const [isEncrypted, setIsEncrypted] = useState(false); // 是否加密
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const [loading, setLoading] = useState(true); // 新增加载状态
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const pw = queryParams.get("pw");
|
||||||
|
const [isPasswordValid, setIsPasswordValid] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const getCodeDetail = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getCodeDetails(String(id)); // 确保 id 存在
|
||||||
|
setCodeDetail(data.data);
|
||||||
|
setIsEncrypted(data.data.isEncrypted);
|
||||||
|
setPassword(data.data.password);
|
||||||
|
setExpirationTime(data.data.date);
|
||||||
|
setActiveTabKey(data.data.tabs[0].key);
|
||||||
|
if (data.data.content) {
|
||||||
|
const languageMatch = data.data.content.match(/```(\w+)/); // 匹配代码块语言
|
||||||
|
if (languageMatch) {
|
||||||
|
const lang = languageMatch[1]; // 提取语言类型
|
||||||
|
setLanguage(languageFullNames[lang] || lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error("获取代码详情失败。");
|
||||||
|
} finally {
|
||||||
|
setLoading(false); // 数据请求完成,不管成功还是失败都结束加载状态
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getCodeDetail();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const verifyAccess = async () => {
|
||||||
|
if (!codeDetails) return;
|
||||||
|
|
||||||
|
if (codeDetails.isEncrypted) {
|
||||||
|
if (pw) {
|
||||||
|
// 如果链接中有 pw 参数,验证密码
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
"输入的密码:",
|
||||||
|
pw,
|
||||||
|
"数据库中的密码:",
|
||||||
|
codeDetails.password,
|
||||||
|
);
|
||||||
|
if (pw === codeDetails.password) {
|
||||||
|
setIsPasswordValid(true); // 密码验证通过
|
||||||
|
} else {
|
||||||
|
message.error("密码不正确,将重定向到创建页面。");
|
||||||
|
setIsPasswordValid(false);
|
||||||
|
navigate("/create");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setIsPasswordValid(false);
|
||||||
|
navigate("/create");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果链接中没有 pw 参数,直接允许访问
|
||||||
|
setIsPasswordValid(true); // 无需密码时直接验证通过
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果不加密,直接允许访问
|
||||||
|
setIsPasswordValid(true); // 无需密码时直接验证通过
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
verifyAccess();
|
||||||
|
}, [codeDetails, pw, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 在组件加载完后触发代码高亮
|
||||||
|
if (codeDetails?.content) {
|
||||||
|
hljs.highlightAll();
|
||||||
|
}
|
||||||
|
}, [codeDetails]); // 当 codeDetails 更新时重新高亮
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
<div style={{ color: "#fff", textAlign: "center", marginTop: "50px" }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ color: "#fff", textAlign: "center", marginTop: "50px" }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!codeDetails) {
|
||||||
|
return <div style={{ color: "#fff" }}>未找到代码详情。</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpired = expirationTime && dayjs(expirationTime).isBefore(dayjs());
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
return <div style={{ color: "#fff" }}>当前页面已过期</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopy = (content: string, type: string) => {
|
||||||
|
if (type === "markdown") {
|
||||||
|
if (codeRef.current) {
|
||||||
|
content =
|
||||||
|
codeRef.current.querySelectorAll("pre code")[0].textContent || " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(content)
|
||||||
|
.then(() => {
|
||||||
|
message.success("代码已复制到剪贴板!");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message.error("复制失败,请重试!");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
navigate(`/edit/${codeDetails.id}`); // 导航到编辑页面,并传递代码ID
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExpirationChange = (newExpirationTime: string) => {
|
||||||
|
setExpirationTime(newExpirationTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!codeDetails) {
|
||||||
|
message.error("没有可下载的代码详情。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
// 创建一个根文件夹,命名为代码标题
|
||||||
|
const rootFolder = zip.folder(codeDetails.title || "CodeDetails");
|
||||||
|
if (!rootFolder) {
|
||||||
|
message.error("无法创建压缩包文件夹。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有标签页,添加代码文件
|
||||||
|
codeDetails.tabs.forEach((tab) => {
|
||||||
|
// 确定文件扩展名
|
||||||
|
const extensionMap: { [key: string]: string } = {
|
||||||
|
JavaScript: "js",
|
||||||
|
TypeScript: "ts",
|
||||||
|
Python: "py",
|
||||||
|
Java: "java",
|
||||||
|
Ruby: "rb",
|
||||||
|
HTML: "html",
|
||||||
|
CSS: "css",
|
||||||
|
C: "c",
|
||||||
|
"C++": "cpp",
|
||||||
|
Go: "go",
|
||||||
|
Bash: "sh",
|
||||||
|
PHP: "php",
|
||||||
|
};
|
||||||
|
|
||||||
|
const language = tab.language;
|
||||||
|
|
||||||
|
const extension = extensionMap[language] || "txt"; // 默认使用 .txt
|
||||||
|
|
||||||
|
const fileName = `${tab.key}.${extension}`;
|
||||||
|
|
||||||
|
rootFolder.file(fileName, tab.content || "");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成 ZIP 压缩包
|
||||||
|
try {
|
||||||
|
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||||
|
saveAs(zipBlob, `${codeDetails.title || "CodeDetails"}.zip`);
|
||||||
|
message.success("代码已成功打包并下载!");
|
||||||
|
} catch (error) {
|
||||||
|
message.error("下载过程中发生错误。");
|
||||||
|
console.error("ZIP 生成错误:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMonacoLanguage(language: string): string {
|
||||||
|
if (language && typeof language === "string") {
|
||||||
|
language = languageMapping[language] || language.toLowerCase();
|
||||||
|
}
|
||||||
|
return language; // 如果找不到映射则返回小写形式
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEncryptionChange = (
|
||||||
|
newEncryptionState: boolean,
|
||||||
|
newPassword: string,
|
||||||
|
) => {
|
||||||
|
setIsEncrypted(newEncryptionState);
|
||||||
|
setPassword(newPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<div className="share-load">
|
||||||
|
<div className="share-download-item">
|
||||||
|
<ShareButton
|
||||||
|
id={id || ""}
|
||||||
|
isEncrypted={isEncrypted}
|
||||||
|
password={password}
|
||||||
|
expirationTime={expirationTime}
|
||||||
|
onEncryptionChange={handleEncryptionChange}
|
||||||
|
onExpirationChange={handleExpirationChange}
|
||||||
|
triggerContent={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ShareAltOutlined />}
|
||||||
|
className="share-button"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="share-page"
|
||||||
|
/>
|
||||||
|
<span>分享</span>
|
||||||
|
</div>
|
||||||
|
<div className="share-download-item">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
className="download-button"
|
||||||
|
onClick={handleDownload}
|
||||||
|
></Button>
|
||||||
|
<span>下载</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="details-container">
|
||||||
|
<div className="details-header">
|
||||||
|
<div className="details-image">代码详情</div>
|
||||||
|
<div className="details-title">
|
||||||
|
{codeDetails.title}
|
||||||
|
{isEncrypted && (
|
||||||
|
<LockOutlined
|
||||||
|
className="encrypted-icon"
|
||||||
|
style={{ color: "yellow", marginLeft: "10px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="details-actions">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
style={{ marginRight: "0px" }}
|
||||||
|
onClick={handleEdit}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<ShareButton
|
||||||
|
id={id || ""}
|
||||||
|
isEncrypted={isEncrypted}
|
||||||
|
password={password}
|
||||||
|
expirationTime={expirationTime}
|
||||||
|
onEncryptionChange={handleEncryptionChange}
|
||||||
|
onExpirationChange={handleExpirationChange}
|
||||||
|
triggerContent={
|
||||||
|
<Button type="default" icon={<ShareAltOutlined />}>
|
||||||
|
分享
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className="share-page"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 其他详情内容 */}
|
||||||
|
<div className="details-content">
|
||||||
|
{/* 在这里添加代码详情的内容,例如代码块、描述等 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "96.8%",
|
||||||
|
marginLeft: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={codeRef}>
|
||||||
|
<ReactMarkdown
|
||||||
|
rehypePlugins={[rehypeHighlight, rehypeSanitize as any]}
|
||||||
|
>
|
||||||
|
{codeDetails?.content || ""}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
{language && (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
style={{ marginRight: 16 }}
|
||||||
|
className="language"
|
||||||
|
>
|
||||||
|
{language} {/* 显示语言全称 */}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
onClick={() =>
|
||||||
|
handleCopy(
|
||||||
|
codeDetails?.tabs.find((tab) => tab.key === activeTabKey)
|
||||||
|
?.content || "",
|
||||||
|
"markdown",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="copy"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="details-tabs">
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTabKey} // 默认显示第一个 tab
|
||||||
|
type="card"
|
||||||
|
onChange={(key) => setActiveTabKey(key)}
|
||||||
|
items={codeDetails.tabs.map((tab) => ({
|
||||||
|
key: tab.key,
|
||||||
|
label: tab.title,
|
||||||
|
children: (
|
||||||
|
<CodeEditor
|
||||||
|
language={getMonacoLanguage(tab.language)}
|
||||||
|
theme={codeDetails.editorSettings.theme}
|
||||||
|
content={tab.content}
|
||||||
|
readOnly={true} // 可编辑模式设置为只读
|
||||||
|
height="500px"
|
||||||
|
className="code-editor"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
closable: false, // 禁用关闭
|
||||||
|
}))}
|
||||||
|
></Tabs>
|
||||||
|
<div
|
||||||
|
className="tab-icon"
|
||||||
|
onClick={() =>
|
||||||
|
handleCopy(
|
||||||
|
codeDetails?.tabs.find((tab) => tab.key === activeTabKey)
|
||||||
|
?.content || "",
|
||||||
|
"tab-icon",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CopyOutlined
|
||||||
|
style={{ fontSize: "24px", cursor: "pointer" }}
|
||||||
|
className="copy-outlined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tab-laguage">
|
||||||
|
{codeDetails.tabs.find((tab) => tab.key === activeTabKey)
|
||||||
|
?.language ||
|
||||||
|
codeDetails.tabs.find(
|
||||||
|
(tab) => tab.key === codeDetails.activeTabKey,
|
||||||
|
)?.language}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bottom-box">
|
||||||
|
{isEncrypted && (
|
||||||
|
<div className="encryption-box">
|
||||||
|
<span>加密</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 右边的标签框 */}
|
||||||
|
<div className="tags-box">
|
||||||
|
{codeDetails?.label?.map((tag: string, index: number) => (
|
||||||
|
<Tag key={index} className="tag-item">
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* 过期时间 */}
|
||||||
|
<div className="expiration-box">
|
||||||
|
<span className="expiration-text">
|
||||||
|
过期时间:{expirationTime || "永久"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "96.8%",
|
||||||
|
marginLeft: "24px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="submit-time">创建于 {codeDetails.submitTime}</div>
|
||||||
|
<div className="more-outlined">
|
||||||
|
<MoreActionsPopover
|
||||||
|
codeId={String(codeDetails.id)}
|
||||||
|
isEncrypted={isEncrypted}
|
||||||
|
onCancelEncryption={() => {
|
||||||
|
setIsEncrypted(false);
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
setCodeDetail(null);
|
||||||
|
|
||||||
|
navigate("/create");
|
||||||
|
}}
|
||||||
|
onEncrypt={(password: string) => {
|
||||||
|
setIsEncrypted(true);
|
||||||
|
setPassword(password);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeDetailsPage;
|
||||||
198
homework/client/src/pages/CodeListPage.tsx
Normal file
198
homework/client/src/pages/CodeListPage.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
// pages/CodeListPage.tsx
|
||||||
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
|
import CodeCard from "../components/CodeCard";
|
||||||
|
import { getSettings } from "../api/settings";
|
||||||
|
import { message, Input, Pagination, Spin } from "antd";
|
||||||
|
import { CodeState } from "../types/codeTypes";
|
||||||
|
import FilterSelect from "../components/FilterSelectProps"; // 引入 FilterSelect
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
const CodeListPage: React.FC = () => {
|
||||||
|
const [codeList, setCodeList] = useState<CodeState[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchText, setSearchText] = useState(""); // 用于搜索标题
|
||||||
|
const [encryptionFilter, setEncryptionFilter] = useState<
|
||||||
|
"all" | "encrypted" | "unencrypted" | undefined
|
||||||
|
>(undefined); // 用于过滤加密状态
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
); // 用于选择编程语言
|
||||||
|
const [currentPage, setCurrentPage] = useState<number>(1); // 当前页码
|
||||||
|
const [pageSize, setPageSize] = useState<number>(6); // 每页数量,可由用户更改
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSettings();
|
||||||
|
if (res.success) {
|
||||||
|
setCodeList(res.tabs);
|
||||||
|
} else {
|
||||||
|
message.error("获取列表失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error("获取数据时发生错误");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDeleteSuccess = () => {
|
||||||
|
// 删除成功后重新获取列表数据
|
||||||
|
setTimeout(fetchData, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取所有唯一的编程语言标签
|
||||||
|
const languageOptions = useMemo(() => {
|
||||||
|
const labels = codeList.flatMap((item) => item.label);
|
||||||
|
return Array.from(new Set(labels)).map((label) => ({
|
||||||
|
value: label,
|
||||||
|
label,
|
||||||
|
}));
|
||||||
|
}, [codeList]);
|
||||||
|
|
||||||
|
// 定义分享范围的选项
|
||||||
|
const encryptionOptions = [
|
||||||
|
{ value: "all", label: "全部" },
|
||||||
|
{ value: "encrypted", label: "加密" },
|
||||||
|
{ value: "unencrypted", label: "公开" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据搜索、加密和编程语言过滤条件对数据进行过滤
|
||||||
|
const filteredList = useMemo(() => {
|
||||||
|
const filtered = codeList.filter((item) => {
|
||||||
|
const matchSearch = item.title.includes(searchText);
|
||||||
|
|
||||||
|
let matchEncryption = true;
|
||||||
|
if (encryptionFilter === "encrypted") {
|
||||||
|
matchEncryption = item.isEncrypted === true;
|
||||||
|
} else if (encryptionFilter === "unencrypted") {
|
||||||
|
matchEncryption = item.isEncrypted === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchLanguage = true;
|
||||||
|
if (selectedLanguage) {
|
||||||
|
matchLanguage = item.label.includes(selectedLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchSearch && matchEncryption && matchLanguage;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当过滤条件变化时校正当前页码
|
||||||
|
const maxPage = Math.ceil(filtered.length / pageSize) || 1;
|
||||||
|
if (currentPage > maxPage) {
|
||||||
|
setCurrentPage(maxPage); // 修正当前页码
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}, [
|
||||||
|
codeList,
|
||||||
|
searchText,
|
||||||
|
encryptionFilter,
|
||||||
|
selectedLanguage,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 基于当前页码对filteredList进行分页切片
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const currentData = filteredList.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin
|
||||||
|
spinning={loading}
|
||||||
|
tip="正在加载..."
|
||||||
|
wrapperClassName="code-list-container"
|
||||||
|
>
|
||||||
|
<div className="codelist-page">
|
||||||
|
{/* 增加输入框和下拉框,用于搜索和过滤 */}
|
||||||
|
<div className="filter-row">
|
||||||
|
<Search
|
||||||
|
placeholder="输入关键词搜索"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
setCurrentPage(1); // 当搜索条件变化时,回到第一页
|
||||||
|
}}
|
||||||
|
className="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="select-filter-all">
|
||||||
|
{/* 使用通用的 FilterSelect 组件 */}
|
||||||
|
<FilterSelect
|
||||||
|
placeholder="分享范围"
|
||||||
|
options={encryptionOptions}
|
||||||
|
value={encryptionFilter}
|
||||||
|
onChange={(value) => {
|
||||||
|
setEncryptionFilter(
|
||||||
|
value as "all" | "encrypted" | "unencrypted" | undefined,
|
||||||
|
);
|
||||||
|
setCurrentPage(1); // 切换过滤条件时重置至第一页
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
className="select-filter" // 保持原有样式
|
||||||
|
/>
|
||||||
|
<FilterSelect
|
||||||
|
placeholder="编程语言"
|
||||||
|
options={languageOptions}
|
||||||
|
value={selectedLanguage}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedLanguage(value as string | undefined); // 类型断言
|
||||||
|
setCurrentPage(1); // 切换过滤条件时重置至第一页
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
className="select-filter" // 保持原有样式
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="codelist-container">
|
||||||
|
{currentData.length === 0 ? (
|
||||||
|
<div className="empty-data">暂无数据</div>
|
||||||
|
) : (
|
||||||
|
currentData.map((item) => (
|
||||||
|
<CodeCard
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
title={item.title}
|
||||||
|
isEncrypted={item.isEncrypted}
|
||||||
|
submitTime={item.submitTime}
|
||||||
|
tabContent={
|
||||||
|
item.tabs.find((tab) => tab.key === "1")?.content || ""
|
||||||
|
}
|
||||||
|
label={item.label}
|
||||||
|
language={
|
||||||
|
item.tabs.find((tab) => tab.key === "1")?.language || ""
|
||||||
|
}
|
||||||
|
password={item.password}
|
||||||
|
date={item.date}
|
||||||
|
onDeleteSuccess={handleDeleteSuccess}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页组件 */}
|
||||||
|
<div style={{ textAlign: "center" }} className="gray-theme-pagination">
|
||||||
|
<Pagination
|
||||||
|
current={currentPage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
total={filteredList.length}
|
||||||
|
onChange={(page) => setCurrentPage(page)}
|
||||||
|
onShowSizeChange={(current, size) => {
|
||||||
|
setPageSize(size);
|
||||||
|
setCurrentPage(1); // 切换每页数量时回到第一页
|
||||||
|
}}
|
||||||
|
showSizeChanger
|
||||||
|
pageSizeOptions={["6", "10", "20", "50"]}
|
||||||
|
showTotal={(total) => `共 ${total} 条数据`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeListPage;
|
||||||
17
homework/client/src/pages/CreateCodePage.tsx
Normal file
17
homework/client/src/pages/CreateCodePage.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// pages/CreateCodePage.tsx
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import CodePageLayout from "../components/CodePage";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { clearCodeDetails } from "../redux/slice";
|
||||||
|
|
||||||
|
const CreateCodePage: React.FC = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 页面加载时清空 Redux 中的代码详情
|
||||||
|
dispatch(clearCodeDetails());
|
||||||
|
}, [dispatch]);
|
||||||
|
return <CodePageLayout isEditMode={false} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateCodePage;
|
||||||
52
homework/client/src/pages/EditCodePage.tsx
Normal file
52
homework/client/src/pages/EditCodePage.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// pages/EditCodePage.tsx
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { message, Spin } from "antd"; // 引入 Spin 组件
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { getCodeDetails } from "../api/settings";
|
||||||
|
import { setCodeDetails } from "../redux/slice";
|
||||||
|
import CodePageLayout from "../components/CodePage";
|
||||||
|
import { RootState } from "../redux/store";
|
||||||
|
|
||||||
|
const EditCodePage: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const codeDetails = useSelector((state: RootState) => state.code);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
const loadCodeDetails = async () => {
|
||||||
|
try {
|
||||||
|
const details = await getCodeDetails(id);
|
||||||
|
console.log("Fetched details:", details.data);
|
||||||
|
dispatch(setCodeDetails(details.data));
|
||||||
|
} catch (error) {
|
||||||
|
message.error("获取代码详情失败。");
|
||||||
|
console.error("获取代码详情错误:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadCodeDetails();
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: "center", marginTop: "50px" }}>
|
||||||
|
<Spin size="large" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeDetails ? (
|
||||||
|
<CodePageLayout isEditMode={true} />
|
||||||
|
) : (
|
||||||
|
<div>没有找到相关数据。</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCodePage;
|
||||||
30
homework/client/src/pages/Layout.tsx
Normal file
30
homework/client/src/pages/Layout.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// src/layouts/Layout.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { Layout } from "antd";
|
||||||
|
import { Outlet } from "react-router-dom"; // 引入 Outlet
|
||||||
|
import "../css/Layout.css";
|
||||||
|
import HeaderComponent from "../components/Header";
|
||||||
|
|
||||||
|
const { Content } = Layout;
|
||||||
|
|
||||||
|
const LayoutComponent: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: "100vh", background: "rgb(12,12,12)" }}>
|
||||||
|
{/* Header 始终显示 */}
|
||||||
|
<HeaderComponent />
|
||||||
|
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
paddingTop: "20px",
|
||||||
|
width: "90%",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 渲染子路由的内容 */}
|
||||||
|
<Outlet />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutComponent;
|
||||||
1
homework/client/src/react-app-env.d.ts
vendored
Normal file
1
homework/client/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
147
homework/client/src/redux/slice.ts
Normal file
147
homework/client/src/redux/slice.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// src/store/codeSlice.ts
|
||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { CodeTab, CodeState } from "../types/codeTypes";
|
||||||
|
|
||||||
|
const initialState: CodeState = {
|
||||||
|
id: "",
|
||||||
|
tabs: [{ key: "1", title: "代码 1", content: "", language: "JavaScript" }],
|
||||||
|
activeTabKey: "1",
|
||||||
|
editorSettings: {
|
||||||
|
theme: "vs-dark",
|
||||||
|
},
|
||||||
|
title: "代码片段",
|
||||||
|
label: [],
|
||||||
|
content: "",
|
||||||
|
isEncrypted: false,
|
||||||
|
password: "",
|
||||||
|
date: "",
|
||||||
|
submitTime: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建 slice
|
||||||
|
const codeSlice = createSlice({
|
||||||
|
name: "code",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setCodeDetails(state, action: PayloadAction<CodeState>) {
|
||||||
|
state.tabs = action.payload.tabs;
|
||||||
|
state.activeTabKey = action.payload.activeTabKey;
|
||||||
|
state.password = action.payload.password;
|
||||||
|
state.date = action.payload.date;
|
||||||
|
state.submitTime = action.payload.submitTime;
|
||||||
|
// 同步 details 中的字段到其他状态
|
||||||
|
state.id = action.payload.id;
|
||||||
|
state.title = action.payload.title;
|
||||||
|
state.isEncrypted = action.payload.isEncrypted;
|
||||||
|
state.content = action.payload.content;
|
||||||
|
state.label = action.payload.label;
|
||||||
|
state.editorSettings = action.payload.editorSettings;
|
||||||
|
// 根据需要同步其他字段
|
||||||
|
},
|
||||||
|
// 添加 Tab
|
||||||
|
addTab(state, action: PayloadAction<CodeTab>) {
|
||||||
|
state.tabs.push(action.payload);
|
||||||
|
state.activeTabKey = action.payload.key;
|
||||||
|
},
|
||||||
|
// 激活 Tab
|
||||||
|
setActiveTab(state, action: PayloadAction<string>) {
|
||||||
|
state.activeTabKey = action.payload;
|
||||||
|
},
|
||||||
|
// 更新 Tab 内容
|
||||||
|
updateTabContent(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ key: string; content: string }>,
|
||||||
|
) {
|
||||||
|
const { key, content } = action.payload;
|
||||||
|
const tab = state.tabs.find((t) => t.key === key);
|
||||||
|
if (tab) {
|
||||||
|
tab.content = content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 删除 Tab
|
||||||
|
removeTab(state, action: PayloadAction<string>) {
|
||||||
|
const targetKey = action.payload;
|
||||||
|
const newTabs = state.tabs.filter((tab) => tab.key !== targetKey);
|
||||||
|
state.tabs = newTabs;
|
||||||
|
if (newTabs.length) {
|
||||||
|
// 如果删除的 Tab 是当前激活的 Tab,更新激活 Tab
|
||||||
|
state.activeTabKey = newTabs[0].key;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 设置编辑器主题
|
||||||
|
setEditorTheme(state, action: PayloadAction<string>) {
|
||||||
|
state.editorSettings.theme = action.payload;
|
||||||
|
},
|
||||||
|
// 设置语言模式
|
||||||
|
setEditorLanguage(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ key: string; language: string }>,
|
||||||
|
) {
|
||||||
|
const { key, language } = action.payload;
|
||||||
|
const tab = state.tabs.find((t) => t.key === key);
|
||||||
|
if (tab) {
|
||||||
|
tab.language = language;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 更新标题
|
||||||
|
setTitle(state, action: PayloadAction<string>) {
|
||||||
|
state.title = action.payload;
|
||||||
|
},
|
||||||
|
addLabel(state, action: PayloadAction<string>) {
|
||||||
|
if (!state.label.includes(action.payload)) {
|
||||||
|
state.label.push(action.payload); // 确保标签不会重复
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 移除标签
|
||||||
|
removeLabel(state, action: PayloadAction<string>) {
|
||||||
|
state.label = state.label.filter((label) => label !== action.payload);
|
||||||
|
},
|
||||||
|
// 更新内容
|
||||||
|
setContent(state, action: PayloadAction<string>) {
|
||||||
|
state.content = action.payload;
|
||||||
|
},
|
||||||
|
// 设置加密状态
|
||||||
|
toggleEncryption(state, action: PayloadAction<boolean>) {
|
||||||
|
state.isEncrypted = action.payload;
|
||||||
|
},
|
||||||
|
// 设置密码
|
||||||
|
setPassword(state, action: PayloadAction<string>) {
|
||||||
|
state.password = action.payload;
|
||||||
|
},
|
||||||
|
// 设置日期
|
||||||
|
setDate(state, action: PayloadAction<string>) {
|
||||||
|
state.date = action.payload;
|
||||||
|
},
|
||||||
|
setInitialSettings(state, action: PayloadAction<CodeState>) {
|
||||||
|
return { ...state, ...action.payload };
|
||||||
|
},
|
||||||
|
setSubmitTime(state, action: PayloadAction<string>) {
|
||||||
|
state.submitTime = action.payload;
|
||||||
|
},
|
||||||
|
clearCodeDetails() {
|
||||||
|
return initialState;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
addTab,
|
||||||
|
setActiveTab,
|
||||||
|
updateTabContent,
|
||||||
|
removeTab,
|
||||||
|
setEditorTheme,
|
||||||
|
setEditorLanguage,
|
||||||
|
setTitle,
|
||||||
|
addLabel,
|
||||||
|
removeLabel,
|
||||||
|
setContent,
|
||||||
|
toggleEncryption,
|
||||||
|
setPassword,
|
||||||
|
setDate,
|
||||||
|
setInitialSettings,
|
||||||
|
setSubmitTime,
|
||||||
|
setCodeDetails,
|
||||||
|
clearCodeDetails,
|
||||||
|
} = codeSlice.actions;
|
||||||
|
|
||||||
|
export default codeSlice.reducer;
|
||||||
50
homework/client/src/redux/store.ts
Normal file
50
homework/client/src/redux/store.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// src/store/store.ts
|
||||||
|
import { configureStore, combineReducers } from "@reduxjs/toolkit";
|
||||||
|
import {
|
||||||
|
persistStore,
|
||||||
|
persistReducer,
|
||||||
|
FLUSH,
|
||||||
|
REHYDRATE,
|
||||||
|
PAUSE,
|
||||||
|
PERSIST,
|
||||||
|
PURGE,
|
||||||
|
REGISTER,
|
||||||
|
} from "redux-persist";
|
||||||
|
import storage from "redux-persist/lib/storage"; // 默认使用 localStorage
|
||||||
|
import codeReducer from "./slice";
|
||||||
|
|
||||||
|
// 配置持久化选项
|
||||||
|
const persistConfig = {
|
||||||
|
key: "root", // 关键键名
|
||||||
|
storage, // 存储引擎
|
||||||
|
whitelist: ["codeList", "code"], // 仅持久化 codeList slice,你可以根据需要添加其他 slice
|
||||||
|
// blacklist: ["code"], // 或者排除特定 slice
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组合所有的 reducers
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
code: codeReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建持久化的 reducer
|
||||||
|
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||||
|
|
||||||
|
// 配置 store
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: persistedReducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: {
|
||||||
|
// redux-persist 需要忽略一些特定的 action 类型
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建持久化的 store
|
||||||
|
export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export default store;
|
||||||
24
homework/client/src/router/index.tsx
Normal file
24
homework/client/src/router/index.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
|
import LayoutComponent from "../pages/Layout";
|
||||||
|
import CreateCodePage from "../pages/CreateCodePage";
|
||||||
|
import CodeListPage from "../pages/CodeListPage";
|
||||||
|
import CodeDetailsPage from "../pages/CodeDetailsPage";
|
||||||
|
import EditCodePage from "../pages/EditCodePage";
|
||||||
|
const RouterConfig: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<LayoutComponent />}>
|
||||||
|
<Route index element={<Navigate to="/create" replace />} />
|
||||||
|
|
||||||
|
{/* 子路由 */}
|
||||||
|
<Route path="create" element={<CreateCodePage />} />
|
||||||
|
<Route path="list" element={<CodeListPage />} />
|
||||||
|
<Route path="details/:id" element={<CodeDetailsPage />} />
|
||||||
|
<Route path="/edit/:id" element={<EditCodePage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RouterConfig;
|
||||||
67
homework/client/src/types/codeTypes.ts
Normal file
67
homework/client/src/types/codeTypes.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 定义 Tab 和状态的结构
|
||||||
|
export interface CodeTab {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorSettings {
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeState {
|
||||||
|
id: string;
|
||||||
|
tabs: CodeTab[];
|
||||||
|
activeTabKey: string;
|
||||||
|
editorSettings: EditorSettings;
|
||||||
|
title: string;
|
||||||
|
label: string[];
|
||||||
|
content: string;
|
||||||
|
isEncrypted: boolean;
|
||||||
|
password: string;
|
||||||
|
date: string;
|
||||||
|
submitTime: string;
|
||||||
|
}
|
||||||
|
export interface GetCodeDetailsResponse {
|
||||||
|
data: CodeState;
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
// 定义 API 请求和响应的接口
|
||||||
|
export interface SubmitSettingsPayload
|
||||||
|
extends Omit<CodeState, "loading" | "error"> {}
|
||||||
|
export interface SubmitSettingsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: string;
|
||||||
|
message: string | object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSettingsResponse {
|
||||||
|
success: boolean;
|
||||||
|
tabs: CodeState[];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSettingsPayload
|
||||||
|
extends Partial<Omit<CodeState, "loading" | "error">> {}
|
||||||
|
export interface UpdateSettingsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: string;
|
||||||
|
message: string | object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteSettingsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ModalMode {
|
||||||
|
Encrypt = "encrypt", // eslint-disable-line no-unused-vars
|
||||||
|
CancelEncrypt = "cancel-encrypt", // eslint-disable-line no-unused-vars
|
||||||
|
Delete = "delete", // eslint-disable-line no-unused-vars
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CACHE_DURATION_MS = 1 * 60 * 1000; // 5 分钟
|
||||||
|
export const CACHE_TIMESTAMP_KEY = "codeListCacheTimestamp";
|
||||||
20
homework/client/tsconfig.json
Normal file
20
homework/client/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
2
homework/server/.eslintignore
Normal file
2
homework/server/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
models/
|
||||||
26
homework/server/.eslintrc.json
Normal file
26
homework/server/.eslintrc.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:node/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.17.0"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"no-useless-catch": 0
|
||||||
|
// 在这里添加自定义的规则
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
7
homework/server/.gitignore
vendored
Normal file
7
homework/server/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
2
homework/server/.prettierignore
Normal file
2
homework/server/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
models/
|
||||||
11
homework/server/.prettierrc
Normal file
11
homework/server/.prettierrc
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
68
homework/server/README.md
Normal file
68
homework/server/README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# 接口文档
|
||||||
|
|
||||||
|
## 1. 数据模块
|
||||||
|
|
||||||
|
接口名称:新增接口【已完成】
|
||||||
|
|
||||||
|
接口路径: /api/data
|
||||||
|
|
||||||
|
请求方法: POST
|
||||||
|
|
||||||
|
**请求参数**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 是否必须 | 描述 |
|
||||||
|
| ----------- | ------ | ---- | ---- |
|
||||||
|
| name | string | 是 |数据名称 |
|
||||||
|
| description | string | 否 | 数据描述 |
|
||||||
|
| tags | string | 否 | 数据标签列表(必须是标签列表中的id)|
|
||||||
|
请求示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "数据名称",
|
||||||
|
"description": "数据描述",
|
||||||
|
"tags": ["标签id1", "标签id2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应结果**
|
||||||
|
| 字段名 | 类型 | 描述 |
|
||||||
|
| --------- | --------- | ---- |
|
||||||
|
| code | number | 响应状态|
|
||||||
|
| msg | string | 响应消息|
|
||||||
|
|
||||||
|
|
||||||
|
响应类型:JSON
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 201,
|
||||||
|
"msg": "新增成功"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接口名称:查询接口【完成80%,过滤逻辑待补充】
|
||||||
|
|
||||||
|
接口名称:编辑接口【待完成】
|
||||||
|
|
||||||
|
接口名称:删除接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
|
|
||||||
|
## 2. 标签模块
|
||||||
|
|
||||||
|
接口名称:新增接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
|
接口名称:查询接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
|
接口名称:编辑接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
|
接口名称:删除接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
|
接口名称:批量删除接口【待完成】
|
||||||
|
|
||||||
|
## 3. 语言切换模块
|
||||||
|
|
||||||
|
|
||||||
|
接口名称:查询接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
|
接口名称:设置接口【已完成,请自行查看代码】
|
||||||
|
|
||||||
42
homework/server/app.js
Normal file
42
homework/server/app.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Koa from 'koa';
|
||||||
|
import { koaBody } from 'koa-body';
|
||||||
|
import serve from 'koa-static';
|
||||||
|
|
||||||
|
import routes from './router.js';
|
||||||
|
|
||||||
|
const app = new Koa();
|
||||||
|
|
||||||
|
// 全局异常处理
|
||||||
|
process.on('uncaughtException', (err, origin) => {
|
||||||
|
console.log(`Caught exception: ${err}\n` + `Exception origin: ${origin}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(serve('../client/build'));
|
||||||
|
|
||||||
|
// 统一接口错误处理
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
try {
|
||||||
|
await next();
|
||||||
|
if (ctx.response.status === 404 && !ctx.response.body) {
|
||||||
|
ctx.throw(404);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const { url = '' } = ctx.request;
|
||||||
|
const { status = 500, message } = error;
|
||||||
|
if (url.startsWith('/api')) {
|
||||||
|
ctx.status = typeof status === 'number' ? status : 500;
|
||||||
|
ctx.body = {
|
||||||
|
msg: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.use(koaBody());
|
||||||
|
// 加载数据路由
|
||||||
|
app.use(routes.routes());
|
||||||
|
// 静态资源目录,
|
||||||
|
|
||||||
|
|
||||||
|
app.listen(3001, () => {
|
||||||
|
console.log('Server is running on http://localhost:3001');
|
||||||
|
});
|
||||||
61
homework/server/controller/tabsController.js
Normal file
61
homework/server/controller/tabsController.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// controllers/tabsController.js
|
||||||
|
import tabsService from '../service/tabsService.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
async function getTabs(ctx) {
|
||||||
|
const tabs = await tabsService.getTabs();
|
||||||
|
ctx.status=200;
|
||||||
|
ctx.body = { tabs };
|
||||||
|
}
|
||||||
|
async function findTabById(ctx) {
|
||||||
|
const { tabKey } = ctx.params;
|
||||||
|
const tab = await tabsService.findTabById(tabKey);
|
||||||
|
ctx.status=200;
|
||||||
|
console.log(tab);
|
||||||
|
ctx.body = { data: tab };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTab(ctx) {
|
||||||
|
console.log(`Received POST request on /api/tab`);
|
||||||
|
const tab = ctx.request.body;
|
||||||
|
if (!tab.title) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = { message: 'Title is required!' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingTab = await tabsService.findTabByTitle(tab.title);
|
||||||
|
if (existingTab) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = { message: `A tab with title '${tab.title}' already exists!` };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = uuidv4();
|
||||||
|
tab.id = id
|
||||||
|
const newTab = { ...tab };
|
||||||
|
await tabsService.addTab(newTab);
|
||||||
|
ctx.status = 201;
|
||||||
|
ctx.body = { data : id ,message: 'Tab added successfully!' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTab(ctx) {
|
||||||
|
const updatedTab = ctx.request.body;
|
||||||
|
if(updatedTab.title){
|
||||||
|
const existingTab = await tabsService.findTabByTitle(updatedTab.title);
|
||||||
|
if (existingTab && existingTab.id !=updatedTab.id ) {
|
||||||
|
ctx.status = 400;
|
||||||
|
ctx.body = { message: `A tab with title '${updatedTab.title}' already exists!` };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await tabsService.updateTab(updatedTab.id, updatedTab);
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.body = { data : updatedTab.id,message: 'Tab updated successfully!' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTab(ctx) {
|
||||||
|
const { tabKey } = ctx.params;
|
||||||
|
await tabsService.deleteTab(tabKey);
|
||||||
|
ctx.status = 200;
|
||||||
|
ctx.body = { message: 'Tab deleted successfully!' };
|
||||||
|
}
|
||||||
|
export default { addTab, getTabs,updateTab,deleteTab,findTabById };
|
||||||
393
homework/server/db.json
Normal file
393
homework/server/db.json
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
{
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"id": "b7c8f242-fb2f-41b6-be2f-908ab5d20aaa",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "dfsdfs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "fdfsad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "1",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
"title": "代码片段csdsfdg",
|
||||||
|
"label": [
|
||||||
|
"JavaScript",
|
||||||
|
"设计模式"
|
||||||
|
],
|
||||||
|
"content": "sssss\n\n```js\nsleep(1000)\n```",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "Va5Bbw",
|
||||||
|
"date": "2024-12-16T16:00:00.000Z",
|
||||||
|
"submitTime": "2024-12-12 23:58:57"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c9adebd5-301e-4de0-be81-df89c67ef356",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "qqqqqqqqqqqqqqqqqqqsss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "aaaaaaaa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3",
|
||||||
|
"title": "代码 3",
|
||||||
|
"content": "",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "3",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
"title": "代码片段edsxx",
|
||||||
|
"label": [
|
||||||
|
"JavaScript"
|
||||||
|
],
|
||||||
|
"content": "xzxzxz",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "CCnC",
|
||||||
|
"date": "2024-12-20 22:47:56",
|
||||||
|
"submitTime": "2024-12-20 16:54:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5e913ec5-3a77-4d5c-bbe5-aac32f5d8439",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "console.log(gfddd)",
|
||||||
|
"language": "TypeScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "1",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段vh",
|
||||||
|
"label": [
|
||||||
|
"JavaScript"
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "MQ5o5pn",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-13 11:59:43"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a28abb3d-5162-4175-ac6a-c555dcfbb314",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "this",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "RegExp",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "2",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段tttttt",
|
||||||
|
"label": [
|
||||||
|
"JavaScript"
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": false,
|
||||||
|
"password": "",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-13 13:26:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0d35b596-dd6d-468d-89b4-9523bfd2fb6b",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "1",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段ddddvbh",
|
||||||
|
"label": [],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "T9uME8",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-13 16:14:32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "389589d4-ba7b-4149-b59b-c6633795169f",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "1",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段fhgfbbcxvsadf",
|
||||||
|
"label": [
|
||||||
|
"Java",
|
||||||
|
"C++"
|
||||||
|
],
|
||||||
|
"content": "ssss\n\n```js\nsleep(1000)\n```",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "UW24",
|
||||||
|
"date": "2024-12-26 00:00:00",
|
||||||
|
"submitTime": "2024-12-18 21:23:39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f8c7eb70-f819-4872-a0bc-41cc29ce8595",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "console.log(1111)",
|
||||||
|
"language": "TypeScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "sdsadas",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3",
|
||||||
|
"title": "代码 3",
|
||||||
|
"content": "sssss",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "2",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段ssssssssscsdgdfg",
|
||||||
|
"label": [
|
||||||
|
"C++",
|
||||||
|
"Python"
|
||||||
|
],
|
||||||
|
"content": "\n```js\nseel90\n```",
|
||||||
|
"isEncrypted": false,
|
||||||
|
"password": "",
|
||||||
|
"date": "2024-12-18T19:30:44.316Z",
|
||||||
|
"submitTime": "2024-12-18 21:31:11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "42815f51-dd4e-4c03-aec5-a7ae313547ad",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "eeeeeeeeeeeeee",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "eeeeeeeeeeeeeeeee",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "2",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段hbgfnfgdng",
|
||||||
|
"label": [],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": false,
|
||||||
|
"password": "",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-19 10:34:27"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1cda0426-b614-4746-aa02-62cffaa1b2e3",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "console.error(wwww)",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "1312312341",
|
||||||
|
"language": "TypeScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "1",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段hgfhfghfgjhgj",
|
||||||
|
"label": [
|
||||||
|
"Java",
|
||||||
|
"JavaScript",
|
||||||
|
"C++",
|
||||||
|
"Python"
|
||||||
|
],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "vaJeiYTB",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-19 13:17:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8738155a-59f9-4ccd-8188-8a2fe5bbb9c1",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "dsadasdas",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "dsafedfdsfsdFS",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "2",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段FDSFDSF",
|
||||||
|
"label": [
|
||||||
|
"JavaScript",
|
||||||
|
"Python"
|
||||||
|
],
|
||||||
|
"content": "FDSAFA\n\n```js\nDSFDFDS\n```",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "PCwkHc",
|
||||||
|
"date": "2024-12-28 00:00:00",
|
||||||
|
"submitTime": "2024-12-20 12:19:20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "327463d8-c293-4f66-bcf6-b74bc9bfaead",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "fdsfdsfsd",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "fcvdsafdsffdsfds",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "1",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段gfdgfhfgs",
|
||||||
|
"label": [],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": false,
|
||||||
|
"password": "",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-20 14:58:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3cf5cd69-cb30-47ad-83c8-ebf4d5a0022e",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "fdsafsdafsad",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "fdsfcxzcxzf",
|
||||||
|
"language": "JavaScript"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "2",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "代码片段fdsafdsafgfdgfd",
|
||||||
|
"label": [],
|
||||||
|
"content": "",
|
||||||
|
"isEncrypted": false,
|
||||||
|
"password": "",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-20 14:59:12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e5594a78-5b38-4828-bca0-89f2ea7eaa22",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"title": "代码 1",
|
||||||
|
"content": "console.log(111112222);",
|
||||||
|
"language": "JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"title": "代码 2",
|
||||||
|
"content": "console.log(\"这是ts写的\")",
|
||||||
|
"language": "TypeScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "3",
|
||||||
|
"title": "代码 3",
|
||||||
|
"content": "print(\"这是python\")",
|
||||||
|
"language": "Python"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"activeTabKey": "3",
|
||||||
|
"editorSettings": {
|
||||||
|
"theme": "vs-dark"
|
||||||
|
},
|
||||||
|
"title": "我的第一个代码",
|
||||||
|
"label": [
|
||||||
|
"Java",
|
||||||
|
"Python",
|
||||||
|
"JavaScript"
|
||||||
|
],
|
||||||
|
"content": "**你好这是我的第一个代码**\n\n```js\nprint(\"这是我的第一个代码\")\n```",
|
||||||
|
"isEncrypted": true,
|
||||||
|
"password": "laCI",
|
||||||
|
"date": "",
|
||||||
|
"submitTime": "2024-12-20 16:18:02"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
homework/server/lib/db.js
Normal file
5
homework/server/lib/db.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { JSONFilePreset } from 'lowdb/node';
|
||||||
|
|
||||||
|
const defaultData = { posts: [] };
|
||||||
|
const db = await JSONFilePreset('db.json', defaultData);
|
||||||
|
export default db
|
||||||
46
homework/server/model/TabsModel.js
Normal file
46
homework/server/model/TabsModel.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// models/TabsModel.js
|
||||||
|
import db from '../lib/db.js';
|
||||||
|
|
||||||
|
class TabsModel {
|
||||||
|
static async getAllTabs() {
|
||||||
|
await db.read();
|
||||||
|
return db.data.tabs;
|
||||||
|
}
|
||||||
|
static async getTabById(id){
|
||||||
|
await db.read();
|
||||||
|
return db.data.tabs.find(tab => tab.id === id);
|
||||||
|
}
|
||||||
|
static async addTab(tab) {
|
||||||
|
await db.read();
|
||||||
|
db.data.tabs.push(tab);
|
||||||
|
await db.write();
|
||||||
|
}
|
||||||
|
static async findTabByTitle(title) {
|
||||||
|
await db.read();
|
||||||
|
return db.data.tabs.find(tab => tab.title === title);
|
||||||
|
}
|
||||||
|
static async updateTab(tabKey, updatedTab) {
|
||||||
|
await db.read();
|
||||||
|
const tab = db.data.tabs.find(tab => tab.id === tabKey);
|
||||||
|
if (tab) {
|
||||||
|
console.log(tab,updatedTab)
|
||||||
|
// 仅更新传入的字段
|
||||||
|
Object.keys(updatedTab).forEach(key => {
|
||||||
|
// 确保只更新存在的字段,防止意外修改
|
||||||
|
if (tab.hasOwnProperty(key)) {
|
||||||
|
console.log(key)
|
||||||
|
tab[key] = updatedTab[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await db.write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteTab(tabKey) {
|
||||||
|
await db.read();
|
||||||
|
db.data.tabs = db.data.tabs.filter(tab => tab.id !== tabKey);
|
||||||
|
await db.write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabsModel;
|
||||||
3008
homework/server/package-lock.json
generated
Normal file
3008
homework/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
homework/server/package.json
Normal file
36
homework/server/package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "nodemon app.js --ignore 'model/*'",
|
||||||
|
"start": "node app.js",
|
||||||
|
"eslint": "eslint --fix '**/*.js'",
|
||||||
|
"prettier": "prettier --write '**/*.js'"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.9",
|
||||||
|
"koa": "^2.14.2",
|
||||||
|
"koa-body": "^6.0.1",
|
||||||
|
"koa-qs": "^3.0.0",
|
||||||
|
"koa-router": "^12.0.0",
|
||||||
|
"koa-static": "^5.0.0",
|
||||||
|
"lowdb": "^7.0.1",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/sqlite3": "^3.1.11",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"nodemon": "^3.1.0",
|
||||||
|
"prettier": "^3.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
homework/server/router.js
Normal file
15
homework/server/router.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Router from 'koa-router';
|
||||||
|
import tabsController from './controller/tabsController.js';
|
||||||
|
|
||||||
|
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
// 定义标签路由
|
||||||
|
router.post('/tab', tabsController.addTab);
|
||||||
|
router.get('/tab', tabsController.getTabs);
|
||||||
|
router.get('/tab/:tabKey',tabsController.findTabById);
|
||||||
|
router.put('/tab/:tabKey', tabsController.updateTab);
|
||||||
|
router.delete('/tab/:tabKey', tabsController.deleteTab);
|
||||||
|
|
||||||
|
|
||||||
|
export default router;
|
||||||
33
homework/server/service/tabsService.js
Normal file
33
homework/server/service/tabsService.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// services/tabsService.js
|
||||||
|
import TabsModel from '../model/TabsModel.js';
|
||||||
|
|
||||||
|
async function getTabs() {
|
||||||
|
return await TabsModel.getAllTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTab(tab) {
|
||||||
|
await TabsModel.addTab(tab);
|
||||||
|
}
|
||||||
|
async function findTabByTitle(title) {
|
||||||
|
return await TabsModel.findTabByTitle(title);
|
||||||
|
}
|
||||||
|
async function findTabById(id) {
|
||||||
|
return await TabsModel.getTabById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTab(tabKey, updatedTab) {
|
||||||
|
await TabsModel.updateTab(tabKey, updatedTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTab(tabKey) {
|
||||||
|
await TabsModel.deleteTab(tabKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getTabs,
|
||||||
|
addTab,
|
||||||
|
updateTab,
|
||||||
|
deleteTab,
|
||||||
|
findTabByTitle,
|
||||||
|
findTabById
|
||||||
|
};
|
||||||
1
practice/README.md
Normal file
1
practice/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
此目录存放课堂作业,可以在此文件添加作作业思路和对题目的看法
|
||||||
Loading…
x
Reference in New Issue
Block a user