This commit is contained in:
王思婕 2024-12-20 19:37:43 +08:00
commit a65ce00766
63 changed files with 34185 additions and 0 deletions

1
homework/README.md Normal file
View File

@ -0,0 +1 @@
此目录存放本周课后作业,可以在此文件添加作业设计思路和流程图等

View 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
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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/).

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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: "删除过程中发生错误",
};
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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>
);

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

File diff suppressed because it is too large Load Diff

View 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>,
);

View 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

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View 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;

View 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;

View 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;

View 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";

View 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"]
}

View File

@ -0,0 +1,2 @@
node_modules/
models/

View 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
View 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

View File

@ -0,0 +1,2 @@
node_modules/
models/

View 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
View 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
View 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');
});

View 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
View 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"
}
]
}

View File

@ -0,0 +1,5 @@
import { JSONFilePreset } from 'lowdb/node';
const defaultData = { posts: [] };
const db = await JSONFilePreset('db.json', defaultData);
export default db

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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;

View 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
View File

@ -0,0 +1 @@
此目录存放课堂作业,可以在此文件添加作作业思路和对题目的看法