Showing preview only (698K chars total). Download the full file or copy to clipboard to get everything.
Repository: itwanger/paicoding-admin
Branch: master
Commit: 357d2cced2af
Files: 225
Total size: 591.4 KB
Directory structure:
gitextract_u_5er_hd/
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .prettierignore
├── .prettierrc.js
├── .qoder/
│ └── settings.json
├── .stylelintignore
├── .stylelintrc.js
├── .trae/
│ └── documents/
│ ├── 在文章编辑页集成 Moveable.js 实现图片缩放.md
│ └── 文章编辑页:导入 Markdown + 修复 Word 图片清晰度.md
├── .vscode/
│ └── extensions.json
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── deploy-front.sh
├── index.html
├── launch.sh
├── lint-staged.config.js
├── package.json
├── postcss.config.js
├── src/
│ ├── App.tsx
│ ├── api/
│ │ ├── config/
│ │ │ └── servicePort.ts
│ │ ├── helper/
│ │ │ ├── axiosCancel.ts
│ │ │ └── checkStatus.ts
│ │ ├── index.ts
│ │ ├── interface/
│ │ │ └── index.ts
│ │ └── modules/
│ │ ├── aiConfig.ts
│ │ ├── article.ts
│ │ ├── author.ts
│ │ ├── category.ts
│ │ ├── column.ts
│ │ ├── comment.ts
│ │ ├── common.ts
│ │ ├── config.ts
│ │ ├── global.ts
│ │ ├── login.ts
│ │ ├── resume.ts
│ │ ├── sensitive.ts
│ │ ├── statistics.ts
│ │ ├── tag.ts
│ │ ├── user.ts
│ │ └── wxMenu.ts
│ ├── assets/
│ │ ├── fonts/
│ │ │ ├── DIN.otf
│ │ │ └── font.less
│ │ └── iconfont/
│ │ └── iconfont.less
│ ├── components/
│ │ ├── ErrorMessage/
│ │ │ ├── 403.tsx
│ │ │ ├── 404.tsx
│ │ │ ├── 500.tsx
│ │ │ └── index.less
│ │ ├── Loading/
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── SwitchDark/
│ │ │ └── index.tsx
│ │ ├── common-wrap/
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── second-sure-modal/
│ │ │ └── index.tsx
│ │ └── svgIcon/
│ │ └── index.tsx
│ ├── config/
│ │ ├── config.ts
│ │ ├── nprogress.ts
│ │ └── serviceLoading.tsx
│ ├── enums/
│ │ ├── common.ts
│ │ └── httpEnum.ts
│ ├── hooks/
│ │ └── useTheme.ts
│ ├── index.scss
│ ├── layouts/
│ │ ├── components/
│ │ │ ├── Footer/
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ ├── Header/
│ │ │ │ ├── components/
│ │ │ │ │ ├── AssemblySize.tsx
│ │ │ │ │ ├── AvatarIcon.tsx
│ │ │ │ │ ├── BreadcrumbNav.tsx
│ │ │ │ │ ├── CollapseIcon.tsx
│ │ │ │ │ ├── Fullscreen.tsx
│ │ │ │ │ ├── InfoModal.tsx
│ │ │ │ │ ├── PasswordModal.tsx
│ │ │ │ │ └── Theme.tsx
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ ├── Menu/
│ │ │ │ ├── components/
│ │ │ │ │ └── Logo.tsx
│ │ │ │ ├── index.css
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ └── Tabs/
│ │ │ ├── components/
│ │ │ │ └── MoreButton.tsx
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── index.less
│ │ └── index.tsx
│ ├── main.tsx
│ ├── redux/
│ │ ├── index.ts
│ │ ├── interface/
│ │ │ └── index.ts
│ │ ├── modules/
│ │ │ ├── auth/
│ │ │ │ ├── action.ts
│ │ │ │ └── reducer.ts
│ │ │ ├── breadcrumb/
│ │ │ │ ├── action.ts
│ │ │ │ └── reducer.ts
│ │ │ ├── disc/
│ │ │ │ ├── action.ts
│ │ │ │ └── reducer.ts
│ │ │ ├── global/
│ │ │ │ ├── action.ts
│ │ │ │ └── reducer.ts
│ │ │ ├── menu/
│ │ │ │ ├── action.ts
│ │ │ │ └── reducer.ts
│ │ │ └── tabs/
│ │ │ ├── action.ts
│ │ │ └── reducer.ts
│ │ └── mutation-types.ts
│ ├── routers/
│ │ ├── constant.tsx
│ │ ├── index.tsx
│ │ ├── interface/
│ │ │ └── index.ts
│ │ ├── modules/
│ │ │ ├── aiConfig.tsx
│ │ │ ├── article.tsx
│ │ │ ├── author.tsx
│ │ │ ├── category.tsx
│ │ │ ├── column.tsx
│ │ │ ├── config.tsx
│ │ │ ├── error.tsx
│ │ │ ├── global.tsx
│ │ │ ├── home.tsx
│ │ │ ├── resume.tsx
│ │ │ ├── sensitive.tsx
│ │ │ ├── statistics.tsx
│ │ │ ├── tag.tsx
│ │ │ └── wxMenu.tsx
│ │ ├── route.tsx
│ │ └── utils/
│ │ ├── authRouter.tsx
│ │ └── lazyLoad.tsx
│ ├── styles/
│ │ ├── common.less
│ │ ├── reset.less
│ │ ├── theme/
│ │ │ ├── theme-dark.less
│ │ │ └── theme-default.less
│ │ └── var.less
│ ├── typings/
│ │ ├── common.ts
│ │ ├── global.d.ts
│ │ ├── plugins.d.ts
│ │ └── window.d.ts
│ ├── utils/
│ │ ├── getEnv.ts
│ │ ├── is/
│ │ │ └── index.ts
│ │ └── util.ts
│ ├── views/
│ │ ├── aiConfig/
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── article/
│ │ │ ├── components/
│ │ │ │ ├── debounceselect/
│ │ │ │ │ ├── index.scss
│ │ │ │ │ └── index.tsx
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── edit/
│ │ │ │ ├── index.scss
│ │ │ │ ├── index.tsx
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ └── list/
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── author/
│ │ │ ├── loginAudit/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── whitelist/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ └── zsxqlist/
│ │ │ ├── components/
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── category/
│ │ │ ├── components/
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.css
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── column/
│ │ │ ├── article/
│ │ │ │ ├── components/
│ │ │ │ │ ├── DatePicker.tsx
│ │ │ │ │ ├── debounceselect/
│ │ │ │ │ │ ├── DebounceSelect.tsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ ├── search/
│ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── tableselect/
│ │ │ │ │ └── TableSelect.tsx
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ └── setting/
│ │ │ ├── articlesort/
│ │ │ │ ├── index.scss
│ │ │ │ ├── index.tsx
│ │ │ │ ├── search.scss
│ │ │ │ └── search.tsx
│ │ │ ├── components/
│ │ │ │ ├── authorselect/
│ │ │ │ │ ├── index.scss
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── imgupload/
│ │ │ │ │ └── index.tsx
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── groups/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.css
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── comment/
│ │ │ ├── components/
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── config/
│ │ │ ├── components/
│ │ │ │ ├── imgupload/
│ │ │ │ │ ├── ImgCropUpload.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.css
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── global/
│ │ │ ├── components/
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── home/
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── login/
│ │ │ ├── components/
│ │ │ │ └── LoginForm.tsx
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── resume/
│ │ │ ├── components/
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.css
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── sensitive/
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── statistics/
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ ├── tag/
│ │ │ ├── components/
│ │ │ │ └── search/
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── index.css
│ │ │ ├── index.scss
│ │ │ └── index.tsx
│ │ └── wxMenu/
│ │ ├── index.scss
│ │ └── index.tsx
│ └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# @see: http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
indent_style = tab # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
max_line_length = 130 # 最大行长度
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪
================================================
FILE: .eslintignore
================================================
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
.eslintrc.js
.prettierrc.js
/src/mock/*
================================================
FILE: .eslintrc.js
================================================
// @see: http://eslint.cn
module.exports = {
settings: {
react: {
version: "detect"
}
},
root: true,
env: {
browser: true,
node: true,
es6: true
},
/* 指定如何解析语法 */
parser: "@typescript-eslint/parser",
/* 优先级低于 parse 的语法解析配置 */
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
jsxPragma: "React",
ecmaFeatures: {
jsx: true
}
},
plugins: ["react", "@typescript-eslint", "react-hooks", "prettier", "simple-import-sort"],
/* 继承某些已有的规则 */
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"prettier",
"plugin:prettier/recommended"
],
/*
* "off" 或 0 ==> 关闭规则
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
*/
rules: {
// eslint (http://eslint.cn/docs/rules)
"no-var": "error", // 要求使用 let 或 const 而不是 var
"no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行
"no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们
"prefer-const": "off", // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
"no-irregular-whitespace": "off", // 禁止不规则的空白
// typeScript (https://typescript-eslint.io/rules)
"@typescript-eslint/no-unused-vars": "off", // 禁止定义未使用的变量
"@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长
"@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。
"@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型
"@typescript-eslint/ban-ts-ignore": "off", // 禁止使用 @ts-ignore
"@typescript-eslint/ban-types": "off", // 禁止使用特定类型
"@typescript-eslint/explicit-function-return-type": "off", // 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明
"@typescript-eslint/no-var-requires": "off", // 不允许在 import 语句中使用 require 语句
"@typescript-eslint/no-empty-function": "off", // 禁止空函数
"@typescript-eslint/no-use-before-define": "off", // 禁止在变量定义之前使用它们
"@typescript-eslint/ban-ts-comment": "off", // 禁止 @ts-<directive> 使用注释或要求在指令后进行描述
"@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!)
"@typescript-eslint/explicit-module-boundary-types": "off", // 要求导出函数和类的公共类方法的显式返回和参数类型
"@typescript-eslint/no-empty-interface": "off",
// react (https://github.com/jsx-eslint/eslint-plugin-react)
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off",
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn"
},
overrides: [
{
files: ["*.js", "*.jsx", "*.ts", "*.tsx"],
rules: {
"simple-import-sort/imports": [
"warn",
{
groups: [
// Packages `react` related packages come first.
["^react", "^@?\\w"],
// Other relative imports. Put same-folder imports and `.` last.
["^(@|components)(/.*|$)", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
// Style imports.
["^.+\\.?(css)$"]
]
}
]
}
}
]
};
================================================
FILE: .gitattributes
================================================
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist.tar.gz
dist-ssr
*.local
stats.html
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vscode/settings.json
dist.zip
.env.deploy
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:lint-staged
================================================
FILE: .prettierignore
================================================
/dist/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*
================================================
FILE: .prettierrc.js
================================================
// @see: https://www.prettier.cn
module.exports = {
// 超过最大值换行
printWidth: 130,
// 缩进字节数
tabWidth: 2,
// 使用制表符而不是空格缩进行
useTabs: true,
// 结尾不用分号(true有,false没有)
semi: true,
// 使用单引号(true单双引号,false双引号)
singleQuote: false,
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
quoteProps: "as-needed",
// 在对象,数组括号与文字之间加空格 "{ foo: bar }"
bracketSpacing: true,
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
trailingComma: "none",
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: false,
// (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 ,always:不省略括号
arrowParens: "avoid",
// 如果文件顶部已经有一个 doclock,这个选项将新建一行注释,并打上@format标记。
insertPragma: false,
// 指定要使用的解析器,不需要写文件开头的 @prettier
requirePragma: false,
// 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
proseWrap: "preserve",
// 在html中空格是否是敏感的 "css" - 遵守CSS显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的
htmlWhitespaceSensitivity: "css",
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
endOfLine: "auto",
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
rangeStart: 0,
rangeEnd: Infinity,
// Vue文件脚本和样式标签缩进
vueIndentScriptAndStyle: false
};
================================================
FILE: .qoder/settings.json
================================================
{
"permissions": {
"ask": [
"Read(!/Users/itwanger/Documents/GitHub/paicoding-admin/**)",
"Edit(!/Users/itwanger/Documents/GitHub/paicoding-admin/**)"
],
"allow": [
"Read(/Users/itwanger/Documents/GitHub/paicoding-admin/**)",
"Edit(/Users/itwanger/Documents/GitHub/paicoding-admin/**)"
]
},
"memoryImport": {},
"monitoring": {}
}
================================================
FILE: .stylelintignore
================================================
/dist/*
/public/*
public/*
================================================
FILE: .stylelintrc.js
================================================
module.exports = {
// 引入标准配置文件和scss配置扩展
extends: ["stylelint-config-standard", "stylelint-config-recommended-scss"],
rules: {
// 引号必须为单引号
"string-quotes": ["single"],
// 冒号后要加空格
"declaration-colon-space-after": ["always"],
// 冒号前不加空格
"declaration-colon-space-before": ["never"],
// 变量后必须添加!default,本地局部变量可以不加
"scss/dollar-variable-default": [true, { ignore: "local" }],
// 属性单独成行
"declaration-block-single-line-max-declarations": [1],
// 属性和值前不带厂商标记(通过autofixer自动添加,不要自己手工写)
"property-no-vendor-prefix": [true],
"value-no-vendor-prefix": [true],
// 多选择器必须单独成行,逗号结尾
"selector-list-comma-newline-after": ["always"],
// 不能有无效的16进制颜色值
"color-no-invalid-hex": [true]
},
ignoreFiles: ["src/**/*.tsx", "src/**/*.ts", "src/**/*.jsx", "src/**/*.js"]
};
================================================
FILE: .trae/documents/在文章编辑页集成 Moveable.js 实现图片缩放.md
================================================
## 实施计划:集成 react-moveable 实现图片缩放
### 1. 安装依赖
- 安装 `react-moveable` 库,用于提供图片的拖拽和缩放功能。
### 2. 修改文章编辑页面 ([index.tsx](file:///Users/itwanger/Documents/GitHub/paicoding-admin/src/views/article/edit/index.tsx))
- **引入 Moveable**:导入 `Moveable` 组件。
- **状态管理**:
- `target`: 存储当前选中的图片元素。
- `moveableRef`: 引用 Moveable 实例。
- **自定义 ByteMD 插件 (`imageMoveablePlugin`)**:
- 在预览区域渲染完成后,为所有 `img` 标签绑定点击事件。
- 点击图片时将其设为 `target`;点击非图片区域时清除 `target`。
- **集成 Moveable 组件**:
- 在编辑器预览区域渲染 `Moveable`。
- 配置 `resizable`, `keepRatio`, `snappable` 等属性。
- **实现缩放同步**:
- 在 `onResize` 事件中实时更新图片的 DOM 样式。
- 在 `onResizeEnd` 事件中,将缩放后的尺寸同步回 Markdown 源码中,通过将 `` 转换为 `<img src="url" width="xxx" height="xxx" />` 来实现持久化。
### 3. 样式优化
- 调整 Moveable 控制柄的样式,确保在编辑器内清晰可见且不遮挡操作。
### 4. 验证与测试
- 在编辑页面插入图片,尝试通过 Moveable 进行缩放。
- 确认缩放后的尺寸在保存并重新加载后依然有效。
================================================
FILE: .trae/documents/文章编辑页:导入 Markdown + 修复 Word 图片清晰度.md
================================================
## 目标
- 在 `#/article/edit/index` 增加“导入Markdown”功能:读取本地 `.md/.markdown/.txt` 内容并写入编辑器。
- 导入时不上传图片;图片仍走现有“转链”按钮统一处理外链图片。
- 针对“语雀导出的 Markdown”做净化:去掉 `<font ...>`、空 font、注释、以及图片 URL 周围的干扰字符(如反引号/多余空格)。
## 方案:导入 Markdown(不触发上传)
1. **新增工具栏按钮**
- 在 [search/index.tsx](file:///Users/itwanger/Documents/GitHub/paicoding-admin/src/views/article/edit/search/index.tsx) 增加“导入Markdown”按钮。
- 扩展 props:新增 `handleImportMarkdown`。
2. **实现 `handleImportMarkdown`(核心逻辑)**
- 在 [index.tsx](file:///Users/itwanger/Documents/GitHub/paicoding-admin/src/views/article/edit/index.tsx) 新增 `handleImportMarkdown`:
- 用隐藏 `input[type=file]` 选择文件,accept:`.md,.markdown,.txt,text/markdown,text/plain`。
- `file.text()` 读取原文(去 BOM、统一换行)。
- 调用 `sanitizeYuqueMarkdown(raw)` 做净化(见下)。
- 若编辑器已有内容:弹 Modal 让用户选“替换/追加/取消”(复用 Word 导入的交互风格)。
- 标题同步:若净化后的 Markdown 首个非空行匹配 `# 标题`,提取为 `shortTitle` 并从正文移除该行。
- 写入编辑器:
- 替换:`setContent(md)` + `handleChange({content: md, shortTitle?})`
- 追加:`content + '\n\n---\n\n' + md`
- 明确不做任何图片上传/转链;用户需要时再点击现有“转链”。
## 语雀 Markdown 净化规则(sanitizeYuqueMarkdown)
- **清理 font 包装**:移除所有 `<font ...>` 与 `</font>` 标签,但保留其中的文本内容。
- **移除无意义的空 font 段落**:例如仅包含空白/换行的 font 标签残留。
- **移除 HTML 注释块**:`<!-- ... -->`(支持多行)。
- **修复图片 URL 干扰格式**:将语雀导出常见写法
- `` / `` / ``
- 统一规范成 `` 或 ``(去反引号、去括号内多余空格)。
- **基础规范化**:`
` → `\n`、去零宽字符、连续 3+ 空行压缩为 2 行。
## 影响范围
- 不改“转链”逻辑:仍由 [index.tsx](file:///Users/itwanger/Documents/GitHub/paicoding-admin/src/views/article/edit/index.tsx) 现有 `handleReplaceImgUrl` 统一处理外链图片。
- 不改 Word 导入图片清晰度方案(按你要求先不处理)。
## 涉及文件
- [src/views/article/edit/index.tsx](file:///Users/itwanger/Documents/GitHub/paicoding-admin/src/views/article/edit/index.tsx)
- [src/views/article/edit/search/index.tsx](file:///Users/itwanger/Documents/GitHub/paicoding-admin/src/views/article/edit/search/index.tsx)
## 验证方式
- 进入 `http://127.0.0.1:3301/#/article/edit/index`:
- 导入语雀导出的 `.md`:确认 font/注释被移除、图片语法变为标准 ``。
- 点击“转链”:确认外链图片正常上传替换,且不会重复上传(沿用现有 30 秒缓存策略)。
- 回归:保存/更新、图片缩放与替换、其它表单项不受影响。
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dsznajder.es7-react-js-snippets",
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode"
]
}
================================================
FILE: AGENTS.md
================================================
# AGENTS.md
This file provides guidance to Qoder (qoder.com) when working with code in this repository.
## Project Overview
paicoding-admin is a React-based admin panel for the 技术派 (PaiCoding) community platform. Built with React 18, TypeScript, Vite 3, Ant Design 5.x, Redux, and React Router v6.
## Build and Development Commands
### Development
```bash
npm run dev
# Starts dev server at http://127.0.0.1:3301
# Default login credentials: admin/admin
```
### Build
```bash
# Production build
npm run build:pro
# Development build
npm run build:build:dev
# Test build
npm run build:test
```
### Preview
```bash
npm run preview
```
### Linting and Code Quality
```bash
# ESLint check and auto-fix
npm run lint:eslint
# Prettier formatting
npm run lint:prettier
# Stylelint for CSS/Less/SCSS
npm run lint:stylelint
# Run lint-staged (pre-commit hook)
npm run lint:lint-staged
```
### Git Commits
```bash
# Automated commit flow with commitizen
npm run commit
```
## Architecture
### State Management (Redux)
- Uses Redux with redux-persist for state persistence
- Key modules: `global`, `menu`, `tabs`, `auth`, `breadcrumb`, `disc`
- Location: `src/redux/modules/`
- Store configuration: `src/redux/index.ts`
- Redux DevTools enabled in development
- Middleware: redux-thunk, redux-promise
### Routing
- React Router v6 with lazy loading support
- Route modules auto-loaded from `src/routers/modules/*.tsx` using `import.meta.globEager`
- Main router config: `src/routers/index.tsx`
- Route definitions: `src/routers/route.tsx`
- Supports nested routes, route guards, and multi-tab navigation
### API Layer
- Axios-based HTTP client with custom wrapper class `RequestHttp`
- Global request/response interceptors
- Features: automatic token injection, loading states, error handling, request cancellation
- Base URL configured via `VITE_API_URL` environment variable
- API modules organized by feature in `src/api/modules/`
- Main config: `src/api/index.ts`
### Layout System
- Main layout: `src/layouts/index.tsx`
- Components: Header, Menu, Tabs, Footer
- Uses Ant Design Layout with collapsible sidebar
- Responsive design with window resize handling
### Project Structure
```
src/
├── api/ # API request definitions by module
├── assets/ # Static assets (images, icons, etc.)
├── components/ # Reusable components
├── config/ # Configuration files
├── enums/ # TypeScript enums
├── hooks/ # Custom React hooks
├── layouts/ # Layout components (Header, Menu, Footer, Tabs)
├── redux/ # Redux store and modules
├── routers/ # Route configuration and modules
├── styles/ # Global styles
├── typings/ # TypeScript type definitions
├── utils/ # Utility functions
└── views/ # Page components by feature
```
### Key Views/Features
- `statistics/`: Dashboard with ECharts data visualization
- `config/`: Platform operation configuration
- `article/`: Article management
- `column/`: Column configuration
- `resume/`: Tutorial/course configuration
- `author/`: User/author management
- `category/`: Category management
- `tag/`: Tag management
- `global/`: Global settings
- `login/`: Authentication
## Development Notes
### Environment Variables
- Development: `.env.development` - Backend at `http://127.0.0.1:8080`
- Production: `.env.production`
- Test: `.env.test`
### Backend Integration
- Backend project: [paicoding](https://github.com/itwanger/paicoding)
- Spring Boot-based community platform
- Ensure Redis and backend server are running before starting admin panel
### TypeScript Configuration
- Strict TypeScript enabled
- Path alias `@` configured to `src/`
- Config: `tsconfig.json`
### Vite Configuration
- Proxy configured for `/admin` and `/api/admin` to `http://127.0.0.1:8080`
- Port: 3301 (configurable via `VITE_PORT`)
- Gzip compression enabled for production builds
- Bundle visualization available with `VITE_REPORT`
- SVG icons via vite-plugin-svg-icons
- Config: `vite.config.ts`
### Code Standards
- ESLint with TypeScript, React, and Prettier integration
- Prettier for code formatting
- Stylelint for CSS/Less/SCSS
- Husky + lint-staged for pre-commit hooks
- Commitizen + commitlint for commit message conventions
## Troubleshooting
### Node Modules Issues
If `npm install` fails:
1. Upgrade Node.js to 16+ (recommended 18+)
2. Try: `npm install --registry=http://registry.npmmirror.com`
3. If ECONNRESET error: `npm config set registry http://registry.npmjs.org/`
4. Delete `node_modules` and reinstall
### launch.sh (Mac/Linux)
Helper script for common tasks:
```bash
./launch.sh install # Install dependencies
./launch.sh server # Start dev server
./launch.sh pro # Build, package, and deploy to server
```
If `$'\r': command not found` error:
```bash
sed -i 's/\r//' launch.sh
# or
dos2unix launch.sh
```
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### 0.0.1 (2022-09-19)
### Features
- 🚀 项目初始化(今天的 🧱 格外烫手)
### Bug Fixes
- 🧩 密密麻麻
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
paicoding-admin is the admin dashboard for 技术派 (paicoding), a community platform. Built with React 18, TypeScript, Vite 3, Ant Design 5.x, Redux, and ECharts. It provides comprehensive management capabilities for articles, authors, categories, tags, columns, and site configuration.
Backend API: https://github.com/itwanger/paicoding (Spring Boot based)
## Development Commands
### Local Development
```bash
npm install
# OR if install fails (requires Node.js 16+)
npm install --registry=http://registry.npmmirror.com
npm run dev
# Opens http://127.0.0.1:3301
# Default credentials: admin/admin
```
### Build
```bash
# Production build
npm run build:pro
# Development build
npm run build:dev
# Test environment build
npm run build:test
```
### Code Quality
```bash
# ESLint check and fix
npm run lint:eslint
# Prettier formatting
npm run lint:prettier
# Stylelint for styles
npm run lint:stylelint
# Lint staged files (runs via husky)
npm run lint:lint-staged
```
### Git Commits
```bash
# Interactive commit with commitizen
npm run commit
# This runs: git pull && git add -A && git-cz && git push
```
Commit types follow conventional commits:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Code formatting (not affecting logic)
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `test`: Test changes
- `build`: Build system changes
- `ci`: CI configuration changes
- `chore`: Other changes
### Deployment
```bash
# For Mac/Linux users
./launch.sh install # Install dependencies
./launch.sh server # Start dev server
./launch.sh pro # Build, upload to server, and deploy
# Manual deployment
npm run build:pro # Generates dist/ directory
# Upload dist/ to /home/admin/ on server
# Configure Nginx:
# location ^~ /admin {
# alias /home/admin/dist/;
# index index.html;
# }
```
## Architecture
### State Management (Redux)
Located in `src/redux/modules/`:
- **global**: Token, theme, language, assembly size, loading state
- **menu**: Menu collapse state, menu list
- **tabs**: Multi-tab navigation state
- **auth**: Authentication and button permissions
- **breadcrumb**: Breadcrumb navigation
- **disc**: Discussion/forum state
Redux uses `redux-persist` to persist state to localStorage. Uses `redux-thunk` and `redux-promise` middleware.
### Routing System
- Routes defined in `src/routers/modules/*.tsx` (modular approach)
- Auto-imported via `import.meta.globEager` in `src/routers/index.tsx`
- Route modules: article, author, category, column, config, global, home, statistics, tag, resume, error
- Lazy loading enabled via `src/routers/utils/lazyLoad.tsx`
- Auth protection via HOC in `src/routers/utils/authRouter.tsx`
- Multi-tab support with keep-alive using `react-activation`
### API Layer
- Axios wrapper in `src/api/index.ts` with global interceptors
- Request/response interceptors handle:
- Token injection via `x-access-token` header
- Loading states via NProgress
- Error handling (599 = login expired, redirects to login)
- Duplicate request cancellation
- API modules in `src/api/modules/`: login, article, author, category, column, config, global, statistics, tag, resume, common
- Base URL configured via `.env.*` files: `/api/admin`
- Proxy configured in `vite.config.ts` to forward to `http://127.0.0.1:8080/`
### Layout Structure
Main layout in `src/layouts/index.tsx`:
- **Sider**: Left menu (`LayoutMenu`)
- **Header**: Top navigation with breadcrumb, theme toggle, user avatar (`LayoutHeader`)
- **Tabs**: Multi-tab navigation (`LayoutTabs`)
- **Content**: Main content area (rendered via `<Outlet>`)
- **Footer**: Footer component (`LayoutFooter`)
Responsive: Auto-collapses menu when window width < 1200px.
### Key Features
1. **Custom Components**:
- `DebounceSelect`: Debounced search select (used in article/column forms)
- `TableSelect`: Paginated searchable select with table display
- `ImgCropUpload`: Image upload with crop functionality (Ant Design)
- `DatePicker`: Custom date picker for expireTime fields
- `SecondSureModal`: Secondary confirmation modal
2. **Theme System**:
- Dark mode support
- Gray mode & color-weak mode
- Component size switching (small/middle/large)
- Managed via `src/hooks/useTheme.ts`
3. **Markdown Editor**:
- Uses `@bytemd/react` with plugins: gfm, highlight, math, gemoji, medium-zoom
- Theme support via `juejin-markdown-themes`
## File Organization
```
src/
├── api/ # API modules and Axios configuration
├── assets/ # Static assets (icons, images)
├── components/ # Global reusable components
├── config/ # Configuration (nprogress, service loading)
├── enums/ # Enumerations (HTTP status codes, common enums)
├── hooks/ # Custom React hooks (useTheme, etc.)
├── layouts/ # Layout components (Header, Menu, Tabs, Footer)
├── redux/ # Redux store and modules
├── routers/ # Route configuration (modular)
├── styles/ # Global styles (less files)
├── typings/ # TypeScript type definitions
├── utils/ # Utility functions
├── views/ # Page components (organized by feature)
```
### Views Structure
- `article/`: Article list, edit with markdown editor, search
- `author/`: Author whitelist, zsxq list
- `category/`: Category management
- `column/`: Column settings, article sorting, groups
- `config/`: Site configuration with image uploads
- `global/`: Global settings
- `home/`: Dashboard (default redirect from `/`)
- `login/`: Login page
- `resume/`: Resume management
- `statistics/`: Data statistics with ECharts
- `tag/`: Tag management
## Important Notes
### Environment Configuration
- Development: `VITE_API_URL = '/api/admin'`, proxy to `http://127.0.0.1:8080`
- Backend must be running on port 8080 (paicoding Spring Boot app)
- Redis must be running for backend
### Path Alias
- `@` maps to `src/` directory (configured in `vite.config.ts` and `tsconfig.json`)
### TypeScript
- Strict mode disabled for flexibility
- Type definitions in `src/typings/` and `src/api/interface/`
### Styling
- Uses Less preprocessor
- Global variables in `src/styles/var.less`
- Ant Design theme customization supported
### Known Issues
- If `npm install` fails with Node.js < 16, upgrade Node.js to 18+ and npm to 9+
- OR download pre-packaged node_modules (mentioned in README)
- Windows users: Convert `launch.sh` line endings if needed (`dos2unix launch.sh`)
### Backend Integration
- Expects backend at `http://127.0.0.1:8080`
- Login endpoint returns token stored in Redux + localStorage
- Token sent as `x-access-token` header in all requests
- Response format: `{ status: { code, msg }, result: {...} }`
- Code 599 = not logged in (redirects to login page)
- Code 200 = success
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 SpicyBoy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# paicoding-admin 🚀
## 介绍 📖
<p align="center">
<a href="https://paicoding.com/">
<img src="https://cdn.tobebetterjavaer.com/images/README/1681354262213.png" alt="技术派" width="400">
</a>
</p>
🚀🚀🚀 paicoding-admin,技术派管理端,基于 React18、React-Router v6、React-Hooks、Redux、TypeScript、Vite3、Ant-Design 5.x、Hook Admin、ECharts 的一套社区管理系统,够惊艳哦。
<br><br>
<p align="center">
<a href="https://paicoding.com/article/detail/15"><img src="https://img.shields.io/badge/技术派-学习圈子-green.svg?style=for-the-badge"></a>
<a href="https://paicoding.com/" target="_blank"><img src="https://img.shields.io/badge/技术派-首页-critical?style=for-the-badge"></a>
<a href="https://github.com/itwanger/paicoding-admin" target="_blank"><img src="https://img.shields.io/badge/技术派-管理端-yellow.svg?style=for-the-badge"></a>
<a href="https://gitee.com/itwanger/paicoding-admin" target="_blank"><img src="https://img.shields.io/badge/码云-项目地址-blue.svg?style=for-the-badge"></a>
</p>
## 一、在线预览地址 👀
- Link:[https://paicoding.com/admin](https://paicoding.com/admin)
## 二、Git 仓库地址 (欢迎 Star⭐)
- GitHub:[https://github.com/itwanger/paicoding-admin](https://github.com/itwanger/paicoding-admin)
- 码云:[https://gitee.com/itwanger/paicoding-admin](https://gitee.com/itwanger/paicoding-admin)
## 三、🔨🔨🔨 项目功能
- 🚀 采用最新技术找开发:React18、React-Router v6、React-Hooks、TypeScript、Vite3
- 🚀 采用 Vite3 作为项目开发、打包工具(配置了 Gzip 打包、跨域代理、打包预览工具……)
- 🚀 整个项目集成了 TypeScript (学期来很酷哦 🤣)
- 🚀 使用 redux 做状态管理,集成 immer、react-redux、redux-persist 开发
- 🚀 使用 TypeScript 对 Axios 整个二次封装 (全局错误拦截、常用请求封装、全局请求 Loading、取消重复请求……)
- 🚀 支持 Antd 组件大小切换、暗黑 && 灰色 && 色弱模式
- 🚀 使用 自定义高阶组件 进行路由权限拦截(403 页面)、页面按钮权限配置
- 🚀 支持 React-Router v6 路由懒加载配置、菜单手风琴模式、无限级菜单、多标签页、面包屑导航
- 🚀 使用 Prettier 统一格式化代码,集成 Eslint、Stylelint 代码校验规范(项目规范配置)
- 🚀 使用 husky、lint-staged、commitlint、commitizen、cz-git 规范提交信息(项目规范配置)
## 四、安装使用步骤 📑
### Clone:
```text
# GitHub
git clone https://github.com/itwanger/paicoding-admin.git
```
### Install:
```text
npm install
cnpm install
# npm install 安装失败,请升级 nodejs 到 16 以上,或尝试使用以下命令:
npm install --registry=http://registry.npmmirror.com
# npm install 如果出现 npm ERR! code ECONNRESET 错误,可尝试执行以下命令后再安装
npm config set registry http://registry.npmjs.org/
```
### Run:
将技术派的后端代码和前端代码拉到本地后,先启动 Redis 和服务端端。然后再启动 admin 端,可以通过 VSCode 来进行开发。

```text
npm run dev
```
会自动在浏览器打开 [http://127.0.0.1:3301](http://127.0.0.1:3301),如下所示。

本地的用户名和密码均为 admin 和 admin 。
如果遇到 nodejs 环境的问题实在无法启动,可能是一些依赖包的问题,可以尝试删除 node_modules 文件夹,重新安装依赖包。如果仍然无法解决,可以通过以下方式获取我已经打包好的 node_modules 安装包。
异常堆栈:

解决方法 1:升级 nodejs 到 18 以上,升级 npm 到 9 以上,然后重新 install。

解决方法 2:删除 node_modules 文件夹,在「沉默王二」公众号后台回复「node」下载 node_modules 依赖包。

然后覆盖你本地的 node_modules 包,然后再执行 `npm run dev` 就可以运行起来了。

### Build:
```text
# 生产环境
npm run build:pro
```
## 五、项目截图
### 1、数据统计页(ECharts 真强大):

### 2、运营配置页(Ant 的图片上传组件不错哦):

### 3、文章管理页:

### 4、专栏配置页(自定义下拉框挺好玩的):


### 5、教程配置页(防抖支持搜索的下拉框、自定义支持分页、搜索的下拉框不错哦)


## 六、文件资源目录 📚
```text
pacoding-admin
├─ .vscode # vscode推荐配置
├─ public # 静态资源文件(忽略打包)
├─ src
│ ├─ api # API 接口管理
│ ├─ assets # 静态资源文件
│ ├─ components # 全局组件
│ ├─ config # 全局配置项
│ ├─ enums # 项目枚举
│ ├─ hooks # 常用 Hooks
│ ├─ language # 语言国际化
│ ├─ layouts # 框架布局
│ ├─ routers # 路由管理
│ ├─ redux # redux store
│ ├─ styles # 全局样式
│ ├─ typings # 全局 ts 声明
│ ├─ utils # 工具库
│ ├─ views # 项目所有页面
│ ├─ App.tsx # 入口页面
│ ├─ main.tsx # 入口文件
│ └─ env.d.ts # vite 声明文件
├─ .editorconfig # 编辑器配置(格式化)
├─ .env # vite 常用配置
├─ .env.deploy.example # 部署脚本配置示例
├─ .env.development # 开发环境配置
├─ .env.production # 生产环境配置
├─ .env.test # 测试环境配置
├─ .eslintignore # 忽略 Eslint 校验
├─ .eslintrc.js # Eslint 校验配置
├─ .gitignore # git 提交忽略
├─ .prettierignore # 忽略 prettier 格式化
├─ .prettierrc.js # prettier 配置
├─ .stylelintignore # 忽略 stylelint 格式化
├─ .stylelintrc.js # stylelint 样式格式化配置
├─ CHANGELOG.md # 项目更新日志
├─ commitlint.config.js # git 提交规范配置
├─ deploy-front.sh # 前端生产环境部署脚本
├─ index.html # 入口 html
├─ LICENSE # 开源协议文件
├─ lint-staged.config # lint-staged 配置文件
├─ package-lock.json # 依赖包包版本锁
├─ package.json # 依赖包管理
├─ postcss.config.js # postcss 配置
├─ README.md # README 介绍
├─ tsconfig.json # typescript 全局配置
└─ vite.config.ts # vite 配置
```
## 七、项目后台接口 🧩
> 依托于技术派项目,一个基于 Spring Boot、MyBatis-Plus、MySQL、Redis、ElasticSearch、MongoDB、Docker、RabbitMQ 等技术栈实现的社区系统,采用主流的互联网技术架构、全新的 UI 设计、支持一键源码部署,拥有完整的文章&教程发布/搜索/评论/统计流程等,代码完全开源,没有任何二次封装,是一个非常适合二次开发/实战的现代化社区项目 👍 。
- 网站首页:[https://paicoding.com/](https://paicoding.com/)
- 源码地址:[https://github.com/itwanger/paicoding](https://github.com/itwanger/paicoding)
## 八、生产环境部署
推荐使用根目录下的 `deploy-front.sh` 一键部署脚本。脚本会自动执行 `npm run build:pro`,将 `dist` 压缩为 `dist.zip`,上传到服务器指定目录,删除远端旧的 `dist` 目录并执行 `unzip` 解压。
1、先准备部署配置文件:
```bash
cp .env.deploy.example .env.deploy
```
2、修改 `.env.deploy` 中的部署参数:
```bash
DEPLOY_SERVER_HOST=你的服务器IP
DEPLOY_SERVER_USER=admin
DEPLOY_SERVER_KEY=/Users/yourname/.ssh/id_rsa
DEPLOY_TARGET_DIR=/home/admin
```
其中:
- `DEPLOY_SERVER_HOST`:服务器 IP 或域名
- `DEPLOY_SERVER_USER`:SSH 登录用户,默认 `admin`
- `DEPLOY_SERVER_KEY`:SSH 私钥路径;如果服务器已经配置免密登录,可以留空
- `DEPLOY_TARGET_DIR`:上传并解压 `dist.zip` 的目标目录,默认 `/home/admin`
3、执行部署脚本:
```bash
./deploy-front.sh
```
如果当前脚本没有执行权限,可以先执行:
```bash
chmod +x deploy-front.sh
```
4、脚本完成后,服务器上会得到最新的 `/home/admin/dist` 目录。
如果你想手动部署,也可以按下面的流程执行:
```bash
npm run build:pro
zip -r dist.zip dist
scp dist.zip admin@你的服务器IP:/home/admin/dist.zip
ssh admin@你的服务器IP
cd /home/admin
rm -rf dist
unzip dist.zip
rm -f dist.zip
```
5、如果采用 Nginx 的话,请在 server 节点下进行 location 配置。
```
location ^~ /admin {
alias /home/admin/dist/; # 根 目 录
index index.html;
}
```
### launch.sh
辅助 shell 脚本,针对 mac/linux 用户而言,提供更好的使用姿势
0. 前提说明
当 launch.sh 执行时,提示 `$‘\r‘: command not found`时,主要原因是 windows 系统编写的 shell 脚本,每行结尾是`\r\n`, 而 linux 的结尾是`\n`,可以通过下面几种方式进行处理
```bash
# case1
sed -i 's/\r//' launch.sh
# case2
# sudo apt-get install -y dos2unix
sudo yum install -y dos2unix
dos2unix launch.sh
```
1.安装依赖:
```bash
./launch.sh install
```
2.本地启动:
```bash
./launch.sh server
```
3.打包上传服务器,并使他生效
```bash
# 下面这个动作,包含以下几步
# 1. 打包 -> 生成 dist 目录, 压缩为 dist.tar.gz 包
# 2. 上传到服务器
# 3. 将之前旧的静态资源备份,然后解压新的上传包
./launch.sh pro
```
## 九、友情链接
- [toBeBetterjavaer](https://github.com/itwanger/toBeBetterJavaer) :一份通俗易懂、风趣幽默的 Java 学习指南,内容涵盖 Java 基础、Java 并发编程、Java 虚拟机、Java 企业级开发、Java 面试等核心知识点。学 Java,就认准二哥的 Java 进阶之路 😄
- [paicoding](https://github.com/itwanger/paicoding) :⭐️ 一款好用又强大的开源社区,基于 Spring Boot、MyBatis-Plus、MySQL、Redis、ElasticSearch、MongoDB、Docker、RabbitMQ 等主流技术栈,附详细教程,包括 Java、Spring、MySQL、Redis、微服务&分布式、消息队列等核心知识点。学编程,就上技术派 😁。
## 十、star 趋势图
[](https://star-history.com/#itwanger/paicoding-admin&Date)
## 十一、许可证
[Apache License 2.0](https://github.com/itwanger/paicoding/blob/main/License)
Copyright (c) 2022-2023 技术派(沉默王二、楼仔、一灰、小超)
================================================
FILE: commitlint.config.js
================================================
// @see: https://cz-git.qbenben.com/zh/guide
/** @type {import('cz-git').UserConfig} */
module.exports = {
ignores: [commit => commit.includes("init")],
extends: ["@commitlint/config-conventional"],
rules: {
// @see: https://commitlint.js.org/#/reference-rules
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"subject-case": [0],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release"
]
]
},
prompt: {
messages: {
type: "Select the type of change that you're committing:",
scope: "Denote the SCOPE of this change (optional):",
customScope: "Denote the SCOPE of this change:",
subject: "Write a SHORT, IMPERATIVE tense description of the change:\n",
body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
footerPrefixsSelect: "Select the ISSUES type of changeList by this change (optional):",
customFooterPrefixs: "Input ISSUES prefix:",
footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
confirmCommit: "Are you sure you want to proceed with the commit above?"
// 中文版
// type: "选择你要提交的类型 :",
// scope: "选择一个提交范围(可选):",
// customScope: "请输入自定义的提交范围 :",
// subject: "填写简短精炼的变更描述 :\n",
// body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: "选择关联issue前缀(可选):",
// customFooterPrefixs: "输入自定义issue前缀 :",
// footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
// confirmCommit: "是否提交或修改commit ?"
},
types: [
{
value: "feat",
name: "feat: 🚀 A new feature",
emoji: "🚀"
},
{
value: "fix",
name: "fix: 🧩 A bug fix",
emoji: "🧩"
},
{
value: "docs",
name: "docs: 📚 Documentation only changes",
emoji: "📚"
},
{
value: "style",
name: "style: 🎨 Changes that do not affect the meaning of the code",
emoji: "🎨"
},
{
value: "refactor",
name: "refactor: ♻️ A code change that neither fixes a bug nor adds a feature",
emoji: "♻️"
},
{
value: "perf",
name: "perf: ⚡️ A code change that improves performance",
emoji: "⚡️"
},
{
value: "test",
name: "test: ✅ Adding missing tests or correcting existing tests",
emoji: "✅"
},
{
value: "build",
name: "build: 📦️ Changes that affect the build system or external dependencies",
emoji: "📦️"
},
{
value: "ci",
name: "ci: 🎡 Changes to our CI configuration files and scripts",
emoji: "🎡"
},
{
value: "chore",
name: "chore: 🔨 Other changes that don't modify src or test files",
emoji: "🔨"
},
{
value: "revert",
name: "revert: ⏪️ Reverts a previous commit",
emoji: "⏪️"
}
// 中文版
// { value: "特性", name: "特性: 🚀 新增功能", emoji: "🚀" },
// { value: "修复", name: "修复: 🧩 修复缺陷", emoji: "🧩" },
// { value: "文档", name: "文档: 📚 文档变更", emoji: "📚" },
// { value: "格式", name: "格式: 🎨 代码格式(不影响功能,例如空格、分号等格式修正)", emoji: "🎨" },
// { value: "重构", name: "重构: ♻️ 代码重构(不包括 bug 修复、功能新增)", emoji: "♻️" },
// { value: "性能", name: "性能: ⚡️ 性能优化", emoji: "⚡️" },
// { value: "测试", name: "测试: ✅ 添加疏漏测试或已有测试改动", emoji: "✅" },
// { value: "构建", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)", emoji: "📦️" },
// { value: "集成", name: "集成: 🎡 修改 CI 配置、脚本", emoji: "🎡" },
// { value: "回退", name: "回退: ⏪️ 回滚 commit", emoji: "⏪️" },
// { value: "其他", name: "其他: 🔨 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: "🔨" }
],
useEmoji: true,
themeColorCode: "",
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: "bottom",
customScopesAlias: "custom",
emptyScopesAlias: "empty",
upperCaseSubject: false,
allowBreakingChanges: ["feat", "fix"],
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
issuePrefixs: [{ value: "closed", name: "closed: ISSUES has been processed" }],
customIssuePrefixsAlign: "top",
emptyIssuePrefixsAlias: "skip",
customIssuePrefixsAlias: "custom",
allowCustomIssuePrefixs: true,
allowEmptyIssuePrefixs: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: ""
}
};
================================================
FILE: deploy-front.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$SCRIPT_DIR"
FRONTEND_DIR="${FRONTEND_DIR:-$ROOT_DIR}"
DEFAULT_ENV_FILE="$ROOT_DIR/.env.deploy"
FALLBACK_ENV_FILE="$ROOT_DIR/.env"
ENV_FILE="${ENV_FILE:-$DEFAULT_ENV_FILE}"
if [ -f "$ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$ENV_FILE"
set +a
elif [ "$ENV_FILE" = "$DEFAULT_ENV_FILE" ] && [ -f "$FALLBACK_ENV_FILE" ]; then
set -a
# shellcheck disable=SC1090
. "$FALLBACK_ENV_FILE"
set +a
fi
SERVER_HOST="${DEPLOY_SERVER_HOST:-${SERVER_HOST:-}}"
SERVER_USER="${DEPLOY_SERVER_USER:-${SERVER_USER:-admin}}"
SERVER_KEY="${DEPLOY_SERVER_KEY:-${SERVER_KEY:-}}"
TARGET_DIR="${DEPLOY_TARGET_DIR:-${TARGET_DIR:-/home/admin}}"
BUILD_CMD="${DEPLOY_BUILD_CMD:-${BUILD_CMD:-npm run build:pro}}"
SKIP_BUILD="${DEPLOY_SKIP_BUILD:-${SKIP_BUILD:-0}}"
HEALTHCHECK_URL="${DEPLOY_HEALTHCHECK_URL:-${HEALTHCHECK_URL:-}}"
HEALTHCHECK_TIMEOUT="${DEPLOY_HEALTHCHECK_TIMEOUT:-${HEALTHCHECK_TIMEOUT:-15}}"
ARTIFACT_NAME="dist.zip"
ARTIFACT_PATH="$ROOT_DIR/$ARTIFACT_NAME"
REMOTE_ARTIFACT_PATH="$TARGET_DIR/$ARTIFACT_NAME"
SSH_OPTS=(
-o StrictHostKeyChecking=accept-new
)
log() {
printf '[deploy] %s\n' "$1"
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'Missing required command: %s\n' "$1" >&2
exit 1
fi
}
require_cmd npm
require_cmd zip
require_cmd ssh
require_cmd scp
if [ -n "$HEALTHCHECK_URL" ]; then
require_cmd curl
fi
if [ ! -d "$FRONTEND_DIR" ]; then
printf 'Frontend directory not found: %s\n' "$FRONTEND_DIR" >&2
exit 1
fi
if [ -z "$SERVER_HOST" ]; then
printf 'DEPLOY_SERVER_HOST is required. Set it in %s or export it before running.\n' "$ENV_FILE" >&2
exit 1
fi
if [ -n "$SERVER_KEY" ]; then
if [ ! -f "$SERVER_KEY" ]; then
printf 'SSH key not found: %s\n' "$SERVER_KEY" >&2
exit 1
fi
SSH_OPTS=(
-i "$SERVER_KEY"
"${SSH_OPTS[@]}"
)
fi
cd "$FRONTEND_DIR"
if [ "$SKIP_BUILD" != "1" ]; then
log "building frontend with: $BUILD_CMD"
eval "$BUILD_CMD"
else
log "skipping build because SKIP_BUILD=1"
fi
if [ ! -d "$FRONTEND_DIR/dist" ]; then
printf 'Build output not found: %s\n' "$FRONTEND_DIR/dist" >&2
exit 1
fi
rm -f "$ARTIFACT_PATH"
log "creating artifact: $ARTIFACT_NAME"
zip -qry "$ARTIFACT_PATH" dist
log "ensuring remote target directory exists"
ssh "${SSH_OPTS[@]}" "$SERVER_USER@$SERVER_HOST" "mkdir -p '$TARGET_DIR'"
log "uploading artifact to $SERVER_USER@$SERVER_HOST:$TARGET_DIR"
scp "${SSH_OPTS[@]}" "$ARTIFACT_PATH" "$SERVER_USER@$SERVER_HOST:$REMOTE_ARTIFACT_PATH"
log "replacing remote dist directory"
ssh "${SSH_OPTS[@]}" "$SERVER_USER@$SERVER_HOST" "
set -e
command -v unzip >/dev/null 2>&1 || { echo 'unzip is required on remote host' >&2; exit 1; }
cd '$TARGET_DIR'
rm -rf dist
unzip -oq '$REMOTE_ARTIFACT_PATH' -d '$TARGET_DIR'
rm -f '$REMOTE_ARTIFACT_PATH'
"
log "cleaning up local artifact"
rm -f "$ARTIFACT_PATH"
log "verifying remote dist/index.html"
ssh "${SSH_OPTS[@]}" "$SERVER_USER@$SERVER_HOST" "test -f '$TARGET_DIR/dist/index.html'"
if [ -n "$HEALTHCHECK_URL" ]; then
log "checking health url: $HEALTHCHECK_URL"
curl --fail --silent --show-error --location --max-time "$HEALTHCHECK_TIMEOUT" "$HEALTHCHECK_URL" >/dev/null
fi
log "deploy finished"
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- title %></title>
</head>
<body>
<div id="root">
<style>
html,
body,
#root {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
background-color: #ffffff;
}
.first-loading-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.first-loading-wrap > h1 {
font-size: 128px;
}
.first-loading-wrap .loading-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 98px;
}
.dot {
position: relative;
box-sizing: border-box;
display: inline-block;
width: 32px;
height: 32px;
font-size: 32px;
transform: rotate(45deg);
animation: ant-rotate 1.2s infinite linear;
}
.dot i {
position: absolute;
display: block;
width: 14px;
height: 14px;
background-color: #1890ff;
border-radius: 100%;
opacity: 0.3;
transform: scale(0.75);
transform-origin: 50% 50%;
animation: ant-spin-move 1s infinite linear alternate;
}
.dot i:nth-child(1) {
top: 0;
left: 0;
}
.dot i:nth-child(2) {
top: 0;
right: 0;
animation-delay: 0.4s;
}
.dot i:nth-child(3) {
right: 0;
bottom: 0;
animation-delay: 0.8s;
}
.dot i:nth-child(4) {
bottom: 0;
left: 0;
animation-delay: 1.2s;
}
@keyframes ant-rotate {
to {
transform: rotate(405deg);
}
}
@keyframes ant-spin-move {
to {
opacity: 1;
}
}
</style>
<div class="first-loading-wrap">
<div class="loading-wrap">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
</div>
</div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>
================================================
FILE: launch.sh
================================================
#!/usr/bin/env bash
# file to upload
WEB_PKG="dist.tar.gz"
WEB_PKG_BK="dist_bk.tar.gz"
TMP_WEB_PKG="tmp.tar.gz"
DEPLOY_SCRIPT="launch.sh"
START_FUNC_NAME="start"
BUILD_FUNC_NAME="build"
INSTALL_FUNC_NAME="install"
SERVER_FUNC_NAME="server"
#env, ssh remote, work dir
ENV_PRO="pro"
SSH_HOST_PRO=("admin@39.105.208.175")
WORK_DIR_PRO="/home/admin/workspace/admin/"
ADMIN_WORKSPACE="dist"
function build() {
echo "---- start to build admin ----"
echo "npm run build:pro"
npm run build:pro
tar -zcvf ${WEB_PKG} dist
echo "---------- 静态资源包dist.tar.gz已打包完成 -------------"
}
function upload() {
# upload jar
# rename to *.jar.bak
scp ${WEB_PKG} $1:$2${TMP_WEB_PKG}
ret=$?
if [[ ${ret} -ne 0 ]] ; then
echo "Failed to scp ${WEB_PKG}"
return 1
fi
# upload script
scp ${DEPLOY_SCRIPT} $1:$2
ret=$?
if [[ ${ret} -ne 0 ]] ; then
echo 'Failed to scp launch.sh'
return 1
fi
}
function install() {
npm install
}
function server() {
npm run dev
}
function start() {
echo "---- 开始部署 ----"
cd ${WORK_DIR_PRO}
mv ${WEB_PKG} ${WEB_PKG_BK}
mv ${ADMIN_WORKSPACE} "${ADMIN_WORKSPACE}_bk"
mv ${TMP_WEB_PKG} ${WEB_PKG}
tar -zxvf ${WEB_PKG}
echo "---- 部署完成 ----"
}
function deploy() {
# package
echo "*******Start to package*******"
build $1
ret=$?
if [[ ${ret} -ne 0 ]] ; then
echo 'Failed to compile'
exit ${ret}
fi
if [ "$1" = "${ENV_PRO}" ]; then
SSH_HOST=${SSH_HOST_PRO[@]}
WORK_DIR=${WORK_DIR_PRO}
else
echo "Unknown env: $1"
exit
fi
for host in ${SSH_HOST[@]}
do
# upload jar and launch.sh
echo "*******Start to upload:${host} *******"
upload ${host} ${WORK_DIR}
ret=$?
if [[ ${ret} -ne 0 ]] ; then
echo 'Failed to upload files'
exit ${ret}
fi
done
for host in ${SSH_HOST[@]}
do
# run
echo "*******Start service:${host} *******"
ssh ${host} "bash ${WORK_DIR}${DEPLOY_SCRIPT} ${START_FUNC_NAME}"
echo "*******Done*******"
done
}
if [ "$1" = "${START_FUNC_NAME}" ]; then
start "$@"
elif [ "$1" = "${BUILD_FUNC_NAME}" ]; then
build $1
elif [ "$1" = "${INSTALL_FUNC_NAME}" ]; then
install $1
elif [ "$1" = "${SERVER_FUNC_NAME}" ]; then
server $1
elif [ "$1" = "${ENV_PRO}" ]; then
deploy $1
else
echo "=========== 本地环境安装 & 调试 =============="
echo "安装依赖: ./launch.sh install"
echo "本地启动: ./launch.sh server"
echo "=========== 上传服务器 & 服务器解压使用 =============="
echo "打包 dist.tar.gz: ./launch.sh build"
echo "打包静态资源并上传到服务器 & 解压执行: ./launch.sh pro"
echo "服务器上资源启用: ./launch.sh start"
fi
================================================
FILE: lint-staged.config.js
================================================
module.exports = {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"],
"package.json": ["prettier --write"],
"*.{scss,less,styl}": ["stylelint --fix", "prettier --write"],
"*.md": ["prettier --write"]
};
================================================
FILE: package.json
================================================
{
"name": "react",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"serve": "vite",
"build:dev": "tsc && vite build --mode development",
"build:test": "tsc && vite build --mode test",
"build:pro": "vite build --mode production",
"preview": "vite preview",
"lint:eslint": "eslint --fix --ext .js,.ts,.tsx ./src",
"lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged",
"prepare": "husky install",
"release": "standard-version",
"commit": "git pull && git add -A && git-cz && git push"
},
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@bytemd/plugin-gemoji": "^1.21.0",
"@bytemd/plugin-gfm": "^1.21.0",
"@bytemd/plugin-highlight": "^1.21.0",
"@bytemd/plugin-math": "^1.21.0",
"@bytemd/plugin-medium-zoom": "^1.21.0",
"@bytemd/react": "^1.21.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"antd": "^5.6.2",
"antd-img-crop": "^4.12.2",
"axios": "^0.27.2",
"dayjs": "^1.11.7",
"driver.js": "^0.9.8",
"echarts": "^5.3.0",
"echarts-liquidfill": "^3.1.0",
"github-markdown-css": "^5.4.0",
"i18next": "^21.8.10",
"immer": "^9.0.15",
"js-md5": "^0.7.3",
"juejin-markdown-themes": "^1.32.1",
"loadash": "^1.0.0",
"lodash": "^4.17.21",
"mammoth": "^1.11.0",
"nprogress": "^0.2.0",
"qs": "^6.10.5",
"react": "^18.2.0",
"react-activation": "^0.11.2",
"react-dom": "^18.2.0",
"react-highlight-words": "^0.20.0",
"react-i18next": "^11.17.3",
"react-moveable": "^0.56.0",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
"react-transition-group": "^4.4.2",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"redux-promise": "^0.6.0",
"redux-thunk": "^2.4.2",
"screenfull": "^6.0.2",
"stylelint": "^14.16.1",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-recommended-scss": "^5.0.2"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",
"@commitlint/config-conventional": "^17.0.2",
"@types/lodash": "^4.14.194",
"@types/node": "^17.0.41",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-highlight-words": "^0.16.4",
"@types/react-router-dom": "^5.3.3",
"@types/redux-promise": "^0.5.29",
"@typescript-eslint/eslint-plugin": "^5.27.1",
"@typescript-eslint/parser": "^5.27.1",
"@vitejs/plugin-react": "^1.3.0",
"autoprefixer": "^10.4.7",
"commitizen": "^4.2.4",
"cz-git": "^1.3.4",
"eslint": "^8.17.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.30.0",
"eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"husky": "^8.0.1",
"less": "^4.1.3",
"lint-staged": "^13.0.2",
"postcss": "^8.4.14",
"prettier": "^2.6.2",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.55.0",
"standard-version": "^9.5.0",
"stylelint-config-recess-order": "^3.0.0",
"stylelint-config-standard": "^26.0.0",
"stylelint-less": "^1.0.6",
"typescript": "^4.6.3",
"vite": "^4.3.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint": "^1.6.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
autoprefixer: {}
}
};
================================================
FILE: src/App.tsx
================================================
import { connect } from "react-redux";
import { HashRouter } from "react-router-dom";
import { ConfigProvider } from "antd";
import zhCN from "antd/lib/locale/zh_CN";
import useTheme from "@/hooks/useTheme";
import Router from "@/routers/index";
import AuthRouter from "@/routers/utils/authRouter";
import "./index.scss";
const App = (props: any) => {
const { assemblySize, themeConfig } = props;
// 全局使用主题
useTheme(themeConfig);
return (
<HashRouter>
<ConfigProvider componentSize={assemblySize} locale={zhCN}>
<AuthRouter>
<Router />
</AuthRouter>
</ConfigProvider>
</HashRouter>
);
};
const mapStateToProps = (state: any) => state.global;
const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(App);
================================================
FILE: src/api/config/servicePort.ts
================================================
// 后端微服务端口名
export const PORT1 = "";
================================================
FILE: src/api/helper/axiosCancel.ts
================================================
import axios, { AxiosRequestConfig, Canceler } from "axios";
import qs from "qs";
import { isFunction } from "@/utils/is/index";
// * 声明一个 Map 用于存储每个请求的标识 和 取消函数
let pendingMap = new Map<string, Canceler>();
// * 序列化参数
export const getPendingUrl = (config: AxiosRequestConfig) =>
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join("&");
export class AxiosCanceler {
/**
* @description: 添加请求
* @param {Object} config
*/
addPending(config: AxiosRequestConfig) {
// * 在请求开始前,对之前的请求做检查取消操作
this.removePending(config);
const url = getPendingUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingMap.has(url)) {
// 如果 pending 中不存在当前请求,则添加进去
pendingMap.set(url, cancel);
}
});
}
/**
* @description: 移除请求
* @param {Object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pendingMap.get(url);
cancel && cancel();
pendingMap.delete(url);
}
}
/**
* @description: 清空所有pending
*/
removeAllPending() {
pendingMap.forEach(cancel => {
cancel && isFunction(cancel) && cancel();
});
pendingMap.clear();
}
/**
* @description: 重置
*/
reset(): void {
pendingMap = new Map<string, Canceler>();
}
}
================================================
FILE: src/api/helper/checkStatus.ts
================================================
import { message } from "antd";
/**
* @description: 校验网络请求状态码
* @param {Number} status
* @return void
*/
export const checkStatus = (status: number): void => {
switch (status) {
case 400:
message.error("请求失败!请您稍后重试");
break;
case 401:
message.error("登录失效!请您重新登录");
break;
case 403:
message.error("当前账号无权限访问!");
break;
case 404:
message.error("你所访问的资源不存在!");
break;
case 405:
message.error("请求方式错误!请您稍后重试");
break;
case 408:
message.error("请求超时!请您稍后重试");
break;
case 500:
message.error("服务异常!");
break;
case 502:
message.error("网关错误!");
break;
case 503:
message.error("服务不可用!");
break;
case 504:
message.error("网关超时!");
break;
default:
message.error("请求失败!");
}
};
================================================
FILE: src/api/index.ts
================================================
import { message } from "antd";
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { PaiRes, ResultData } from "@/api/interface";
import { LOGIN_URL } from "@/config/config";
import NProgress from "@/config/nprogress";
import { showFullScreenLoading, tryHideFullScreenLoading } from "@/config/serviceLoading";
import { ResultEnum } from "@/enums/httpEnum";
import { store } from "@/redux";
import { setToken } from "@/redux/modules/global/action";
import { AxiosCanceler } from "./helper/axiosCancel";
import { checkStatus } from "./helper/checkStatus";
const axiosCanceler = new AxiosCanceler();
const config = {
// 默认地址请求地址,可在 .env 开头文件中修改,在 Axios 中使用
// 实例化的使用用到
baseURL: import.meta.env.VITE_API_URL as string,
// 跨域时候允许携带凭证
withCredentials: true
};
class RequestHttp {
service: AxiosInstance;
// 构造方法
public constructor(config: AxiosRequestConfig) {
// 实例化axios
this.service = axios.create(config);
/**
* @description 请求拦截器
* 客户端发送请求 -> [请求拦截器] -> 服务器
* token校验(JWT) : 接受服务器返回的token,存储到redux/本地储存当中
*/
this.service.interceptors.request.use(
(config: AxiosRequestConfig) => {
console.log("发起请求");
// 进度条开始
NProgress.start();
// 将当前请求添加到 pending 中
axiosCanceler.addPending(config);
// 如果当前请求不需要显示 loading
// 在api服务中通过指定的第三个参数: { headers: { noLoading: true } }来控制不显示loading,参见loginApi
config.headers!.noLoading || showFullScreenLoading();
// 从 Redux store 中获取 token 并将其添加到请求的 headers 中。这通常用于身份验证,确保后端可以验证用户的身份。
const token: string = store.getState().global.token;
console.log("token", token);
return { ...config, headers: { ...config.headers, "x-access-token": token } };
},
(error: AxiosError) => {
console.log("error", error);
return Promise.reject(error);
}
);
/**
* @description 响应拦截器
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
(response: AxiosResponse) => {
console.log("response", response);
const { data, config } = response;
// 进度条结束
NProgress.done();
// 在请求结束后,移除本次请求(关闭loading)
axiosCanceler.removePending(config);
tryHideFullScreenLoading();
// 服务器返回的状态码
// 如果响应是文件流(通过 responseType 判断)
if (config.responseType === "blob") {
// 跳过 dataStatus.code 检查
// TODO(防止下载文件的时候返回数据流,没有code,直接报错)
return response;
}
const dataStatus = data.status;
// 登录失效(code == 599)
if (dataStatus && dataStatus.code == ResultEnum.NOT_LOGIN) {
// 重定向到登录页面
store.dispatch(setToken(""));
message.error(dataStatus.msg);
window.location.hash = LOGIN_URL;
return Promise.reject(data);
}
// 全局错误信息拦截
if (dataStatus.code && dataStatus.code !== ResultEnum.SUCCESS) {
message.error(dataStatus.msg);
return Promise.reject(data);
}
// 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑)
return data;
},
async (error: AxiosError) => {
console.log("error", error);
const { response } = error;
NProgress.done();
tryHideFullScreenLoading();
// 请求超时单独判断,请求超时没有 response
if (error.message.indexOf("timeout") !== -1) message.error("请求超时,请稍后再试");
// 根据响应的错误状态码,做不同的处理
if (response) checkStatus(response.status);
// 服务器结果都没有返回(可能服务器错误可能客户端断网) 断网处理:可以跳转到断网页面
if (!window.navigator.onLine) window.location.hash = "/500";
return Promise.reject(error);
}
);
}
// * 常用请求方法封装
down<T>(url: string, config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
console.log("开始执行 get 请求,下载文件", url, config);
return this.service.get(url, config);
}
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.get(url, { params, ..._object });
}
post<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.post(url, params, _object);
}
postForm<T>(url: string, params?: object, _object = {}): Promise<PaiRes<T>> {
return this.service.post(url, params, _object);
}
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.put(url, params, _object);
}
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
return this.service.delete(url, { params, ..._object });
}
}
export default new RequestHttp(config);
================================================
FILE: src/api/interface/index.ts
================================================
// * 请求响应参数(不包含data)
export interface Result {
code: string;
msg: string;
}
// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
data?: T;
status?: Status;
result?: T;
}
export interface Status {
code: number;
msg: string;
}
export interface PaiRes<T = any> {
status: Status;
result?: T;
}
// * 分页响应参数
export interface ResPage<T> {
datalist: T[];
pageNum: number;
pageSize: number;
total: number;
}
// * 分页请求参数
export interface ReqPage {
pageNum: number;
pageSize: number;
}
// * 登录
export namespace Login {
export interface ReqLoginForm {
username: string;
password: string;
}
export interface ResLogin {
access_token: string;
userId: number;
// 登录用户名
userName: string;
// 用户头像
photo: string;
}
export interface ResAuthButtons {
[propName: string]: any;
}
}
================================================
FILE: src/api/modules/aiConfig.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
export type AISourceValue =
| "CHAT_GPT_3_5"
| "CHAT_GPT_4"
| "PAI_AI"
| "XUN_FEI_AI"
| "ZHI_PU_AI"
| "ZHIPU_CODING"
| "ALI_AI"
| "DEEP_SEEK"
| "DOU_BAO_AI";
export interface GptModelConfig {
keys?: string[];
proxy?: boolean;
apiHost?: string;
timeOut?: number;
maxToken?: number;
}
export interface ChatGptConfig {
main?: AISourceValue;
gpt35?: GptModelConfig;
gpt4?: GptModelConfig;
}
export interface ZhipuConfig {
apiSecretKey?: string;
requestIdTemplate?: string;
model?: string;
}
export interface XunFeiConfig {
hostUrl?: string;
domain?: string;
appId?: string;
apiKey?: string;
apiSecret?: string;
apiPassword?: string;
}
export interface ZhipuCodingConfig {
apiKey?: string;
apiHost?: string;
model?: string;
timeout?: number;
}
export interface DeepSeekConfig {
apiKey?: string;
apiHost?: string;
model?: string;
timeout?: number;
}
export interface DoubaoConfig {
apiKey?: string;
apiHost?: string;
endPoint?: string;
}
export interface AliConfig {
model?: string;
}
export interface AiConfigAdminDTO {
sources?: AISourceValue[];
chatGpt?: ChatGptConfig;
zhipu?: ZhipuConfig;
zhipuCoding?: ZhipuCodingConfig;
xunFei?: XunFeiConfig;
deepSeek?: DeepSeekConfig;
doubao?: DoubaoConfig;
ali?: AliConfig;
}
export type AiConfigAdminReq = AiConfigAdminDTO;
export interface AiConfigTestReq {
source: AISourceValue;
prompt?: string;
}
export interface AiConfigTestRes {
source?: AISourceValue;
success?: boolean;
message?: string;
answer?: string;
costMs?: number;
}
export const getAiConfigDetailApi = () => {
return http.get<AiConfigAdminDTO>(`${PORT1}/ai/config/detail`);
};
export const saveAiConfigApi = (params: AiConfigAdminReq) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/ai/config/save`, params);
};
export const testAiConfigApi = (params: AiConfigTestReq) => {
return http.post<AiConfigTestRes>(`${PORT1}/ai/config/test`, params);
};
================================================
FILE: src/api/modules/article.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
/**
* @name 文章模块
*/
// 获取列表
export const getArticleListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/article/list`, data);
};
// 更新标题操作
export const updateArticleApi = (params: object | undefined) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/article/update`, params);
};
// 保存操作
export const saveArticleApi = (params: object | undefined) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/article/save`, params);
};
// 转链上传图片的操作
export const saveImgApi = (params: string) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/image/save?img=` + params);
};
// 删除操作
export const delArticleApi = (articleId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/article/delete`, { articleId });
};
// 获取文章
export const getArticleApi = (articleId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/article/detail`, { articleId });
};
// 置顶/加精操作
export const operateArticleApi = (params: object) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/article/operate`, params);
};
// 上线/下线操作
export const examineArticleApi = (params: object | undefined) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/article/examine`, params);
};
// AI 生成标题和简介
export const generateArticleAiApi = (params: { shortTitle: string; content: string }) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/article/generate/seo`, params);
};
// 生成语义 URL
export const generateArticleSlugApi = (params: { title?: string; shortTitle?: string }) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/article/generate/slug`, params, {
headers: { noLoading: true },
timeout: 15000
});
};
================================================
FILE: src/api/modules/author.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
// 添加返回类型
export const getAuthorWhiteListApi = () => {
return http.get(`${PORT1}/author/whitelist/get`);
};
// 获取知识星球用户白名单
export const getZsxqWhiteListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/zsxq/whitelist`, data);
};
// 更新知识星球用户白名单
export const updateZsxqWhiteApi = (params: object | undefined) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/zsxq/whitelist/save`, params);
};
// 审核知识星球用户白名单
export const operateZsxqWhiteApi = (params: object | undefined) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/zsxq/whitelist/operate`, params);
};
// 审核知识星球用户白名单,批量
export const operateBatchZsxqWhiteApi = (params: object | undefined) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/zsxq/whitelist/batchOperate`, params);
};
// 重置操作
export const resetAuthorWhiteApi = (authorId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/zsxq/whitelist/reset`, { authorId });
};
// 保存操作
export const updateAuthorWhiteApi = (authorId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/author/whitelist/add`, { authorId });
};
// 删除作者白名单
export const delAuthorWhiteApi = (authorId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/author/whitelist/remove`, { authorId });
};
================================================
FILE: src/api/modules/category.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { IFormType } from "@/views/config";
/**
* @name 分类模块
*/
// 获取列表
export const getCategoryListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/category/list`, data);
};
// 删除操作
export const delCategoryApi = (categoryId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/category/delete`, { categoryId });
};
// 保存操作
export const updateCategoryApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/category/save`, form);
};
// 上线/下线操作
export const operateCategoryApi = (params: object | undefined) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/category/operate`, params);
};
================================================
FILE: src/api/modules/column.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { IFormType } from "@/views/column/setting";
import { IArticleSortFormType, IGroupFormType } from "@/views/column/setting/articlesort";
import { IMoveType } from "@/views/column/setting/groups";
/**
* @name 教程模块
*/
// 获取列表
export const getColumnListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/column/list`, data);
};
// 添加返回类型
export const getColumnByNameListApi = (key: string) => {
return http.get(`${PORT1}/column/query`, { key });
};
// 获取作者列表,参数为作者名称
export const getAuthorListApi = (key: string) => {
return http.get(`${PORT1}/user/query`, { key });
};
// 保存操作
export const updateColumnApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/column/saveColumn`, form);
};
// 删除专栏操作
export const delColumnApi = (columnId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/column/deleteColumn`, { columnId });
};
// 获取列表
export const getColumnArticleListApi = (data: { columnId: number; pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/column/listColumnArticle`, data);
};
// 根据专栏文章分组的方式,获取文章列表
export const getColumnGroupArticlesApi = (columnId: number) => {
return http.get(`${PORT1}/column/listColumnByGroup`, { columnId });
};
// 获取专栏设置的分组列表
export const getColumnGroupListApi = (columnId: number) => {
return http.get(`${PORT1}/column/listGroups`, { columnId });
};
// 保存专栏文章分组
export const updateGroupApi = (form: IGroupFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/column/saveColumnGroup`, form);
};
// 删除专栏文章分组
export const deleteGroupApi = (groupId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/column/deleteColumnGroup`, { groupId });
};
// 保存操作
export const updateColumnArticleApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/column/saveColumnArticle`, form);
};
// 删除教程操作
export const delColumnArticleApi = (id: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/column/deleteColumnArticle`, { id });
};
// 调整两个教程的顺序
export const sortColumnArticleApi = (activeId: number, overId: number) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/column/sortColumnArticleApi`, { activeId, overId });
};
// 调整教程的顺序
export const sortColumnArticleByIDApi = (form: IArticleSortFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/column/sortColumnArticleByIDApi`, form);
};
// 拖拽移动教程或者分组
export const moveColumnArticleOrGroup = (form: IMoveType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/column/moveColumnArticleOrGroup`, form);
};
================================================
FILE: src/api/modules/comment.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface";
export interface SearchCommentReq {
commentId?: number;
articleId?: number;
articleTitle?: string;
userId?: number;
userName?: string;
content?: string;
commentType?: number;
pageNumber: number;
pageSize: number;
}
export interface CommentSaveReq {
commentId?: number;
articleId: number;
parentCommentId?: number;
topCommentId?: number;
commentContent: string;
}
export interface CommentAdminDTO {
commentId: number;
articleId: number;
articleTitle?: string;
userId: number;
userName?: string;
userAvatar?: string;
commentContent: string;
parentCommentId: number;
topCommentId: number;
parentCommentContent?: string;
topCommentContent?: string;
commentType: number;
replyCount?: number;
praiseCount?: number;
highlightInfo?: string;
createTime?: string;
updateTime?: string;
}
export interface CommentPageDTO {
list: CommentAdminDTO[];
pageNum: number;
pageSize: number;
total: number;
pageTotal: number;
}
export const getCommentListApi = (params: SearchCommentReq) => {
return http.post<CommentPageDTO>(`${PORT1}/comment/list`, params);
};
export const getCommentDetailApi = (commentId: number) => {
return http.get<CommentAdminDTO>(`${PORT1}/comment/detail`, { commentId });
};
export const saveCommentApi = (params: CommentSaveReq) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/comment/save`, params);
};
export const deleteCommentApi = (commentId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/comment/delete`, { commentId });
};
================================================
FILE: src/api/modules/common.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { baseDomain } from "@/utils/util";
/**
* @name 分类模块
*/
// 获取字典值
export const getDiscListApi = () => {
console.log("获取字典,getDiscListApi");
return http.get(`${PORT1}/common/dict`);
};
// 上传图片
export const uploadImgApi = (data: FormData) => {
// 添加时间戳参数,确保每个请求 URL 不同,避免被 AxiosCanceler 取消
return http.post<Login.ResAuthButtons>(`${PORT1}/image/upload?t=${Date.now()}`, data);
};
// 文件上传
export const uploadFileUrl = () => {
return `${baseDomain}/oss/upload`;
};
================================================
FILE: src/api/modules/config.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { IFormType } from "@/views/config";
/**
* @name 分类模块
*/
// 获取列表
export const getConfigListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/config/list`, data);
};
// 删除操作
export const delConfigApi = (configId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/config/delete`, { configId });
};
// 保存操作
export const updateConfigApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/config/save`, form);
};
// 上线/下线操作
export const operateConfigApi = (params: object | undefined) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/config/operate`, params);
};
// 刷新配置缓存
export const refreshConfigApi = () => {
return http.get<Login.ResAuthButtons>(`${PORT1}/config/refresh`);
};
================================================
FILE: src/api/modules/global.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { IFormType } from "@/views/global";
/**
* @name 标签模块
*/
// 获取列表
export const getGlobalConfigListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/global/config/list`, data);
};
// 删除操作
export const delGlobalConfigApi = (id: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/global/config/delete`, { id });
};
// 保存操作
export const updateGlobalConfigApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/global/config/save`, form);
};
================================================
FILE: src/api/modules/login.ts
================================================
import qs from "qs";
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
/**
* @name 登录模块
*/
// * 用户登录接口
export const loginApi = (params: Login.ReqLoginForm) => {
return http.postForm<Login.ResLogin>(PORT1 + `/login`, qs.stringify(params)); // post 请求携带 表单 参数 ==> application/x-www-form-urlencoded
};
// * 退出登录接口
export const logoutApi = () => {
return http.get(PORT1 + `/logout`);
};
/**
* 查询当前登录的用户信息
*/
export const loginUserInfo = () => {
return http.get<Login.ResLogin>(PORT1 + `/info`);
};
// * 获取按钮权限
export const getAuthorButtons = () => {
return http.get<Login.ResAuthButtons>(PORT1 + `/auth/buttons`);
};
// * 获取菜单列表
export const getMenuList = () => {
return http.get<Menu.MenuOptions[]>(PORT1 + `/menu/list`);
};
================================================
FILE: src/api/modules/resume.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { IFormType } from "@/views/resume";
/**
* @name 标签模块
*/
// 获取列表
export const getResumeListApi = (data: { pageNumber: number; pageSize: number }) => {
return http.post(`${PORT1}/resume/list`, data);
};
// 删除操作
export const delResumeApi = (resumeId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/resume/delete?resumeId=${resumeId}`);
};
// 上传
export const replayResumeApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/resume/replay`, form);
};
// 下载
export const downResumeApi = (resumeId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/resume/process?resumeId=${resumeId}`);
};
================================================
FILE: src/api/modules/sensitive.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
export interface SensitiveWordHitDTO {
word: string;
hitCount: number;
}
export interface SensitiveWordConfigDTO {
enable: boolean;
denyWords: string[];
allowWords: string[];
hitTotal?: number;
hitWords?: SensitiveWordHitDTO[];
}
export interface SensitiveWordConfigReq {
enable: boolean;
denyWords: string[];
allowWords: string[];
}
export interface SensitiveWordHitPageReq {
pageNumber: number;
pageSize: number;
}
export interface SensitiveWordHitPageDTO {
list: SensitiveWordHitDTO[];
pageNum: number;
pageSize: number;
total: number;
pageTotal: number;
}
export const getSensitiveWordDetailApi = () => {
return http.get<SensitiveWordConfigDTO>(`${PORT1}/sensitive/detail`);
};
export const saveSensitiveWordConfigApi = (params: SensitiveWordConfigReq) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/sensitive/save`, params);
};
export const getSensitiveWordHitListApi = (params: SensitiveWordHitPageReq) => {
return http.post<SensitiveWordHitPageDTO>(`${PORT1}/sensitive/hit/list`, params);
};
export const clearSensitiveWordHitApi = (word: string) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/sensitive/hit/clear`, { word });
};
================================================
FILE: src/api/modules/statistics.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
/**
* @name 数据统计模块
*/
export const getAllApi = () => {
return http.get(`${PORT1}/statistics/queryTotal`);
};
export const getPvUvApi = (day: number) => {
return http.get(`${PORT1}/statistics/pvUvDayList?day=${day}`);
};
export const download2ExcelPvUvApi = (day: number) => {
return http.down(`${PORT1}/statistics/pvUvDayDownload2Excel?day=${day}`, { responseType: "blob" });
};
================================================
FILE: src/api/modules/tag.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
import { Login } from "@/api/interface/index";
import { IFormType } from "@/views/tag";
/**
* @name 标签模块
*/
// 获取列表
export const getTagListApi = (data: any) => {
return http.post(`${PORT1}/tag/list`, data);
};
// 删除操作
export const delTagApi = (tagId: number) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/tag/delete`, { tagId });
};
// 保存操作
export const updateTagApi = (form: IFormType) => {
return http.post<Login.ResAuthButtons>(`${PORT1}/tag/save`, form);
};
// 上线/下线操作
export const operateTagApi = (params: object | undefined) => {
return http.get<Login.ResAuthButtons>(`${PORT1}/tag/operate`, params);
};
================================================
FILE: src/api/modules/user.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
export interface UserLoginAuditItem {
id: number;
userId?: number;
loginName?: string;
starNumber?: string;
loginType?: number;
loginTypeDesc?: string;
eventType?: string;
eventTypeDesc?: string;
deviceId?: string;
deviceName?: string;
ip?: string;
region?: string;
sessionHash?: string;
riskTag?: string;
reason?: string;
createTime?: string;
}
export interface UserSessionItem {
id: number;
userId?: number;
loginName?: string;
loginType?: number;
loginTypeDesc?: string;
sessionHash?: string;
deviceId?: string;
deviceName?: string;
ip?: string;
region?: string;
sessionState?: string;
sessionStateDesc?: string;
offlineReason?: string;
latestSeenTime?: string;
expireTime?: string;
offlineTime?: string;
createTime?: string;
}
export interface UserShareRiskItem {
userId?: number;
loginName?: string;
starNumber?: string;
kickoutCount?: number;
loginSuccessCount?: number;
deviceCount?: number;
ipCount?: number;
lastKickoutTime?: string;
lastActiveTime?: string;
riskLevel?: string;
riskReason?: string;
forbidden?: boolean;
forbidUntil?: string;
forbidReason?: string;
}
export interface LoginAuditQuery {
userId?: number;
loginName?: string;
starNumber?: string;
deviceId?: string;
ip?: string;
eventType?: string;
pageNumber: number;
pageSize: number;
}
export interface UserShareRiskQuery {
loginName?: string;
recentDays?: number;
minKickoutCount?: number;
minDeviceCount?: number;
minIpCount?: number;
pageNumber: number;
pageSize: number;
}
export interface UserSessionQuery {
userId?: number;
loginName?: string;
deviceId?: string;
ip?: string;
activeOnly?: boolean;
pageNumber: number;
pageSize: number;
}
export interface UserForbidReq {
userId: number;
days?: number;
reason?: string;
}
export interface UserUnforbidReq {
userId: number;
}
export interface PageResult<T> {
list: T[];
pageNum: number;
pageSize: number;
total: number;
pageTotal?: number;
}
export const getLoginAuditListApi = (params: LoginAuditQuery) => {
return http.post<PageResult<UserLoginAuditItem>>(`${PORT1}/user/login-audit`, params);
};
export const getUserSessionListApi = (params: UserSessionQuery) => {
return http.post<PageResult<UserSessionItem>>(`${PORT1}/user/session`, params);
};
export const getUserShareRiskListApi = (params: UserShareRiskQuery) => {
return http.post<PageResult<UserShareRiskItem>>(`${PORT1}/user/share-risk`, params);
};
export const forbidUserApi = (params: UserForbidReq) => {
return http.post<string>(`${PORT1}/user/forbid`, params);
};
export const unforbidUserApi = (params: UserUnforbidReq) => {
return http.post<string>(`${PORT1}/user/unforbid`, params);
};
================================================
FILE: src/api/modules/wxMenu.ts
================================================
import http from "@/api";
import { PORT1 } from "@/api/config/servicePort";
export interface WxMenuButton {
type?: string;
name?: string;
key?: string;
url?: string;
appid?: string;
pagepath?: string;
media_id?: string;
article_id?: string;
sub_button?: WxMenuButton[];
}
export interface WxMenuTree {
button?: WxMenuButton[];
}
export interface WxMenuReplyArticle {
title?: string;
description?: string;
picUrl?: string;
url?: string;
}
export interface WxMenuReply {
replyType?: string;
content?: string;
articles?: WxMenuReplyArticle[];
}
export interface WxMenuKeywordReply {
matchType?: string;
keywords?: string[];
replyType?: string;
reply?: WxMenuReply;
enabled?: boolean;
priority?: number;
title?: string;
}
export interface WxMenuClickReply {
key?: string;
title?: string;
reply?: WxMenuReply;
}
export interface WxMenuAiProviderOption {
code?: number;
value?: string;
name?: string;
syncSupport?: boolean;
}
export interface WxMenuConfig {
menuJson?: string;
comment?: string;
subscribeReply?: WxMenuReply;
defaultReply?: WxMenuReply;
keywordReplies?: WxMenuKeywordReply[];
messageFallbackStrategy?: string;
aiPrompt?: string;
aiProvider?: string;
aiEnable?: boolean;
aiProviderOptions?: WxMenuAiProviderOption[];
clickReplies?: WxMenuClickReply[];
}
export interface WxMenuDetail {
draftConfig?: WxMenuConfig;
draftJson?: string;
draftComment?: string;
draftMenu?: WxMenuTree;
draftValid?: boolean;
draftErrors?: string[];
draftWarnings?: string[];
subscribeReply?: WxMenuReply;
defaultReply?: WxMenuReply;
keywordReplies?: WxMenuKeywordReply[];
messageFallbackStrategy?: string;
aiPrompt?: string;
aiProvider?: string;
aiEnable?: boolean;
aiProviderOptions?: WxMenuAiProviderOption[];
clickReplies?: WxMenuClickReply[];
remoteJson?: string;
remoteMenu?: WxMenuTree;
conditionalMenuCount?: number;
remoteError?: string;
menuJsonTemplate?: string;
menuJsonTips?: string[];
replyTips?: string[];
}
export interface WxMenuSaveReq {
menuJson: string;
comment?: string;
subscribeReply?: WxMenuReply;
defaultReply?: WxMenuReply;
keywordReplies?: WxMenuKeywordReply[];
messageFallbackStrategy?: string;
aiPrompt?: string;
aiProvider?: string;
aiEnable?: boolean;
clickReplies?: WxMenuClickReply[];
}
export interface WxMenuValidateReq {
menuJson?: string;
subscribeReply?: WxMenuReply;
defaultReply?: WxMenuReply;
keywordReplies?: WxMenuKeywordReply[];
messageFallbackStrategy?: string;
aiPrompt?: string;
aiProvider?: string;
aiEnable?: boolean;
clickReplies?: WxMenuClickReply[];
}
export interface WxMenuValidateRes {
valid?: boolean;
normalizedMenuJson?: string;
menuErrors?: string[];
replyErrors?: string[];
errors?: string[];
warnings?: string[];
}
export interface WxMenuPreviewMatchReq extends WxMenuSaveReq {
eventType?: string;
eventKey?: string;
content?: string;
}
export interface WxMenuPreviewMatchRes {
matched?: boolean;
matchedRuleTitle?: string;
matchedRuleType?: string;
matchedKeyword?: string;
reply?: WxMenuReply;
fallbackStrategy?: string;
usedFallback?: boolean;
}
export interface WxMenuPreviewAiReq {
content?: string;
aiPrompt?: string;
aiProvider?: string;
aiEnable?: boolean;
}
export interface WxMenuPreviewAiRes {
success?: boolean;
replyText?: string;
provider?: string;
errorMsg?: string;
}
export interface WxMenuPublishReq {
menuJson?: string;
}
export interface WxMenuPublishRes {
success?: boolean;
errCode?: number;
errMsg?: string;
publishedMenuJson?: string;
}
export const getWxMenuDetailApi = () => {
return http.get<WxMenuDetail>(`${PORT1}/wx/menu/detail`);
};
export const saveWxMenuDraftApi = (params: WxMenuSaveReq) => {
return http.post(`${PORT1}/wx/menu/save`, params);
};
export const validateWxMenuApi = (params?: WxMenuValidateReq) => {
return http.post<WxMenuValidateRes>(`${PORT1}/wx/menu/validate`, params);
};
export const previewWxMenuMatchApi = (params?: WxMenuPreviewMatchReq) => {
return http.post<WxMenuPreviewMatchRes>(`${PORT1}/wx/menu/preview/match`, params);
};
export const previewWxMenuAiApi = (params?: WxMenuPreviewAiReq) => {
return http.post<WxMenuPreviewAiRes>(`${PORT1}/wx/menu/preview/ai`, params);
};
export const publishWxMenuApi = (params?: WxMenuPublishReq) => {
return http.post<WxMenuPublishRes>(`${PORT1}/wx/menu/publish`, params);
};
export const syncWxMenuApi = () => {
return http.post<WxMenuDetail>(`${PORT1}/wx/menu/sync`);
};
================================================
FILE: src/assets/fonts/font.less
================================================
@font-face {
font-family: YouSheBiaoTiHei;
src: url("./YouSheBiaoTiHei.ttf");
}
@font-face {
font-family: MetroDF;
src: url("./MetroDF.ttf");
}
@font-face {
font-family: DIN;
src: url("./DIN.Otf");
}
================================================
FILE: src/assets/iconfont/iconfont.less
================================================
@font-face {
font-family: iconfont;
src: url("iconfont.ttf?t=1648886414212") format("truetype");
}
.iconfont {
font-family: iconfont !important;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-style: normal;
}
.icon-zhongyingwen::before {
content: "\e605";
}
.icon-suoxiao::before {
content: "\e641";
}
.icon-fangda::before {
content: "\e826";
}
.icon-contentright::before {
content: "\e8c9";
}
.icon-zhuti::before {
content: "\e62b";
}
================================================
FILE: src/components/ErrorMessage/403.tsx
================================================
import { useNavigate } from "react-router-dom";
import { Button, Result } from "antd";
import { HOME_URL } from "@/config/config";
import "./index.less";
const NotAuth = () => {
const navigate = useNavigate();
const goHome = () => {
navigate(HOME_URL);
};
return (
<Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
extra={
<Button type="primary" onClick={goHome}>
Back Home
</Button>
}
/>
);
};
export default NotAuth;
================================================
FILE: src/components/ErrorMessage/404.tsx
================================================
import { useNavigate } from "react-router-dom";
import { Button, Result } from "antd";
import { HOME_URL } from "@/config/config";
import "./index.less";
const NotFound = () => {
const navigate = useNavigate();
const goHome = () => {
navigate(HOME_URL);
};
return (
<Result
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Button type="primary" onClick={goHome}>
Back Home
</Button>
}
/>
);
};
export default NotFound;
================================================
FILE: src/components/ErrorMessage/500.tsx
================================================
import { useNavigate } from "react-router-dom";
import { Button, Result } from "antd";
import { HOME_URL } from "@/config/config";
import "./index.less";
const NotNetwork = () => {
const navigate = useNavigate();
const goHome = () => {
navigate(HOME_URL);
};
return (
<Result
status="500"
title="500"
subTitle="Sorry, something went wrong."
extra={
<Button type="primary" onClick={goHome}>
Back Home
</Button>
}
/>
);
};
export default NotNetwork;
================================================
FILE: src/components/ErrorMessage/index.less
================================================
.ant-result {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.ant-result-image {
margin: 0;
}
}
================================================
FILE: src/components/Loading/index.less
================================================
/* 请求 Loading 样式 */
.request-loading {
.ant-spin-text {
margin-top: 5px;
font-size: 18px;
color: #509ff1;
}
.ant-spin-dot-item {
background-color: #509ff1;
}
}
/* 请求 Loading 遮罩层样式 */
#loading {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: rgb(0 0 0 / 50%);
}
================================================
FILE: src/components/Loading/index.tsx
================================================
import { Spin } from "antd";
import "./index.less";
const Loading = ({ tip = "Loading" }: { tip?: string }) => {
return <Spin tip={tip} size="large" className="request-loading" />;
};
export default Loading;
================================================
FILE: src/components/SwitchDark/index.tsx
================================================
import { connect } from "react-redux";
import { Switch } from "antd";
import { setThemeConfig } from "@/redux/modules/global/action";
const SwitchDark = (props: any) => {
const { setThemeConfig, themeConfig } = props;
const onChange = (checked: boolean) => {
setThemeConfig({ ...themeConfig, isDark: checked });
};
return (
<Switch
className="dark"
defaultChecked={themeConfig.isDark}
checkedChildren={<>🌞</>}
unCheckedChildren={<>🌜</>}
onChange={onChange}
/>
);
};
const mapStateToProps = (state: any) => state.global;
const mapDispatchToProps = { setThemeConfig };
export default connect(mapStateToProps, mapDispatchToProps)(SwitchDark);
================================================
FILE: src/components/common-wrap/index.scss
================================================
.content-wrap {
height: 100%;
padding: 10px 8px;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
max-width: 100%;
}
.content-inter-wrap {
flex: 1;
background-color: #fff;
padding: 20px;
border-radius: 6px;
box-sizing: border-box;
overflow-x: auto;
max-width: 100%;
}
@media (max-width: 768px) {
.content-wrap {
padding: 8px 4px;
}
.content-inter-wrap {
padding: 16px 12px;
overflow-x: hidden;
}
}
@media (max-width: 480px) {
.content-wrap {
padding: 4px 2px;
}
.content-inter-wrap {
padding: 12px 8px;
border-radius: 4px;
}
}
================================================
FILE: src/components/common-wrap/index.tsx
================================================
import React, { ReactNode } from "react";
import "./index.scss";
interface IProps {
children: ReactNode;
className?: string;
style?: any;
}
export const ContentWrap = ({ children, className, style }: IProps) => (
<div className={`content-wrap ${className || ""}`} style={style}>
{children}
</div>
);
export const ContentInterWrap = ({ children, className, style }: IProps) => (
<div className={`content-inter-wrap ${className || ""}`} style={style}>
{children}
</div>
);
================================================
FILE: src/components/second-sure-modal/index.tsx
================================================
import React, { useState } from "react";
import { Button, Modal } from "antd";
const SecondSureModal: React.FC = () => {
const [open, setOpen] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [modalText, setModalText] = useState("Content of the modal");
const showModal = () => {
setOpen(true);
};
const handleOk = () => {
setModalText("The modal will be closed after two seconds");
setConfirmLoading(true);
setTimeout(() => {
setOpen(false);
setConfirmLoading(false);
}, 2000);
};
const handleCancel = () => {
console.log("Clicked cancel button");
setOpen(false);
};
return (
<>
<Modal title="Title" open={open} onOk={handleOk} confirmLoading={confirmLoading} onCancel={handleCancel}>
<p>{modalText}</p>
</Modal>
</>
);
};
export default SecondSureModal;
================================================
FILE: src/components/svgIcon/index.tsx
================================================
interface SvgProps {
name: string; // 图标的名称 ==> 必传
color?: string; //图标的颜色 ==> 非必传
prefix?: string; // 图标的前缀 ==> 非必传(默认为"icon")
iconStyle?: { [key: string]: any }; // 图标的样式 ==> 非必传
}
export default function SvgIcon(props: SvgProps) {
const { name, prefix = "icon", iconStyle = { width: "100px", height: "100px" } } = props;
const symbolId = `#${prefix}-${name}`;
return (
<svg aria-hidden="true" style={iconStyle}>
<use href={symbolId} />
</svg>
);
}
================================================
FILE: src/config/config.ts
================================================
// ? 全局不动配置项 只做导出不做修改
// * 首页地址(默认)
export const HOME_URL: string = "/statistics/index";
// * 登录地址
export const LOGIN_URL: string = "/login";
// * Tabs(黑名单地址,不需要添加到 tabs 的路由地址,暂时没用)
export const TABS_BLACK_LIST: string[] = ["/403", "/404", "/500", "/layout", "/login", "/dataScreen"];
// * 高德地图key
export const MAP_KEY: string = "";
================================================
FILE: src/config/nprogress.ts
================================================
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({
easing: "ease", // 动画方式
speed: 500, // 递增进度条的速度
showSpinner: true, // 是否显示加载ico
trickleSpeed: 200, // 自动递增间隔
minimum: 0.3 // 初始化时的最小百分比
});
export default NProgress;
================================================
FILE: src/config/serviceLoading.tsx
================================================
import ReactDOM from "react-dom/client";
import Loading from "@/components/Loading";
let needLoadingRequestCount = 0;
// * 显示loading
export const showFullScreenLoading = () => {
if (needLoadingRequestCount === 0) {
let dom = document.createElement("div");
dom.setAttribute("id", "loading");
document.body.appendChild(dom);
ReactDOM.createRoot(dom).render(<Loading />);
}
needLoadingRequestCount++;
};
// * 隐藏loading
export const tryHideFullScreenLoading = () => {
if (needLoadingRequestCount <= 0) return;
needLoadingRequestCount--;
if (needLoadingRequestCount === 0) {
document.body.removeChild(document.getElementById("loading") as HTMLElement);
}
};
================================================
FILE: src/enums/common.ts
================================================
export enum UpdateEnum {
Save = 0,
Edit
}
export interface IPagination {
current: number;
pageSize: number;
total?: number;
}
export const initPagination: IPagination = {
current: 1,
pageSize: 10,
total: 0
};
export enum PushStatusEnum {
noPublish = "0",
Published = "1",
Offline = "2"
}
export const pushStatusInfo = {
[PushStatusEnum.noPublish]: "default",
[PushStatusEnum.Published]: "success",
[PushStatusEnum.Offline]: "error"
};
================================================
FILE: src/enums/httpEnum.ts
================================================
// * 请求枚举配置
/**
* @description:请求配置
*/
export enum ResultEnum {
SUCCESS = 0,
ERROR = 500,
OVERDUE = 599,
TIMEOUT = 10000,
NOT_LOGIN = 100403003,
TYPE = "success"
}
/**
* @description:请求方法
*/
export enum RequestEnum {
GET = "GET",
POST = "POST",
PATCH = "PATCH",
PUT = "PUT",
DELETE = "DELETE"
}
/**
* @description:常用的contentTyp类型
*/
export enum ContentTypeEnum {
// json
JSON = "application/json;charset=UTF-8",
// text
TEXT = "text/plain;charset=UTF-8",
// form-data 一般配合qs
FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8",
// form-data 上传
FORM_DATA = "multipart/form-data;charset=UTF-8"
}
================================================
FILE: src/hooks/useTheme.ts
================================================
import { ThemeConfigProp } from "@/redux/interface";
import darkTheme from "@/styles/theme/theme-dark.less";
import defaultTheme from "@/styles/theme/theme-default.less";
/**
* @description 全局主题设置
* */
const useTheme = (themeConfig: ThemeConfigProp) => {
const { weakOrGray, isDark } = themeConfig;
const initTheme = () => {
// 灰色和弱色切换
const body = document.documentElement as HTMLElement;
if (!weakOrGray) body.setAttribute("style", "");
if (weakOrGray === "weak") body.setAttribute("style", "filter: invert(80%)");
if (weakOrGray === "gray") body.setAttribute("style", "filter: grayscale(1)");
// 切换暗黑模式
let head = document.getElementsByTagName("head")[0];
const getStyle = head.getElementsByTagName("style");
if (getStyle.length > 0) {
for (let i = 0, l = getStyle.length; i < l; i++) {
if (getStyle[i]?.getAttribute("data-type") === "dark") getStyle[i].remove();
}
}
let styleDom = document.createElement("style");
styleDom.dataset.type = "dark";
styleDom.innerHTML = isDark ? darkTheme : defaultTheme;
head.appendChild(styleDom);
};
initTheme();
return {
initTheme
};
};
export default useTheme;
================================================
FILE: src/index.scss
================================================
.ant-table-tbody > tr > td {
padding: 16px !important;
}
================================================
FILE: src/layouts/components/Footer/index.less
================================================
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
border-top: 1px solid #e4e7ed;
a {
font-size: 14px;
color: #858585;
text-decoration: none;
letter-spacing: 0.5px;
white-space: nowrap;
}
}
================================================
FILE: src/layouts/components/Footer/index.tsx
================================================
import { connect } from "react-redux";
import { baseDomain } from "@/utils/util";
import "./index.less";
const LayoutFooter = (props: any) => {
const { themeConfig } = props;
// 定义一个自动获取年份的方法
const getYear = () => {
return new Date().getFullYear();
};
return (
<>
{!themeConfig.footer && (
<div className="footer">
<a href={baseDomain} target="_blank" rel="noreferrer">
{getYear()} © paicoding-admin By 技术派团队.
</a>
</div>
)}
</>
);
};
const mapStateToProps = (state: any) => state.global;
export default connect(mapStateToProps)(LayoutFooter);
================================================
FILE: src/layouts/components/Header/components/AssemblySize.tsx
================================================
import { connect } from "react-redux";
import { Dropdown, Menu } from "antd";
import { setAssemblySize } from "@/redux/modules/global/action";
const AssemblySize = (props: any) => {
const { assemblySize, setAssemblySize } = props;
// 切换组件大小
const onClick = (e: MenuInfo) => {
setAssemblySize(e.key);
};
const menu = (
<Menu
items={[
{
key: "middle",
disabled: assemblySize == "middle",
label: <span>默认</span>,
onClick
},
{
disabled: assemblySize == "large",
key: "large",
label: <span>大型</span>,
onClick
},
{
disabled: assemblySize == "small",
key: "small",
label: <span>小型</span>,
onClick
}
]}
/>
);
return (
<Dropdown overlay={menu} placement="bottom" trigger={["click"]} arrow={true}>
<i className="icon-style iconfont icon-contentright"></i>
</Dropdown>
);
};
const mapStateToProps = (state: any) => state.global;
const mapDispatchToProps = { setAssemblySize };
export default connect(mapStateToProps, mapDispatchToProps)(AssemblySize);
================================================
FILE: src/layouts/components/Header/components/AvatarIcon.tsx
================================================
/* eslint-disable prettier/prettier */
import { useRef } from "react";
import { connect, useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { Avatar, Dropdown, MenuProps, message, Modal } from "antd";
import { logoutApi } from "@/api/modules/login";
import loginPng from "@/assets/images/logo_md.png";
import { HOME_URL, LOGIN_URL } from "@/config/config";
import { setToken, setUserInfo } from "@/redux/modules/global/action";
import InfoModal from "./InfoModal";
import PasswordModal from "./PasswordModal";
const AvatarIcon = (props: any) => {
const { userInfo, setToken, setUserInfo } = props;
console.log("AvatarIcon setToken setUserInfo", setToken, setUserInfo);
console.log("AvatarIcon userInfo", userInfo );
const navigate = useNavigate();
interface ModalProps {
showModal: (params: Record<string, any>) => void;
}
const passRef = useRef<ModalProps>(null);
const infoRef = useRef<ModalProps>(null);
// 退出登录
const logout = () => {
Modal.confirm({
title: "温馨提示 🧡",
icon: <ExclamationCircleOutlined />,
content: "是否确认退出登录?",
okText: "确认",
cancelText: "取消",
onOk: async () => {
// 此时需要请求服务器端退出登录接口
const { status, result } = await logoutApi();
if (status && status.code == 0 && result) {
// 退出,清除 token,清除用户信息,跳转到登录页
setToken("");
setUserInfo({});
message.success("退出登录成功!");
navigate(LOGIN_URL);
} else {
message.success("退出登录失败:" + status?.msg);
}
}
});
};
const items: MenuProps["items"] = [
{
key: "1",
label: <span className="dropdown-item">首页</span>,
onClick: () => navigate(HOME_URL)
},
{
key: "2",
label: <span className="dropdown-item">个人信息</span>,
onClick: () => infoRef.current!.showModal({
photo: userInfo.photo,
profile: userInfo.profile,
role: userInfo.role,
userName: userInfo.userName,
})
},
{
key: "3",
label: <span className="dropdown-item">修改密码</span>,
onClick: () => passRef.current!.showModal({ name: 11 })
},
{
type: "divider"
},
{
key: "4",
label: <span className="dropdown-item">退出登录</span>,
onClick: logout
}
];
return (
<>
<Dropdown menu={{ items }} placement="bottom" arrow trigger={["click"]}>
<Avatar size="large" src={userInfo.photo || loginPng} />
</Dropdown>
<InfoModal innerRef={infoRef}></InfoModal>
<PasswordModal innerRef={passRef}></PasswordModal>
</>
);
};
const mapDispatchToProps = { setToken, setUserInfo };
export default connect(null, mapDispatchToProps)(AvatarIcon);
================================================
FILE: src/layouts/components/Header/components/BreadcrumbNav.tsx
================================================
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { Breadcrumb } from "antd";
import { HOME_URL } from "@/config/config";
const BreadcrumbNav = (props: any) => {
const { pathname } = useLocation();
const { themeConfig } = props.global;
const breadcrumbList = props.breadcrumb.breadcrumbList[pathname] || [];
console.log({ breadcrumbList });
return (
<>
{!themeConfig.breadcrumb && (
<Breadcrumb>
<Breadcrumb.Item href={`#${HOME_URL}`}>首页</Breadcrumb.Item>
{breadcrumbList.map((item: string) => {
return <Breadcrumb.Item key={item}>{item !== "首页" ? item : null}</Breadcrumb.Item>;
})}
</Breadcrumb>
)}
</>
);
};
const mapStateToProps = (state: any) => state;
export default connect(mapStateToProps)(BreadcrumbNav);
================================================
FILE: src/layouts/components/Header/components/CollapseIcon.tsx
================================================
import { connect } from "react-redux";
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons";
import { updateCollapse } from "@/redux/modules/menu/action";
const CollapseIcon = (props: any) => {
const { isCollapse, updateCollapse } = props;
return (
<div
className="collapsed"
onClick={() => {
updateCollapse(!isCollapse);
}}
>
{isCollapse ? <MenuUnfoldOutlined id="isCollapse" /> : <MenuFoldOutlined id="isCollapse" />}
</div>
);
};
const mapStateToProps = (state: any) => state.menu;
const mapDispatchToProps = { updateCollapse };
export default connect(mapStateToProps, mapDispatchToProps)(CollapseIcon);
================================================
FILE: src/layouts/components/Header/components/Fullscreen.tsx
================================================
import { useEffect, useState } from "react";
import { message } from "antd";
import screenfull from "screenfull";
const Fullscreen = () => {
const [fullScreen, setFullScreen] = useState<boolean>(screenfull.isFullscreen);
useEffect(() => {
screenfull.on("change", () => {
if (screenfull.isFullscreen) setFullScreen(true);
else setFullScreen(false);
return () => screenfull.off("change", () => {});
});
}, []);
const handleFullScreen = () => {
if (!screenfull.isEnabled) message.warning("当前您的浏览器不支持全屏 ❌");
screenfull.toggle();
};
return (
<i className={["icon-style iconfont", fullScreen ? "icon-suoxiao" : "icon-fangda"].join(" ")} onClick={handleFullScreen}></i>
);
};
export default Fullscreen;
================================================
FILE: src/layouts/components/Header/components/InfoModal.tsx
================================================
import { Ref, useImperativeHandle, useState } from "react";
import { Avatar, message, Modal } from "antd";
interface Props {
innerRef: Ref<{ showModal: (params: any) => void } | undefined>;
}
const InfoModal = (props: Props) => {
const [modalVisible, setModalVisible] = useState(false);
const [userInfo, setUserInfo] = useState<Record<string, any>>({}); // 新增状态来存储用户信息
useImperativeHandle(props.innerRef, () => ({
showModal
}));
const showModal = (params: Record<string, any>) => {
console.log(params);
// 把params 显示到 model 中
setUserInfo(params);
setModalVisible(true);
};
const handleCancel = () => {
setModalVisible(false);
};
return (
<Modal title="个人信息" footer={null} open={modalVisible} onCancel={handleCancel} destroyOnClose={true}>
<div className="info-modal">
<div className="info-modal-item">
<span className="info-modal-item-label">头像:</span>
<span className="info-modal-item-value">
<Avatar src={userInfo.photo} />
</span>
</div>
<div className="info-modal-item">
<span className="info-modal-item-label">用户名:</span>
<span className="info-modal-item-value">{userInfo.userName}</span>
</div>
<div className="info-modal-item">
<span className="info-modal-item-label">角色:</span>
<span className="info-modal-item-value">{userInfo.role}</span>
</div>
<div className="info-modal-item">
<span className="info-modal-item-label">个人简介:</span>
<span className="info-modal-item-value">{userInfo.profile}</span>
</div>
</div>
</Modal>
);
};
export default InfoModal;
================================================
FILE: src/layouts/components/Header/components/PasswordModal.tsx
================================================
import { Ref, useImperativeHandle, useState } from "react";
import { message, Modal } from "antd";
interface Props {
innerRef: Ref<{ showModal: (params: any) => void }>;
}
const PasswordModal = (props: Props) => {
const [isModalVisible, setIsModalVisible] = useState(false);
useImperativeHandle(props.innerRef, () => ({
showModal
}));
const showModal = (params: { name: number }) => {
console.log(params);
setIsModalVisible(true);
};
const handleOk = () => {
setIsModalVisible(false);
message.success("修改密码成功 🎉🎉🎉");
};
const handleCancel = () => {
setIsModalVisible(false);
};
return (
<Modal title="修改密码" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel} destroyOnClose={true}>
<p>Some Password...</p>
<p>Some Password...</p>
<p>Some Password...</p>
</Modal>
);
};
export default PasswordModal;
================================================
FILE: src/layouts/components/Header/components/Theme.tsx
================================================
import { useState } from "react";
import { connect } from "react-redux";
import { FireOutlined, SettingOutlined } from "@ant-design/icons";
import { Divider, Drawer, Switch } from "antd";
import SwitchDark from "@/components/SwitchDark";
import { setThemeConfig } from "@/redux/modules/global/action";
import { updateCollapse } from "@/redux/modules/menu/action";
const Theme = (props: any) => {
const [visible, setVisible] = useState<boolean>(false);
const { setThemeConfig, updateCollapse } = props;
const { isCollapse } = props.menu;
const { themeConfig } = props.global;
const { weakOrGray, breadcrumb, tabs, footer } = themeConfig;
const setWeakOrGray = (checked: boolean, theme: string) => {
if (checked) return setThemeConfig({ ...themeConfig, weakOrGray: theme });
setThemeConfig({ ...themeConfig, weakOrGray: "" });
};
const onChange = (checked: boolean, keyName: string) => {
return setThemeConfig({ ...themeConfig, [keyName]: !checked });
};
return (
<>
<i
className="icon-style iconfont icon-zhuti"
onClick={() => {
setVisible(true);
}}
></i>
<Drawer
title="布局设置"
closable={false}
onClose={() => {
setVisible(false);
}}
visible={visible}
width={320}
>
{/* 全局主题 */}
<Divider className="divider">
<FireOutlined />
全局主题
</Divider>
<div className="theme-item">
<span>暗黑模式</span>
<SwitchDark />
</div>
<div className="theme-item">
<span>灰色模式</span>
<Switch
checked={weakOrGray === "gray"}
onChange={e => {
setWeakOrGray(e, "gray");
}}
/>
</div>
<div className="theme-item">
<span>色弱模式</span>
<Switch
checked={weakOrGray === "weak"}
onChange={e => {
setWeakOrGray(e, "weak");
}}
/>
</div>
<br />
{/* 界面设置 */}
<Divider className="divider">
<SettingOutlined />
界面设置
</Divider>
<div className="theme-item">
<span>折叠菜单</span>
<Switch
checked={isCollapse}
onChange={e => {
updateCollapse(e);
}}
/>
</div>
<div className="theme-item">
<span>面包屑导航</span>
<Switch
checked={!breadcrumb}
onChange={e => {
onChange(e, "breadcrumb");
}}
/>
</div>
<div className="theme-item">
<span>标签栏</span>
<Switch
checked={!tabs}
onChange={e => {
onChange(e, "tabs");
}}
/>
</div>
<div className="theme-item">
<span>页脚</span>
<Switch
checked={!footer}
onChange={e => {
onChange(e, "footer");
}}
/>
</div>
</Drawer>
</>
);
};
const mapStateToProps = (state: any) => state;
const mapDispatchToProps = { setThemeConfig, updateCollapse };
export default connect(mapStateToProps, mapDispatchToProps)(Theme);
================================================
FILE: src/layouts/components/Header/index.less
================================================
.ant-layout-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f6f6f6;
.header-lf {
display: flex;
align-items: center;
.collapsed {
margin-right: 20px;
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
}
}
.header-ri {
display: flex;
align-items: center;
.icon-style {
margin-right: 22px;
font-size: 19px;
line-height: 19px;
cursor: pointer;
}
.username {
margin: 0 20px 0 0;
font-size: 15px;
}
.ant-avatar {
cursor: pointer;
}
}
}
.theme-item {
display: flex;
align-items: center;
justify-content: space-between;
margin: 25px 0;
span {
font-size: 14px;
}
.ant-switch {
width: 46px;
}
}
.divider {
margin: 0 0 22px !important;
font-size: 15px !important;
.anticon {
margin-right: 10px;
}
}
.ant-divider-with-text::before,
.ant-divider-with-text::after {
border-top: 1px solid #dcdfe6 !important;
}
================================================
FILE: src/layouts/components/Header/index.tsx
================================================
import { useEffect } from "react";
import { connect } from "react-redux";
import { Layout } from "antd";
import { loginUserInfo } from "@/api/modules/login";
import { getDiscListAction } from "@/redux/modules/disc/action";
import { setToken, setUserInfo } from "@/redux/modules/global/action";
import { setTabsList } from "@/redux/modules/tabs/action";
import AssemblySize from "./components/AssemblySize";
import AvatarIcon from "./components/AvatarIcon";
import BreadcrumbNav from "./components/BreadcrumbNav";
import CollapseIcon from "./components/CollapseIcon";
import Fullscreen from "./components/Fullscreen";
import Theme from "./components/Theme";
import "./index.less";
const LayoutHeader = (props: any) => {
const { setToken, setUserInfo, setTabsList, getDiscListAction } = props;
// 尝试从 props 中获取用户信息,如果没有登录行为,则从后端直接获取
let { userInfo } = props || {};
console.log("LayoutHeader userInfo", userInfo);
const { Header } = Layout;
useEffect(() => {
let toCheck = !userInfo || JSON.stringify(userInfo) === "{}";
console.log("toCheck", toCheck);
if (toCheck) {
const fetchUsrInfo = async () => {
try {
const { status, result } = await loginUserInfo();
console.log("请求用户信息: ", result);
if (status && status.code == 0 && result && result.userId > 0) {
// 保存 token 到 Redux 的状态中
setToken(String(result.userId));
// 保存用户登录信息
setUserInfo(result);
setTabsList([]);
// 获取字典数据
getDiscListAction();
}
} catch (e) {
console.log("初始化用户身份异常!", e);
}
};
// 未拿到用户信息时,主动去拿一下
fetchUsrInfo();
}
}, []);
return (
<Header>
<div className="header-lf">
<CollapseIcon />
<BreadcrumbNav />
</div>
<div className="header-ri">
<AssemblySize />
<Theme />
<Fullscreen />
<span className="username">{userInfo.userName || "技术派"}</span>
<AvatarIcon userInfo={userInfo} />
</div>
</Header>
);
};
const mapStateToProps = (state: any) => state.global;
const mapDispatchToProps = { setToken, setUserInfo, getDiscListAction, setTabsList };
export default connect(mapStateToProps, mapDispatchToProps)(LayoutHeader);
================================================
FILE: src/layouts/components/Menu/components/Logo.tsx
================================================
import { connect } from "react-redux";
import logo from "@/assets/images/logo.svg";
import logoMd from "@/assets/images/logo_md.png";
const Logo = (props: any) => {
const { isCollapse } = props;
return (
<div className="logo-box">
<img src={!isCollapse ? logo : logoMd} alt="logo" className={!isCollapse ? "logo-img" : "logo-img-md"} />
</div>
);
};
const mapStateToProps = (state: any) => state.menu;
export default connect(mapStateToProps)(Logo);
================================================
FILE: src/layouts/components/Menu/index.css
================================================
.menu {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
/* 去除菜单 Loading 遮罩层 */
}
.menu .logo-box {
display: flex;
align-items: center;
justify-content: center;
height: 55px;
}
.menu .logo-box .logo-img {
width: 100px;
margin: 0;
}
.menu .logo-box .logo-img-md {
width: 30px;
}
.menu .logo-box .logo-text {
margin: 0 0 0 10px;
font-size: 24px;
font-weight: bold;
color: #dadada;
white-space: nowrap;
}
.menu .ant-menu-root {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.menu .ant-spin-nested-loading,
.menu .ant-spin-container {
display: flex;
flex-direction: column;
height: 100%;
}
.menu .ant-spin-nested-loading .ant-spin,
.menu .ant-spin-container .ant-spin {
max-height: 100% !important;
}
.menu .ant-spin-nested-loading .ant-spin-container::after,
.menu .ant-spin-container .ant-spin-container::after {
background: transparent !important;
}
.menu .ant-spin-nested-loading .ant-spin-blur,
.menu .ant-spin-container .ant-spin-blur {
overflow: auto !important;
clear: none !important;
opacity: 1 !important;
}
================================================
FILE: src/layouts/components/Menu/index.less
================================================
.menu {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
.logo-box {
display: flex;
align-items: center;
justify-content: center;
height: 55px;
.logo-img {
width: 100px;
margin: 0;
}
.logo-img-md {
width: 30px;
}
.logo-text {
margin: 0 0 0 10px;
font-size: 24px;
font-weight: bold;
color: #dadada;
white-space: nowrap;
}
}
.ant-menu-root {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
/* 去除菜单 Loading 遮罩层 */
.ant-spin-nested-loading,
.ant-spin-container {
display: flex;
flex-direction: column;
height: 100%;
.ant-spin {
max-height: 100% !important;
}
.ant-spin-container::after {
background: transparent !important;
}
.ant-spin-blur {
overflow: auto !important;
clear: none !important;
opacity: 1 !important;
}
}
}
================================================
FILE: src/layouts/components/Menu/index.tsx
================================================
/**
* 菜单控制
*/
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import * as Icons from "@ant-design/icons";
import type { MenuProps } from "antd";
import { Menu, Spin } from "antd";
import { setAuthRouter } from "@/redux/modules/auth/action";
import { setBreadcrumbList } from "@/redux/modules/breadcrumb/action";
import { setMenuList } from "@/redux/modules/menu/action";
import { currentMenuList } from "@/routers/route";
import { getOpenKeys, searchRoute } from "@/utils/util";
import Logo from "./components/Logo";
import "./index.less";
const LayoutMenu = (props: any) => {
const { pathname } = useLocation();
const { isCollapse } = props;
const [selectedKeys, setSelectedKeys] = useState<string[]>([pathname]);
const [openKeys, setOpenKeys] = useState<string[]>([]);
// 刷新页面菜单保持高亮
useEffect(() => {
setSelectedKeys([pathname]);
isCollapse ? null : setOpenKeys(getOpenKeys(pathname));
}, [pathname, isCollapse]);
// 设置当前展开的 subMenu
const onOpenChange = (openKeys: string[]) => {
if (openKeys.length === 0 || openKeys.length === 1) return setOpenKeys(openKeys);
const latestOpenKey = openKeys[openKeys.length - 1];
if (latestOpenKey.includes(openKeys[0])) return setOpenKeys(openKeys);
setOpenKeys([latestOpenKey]);
};
// 定义 menu 类型
type MenuItem = Required<MenuProps>["items"][number];
// 动态渲染 Icon 图标
const customIcons: { [key: string]: any } = Icons;
// 处理后台返回菜单 key 值为 antd 菜单需要的 key 值
// const deepLoopFloat = (menuList: Menu.MenuOptions[], newArr: MenuItem[] = []) => {
// menuList.forEach((item: Menu.MenuOptions) => {
// // 下面判断代码解释 *** !item?.children?.length ==> (!item.children || item.children.length === 0)
// if (!item?.children?.length) return newArr.push(getItem(item.title, item.path, addIcon(item.icon!)));
// newArr.push(getItem(item.title, item.path, addIcon(item.icon!), deepLoopFloat(item.children)));
// });
// return newArr;
// };
// 获取菜单列表并处理成 antd menu 需要的格式
// const [menuList, setMenuList] = useState<MenuItem[]>([]);
const [loading, setLoading] = useState(false);
// const getMenuData = async () => {
// // setLoading(true);
// try {
// // const { data } = await getMenuList();
// // if (!data) return;
// setMenuList(deepLoopFloat(currentMenuList));
// // 存储处理过后的所有面包屑导航栏到 redux 中
// setBreadcrumbList(findAllBreadcrumb(currentMenuList));
// // 把路由菜单处理成一维数组,存储到 redux 中,做菜单权限判断
// const dynamicRouter = handleRouter(currentMenuList);
// setAuthRouter(dynamicRouter);
// setMenuListAction(currentMenuList);
// } finally {
// setLoading(false);
// }
// };
// useEffect(() => {
// getMenuData();
// }, []);
// 点击当前菜单跳转页面
const navigate = useNavigate();
const clickMenu: MenuProps["onClick"] = ({ key }: { key: string }) => {
const route = searchRoute(key, props.menuList);
console.log({ route, props });
if (route.isLink) window.open(route.isLink, "_blank");
console.log({ key });
navigate(key);
};
return (
<div className="menu">
<Spin spinning={loading} tip="Loading...">
<Logo></Logo>
<Menu
theme="dark"
mode="inline"
triggerSubMenuAction="click"
openKeys={openKeys}
selectedKeys={selectedKeys}
// items={menuList}
items={currentMenuList}
onClick={clickMenu}
onOpenChange={onOpenChange}
></Menu>
</Spin>
</div>
);
};
const mapStateToProps = (state: any) => state.menu;
const mapDispatchToProps = { setMenuList, setBreadcrumbList, setAuthRouter };
export default connect(mapStateToProps, mapDispatchToProps)(LayoutMenu);
================================================
FILE: src/layouts/components/Tabs/components/MoreButton.tsx
================================================
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import { DownOutlined } from "@ant-design/icons";
import { Button, Dropdown, Menu } from "antd";
import { HOME_URL } from "@/config/config";
const MoreButton = (props: any) => {
const { t } = useTranslation();
const { pathname } = useLocation();
const navigate = useNavigate();
// close multipleTab
const closeMultipleTab = (tabPath?: string) => {
const handleTabsList = props.tabsList.filter((item: Menu.MenuOptions) => {
return item.path === tabPath || item.path === HOME_URL;
});
props.setTabsList(handleTabsList);
tabPath ?? navigate(HOME_URL);
};
const menu = (
<Menu
items={[
{
key: "1",
label: <span>{t("tabs.closeCurrent")}</span>,
onClick: () => props.delTabs(pathname)
},
{
key: "2",
label: <span>{t("tabs.closeOther")}</span>,
onClick: () => closeMultipleTab(pathname)
},
{
key: "3",
label: <span>{t("tabs.closeAll")}</span>,
onClick: () => closeMultipleTab()
}
]}
/>
);
return (
<Dropdown overlay={menu} placement="bottom" arrow={{ pointAtCenter: true }} trigger={["click"]}>
<Button className="more-button" type="primary" size="small">
{t("tabs.more")} <DownOutlined />
</Button>
</Dropdown>
);
};
export default MoreButton;
================================================
FILE: src/layouts/components/Tabs/index.less
================================================
.tabs {
position: relative;
border-bottom: 1px solid #e4e7ed;
.ant-tabs {
padding: 0 90px 0 13px;
.ant-tabs-nav {
margin: 0;
&::before {
border: none;
}
.ant-tabs-ink-bar {
visibility: visible;
}
.ant-tabs-tab-with-remove.ant-tabs-tab-active {
.ant-tabs-tab-remove {
top: 1px;
margin: 7px;
color: @primary-color !important;
opacity: 1 !important;
}
.ant-tabs-tab-btn {
transform: translateX(-9px);
}
}
.ant-tabs-tab {
padding: 8px 22px;
color: #cccccc;
background: none;
border: none;
transition: none;
.anticon-home {
margin-right: 7px;
}
.ant-tabs-tab-remove {
position: absolute;
right: 0;
color: #cccccc;
opacity: 0;
transition: 0.1s ease-in-out;
&:hover {
color: @primary-color;
}
}
}
.ant-tabs-tab.ant-tabs-tab-with-remove {
&:hover {
.ant-tabs-tab-remove {
top: 1px;
margin: 7px;
opacity: 1;
transition: 0.1s ease-in-out;
}
.ant-tabs-tab-btn {
transform: translateX(-9px);
}
}
}
}
}
.more-button {
position: absolute;
top: 8px;
right: 13px;
padding-left: 10px;
font-size: 12px;
}
}
/* tabs 超出显示的样式 */
.ant-tabs-dropdown {
.ant-tabs-dropdown-menu-item {
.anticon-home {
margin-right: 7px;
}
}
}
/* tabs 不受全局组件大小影响 */
.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab,
.ant-tabs-large > .ant-tabs-nav .ant-tabs-tab {
padding: 8px 22px !important;
font-size: 14px !important;
}
================================================
FILE: src/layouts/components/Tabs/index.tsx
================================================
import { useEffect, useState } from "react";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { HomeFilled } from "@ant-design/icons";
import { message, Tabs } from "antd";
import { HOME_URL } from "@/config/config";
import { setTabsList } from "@/redux/modules/tabs/action";
import { routerArray } from "@/routers";
import { searchRoute } from "@/utils/util";
import MoreButton from "./components/MoreButton";
import "./index.less";
const LayoutTabs = (props: any) => {
const { tabsList } = props.tabs;
const { themeConfig } = props.global;
const { setTabsList } = props;
const { TabPane } = Tabs;
const { pathname } = useLocation();
const navigate = useNavigate();
const [activeValue, setActiveValue] = useState<string>(pathname);
useEffect(() => {
addTabs();
}, [pathname]);
// click tabs
const clickTabs = (path: string) => {
navigate(path);
};
// add tabs
const addTabs = () => {
const route = searchRoute(pathname, routerArray);
let newTabsList = JSON.parse(JSON.stringify(tabsList));
if (tabsList.every((item: any) => item.path !== route.path)) {
newTabsList.push({ title: route.meta!.title, path: route.path });
}
setTabsList(newTabsList);
setActiveValue(pathname);
};
// delete tabs
const delTabs = (tabPath?: string) => {
if (tabPath === HOME_URL) return;
if (pathname === tabPath) {
tabsList.forEach((item: Menu.MenuOptions, index: number) => {
if (item.path !== pathname) return;
const nextTab = tabsList[index + 1] || tabsList[index - 1];
if (!nextTab) return;
navigate(nextTab.path);
});
}
message.success("你删除了Tabs标签 😆😆😆");
setTabsList(tabsList.filter((item: Menu.MenuOptions) => item.path !== tabPath));
};
return (
<>
{!themeConfig.tabs && (
<div className="tabs">
<Tabs
animated
activeKey={activeValue}
onChange={clickTabs}
hideAdd
type="editable-card"
onEdit={path => {
delTabs(path as string);
}}
>
{tabsList.map((item: Menu.MenuOptions) => {
return (
<TabPane
key={item.path}
tab={
<span>
{item.path == HOME_URL ? <HomeFilled /> : ""}
{item.title}
</span>
}
closable={item.path !== HOME_URL}
></TabPane>
);
})}
</Tabs>
<MoreButton tabsList={tabsList} delTabs={delTabs} setTabsList={setTabsList}></MoreButton>
</div>
)}
</>
);
};
const mapStateToProps = (state: any) => state;
const mapDispatchToProps = { setTabsList };
export default connect(mapStateToProps, mapDispatchToProps)(LayoutTabs);
================================================
FILE: src/layouts/index.less
================================================
.container {
display: flex;
min-width: 950px;
height: 100%;
max-width: 100%;
overflow-x: hidden;
box-sizing: border-box;
.ant-layout-sider {
box-sizing: border-box;
flex-shrink: 0;
}
.ant-layout {
/* 防止 tabs 超出不收缩 */
overflow-x: hidden;
min-width: 0;
flex: 1;
.ant-layout-content {
box-sizing: border-box;
flex: 1;
padding: 10px 12px;
overflow-x: hidden;
max-width: 100%;
}
&-sider {
background: #001529;
}
}
// 平板和手机端适配
@media (max-width: 768px) {
min-width: 100%;
.ant-layout {
width: 100%;
min-width: 0;
}
.ant-layout-content {
padding: 8px 6px;
}
}
@media (max-width: 480px) {
.ant-layout-content {
padding: 4px 2px;
}
}
}
================================================
FILE: src/layouts/index.tsx
================================================
import { useEffect } from "react";
import { connect } from "react-redux";
import { Outlet } from "react-router-dom";
import { Layout } from "antd";
import { updateCollapse } from "@/redux/modules/menu/action";
import LayoutFooter from "./components/Footer";
import LayoutHeader from "./components/Header";
import LayoutMenu from "./components/Menu";
import LayoutTabs from "./components/Tabs";
import "./index.less";
const LayoutIndex = (props: any) => {
const { Sider, Content } = Layout;
const { isCollapse, updateCollapse, setAuthButtons } = props;
// 监听窗口大小变化
const listeningWindow = () => {
window.onresize = () => {
return (() => {
let screenWidth = document.body.clientWidth;
if (!isCollapse && screenWidth < 1200) updateCollapse(true);
if (!isCollapse && screenWidth > 1200) updateCollapse(false);
})();
};
};
useEffect(() => {
listeningWindow();
}, []);
return (
// 这里不用 Layout 组件原因是切换页面时样式会先错乱然后在正常显示,造成页面闪屏效果
<section className="container">
<Sider trigger={null} collapsed={props.isCollapse} width={220} theme="dark">
<LayoutMenu></LayoutMenu>
</Sider>
<Layout>
<LayoutHeader></LayoutHeader>
<LayoutTabs></LayoutTabs>
<Content>
<Outlet></Outlet>
</Content>
<LayoutFooter></LayoutFooter>
</Layout>
</section>
);
};
const mapStateToProps = (state: any) => state.menu;
const mapDispatchToProps = { updateCollapse };
export default connect(mapStateToProps, mapDispatchToProps)(LayoutIndex);
================================================
FILE: src/main.tsx
================================================
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import App from "@/App";
import { persistor, store } from "@/redux";
import "@/styles/reset.less";
import "@/assets/iconfont/iconfont.less";
import "@/assets/fonts/font.less";
import "@/styles/common.less";
import "virtual:svg-icons-register";
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
);
================================================
FILE: src/redux/index.ts
================================================
/* eslint-disable simple-import-sort/imports */
/* eslint-disable prettier/prettier */
import { Action, combineReducers, compose, legacy_createStore as createStore, Store } from "redux";
import { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { applyMiddleware } from "redux";
import { persistReducer, persistStore } from "redux-persist";
import storage from "redux-persist/lib/storage";
import reduxPromise from "redux-promise";
import reduxThunk from "redux-thunk";
import auth from "./modules/auth/reducer";
import breadcrumb from "./modules/breadcrumb/reducer";
import disc from "./modules/disc/reducer";
import global from "./modules/global/reducer";
import menu from "./modules/menu/reducer";
import tabs from "./modules/tabs/reducer";
// 创建reducer(拆分reducer)
const reducer = combineReducers({
global,
menu,
tabs,
auth,
breadcrumb,
disc
});
export type RootState = ReturnType<typeof reducer>;
// 定义自定义的 thunk action 类型
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
// 定义自定义的 dispatch 类型
export type AppDispatch = ThunkDispatch<RootState, unknown, Action<string>>;
// redux 持久化配置
const persistConfig = {
key: "redux-state",
storage: storage
};
const persistReducerConfig = persistReducer(persistConfig, reducer);
// 开启 redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 使用 redux 中间件
const middleWares = applyMiddleware(reduxThunk, reduxPromise);
// 创建 store
const store: Store = createStore(persistReducerConfig, composeEnhancers(middleWares));
// 创建持久化 store
const persistor = persistStore(store);
export { persistor, store };
================================================
FILE: src/redux/interface/index.ts
================================================
import type { SizeType } from "antd/lib/config-provider/SizeContext";
import { MapItem } from "@/typings/common";
/* themeConfigProp */
export interface ThemeConfigProp {
primary: string;
isDark: boolean;
weakOrGray: string;
breadcrumb: boolean;
tabs: boolean;
footer: boolean;
}
/* GlobalState */
export interface GlobalState {
token: string;
userInfo: any;
assemblySize: SizeType;
themeConfig: ThemeConfigProp;
}
/* MenuState */
export interface MenuState {
isCollapse: boolean;
menuList: Menu.MenuOptions[];
}
/* TabsState */
export interface TabsState {
tabsActive: string;
tabsList: Menu.MenuOptions[];
}
/* BreadcrumbState */
export interface BreadcrumbState {
breadcrumbList: {
[propName: string]: any;
};
}
/* AuthState */
export interface AuthState {
authButtons: {
[propName: string]: any;
};
authRouter: string[];
}
/**discState */
export interface DiscState {
disc: MapItem[];
}
================================================
FILE: src/redux/modules/auth/action.ts
================================================
import * as types from "@/redux/mutation-types";
// * setAuthButtons
export const setAuthButtons = (authButtons: { [propName: string]: any }) => ({
type: types.SET_AUTH_BUTTONS,
authButtons
});
// * setAuthRouter
export const setAuthRouter = (authRouter: string[]) => ({
type: types.SET_AUTH_ROUTER,
authRouter
});
================================================
FILE: src/redux/modules/auth/reducer.ts
================================================
import produce from "immer";
import { AnyAction } from "redux";
import { AuthState } from "@/redux/interface";
import * as types from "@/redux/mutation-types";
const authState: AuthState = {
authButtons: {},
authRouter: []
};
// auth reducer
const auth = (state: AuthState = authState, action: AnyAction) =>
produce(state, draftState => {
switch (action.type) {
case types.SET_AUTH_BUTTONS:
draftState.authButtons = action.authButtons;
break;
case types.SET_AUTH_ROUTER:
draftState.authRouter = action.authRouter;
break;
default:
return draftState;
}
});
export default auth;
================================================
FILE: src/redux/modules/breadcrumb/action.ts
================================================
import * as types from "@/redux/mutation-types";
// * setBreadcrumbList
export const setBreadcrumbList = (breadcrumbList: { [propName: string]: any }) => ({
type: types.SET_BREADCRUMB_LIST,
breadcrumbList
});
================================================
FILE: src/redux/modules/breadcrumb/reducer.ts
================================================
import produce from "immer";
import { AnyAction } from "redux";
import { BreadcrumbState } from "@/redux/interface";
import * as types from "@/redux/mutation-types";
const breadcrumbState: BreadcrumbState = {
breadcrumbList: {}
};
// breadcrumb reducer
const breadcrumb = (state: BreadcrumbState = breadcrumbState, action: AnyAction) =>
produce(state, draftState => {
switch (action.type) {
case types.SET_BREADCRUMB_LIST:
draftState.breadcrumbList = action.breadcrumbList;
break;
default:
return draftState;
}
});
export default breadcrumb;
================================================
FILE: src/redux/modules/disc/action.ts
================================================
import { toPairs } from "lodash";
import { Dispatch } from "redux";
import { getDiscListApi } from "@/api/modules/common";
import * as types from "@/redux/mutation-types";
// * setDiscList
// export const setDiscList = discList => ({
// type: types.UPDATE_DISC,
// discList
// });
// * redux-promise《async/await》
const dictTransform = (dict = {}, keys = ["id", "title"]) => {
console.log("字典 d", dict);
return toPairs(dict).map(item => {
return {
[keys[0]]: item[0],
[keys[1]]: item[1]
};
});
};
// * redux-thunk
// 获取字典数据
// 异步 action creator
export const getDiscListAction = () => {
return async (dispatch: Dispatch) => {
const { result } = (await getDiscListApi()) || {};
console.log("获取字典,getDiscListAction");
let dictionaryMap = {};
for (const key in result as object) {
if (Object.getOwnPropertyDescriptor(result, key)) {
// @ts-ignore
dictionaryMap[key] = result[key];
// @ts-ignore
dictionaryMap[`${key}List`] = dictTransform(result[key], ["value", "label"]);
}
}
console.log("字典", dictionaryMap);
// 分发 action 更新 state
dispatch({
type: types.UPDATE_DISC,
discList: dictionaryMap
});
};
};
================================================
FILE: src/redux/modules/disc/reducer.ts
================================================
import produce from "immer";
import { AnyAction } from "redux";
import { DiscState } from "@/redux/interface";
import * as types from "@/redux/mutation-types";
const discState: DiscState = {
disc: []
};
// disc reducer
const disc = (state: DiscState = discState, action: AnyAction) =>
produce(state, draftState => {
switch (action.type) {
case types.UPDATE_DISC:
draftState.disc = action.discList;
break;
default:
return draftState;
}
});
export default disc;
================================================
FILE: src/redux/modules/global/action.ts
================================================
import { ThemeConfigProp } from "@/redux/interface/index";
import * as types from "@/redux/mutation-types";
import { MapItem } from "@/typings/common";
// * setToken
export const setToken = (token: string) => ({
type: types.SET_TOKEN,
token
});
export const setUserInfo = (userInfo: MapItem) => {
return {
type: types.USER_INFO,
userInfo
};
};
// * setAssemblySize
export const setAssemblySize = (assemblySize: string) => ({
type: types.SET_ASSEMBLY_SIZE,
assemblySize
});
// * setThemeConfig
export const setThemeConfig = (themeConfig: ThemeConfigProp) => ({
type: types.SET_THEME_CONFIG,
themeConfig
});
================================================
FILE: src/redux/modules/global/reducer.ts
================================================
import produce from "immer";
import { AnyAction } from "redux";
import { GlobalState } from "@/redux/interface";
import * as types from "@/redux/mutation-types";
const globalState: GlobalState = {
token: "",
userInfo: {},
assemblySize: "middle",
themeConfig: {
// 默认 primary 主题颜色
primary: "#1890ff",
// 深色模式
isDark: false,
// 色弱模式(weak) || 灰色模式(gray)
weakOrGray: "",
// 面包屑导航
breadcrumb: true,
// 标签页
tabs: true,
// 页脚
footer: true
}
};
// global reducer
const global = (state: GlobalState = globalState, action: AnyAction) => {
console.log({ action });
return produce(state, draftState => {
switch (action.type) {
case types.SET_TOKEN:
draftState.token = action.token;
break;
case types.USER_INFO:
draftState.userInfo = action.userInfo;
break;
case types.SET_ASSEMBLY_SIZE:
draftState.assemblySize = action.assemblySize;
break;
case types.SET_THEME_CONFIG:
draftState.themeConfig = action.themeConfig;
break;
default:
return draftState;
}
});
};
export default global;
================================================
FILE: src/redux/modules/menu/action.ts
================================================
import { Dispatch } from "react";
import { getMenuList } from "@/api/modules/login";
import * as types from "@/redux/mutation-types";
// * updateCollapse
export const updateCollapse = (isCollapse: boolean) => ({
type: types.UPDATE_COLLAPSE,
isCollapse
});
// * setMenuList
export const setMenuList = (menuList: Menu.MenuOptions[]) => ({
type: types.SET_MENU_LIST,
menuList
});
// ? 下面方法仅为测试使用,不参与任何功能开发
interface MenuProps {
type: string;
menuList: Menu.MenuOptions[];
}
// * redux-thunk
export const getMenuListActionThunk = () => {
return async (dispatch: Dispatch<MenuProps>) => {
const res = await getMenuList();
dispatch({
type: types.SET_MENU_LIST,
menuList: (res.data as Menu.MenuOptions[]) ?? []
});
};
};
// * redux-promise《async/await》
export const getMenuListAction = async (): Promise<MenuProps> => {
const res = await getMenuList();
return {
type: types.SET_MENU_LIST,
menuList: res.data ? res.data : []
};
};
// * redux-promise《.then/.catch》
export const getMenuListActionPromise = (): Promise<MenuProps> => {
return getMenuList().then(res => {
return {
type: types.SET_MENU_LIST,
menuList: res.data ? res.data : []
};
});
};
================================================
FILE: src/redux/modules/menu/reducer.ts
================================================
import produce from "immer";
import { AnyAction } from "redux";
import { MenuState } from "@/redux/interface";
import * as types from "@/redux/mutation-types";
const menuState: MenuState = {
isCollapse: false,
menuList: []
};
// menu reducer
const menu = (state: MenuState = menuState, action: AnyAction) =>
produce(state, draftState => {
switch (action.type) {
case types.UPDATE_COLLAPSE:
draftState.isCollapse = action.isCollapse;
break;
case types.SET_MENU_LIST:
draftState.menuList = action.menuList;
break;
default:
return draftState;
}
});
export default menu;
================================================
FILE: src/redux/modules/tabs/action.ts
================================================
import * as types from "@/redux/mutation-types";
// * setTabsList
export const setTabsList = (tabsList: Menu.MenuOptions[]) => ({
type: types.SET_TABS_LIST,
tabsList
});
// * setTabsActive
export const setTabsActive = (tabsActive: string) => ({
type: types.SET_TABS_ACTIVE,
tabsActive
});
================================================
FILE: src/redux/modules/tabs/reducer.ts
================================================
import produce from "immer";
import { AnyAction } from "redux";
import { HOME_URL } from "@/config/config";
import { TabsState } from "@/redux/interface";
import * as types from "@/redux/mutation-types";
const tabsState: TabsState = {
// tabsActive 其实没啥用,使用 pathname 就可以了😂
tabsActive: HOME_URL,
tabsList: [{ title: "首页", path: HOME_URL }]
};
// tabs reducer
const tabs = (state: TabsState = tabsState, action: AnyAction) =>
produce(state, draftState => {
switch (action.type) {
case types.SET_TABS_LIST:
draftState.tabsList = action.tabsList;
break;
case types.SET_TABS_ACTIVE:
draftState.tabsActive = action.tabsActive;
break;
default:
return draftState;
}
});
export default tabs;
================================================
FILE: src/redux/mutation-types.ts
================================================
// 更新 menu 折叠状态
export const UPDATE_COLLAPSE = "UPDATE_ASIDE_COLLAPSE";
// 设置 menuList
export const SET_MENU_LIST = "SET_MENU_LIST";
// 设置 tabsList
export const SET_TABS_LIST = "SET_TABS_LIST";
// 设置 tabsActive
export const SET_TABS_ACTIVE = "SET_TABS_ACTIVE";
// 设置 breadcrumb
export const SET_BREADCRUMB_LIST = "SET_BREADCRUMB_LIST";
// 设置 authButtons
export const SET_AUTH_BUTTONS = "SET_AUTH_BUTTONS";
// 设置 authRouter
export const SET_AUTH_ROUTER = "SET_AUTH_ROUTER";
// 设置 token
export const SET_TOKEN = "SET_TOKEN";
// 设置 userInfo
export const USER_INFO = "USER_INFO";
// 设置 assemblySize
export const SET_ASSEMBLY_SIZE = "SET_ASSEMBLY_SIZE";
// 设置 setThemeConfig
export const SET_THEME_CONFIG = "SET_THEME_CONFIG";
// 设置字典值
export const UPDATE_DISC = "UPDATE_DISC";
================================================
FILE: src/routers/constant.tsx
================================================
import Layout from "@/layouts/index";
/**
* @description: default layout
*/
export const LayoutIndex = () => <Layout />;
================================================
FILE: src/routers/index.tsx
================================================
import { Navigate, useRoutes } from "react-router-dom";
import { RouteObject } from "@/routers/interface";
import Login from "@/views/login/index";
// * 导入所有router
const metaRouters = import.meta.globEager("./modules/*.tsx") as Record<string, Record<string, RouteObject[]>>;
console.log("metaRouters", metaRouters);
// * 处理路由
export const routerArray: RouteObject[] = [];
Object.keys(metaRouters).forEach(item => {
console.log("item", item);
const router = metaRouters[item];
Object.keys(router).forEach((key: any) => {
console.log("key", key);
routerArray.push(...router[key]);
});
});
export const rootRouter: RouteObject[] = [
{
path: "/",
element: <Navigate to="/login" />
},
{
path: "/login",
element: <Login />,
meta: {
title: "登录页",
key: "login"
}
},
...routerArray,
{
path: "*",
element: <Navigate to="/404" />
}
];
const Router = () => {
const routes = useRoutes(rootRouter);
console.log("routes", { routes });
return routes;
};
export default Router;
================================================
FILE: src/routers/interface/index.ts
================================================
export interface MetaProps {
keepAlive?: boolean;
requiresAuth?: boolean;
title: string;
key?: string;
}
export interface RouteObject {
caseSensitive?: boolean;
children?: RouteObject[];
element?: React.ReactNode;
index?: false;
path?: string;
meta?: MetaProps;
isLink?: string;
}
================================================
FILE: src/routers/modules/aiConfig.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import AiConfigPage from "@/views/aiConfig";
const aiConfigRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/ai/config/index",
element: <AiConfigPage />,
meta: {
title: "AI模型配置",
key: "ai-config"
}
}
]
}
];
export default aiConfigRouter;
================================================
FILE: src/routers/modules/article.tsx
================================================
import React from "react";
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Article from "@/views/article/list";
import lazyLoad from "../utils/lazyLoad";
const articleRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/article",
meta: {
// requiresAuth: true,
title: "文章",
key: "/article"
},
children: [
{
path: "/article/list/index",
element: lazyLoad(React.lazy(() => import("@/views/article/list/index"))),
meta: {
title: "文章列表",
key: "/article/list/index"
}
},
{
path: "/article/edit/index",
element: lazyLoad(React.lazy(() => import("@/views/article/edit/index"))),
meta: {
title: "文章编辑",
key: "/article/edit/index"
}
},
{
path: "/article/comment/index",
element: lazyLoad(React.lazy(() => import("@/views/comment/index"))),
meta: {
title: "评论管理",
key: "/article/comment/index"
}
}
]
}
]
}
];
export default articleRouter;
================================================
FILE: src/routers/modules/author.tsx
================================================
import React from "react";
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import lazyLoad from "../utils/lazyLoad";
const columnRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/author",
meta: {
// requiresAuth: true,
title: "用户管理",
key: "/author"
},
children: [
{
path: "/author/whitelist/index",
element: lazyLoad(React.lazy(() => import("@/views/author/whitelist/index"))),
meta: {
title: "作者白名单",
key: "/author/whitelist/index"
}
},
{
path: "/author/zsxqlist/index",
element: lazyLoad(React.lazy(() => import("@/views/author/zsxqlist/index"))),
meta: {
title: "星球白名单",
key: "/author/zsxqlist/index"
}
},
{
path: "/author/login-audit/index",
element: lazyLoad(React.lazy(() => import("@/views/author/loginAudit/index"))),
meta: {
title: "登录审计",
key: "/author/login-audit/index"
}
}
]
}
]
}
];
export default columnRouter;
================================================
FILE: src/routers/modules/category.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Sort from "@/views/category";
const sortRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/category/index",
element: <Sort />,
meta: {
// requiresAuth: true,
title: "分类",
key: "sort"
}
}
]
}
];
export default sortRouter;
================================================
FILE: src/routers/modules/column.tsx
================================================
import React from "react";
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import lazyLoad from "../utils/lazyLoad";
const columnRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/column",
meta: {
// requiresAuth: true,
title: "专栏",
key: "/column"
},
children: [
{
path: "/column/setting/index",
element: lazyLoad(React.lazy(() => import("@/views/column/setting/index"))),
meta: {
title: "专栏设置",
key: "/column/setting/index"
}
},
{
path: "/column/setting/index/articlesort",
element: lazyLoad(React.lazy(() => import("@/views/column/setting/articlesort/index"))),
meta: {
title: "教程排序",
key: "/column/setting/index/articlesort"
}
},
{
path: "/column/setting/index/groups",
element: lazyLoad(React.lazy(() => import("@/views/column/setting/groups/index"))),
meta: {
title: "专栏分组",
key: "/column/setting/index/groups"
}
},
{
path: "/column/article/index",
element: lazyLoad(React.lazy(() => import("@/views/column/article/index"))),
meta: {
title: "添加教程",
key: "/column/article/index"
}
}
]
}
]
}
];
export default columnRouter;
================================================
FILE: src/routers/modules/config.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Banner from "@/views/config";
const configRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/config/index",
element: <Banner />,
meta: {
// requiresAuth: true,
title: "配置",
key: "config"
}
}
]
}
];
export default configRouter;
================================================
FILE: src/routers/modules/error.tsx
================================================
import React from "react";
import { RouteObject } from "@/routers/interface";
import lazyLoad from "@/routers/utils/lazyLoad";
// 错误页面模块
const errorRouter: Array<RouteObject> = [
{
path: "/403",
element: lazyLoad(React.lazy(() => import("@/components/ErrorMessage/403"))),
meta: {
// requiresAuth: true,
title: "403页面",
key: "403"
}
},
{
path: "/404",
element: lazyLoad(React.lazy(() => import("@/components/ErrorMessage/404"))),
meta: {
// requiresAuth: false,
title: "404页面",
key: "404"
}
},
{
path: "/500",
element: lazyLoad(React.lazy(() => import("@/components/ErrorMessage/500"))),
meta: {
// requiresAuth: false,
title: "500页面",
key: "500"
}
}
];
export default errorRouter;
================================================
FILE: src/routers/modules/global.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Banner from "@/views/global";
const configRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/global/index",
element: <Banner />,
meta: {
// requiresAuth: true,
title: "全局",
key: "global"
}
}
]
}
];
export default configRouter;
================================================
FILE: src/routers/modules/home.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Home from "@/views/home";
// 首页模块
const homeRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/home/index",
element: <Home />,
meta: {
// requiresAuth: true,
title: "首页",
key: "home"
}
}
]
}
];
export default homeRouter;
================================================
FILE: src/routers/modules/resume.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Label from "@/views/resume";
const labelRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/resume/index",
element: <Label />,
meta: {
// requiresAuth: true,
title: "简历",
key: "resume"
}
}
]
}
];
export default labelRouter;
================================================
FILE: src/routers/modules/sensitive.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Sensitive from "@/views/sensitive";
const sensitiveRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/sensitive/index",
element: <Sensitive />,
meta: {
title: "敏感词管理",
key: "sensitive"
}
}
]
}
];
export default sensitiveRouter;
================================================
FILE: src/routers/modules/statistics.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Statistics from "@/views/statistics";
const statisticsRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/statistics/index",
element: <Statistics />,
meta: {
// requiresAuth: true,
title: "数据统计",
key: "statistics"
}
}
]
}
];
export default statisticsRouter;
================================================
FILE: src/routers/modules/tag.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import Label from "@/views/tag";
const labelRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/tag/index",
element: <Label />,
meta: {
// requiresAuth: true,
title: "标签",
key: "tag"
}
}
]
}
];
export default labelRouter;
================================================
FILE: src/routers/modules/wxMenu.tsx
================================================
import { LayoutIndex } from "@/routers/constant";
import { RouteObject } from "@/routers/interface";
import WxMenuPage from "@/views/wxMenu";
const wxMenuRouter: Array<RouteObject> = [
{
element: <LayoutIndex />,
children: [
{
path: "/wx/menu/index",
element: <WxMenuPage />,
meta: {
title: "微信配置",
key: "wx-menu"
}
}
]
}
];
export default wxMenuRouter;
================================================
FILE: src/routers/route.tsx
================================================
/* eslint-disable prettier/prettier */
import {
AlertOutlined,
ApiOutlined,
BarsOutlined,
CalendarOutlined,
FileAddOutlined,
FilePptOutlined,
FileTextOutlined,
LineChartOutlined,
MessageOutlined,
ReadOutlined,
SettingOutlined,
SmileOutlined,
TagsOutlined,
UserOutlined,
WechatOutlined
} from "@ant-design/icons";
export const currentMenuList = [
{ key: "/statistics/index", icon: <LineChartOutlined />, children: undefined, label: "数据统计" },
{ key: "/config/index", icon: <CalendarOutlined />, children: undefined, label: "运营配置" },
{ key: "/global/index", icon: <SettingOutlined />, children: undefined, label: "全局配置" },
{ key: "/sensitive/index", icon: <AlertOutlined />, children: undefined, label: "敏感词管理" },
{ key: "/ai/config/index", icon: <ApiOutlined />, children: undefined, label: "AI模型配置" },
{ key: "/wx/menu/index", icon: <WechatOutlined />, children: undefined, label: "微信配置" },
{ key: "/category/index", icon: <BarsOutlined />, children: undefined, label: "分类管理" },
{ key: "/tag/index", icon: <TagsOutlined />, children: undefined, label: "标签管理" },
{ key: "/resume/index", icon: <FileTextOutlined />, children: undefined, label: "简历管理" },
{
key: "/article",
icon: <ReadOutlined />,
children: [
{ key: "/article/list/index", icon: <FilePptOutlined />, children: undefined, label: "文章列表" },
{ key: "/article/edit/index", icon: <FileAddOutlined />, children: undefined, label: "文章编辑" },
{ key: "/article/comment/index", icon: <MessageOutlined />, children: undefined, label: "评论管理" }
],
label: "文章管理"
},
{
key: "/author",
icon: <UserOutlined />,
children: [
{ key: "/author/whitelist/index", icon: <SmileOutlined />, children: undefined, label: "作者白名单" },
{ key: "/author/zsxqlist/index", icon: <SmileOutlined />, children: undefined, label: "星球白名单" },
{ key: "/author/login-audit/index", icon: <SmileOutlined />, children: undefined, label: "登录审计" }
],
label: "用户管理"
},
{
key: "/column",
icon: <ReadOutlined />,
children: [
{ key: "/column/setting/index", icon: <FilePptOutlined />, children: undefined, label: "专栏配置" },
{ key: "/column/article/index", icon: <FileAddOutlined />, children: undefined, label: "教程配置" }
],
label: "教程管理"
}
];
================================================
FILE: src/routers/utils/authRouter.tsx
================================================
// 路由权限控制
import { Navigate, useLocation } from "react-router-dom";
import { AxiosCanceler } from "@/api/helper/axiosCancel";
import { HOME_URL, LOGIN_URL } from "@/config/config";
import { store } from "@/redux/index";
import { rootRouter } from "@/routers/index";
import { searchRoute } from "@/utils/util";
const axiosCanceler = new AxiosCanceler();
/**
* @description 路由守卫组件
* */
const AuthRouter = (props: { children: JSX.Element }) => {
const { pathname } = useLocation();
const route = searchRoute(pathname, rootRouter);
console.log({ route });
// * 在跳转路由之前,清除所有的请求
axiosCanceler.removeAllPending();
console.log({ props });
// * 判断当前路由是否需要访问权限(不需要权限直接放行)
if (!route.meta?.requiresAuth) return props.children;
// * 判断是否有Token
const token = store.getState().global.token;
if (!token) return <Navigate to={LOGIN_URL} replace />;
// * Dynamic Router(动态路由,根据后端返回的菜单数据生成的一维数组)
const dynamicRouter = store.getState().auth.authRouter;
// * Static Router(静态路由,必须配置首页地址,否则不能进首页获取菜单、按钮权限等数据),获取数据的时候会loading,所有配置首页地址也没问题
const staticRouter = [HOME_URL, "/403"];
const routerList = dynamicRouter.concat(staticRouter);
// * 如果访问的地址没有在路由表中重定向到403页面
if (routerList.indexOf(pathname) == -1) return <Navigate to="/403" />;
// * 当前账号有权限返回 Router,正常访问页面
return props.children;
};
export default AuthRouter;
================================================
FILE: src/routers/utils/lazyLoad.tsx
================================================
import React, { Suspense } from "react";
import { Spin } from "antd";
/**
* @description 路由懒加载
* @param {Element} Comp 需要访问的组件
* @returns element
*/
const lazyLoad = (Comp: React.LazyExoticComponent<any>): React.ReactNode => {
return (
<Suspense
fallback={
<Spin
size="large"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%"
}}
/>
}
>
<Comp />
</Suspense>
);
};
export default lazyLoad;
================================================
FILE: src/styles/common.less
================================================
/* 常用 flex */
.flx-center {
display: flex;
align-items: center;
justify-content: center;
}
.flx-justify-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flx-align-center {
display: flex;
align-items: center;
}
/* 清除浮动 */
.clearfix::after {
display: block;
height: 0;
overflow: hidden;
clear: both;
content: "";
}
/* 文字单行省略号 */
.sle {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 文字多行省略号 */
.mle {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
/* 文字多了自動換行 */
.break-word {
word-break: break-all;
word-wrap: break-word;
}
/* page switch animation */
.fade-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-enter-active,
.fade-exit-active {
opacity: 1;
transition: all 0.2s ease-out;
transform: translateX(0);
}
.fade-exit {
opacity: 0;
transform: translateX(30px);
}
/* scroll bar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: #ffffff;
}
::-webkit-scrollbar-thumb {
background-color: #dddee0;
border-radius: 20px;
box-shadow: inset 0 0 0 #ffffff;
}
/* card 卡片样式 */
.card {
box-sizing: border-box;
padding: 20px;
overflow-x: hidden;
border: 1px solid #e4e7ed;
border-radius: 4px;
}
/* content-box */
.content-box {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.text {
margin: 30px 0;
font-size: 23px;
font-weight: 700;
text-align: center;
a {
text-decoration: underline !important;
}
}
}
================================================
FILE: src/styles/reset.less
================================================
/* Reset style sheet */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
padding: 0;
margin: 0;
font: inherit;
font-size: 100%;
vertical-align: baseline;
border: 0;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
padding: 0;
margin: 0;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote::before,
blockquote::after,
q::before,
q::after {
content: "";
content: none;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
html,
body,
#root {
width: 100%;
height: 100%;
}
================================================
FILE: src/styles/theme/theme-dark.less
================================================
/* 自定义 antd 暗黑模式样式 */
@dark-main-bg-color: #141414;
@dark-bg-color: #1f1f1f;
@dark-border-color: #414243;
@dark-text-color: #d9d9d9;
@dark-shadow-color: 5px 5px 15px rgb(255 255 255 / 20%);
@dark-scrollbar-bg-color: #686868;
/* 需要自定义覆盖的样式 */
body {
background-color: @dark-main-bg-color !important;
// guide
#driver-highlighted-element-stage {
background-color: #525457 !important;
}
}
/* login container(先固定样式) */
.login-container {
background-color: @dark-main-bg-color !important;
.login-box {
background-color: rgb(0 0 0 / 80%) !important;
.login-form {
background-color: #000 !important;
box-shadow: @dark-shadow-color !important;
.login-logo {
.logo-text {
color: @dark-text-color !important;
}
}
.login-btn {
.ant-btn-default {
color: @dark-text-color !important;
}
}
}
}
}
/* container */
.container {
/* sider */
.ant-layout-sider {
border-right: 1px solid @dark-border-color !important;
.ant-menu {
&::-webkit-scrollbar {
background-color: @dark-bg-color !important;
}
&::-webkit-scrollbar-thumb {
background-color: @dark-scrollbar-bg-color !important;
}
}
.logo-box {
border-bottom: 1px solid @dark-border-color !important;
}
}
/* layout */
.ant-layout {
background-color: @dark-main-bg-color !important;
.ant-layout-header,
.tabs,
.footer,
.card {
background-color: @dark-bg-color !important;
border-color: @dark-border-color !important;
.text {
color: @dark-text-color !important;
}
}
.ant-layout-header {
height: 55px;
padding: 0 40px 0 20px;
.icon-style,
.username {
color: @dark-text-color !important;
}
}
.footer {
a {
color: @dark-text-color !important;
}
}
.ant-layout-content {
&::-webkit-scrollbar {
background-color: @dark-main-bg-color !important;
}
&::-webkit-scrollbar-thumb {
background-color: @dark-scrollbar-bg-color !important;
}
.card {
&::-webkit-scrollbar {
background-color: @dark-bg-color !important;
}
&::-webkit-scrollbar-thumb {
background-color: @dark-scrollbar-bg-color !important;
}
}
}
}
}
================================================
FILE: src/styles/theme/theme-default.less
================================================
/* 自定义 antd 默认样式 */
@light-bg-color: #ffffff;
@light-main-bg-color: #f0f2f5;
@light-border-color: #e4e7ed;
@light-border-header-color: #f6f6f6;
@light-text-color: rgba(0, 0, 0, 0.85);
@light-shadow-color: 0 0 12px #0000000d;
@light-scrollbar-bg-color: #dddee0;
/* 需要自定义覆盖的样式 */
body {
background-color: @light-bg-color !important;
#driver-highlighted-element-stage {
background-color: #fff !important;
}
}
/* login container(先固定样式) */
.login-container {
background-color: #eeeeee !important;
.login-box {
background-color: hsl(0deg 0% 100% / 80%) !important;
.login-form {
background-color: #000 !important;
box-shadow: 2px 3px 7px rgb(0 0 0 / 20%) !important;
.login-logo {
.logo-text {
color: #475768 !important;
}
}
.login-btn {
.ant-btn-default {
color: #606266 !important;
}
}
}
}
}
/* container */
.container {
/* sider */
.ant-layout-sider {
border-right: 1px solid @light-border-color !important;
.ant-menu {
&::-webkit-scrollbar {
background-color: #001529 !important;
}
&::-webkit-scrollbar-thumb {
background-color: #41444b !important;
}
}
.logo-box {
border-bottom: 1px solid #010b14 !important;
}
}
/* layout */
.ant-layout {
background-color: @light-main-bg-color !important;
.tabs,
.footer,
.card {
background-color: @light-bg-color !important;
border-color: @light-border-color !important;
}
.ant-layout-header {
height: 55px;
padding: 0 40px 0 20px;
background-color: @light-bg-color !important;
border-color: @light-border-header-color !important;
.icon-style,
.username {
color: @light-text-color !important;
}
}
.footer {
a {
color: @light-text-color !important;
}
}
.card {
box-shadow: @light-shadow-color !important;
.text {
color: #585858 !important;
}
}
.ant-layout-content {
&::-webkit-scrollbar {
background-color: @light-main-bg-color !important;
}
&::-webkit-scrollbar-thumb {
background-color: @light-scrollbar-bg-color !important;
}
.card {
&::-webkit-scrollbar {
background-color: @light-bg-color !important;
}
&::-webkit-scrollbar-thumb {
background-color: @light-scrollbar-bg-color !important;
}
}
}
}
}
================================================
FILE: src/styles/var.less
================================================
/* Global definition less */
@primary-color: #1890ff;
================================================
FILE: src/typings/common.ts
================================================
export interface MapItem {
[key: string]: any;
}
================================================
FILE: src/typings/global.d.ts
================================================
// * Menu
declare namespace Menu {
interface MenuOptions {
path: string;
title: string;
icon?: string;
isLink?: string;
close?: boolean;
children?: MenuOptions[];
}
}
// * Vite
declare type Recordable<T = any> = Record<string, T>;
declare interface ViteEnv {
VITE_API_URL: string;
VITE_DOMAIN: string;
VITE_PORT: number;
VITE_OPEN: boolean;
VITE_GLOB_APP_TITLE: string;
VITE_DROP_CONSOLE: boolean;
VITE_PROXY_URL: string;
VITE_BUILD_GZIP: boolean;
VITE_REPORT: boolean;
}
// * Dropdown MenuInfo
declare interface MenuInfo {
key: string;
keyPath: string[];
/** @deprecated This will not support in future. You should avoid to use this */
item: React.ReactInstance;
domEvent: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;
}
================================================
FILE: src/typings/plugins.d.ts
================================================
declare module "qs";
declare module "nprogress";
declare module "js-md5";
declare module "react-transition-group";
================================================
FILE: src/typings/window.d.ts
================================================
// * global
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;
}
interface Navigator {
msSaveOrOpenBlob: (blob: Blob, fileName: string) => void;
}
}
export {};
================================================
FILE: src/utils/getEnv.ts
================================================
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
export function isDevFn(mode: string): boolean {
return mode === "development";
}
export function isProdFn(mode: string): boolean {
return mode === "production";
}
/**
* Whether to generate package preview
*/
export function isReportMode(): boolean {
return process.env.VITE_REPORT === "true";
}
// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable): ViteEnv {
const ret: any = {};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n");
realName = realName === "true" ? true : realName === "false" ? false : realName;
if (envName === "VITE_PORT") {
realName = Number(realName);
}
if (envName === "VITE_PROXY") {
try {
realName = JSON.parse(realName);
} catch (error) {
console.log(error);
}
}
ret[envName] = realName;
process.env[envName] = realName;
}
return ret;
}
/**
* Get the environment variables starting with the specified prefix
* @param match prefix
* @param confFiles ext
*/
export function getEnvConfig(match = "VITE_GLOB_", confFiles = [".env", ".env.production"]) {
let envConfig = {};
confFiles.forEach(item => {
try {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
envConfig = { ...envConfig, ...env };
} catch (error) {
console.error(`Error in parsing ${item}`, error);
}
});
Object.keys(envConfig).forEach(key => {
const reg = new RegExp(`^(${match})`);
if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key);
}
});
return envConfig;
}
/**
* Get user root directory
* @param dir file path
*/
export function getRootPath(...dir: string[]) {
return path.resolve(process.cwd(), ...dir);
}
================================================
FILE: src/utils/is/index.ts
================================================
const toString = Object.prototype.toString;
/**
* @description: 判断值是否未某个类型
*/
export function is(val: unknown, type: string) {
return toString.call(val) === `[object ${type}]`;
}
/**
* @description: 是否为函数
*/
export function isFunction<T = Function>(val: unknown): val is T {
return is(val, "Function");
}
/**
* @description: 是否已定义
*/
export const isDef = <T = unknown>(val?: T): val is T => {
return typeof val !== "undefined";
};
export const isUnDef = <T = unknown>(val?: T): val is T => {
return !isDef(val);
};
/**
* @description: 是否为对象
*/
export const isObject = (val: any): val is Record<any, any> => {
return val !== null && is(val, "Object");
};
/**
* @description: 是否为时间
*/
export function isDate(val: unknown): val is Date {
return is(val, "Date");
}
/**
* @description: 是否为数值
*/
export function isNumber(val: unknown): val is number {
return is(val, "Number");
}
/**
* @description: 是否为AsyncFunction
*/
export function isAsyncFunction<T = any>(val: unknown): val is Promise<T> {
return is(val, "AsyncFunction");
}
/**
* @description: 是否为promise
*/
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch);
}
/**
* @description: 是否为字符串
*/
export function isString(val: unknown): val is string {
return is(val, "String");
}
/**
* @description: 是否为boolean类型
*/
export function isBoolean(val: unknown): val is boolean {
return is(val, "Boolean");
}
/**
* @description: 是否为数组
*/
export function isArray(val: any): val is Array<any> {
return val && Array.isArray(val);
}
/**
* @description: 是否客户端
*/
export const isClient = () => {
return typeof window !== "undefined";
};
/**
* @description: 是否为浏览器
*/
export const isWindow = (val: any): val is Window => {
return typeof window !== "undefined" && is(val, "Window");
};
export const isElement = (val: unknown): val is Element => {
return isObject(val) && !!val.tagName;
};
export const isServer = typeof window === "undefined";
// 是否为图片节点
export function isImageDom(o: Element) {
return o && ["IMAGE", "IMG"].includes(o.tagName);
}
export function isNull(val: unknown): val is null {
return val === null;
}
export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val);
}
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}
================================================
FILE: src/utils/util.ts
================================================
import { RouteObject } from "@/routers/interface";
/**
* @description 获取当前域名
*/
export const baseDomain = import.meta.env.VITE_DOMAIN;
/**
* @description 获取localStorage
* @param {String} key Storage名称
* @return string
*/
export const localGet = (key: string) => {
const value = window.localStorage.getItem(key);
try {
return JSON.parse(window.localStorage.getItem(key) as string);
} catch (error) {
return value;
}
};
/**
* @description 存储localStorage
* @param {String} key Storage名称
* @param {Any} value Storage值
* @return void
*/
export const localSet = (key: string, value: any) => {
window.localStorage.setItem(key, JSON.stringify(value));
};
/**
* @description 清除localStorage
* @param {String} key Storage名称
* @return void
*/
export const localRemove = (key: string) => {
window.localStorage.removeItem(key);
};
/**
* @description 清除所有localStorage
* @return void
*/
export const localClear = () => {
window.localStorage.clear();
};
/**
* @description 获取需要展开的 subMenu
* @param {String} path 当前访问地址
* @returns array
*/
export const getOpenKeys = (path: string) => {
let newStr: string = "";
let newArr: any[] = [];
let arr = path.split("/").map(i => "/" + i);
for (let i = 1; i < arr.length - 1; i++) {
newStr += arr[i];
newArr.push(newStr);
}
return newArr;
};
/**
* @description 递归查询对应的路由
* @param {String} path 当前访问地址
* @param {Array} routes 路由列表
* @returns array
*/
export const searchRoute = (path: string, routes: RouteObject[] = []): RouteObject => {
let result: RouteObject = {};
for (let item of routes) {
if (item.path === path) return item;
if (item.children) {
const res = searchRoute(path, item.children);
if (Object.keys(res).length) result = res;
}
}
return result;
};
/**
* @description 递归当前路由的 所有 关联的路由,生成面包屑导航栏
* @param {String} path 当前访问地址
* @param {Array} menuList 菜单列表
* @returns array
*/
export const getBreadcrumbList = (path: string, menuList: Menu.MenuOptions[]) => {
let tempPath: any[] = [];
try {
const getNodePath = (node: Menu.MenuOptions) => {
tempPath.push(node);
// 找到符合条件的节点,通过throw终止掉递归
if (node.path === path) {
throw new Error("GOT IT!");
}
if (node.children && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
getNodePath(node.children[i]);
}
// 当前节点的子节点遍历完依旧没找到,则删除路径中的该节点
tempPath.pop();
} else {
// 找到叶子节点时,删除路径当中的该叶子节点
tempPath.pop();
}
};
for (let i = 0; i < menuList.length; i++) {
getNodePath(menuList[i]);
}
} catch (e) {
return tempPath.map(item => item.title);
}
};
/**
* @description 双重递归 找出所有 面包屑 生成对象存到 redux 中,就不用每次都去递归查找了
* @param {String} menuList 当前菜单列表
* @returns object
*/
export const findAllBreadcrumb = (menuList: Menu.MenuOptions[]): { [key: string]: any } => {
let handleBreadcrumbList: any = {};
const loop = (menuItem: Menu.MenuOptions) => {
// 下面判断代码解释 *** !item?.children?.length ==> (item.children && item.children.length > 0)
if (menuItem?.children?.length) menuItem.children.forEach(item => loop(item));
else handleBreadcrumbList[menuItem.path] = getBreadcrumbList(menuItem.path, menuList);
};
menuList.forEach(item => loop(item));
return handleBreadcrumbList;
};
/**
* @description 使用递归处理路由菜单,生成一维数组,做菜单权限判断
* @param {Array} menuList 所有菜单列表
* @param {Array} newArr 菜单的一维数组
* @return array
*/
export function handleRouter(routerList: Menu.MenuOptions[], newArr: string[] = []) {
routerList.forEach((item: Menu.MenuOptions) => {
typeof item === "object" && item.path && newArr.push(item.path);
item.children && item.children.length && handleRouter(item.children, newArr);
});
return newArr;
}
/**
* @description 判断数据类型
* @param {Any} val 需要判断类型的数据
* @return string
*/
export const isType = (val: any) => {
if (val === null) return "null";
if (typeof val !== "object") return typeof val;
else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase();
};
/**
* @description 对象数组深克隆
* @param {Object} obj 源对象
* @return object
*/
export const deepCopy = <T>(obj: any): T => {
let newObj: any;
try {
newObj = obj.push ? [] : {};
} catch (error) {
newObj = {};
}
for (let attr in obj) {
if (typeof obj[attr] === "object") {
newObj[attr] = deepCopy(obj[attr]);
} else {
newObj[attr] = obj[attr];
}
}
return newObj;
};
/**
* @description 生成随机数
* @param {Number} min 最小值
* @param {Number} max 最大值
* @return number
*/
export function randomNum(min: number, max: number): number {
let num = Math.floor(Math.random() * (min - max) + max);
return num;
}
export const getCompleteUrl = (partialUrl: string | undefined) => {
// 域名,展示图片的时候用
let completeUrl = partialUrl;
// 如果partialUrl为绝对路径,直接返回
if (partialUrl && partialUrl.indexOf("http") === -1 && partialUrl.indexOf("https") === -1) {
completeUrl = baseDomain + partialUrl;
}
return completeUrl;
};
================================================
FILE: src/views/aiConfig/index.scss
================================================
.ai-config-page {
&__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
&__toolbar-meta {
max-width: 760px;
}
&__toolbar-actions {
flex-shrink: 0;
}
&__section-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 16px;
}
&__section-card {
height: 100%;
}
&__sub-card {
height: 100%;
border-radius: 10px;
background: #fafafa;
}
&__sub-card-title {
font-size: 15px;
font-weight: 600;
color: #1f1f1f;
}
&__source-group {
width: 100%;
}
&__source-option {
display: flex;
align-items: center;
min-height: 32px;
}
&__test-result {
padding-top: 8px;
}
&__test-result-text {
margin-top: 8px;
margin-bottom: 0;
padding: 12px;
border-radius: 8px;
background: #fafafa;
white-space: pre-wrap;
word-break: break-word;
}
&__number-input,
.ant-input-number {
width: 100%;
}
}
@media (max-width: 1200px) {
.ai-config-page {
&__section-grid {
grid-template-columns: 1fr;
}
}
}
@media (max-width: 768px) {
.ai-config-page {
&__toolbar {
flex-direction: column;
align-items: flex-start;
}
&__toolbar-actions {
width: 100%;
}
}
}
================================================
FILE: src/views/aiConfig/index.tsx
================================================
import { FC, useEffect, useMemo, useState } from "react";
import { ReloadOutlined, SaveOutlined } from "@ant-design/icons";
import {
Alert,
Button,
Card,
Checkbox,
Col,
Divider,
Form,
Input,
InputNumber,
message,
Modal,
Row,
Space,
Typography
} from "antd";
import {
AiConfigAdminDTO,
AiConfigAdminReq,
AISourceValue,
getAiConfigDetailApi,
saveAiConfigApi,
testAiConfigApi
} from "@/api/modules/aiConfig";
import { ContentInterWrap, ContentWrap } from "@/components/common-wrap";
import "./index.scss";
const { Paragraph, Text, Title } = Typography;
interface AiConfigFormValues {
sources: AISourceValue[];
zhipu: {
apiSecretKey: string;
requestIdTemplate: string;
model: string;
};
zhipuCoding: {
apiKey: string;
apiHost: string;
model: string;
timeout?: number | null;
};
xunFei: {
hostUrl: string;
domain: string;
appId: string;
apiKey: string;
apiSecret: string;
apiPassword: string;
};
deepSeek: {
apiKey: string;
apiHost: string;
model: string;
timeout?: number | null;
};
doubao: {
apiKey: string;
apiHost: string;
endPoint: string;
};
ali: {
model: string;
};
}
const AI_SOURCE_OPTIONS: Array<{ label: string; value: AISourceValue }> = [
{ label: "技术派默认", value: "PAI_AI" },
{ label: "智谱", value: "ZHI_PU_AI" },
{ label: "智谱 Coding", value: "ZHIPU_CODING" },
{ label: "讯飞", value: "XUN_FEI_AI" },
{ label: "阿里", value: "ALI_AI" },
{ label: "DeepSeek", value: "DEEP_SEEK" },
{ label: "豆包", value: "DOU_BAO_AI" }
];
const REMOVED_SOURCE_VALUES: AISourceValue[] = ["CHAT_GPT_3_5", "CHAT_GPT_4"];
const AI_SOURCE_LABEL_MAP = AI_SOURCE_OPTIONS.reduce<Record<AISourceValue, string>>((result, item) => {
result[item.value] = item.label;
return result;
}, {} as Record<AISourceValue, string>);
const AI_TEST_PROMPT = "你正在执行后台 AI 配置连通性测试。请忽略其他上下文,只输出“连接成功”。";
const defaultFormValues: AiConfigFormValues = {
sources: [],
zhipu: {
apiSecretKey: "",
requestIdTemplate: "",
model: ""
},
zhipuCoding: {
apiKey: "",
apiHost: "",
model: "GLM-5",
timeout: undefined
},
xunFei: {
hostUrl: "",
domain: "",
appId: "",
apiKey: "",
apiSecret: "",
apiPassword: ""
},
deepSeek: {
apiKey: "",
apiHost: "",
model: "deepseek-chat",
timeout: undefined
},
doubao: {
apiKey: "",
apiHost: "",
endPoint: ""
},
ali: {
model: ""
}
};
const normalizeFormValues = (detail?: AiConfigAdminDTO): AiConfigFormValues => ({
sources: (detail?.sources || []).filter(item => !REMOVED_SOURCE_VALUES.includes(item)),
zhipu: {
apiSecretKey: detail?.zhipu?.apiSecretKey || "",
requestIdTemplate: detail?.zhipu?.requestIdTemplate || "",
model: detail?.zhipu?.model || ""
},
zhipuCoding: {
apiKey: detail?.zhipuCoding?.apiKey || "",
apiHost: detail?.zhipuCoding?.apiHost || "",
model: detail?.zhipuCoding?.model || "GLM-5",
timeout: detail?.zhipuCoding?.timeout
},
xunFei: {
hostUrl: detail?.xunFei?.hostUrl || "",
domain: detail?.xunFei?.domain || "",
appId: detail?.xunFei?.appId || "",
apiKey: detail?.xunFei?.apiKey || "",
apiSecret: detail?.xunFei?.apiSecret || "",
apiPassword: detail?.xunFei?.apiPassword || ""
},
deepSeek: {
apiKey: detail?.deepSeek?.apiKey || "",
apiHost: detail?.deepSeek?.apiHost || "",
model: detail?.deepSeek?.model || "deepseek-chat",
timeout: detail?.deepSeek?.timeout
},
doubao: {
apiKey: detail?.doubao?.apiKey || "",
apiHost: detail?.doubao?.apiHost || "",
endPoint: detail?.doubao?.endPoint || ""
},
ali: {
model: detail?.ali?.model || ""
}
});
const AiConfigPage: FC = () => {
const [formRef] = Form.useForm<AiConfigFormValues>();
const [loading, setLoading] = useState<boolean>(false);
const [saving, setSaving] = useState<boolean>(false);
const [testingProvider, setTestingProvider] = useState<AISourceValue | null>(null);
const sourceOptions = useMemo(
() =>
AI_SOURCE_OPTIONS.map(item => ({
...item,
label: <span className="ai-config-page__source-option">{item.label}</span>
})),
[]
);
const loadDetail = async () => {
setLoading(true);
try {
const { status, result } = await getAiConfigDetailApi();
if (status?.code === 0) {
formRef.setFieldsValue(normalizeFormValues(result));
return;
}
message.error(status?.msg || "加载 AI 模型配置失败");
} catch (error) {
message.error("加载 AI 模型配置失败,请稍后重试");
} finally {
setLoading(false);
}
};
useEffect(() => {
formRef.setFieldsValue(defaultFormValues);
loadDetail();
}, []);
const buildPayload = (values: AiConfigFormValues): AiConfigAdminReq => ({
sources: (values.sources || []).filter(item => !REMOVED_SOURCE_VALUES.includes(item)),
zhipu: {
apiSecretKey: values.zhipu.apiSecretKey || "",
requestIdTemplate: values.zhipu.requestIdTemplate || "",
model: values.zhipu.model || ""
},
zhipuCoding: {
apiKey: values.zhipuCoding.apiKey || "",
apiHost: values.zhipuCoding.apiHost || "",
model: values.zhipuCoding.model || "",
timeout: values.zhipuCoding.timeout ?? undefined
},
xunFei: {
hostUrl: values.xunFei.hostUrl || "",
domain: values.xunFei.domain || "",
appId: values.xunFei.appId || "",
apiKey: values.xunFei.apiKey || "",
apiSecret: values.xunFei.apiSecret || "",
apiPassword: values.xunFei.apiPassword || ""
},
deepSeek: {
apiKey: values.deepSeek.apiKey || "",
apiHost: values.deepSeek.apiHost || "",
model: values.deepSeek.model || "",
timeout: values.deepSeek.timeout ?? undefined
},
doubao: {
apiKey: values.doubao.apiKey || "",
apiHost: values.doubao.apiHost || "",
endPoint: values.doubao.endPoint || ""
},
ali: {
model: values.ali.model || ""
}
});
const persistConfig = async (showSuccessMessage = true, reloadAfterSave = true) => {
const values = await formRef.validateFields();
const payload = buildPayload(values);
const { status } = await saveAiConfigApi(payload);
if (status?.code !== 0) {
message.error(status?.msg || "AI 模型配置保存失败");
return false;
}
if (showSuccessMessage) {
message.success("AI 模型配置保存成功");
}
if (reloadAfterSave) {
loadDetail();
}
return true;
};
const handleSave = async () => {
setSaving(true);
try {
await persistConfig(true, true);
} catch (error) {
message.error("AI 模型配置保存失败,请稍后重试");
} finally {
setSaving(false);
}
};
const handleTestProvider = async (provider: AISourceValue) => {
setTestingProvider(provider);
try {
const saved = await persistConfig(false, false);
if (!saved) {
return;
}
const { status, result } = await testAiConfigApi({
source: provider,
prompt: AI_TEST_PROMPT
});
if (status?.code !== 0) {
message.error(status?.msg || `${AI_SOURCE_LABEL_MAP[provider]} 连通测试失败`);
return;
}
if (result?.success) {
Modal.success({
title: `${AI_SOURCE_LABEL_MAP[provider]} 连通测试成功`,
content: (
<div className="ai-config-page__test-result">
<Text type="secondary">模型返回内容</Text>
<Paragraph copyable={{ text: result.answer || "" }} className="ai-config-page__test-result-text">
{result.answer || result.message || "连接成功"}
</Paragraph>
</div>
)
});
return;
}
Modal.error({
title: `${AI_SOURCE_LABEL_MAP[provider]} 连通测试失败`,
content: result?.message || "未拿到有效响应,请检查当前配置"
});
} catch (error) {
message.error(`${AI_SOURCE_LABEL_MAP[provider]} 连通测试失败,请稍后重试`);
} finally {
setTestingProvider(null);
}
};
const renderTestButton = (provider: AISourceValue) => (
<Button size="small" loading={testingProvider === provider} onClick={() => handleTestProvider(provider)}>
保存并测试
</Button>
);
const renderTestingHint = (provider: AISourceValue) =>
testingProvider === provider ? <Text type="secondary">正在保存当前配置并执行连通测试...</Text> : null;
return (
<div className="ai-config-page">
<ContentWrap>
<ContentInterWrap>
<div className="ai-config-page__toolbar">
<div className="ai-config-page__toolbar-meta">
<Title level={4} style={{ marginBottom: 8 }}>
AI 模型配置
</Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
这里单独维护 admin 端使用的 AI 模型配置,会直接读取并保存后端新增的 AI 配置接口,不再和通用全局配置项混在一起。
</Paragraph>
</div>
<Space className="ai-config-page__toolbar-actions">
<Button icon={<ReloadOutlined />} onClick={loadDetail} loading={loading}>
刷新
</Button>
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}>
保存配置
</Button>
</Space>
</div>
<Alert
showIcon
type="info"
message="配置说明"
description="当前 admin 端已移除 ChatGPT 配置入口。连通测试会先保存当前页面配置,再调用后端 AI 配置测试接口执行真实探测。"
/>
<Form form={formRef} layout="vertical" initialValues={defaultFormValues} disabled={loading}>
<Card className="ai-config-page__section-card" title="启用模型源">
<Form.Item
label="当前启用的模型"
name="sources"
rules={[{ required: true, type: "array", min: 1, message: "请至少选择一个启用的模型源" }]}
>
<Checkbox.Group className="ai-config-page__source-group" options={sourceOptions} />
</Form.Item>
<Text type="secondary">这里控制后端可参与路由和降级选择的 AI Source 列表。</Text>
</Card>
<div className="ai-config-page__section-grid">
<Card className="ai-config-page__section-card" title="智谱" extra={renderTestButton("ZHI_PU_AI")}>
<Form.Item label="API Secret Key" name={["zhipu", "apiSecretKey"]}>
<Input.Password allowClear placeholder="请输入智谱 API Secret Key" />
</Form.Item>
<Form.Item label="Request ID 模板" name={["zhipu", "requestIdTemplate"]}>
<Input allowClear placeholder="例如:paicoding-%d" />
</Form.Item>
<Form.Item
label="模型名"
name={["zhipu", "model"]}
extra="当前后端智谱配置未开放 Base URL,默认走智谱 SDK 内置地址;这里提供官方常用模型,也支持手动输入新编码。"
>
<Input allowClear placeholder="请输入智谱模型编码,例如:glm-5" />
</Form.Item>
{renderTestingHint("ZHI_PU_AI")}
</Card>
<Card className="ai-config-page__section-card" title="智谱 Coding" extra={renderTestButton("ZHIPU_CODING")}>
<Form.Item label="API Key" name={["zhipuCoding", "apiKey"]}>
<Input.Password allowClear placeholder="请输入智谱 Coding API Key" />
</Form.Item>
<Form.Item label="API Host" name={["zhipuCoding", "apiHost"]} extra="这里可配置智谱 Coding 的 Base URL。">
<Input allowClear placeholder="例如:https://open.bigmodel.cn/api/coding/paas/v4" />
</Form.Item>
<Form.Item label="模型名" name={["zhipuCoding", "model"]}>
<Input allowClear placeholder="请输入模型编码,例如:GLM-5" />
</Form.Item>
<Form.Item label="超时时间" name={["zhipuCoding", "timeout"]}>
<InputNumber className="ai-config-page__number-input" min={0} precision={0} placeholder="单位:毫秒" />
</Form.Item>
{renderTestingHint("ZHIPU_CODING")}
</Card>
<Card className="ai-config-page__section-card" title="讯飞" extra={renderTestButton("XUN_FEI_AI")}>
<Row gutter={[16, 0]}>
<Col xs={24} md={12}>
<Form.Item label="Host URL" name={["xunFei", "hostUrl"]}>
<Input allowClear placeholder="请输入讯飞 Host URL" />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="Domain" name={["xunFei", "domain"]}>
<Input allowClear placeholder="例如:generalv3.5" />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="App ID" name={["xunFei", "appId"]}>
<Input allowClear placeholder="请输入讯飞 App ID" />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="API Key" name={["xunFei", "apiKey"]}>
<Input.Password allowClear placeholder="请输入讯飞 API Key" />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="API Secret" name={["xunFei", "apiSecret"]}>
<Input.Password allowClear placeholder="请输入讯飞 API Secret" />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="API Password" name={["xunFei", "apiPassword"]}>
<Input.Password allowClear placeholder="请输入讯飞 API Password" />
</Form.Item>
</Col>
</Row>
<Text type="secondary">这里的测试会直接调用后端新增的 AI 配置测试接口。</Text>
</Card>
<Card className="ai-config-page__section-card" title="DeepSeek" extra={renderTestButton("DEEP_SEEK")}>
<Form.Item label="API Key" name={["deepSeek", "apiKey"]}>
<Input.Password allowClear placeholder="请输入 DeepSeek API Key" />
</Form.Item>
<Form.Item label="API Host" name={["deepSeek", "apiHost"]}>
<Input allowClear placeholder="例如:https://api.deepseek.com" />
</Form.Item>
<Form.Item label="模型名" name={["deepSeek", "model"]}>
<Input allowClear placeholder="请输入 DeepSeek 模型名,例如:deepseek-chat" />
</Form.Item>
<Form.Item label="超时时间" name={["deepSeek", "timeout"]}>
<InputNumber className="ai-config-page__number-input" min={0} precision={0} placeholder="单位:毫秒" />
</Form.Item>
{renderTestingHint("DEEP_SEEK")}
</Card>
<Card className="ai-config-page__section-card" title="豆包" extra={renderTestButton("DOU_BAO_AI")}>
<Form.Item label="API Key" name={["doubao", "apiKey"]}>
<Input.Password allowClear placeholder="请输入豆包 API Key" />
</Form.Item>
<Form.Item label="API Host" name={["doubao", "apiHost"]}>
<Input allowClear placeholder="请输入豆包 API Host" />
</Form.Item>
<Form.Item label="End Point" name={["doubao", "endPoint"]}>
<Input allowClear placeholder="请输入豆包 End Point" />
</Form.Item>
{renderTestingHint("DOU_BAO_AI")}
</Card>
</div>
<Divider />
<Card className="ai-config-page__section-card" title="阿里" extra={renderTestButton("ALI_AI")}>
<Form.Item label="模型名" name={["ali", "model"]}>
<Input allowClear placeholder="请输入阿里模型名" />
</Form.Item>
{renderTestingHint("ALI_AI")}
</Card>
</Form>
</ContentInterWrap>
</ContentWrap>
</div>
);
};
export default AiConfigPage;
================================================
FILE: src/views/article/components/debounceselect/index.scss
================================================
================================================
FILE: src/views/article/components/debounceselect/index.tsx
================================================
/* eslint-disable prettier/prettier */
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React from "react";
import { message,Select, SelectProps, Spin } from "antd";
import { on } from "events";
import { debounce, set } from "lodash";
import { getTagListApi } from "@/api/modules/tag";
import { MapItem } from "@/typings/common";
// 导入 index.scss 文件
import "./index.scss";
export interface DebounceSelectProps<ValueType = any> extends Omit<SelectProps<ValueType | ValueType[]>, "options" | "children"> {
debounceTimeout?: number;
}
export interface IPagination {
current: number;
pageSize: number;
total?: number;
}
export const initPagination: IPagination = {
current: 1,
pageSize: 10,
total: 0
};
export interface IFormType {
// 上下线
status: number;
// 标签名
tag: string;
}
const defaultInitForm: IFormType = {
// 上下线
status: 1,
tag: ""
}
// Usage of DebounceSelect
interface TagValue {
key: string;
label: string;
value: string;
}
function DebounceSelect<ValueType extends { key?: string; label: React.ReactNode; value: string | number } = any>({
debounceTimeout = 800,
...props
}: DebounceSelectProps<ValueType>) {
// 使用useState定义state变量,用于保存选项列表和加载状态
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState<ValueType[]>([]);
// 使用useRef定义ref变量,用于记录请求的次数
const fetchRef = useRef(0);
// 刷新函数
const [query, setQuery] = useState<number>(0);
// 分页
const [pagination, setPagination] = useState<IPagination>(initPagination);
const { current, pageSize } = pagination;
// 查询表单值
const [searchForm, setSearchForm] = useState<IFormType>(defaultInitForm);
// 检测滚动到底部的逻辑
// @ts-ignore
const handleScroll = event => {
const { scrollTop, scrollHeight, clientHeight } = event.target;
// 距离底部 10px 时触发,增加一点缓冲
if (scrollTop + clientHeight >= scrollHeight - 10) {
if (fetching) return; // 如果正在加载,则不触发
// 检查是否还有更多数据
if (pagination.total && options.length >= pagination.total) {
return;
}
setPagination(prev => ({ ...prev, current: prev.current + 1 }));
onSure();
}
};
// 查询表单值改变
const handleSearchChange = (item: MapItem) => {
setSearchForm({ ...searchForm, ...item });
};
const onSure = useCallback(() => {
setQuery(prev => prev + 1);
}, []);
const debounceFetcher = useMemo(() => {
const loadOptions = debounce(async (value: string) => {
handleSearchChange({ tag: value });
setOptions([]);
setPagination(initPagination);
// 一切准备好后,开始请求数据
onSure();
}, debounceTimeout);
return loadOptions;
}, [debounceTimeout]);
useEffect(() => {
let isActive = true;
const fetchId = ++fetchRef.current;
setFetching(true);
// 调用 API 并处理 Promise
getTagListApi({
...searchForm,
pageNumber: current,
pageSize
}).then(({ status, result }) => {
if (isActive && fetchId === fetchRef.current) {
const { code } = status || {};
//@ts-ignore
const { list, pageNum, pageSize: resPageSize, pageTotal, total } = result || {};
if (code === 0) {
if (list && list.length > 0) {
setPagination({ current: Number(pageNum), pageSize: resPageSize, total });
const newList = list.map((item: MapItem) => ({
key: item?.tagId,
label: item?.tag,
value: item?.tag
}));
setOptions(prevOptions => {
// 使用 Map 过滤掉重复的 key
const allOptions = [...prevOptions, ...newList];
const uniqueOptionsMap = new Map();
allOptions.forEach(opt => {
if (opt.key) {
uniqueOptionsMap.set(opt.key, opt);
} else {
// 如果没有 key,用 value 作为 fallback
uniqueOptionsMap.set(opt.value, opt);
}
});
return Array.from(uniqueOptionsMap.values());
});
} else if (current === 1) {
gitextract_u_5er_hd/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .qoder/ │ └── settings.json ├── .stylelintignore ├── .stylelintrc.js ├── .trae/ │ └── documents/ │ ├── 在文章编辑页集成 Moveable.js 实现图片缩放.md │ └── 文章编辑页:导入 Markdown + 修复 Word 图片清晰度.md ├── .vscode/ │ └── extensions.json ├── AGENTS.md ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── deploy-front.sh ├── index.html ├── launch.sh ├── lint-staged.config.js ├── package.json ├── postcss.config.js ├── src/ │ ├── App.tsx │ ├── api/ │ │ ├── config/ │ │ │ └── servicePort.ts │ │ ├── helper/ │ │ │ ├── axiosCancel.ts │ │ │ └── checkStatus.ts │ │ ├── index.ts │ │ ├── interface/ │ │ │ └── index.ts │ │ └── modules/ │ │ ├── aiConfig.ts │ │ ├── article.ts │ │ ├── author.ts │ │ ├── category.ts │ │ ├── column.ts │ │ ├── comment.ts │ │ ├── common.ts │ │ ├── config.ts │ │ ├── global.ts │ │ ├── login.ts │ │ ├── resume.ts │ │ ├── sensitive.ts │ │ ├── statistics.ts │ │ ├── tag.ts │ │ ├── user.ts │ │ └── wxMenu.ts │ ├── assets/ │ │ ├── fonts/ │ │ │ ├── DIN.otf │ │ │ └── font.less │ │ └── iconfont/ │ │ └── iconfont.less │ ├── components/ │ │ ├── ErrorMessage/ │ │ │ ├── 403.tsx │ │ │ ├── 404.tsx │ │ │ ├── 500.tsx │ │ │ └── index.less │ │ ├── Loading/ │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── SwitchDark/ │ │ │ └── index.tsx │ │ ├── common-wrap/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── second-sure-modal/ │ │ │ └── index.tsx │ │ └── svgIcon/ │ │ └── index.tsx │ ├── config/ │ │ ├── config.ts │ │ ├── nprogress.ts │ │ └── serviceLoading.tsx │ ├── enums/ │ │ ├── common.ts │ │ └── httpEnum.ts │ ├── hooks/ │ │ └── useTheme.ts │ ├── index.scss │ ├── layouts/ │ │ ├── components/ │ │ │ ├── Footer/ │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── Header/ │ │ │ │ ├── components/ │ │ │ │ │ ├── AssemblySize.tsx │ │ │ │ │ ├── AvatarIcon.tsx │ │ │ │ │ ├── BreadcrumbNav.tsx │ │ │ │ │ ├── CollapseIcon.tsx │ │ │ │ │ ├── Fullscreen.tsx │ │ │ │ │ ├── InfoModal.tsx │ │ │ │ │ ├── PasswordModal.tsx │ │ │ │ │ └── Theme.tsx │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── Menu/ │ │ │ │ ├── components/ │ │ │ │ │ └── Logo.tsx │ │ │ │ ├── index.css │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── Tabs/ │ │ │ ├── components/ │ │ │ │ └── MoreButton.tsx │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── main.tsx │ ├── redux/ │ │ ├── index.ts │ │ ├── interface/ │ │ │ └── index.ts │ │ ├── modules/ │ │ │ ├── auth/ │ │ │ │ ├── action.ts │ │ │ │ └── reducer.ts │ │ │ ├── breadcrumb/ │ │ │ │ ├── action.ts │ │ │ │ └── reducer.ts │ │ │ ├── disc/ │ │ │ │ ├── action.ts │ │ │ │ └── reducer.ts │ │ │ ├── global/ │ │ │ │ ├── action.ts │ │ │ │ └── reducer.ts │ │ │ ├── menu/ │ │ │ │ ├── action.ts │ │ │ │ └── reducer.ts │ │ │ └── tabs/ │ │ │ ├── action.ts │ │ │ └── reducer.ts │ │ └── mutation-types.ts │ ├── routers/ │ │ ├── constant.tsx │ │ ├── index.tsx │ │ ├── interface/ │ │ │ └── index.ts │ │ ├── modules/ │ │ │ ├── aiConfig.tsx │ │ │ ├── article.tsx │ │ │ ├── author.tsx │ │ │ ├── category.tsx │ │ │ ├── column.tsx │ │ │ ├── config.tsx │ │ │ ├── error.tsx │ │ │ ├── global.tsx │ │ │ ├── home.tsx │ │ │ ├── resume.tsx │ │ │ ├── sensitive.tsx │ │ │ ├── statistics.tsx │ │ │ ├── tag.tsx │ │ │ └── wxMenu.tsx │ │ ├── route.tsx │ │ └── utils/ │ │ ├── authRouter.tsx │ │ └── lazyLoad.tsx │ ├── styles/ │ │ ├── common.less │ │ ├── reset.less │ │ ├── theme/ │ │ │ ├── theme-dark.less │ │ │ └── theme-default.less │ │ └── var.less │ ├── typings/ │ │ ├── common.ts │ │ ├── global.d.ts │ │ ├── plugins.d.ts │ │ └── window.d.ts │ ├── utils/ │ │ ├── getEnv.ts │ │ ├── is/ │ │ │ └── index.ts │ │ └── util.ts │ ├── views/ │ │ ├── aiConfig/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── article/ │ │ │ ├── components/ │ │ │ │ ├── debounceselect/ │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── edit/ │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ └── list/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── author/ │ │ │ ├── loginAudit/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── whitelist/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ └── zsxqlist/ │ │ │ ├── components/ │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── category/ │ │ │ ├── components/ │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.css │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── column/ │ │ │ ├── article/ │ │ │ │ ├── components/ │ │ │ │ │ ├── DatePicker.tsx │ │ │ │ │ ├── debounceselect/ │ │ │ │ │ │ ├── DebounceSelect.tsx │ │ │ │ │ │ └── index.scss │ │ │ │ │ ├── search/ │ │ │ │ │ │ ├── index.scss │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── tableselect/ │ │ │ │ │ └── TableSelect.tsx │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ └── setting/ │ │ │ ├── articlesort/ │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── search.scss │ │ │ │ └── search.tsx │ │ │ ├── components/ │ │ │ │ ├── authorselect/ │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── imgupload/ │ │ │ │ │ └── index.tsx │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── groups/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.css │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── comment/ │ │ │ ├── components/ │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── config/ │ │ │ ├── components/ │ │ │ │ ├── imgupload/ │ │ │ │ │ ├── ImgCropUpload.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.css │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── global/ │ │ │ ├── components/ │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── home/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── login/ │ │ │ ├── components/ │ │ │ │ └── LoginForm.tsx │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── resume/ │ │ │ ├── components/ │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.css │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── sensitive/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── statistics/ │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── tag/ │ │ │ ├── components/ │ │ │ │ └── search/ │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.css │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── wxMenu/ │ │ ├── index.scss │ │ └── index.tsx │ └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts
SYMBOL INDEX (305 symbols across 68 files)
FILE: src/api/config/servicePort.ts
constant PORT1 (line 2) | const PORT1 = "";
FILE: src/api/helper/axiosCancel.ts
class AxiosCanceler (line 13) | class AxiosCanceler {
method addPending (line 18) | addPending(config: AxiosRequestConfig) {
method removePending (line 36) | removePending(config: AxiosRequestConfig) {
method removeAllPending (line 50) | removeAllPending() {
method reset (line 60) | reset(): void {
FILE: src/api/index.ts
class RequestHttp (line 24) | class RequestHttp {
method constructor (line 28) | public constructor(config: AxiosRequestConfig) {
method down (line 114) | down<T>(url: string, config: AxiosRequestConfig = {}): Promise<AxiosRe...
method get (line 118) | get<T>(url: string, params?: object, _object = {}): Promise<ResultData...
method post (line 121) | post<T>(url: string, params?: object, _object = {}): Promise<ResultDat...
method postForm (line 125) | postForm<T>(url: string, params?: object, _object = {}): Promise<PaiRe...
method put (line 129) | put<T>(url: string, params?: object, _object = {}): Promise<ResultData...
method delete (line 132) | delete<T>(url: string, params?: any, _object = {}): Promise<ResultData...
FILE: src/api/interface/index.ts
type Result (line 2) | interface Result {
type ResultData (line 8) | interface ResultData<T = any> extends Result {
type Status (line 14) | interface Status {
type PaiRes (line 18) | interface PaiRes<T = any> {
type ResPage (line 24) | interface ResPage<T> {
type ReqPage (line 32) | interface ReqPage {
type ReqLoginForm (line 39) | interface ReqLoginForm {
type ResLogin (line 43) | interface ResLogin {
type ResAuthButtons (line 51) | interface ResAuthButtons {
FILE: src/api/modules/aiConfig.ts
type AISourceValue (line 5) | type AISourceValue =
type GptModelConfig (line 16) | interface GptModelConfig {
type ChatGptConfig (line 24) | interface ChatGptConfig {
type ZhipuConfig (line 30) | interface ZhipuConfig {
type XunFeiConfig (line 36) | interface XunFeiConfig {
type ZhipuCodingConfig (line 45) | interface ZhipuCodingConfig {
type DeepSeekConfig (line 52) | interface DeepSeekConfig {
type DoubaoConfig (line 59) | interface DoubaoConfig {
type AliConfig (line 65) | interface AliConfig {
type AiConfigAdminDTO (line 69) | interface AiConfigAdminDTO {
type AiConfigAdminReq (line 80) | type AiConfigAdminReq = AiConfigAdminDTO;
type AiConfigTestReq (line 82) | interface AiConfigTestReq {
type AiConfigTestRes (line 87) | interface AiConfigTestRes {
FILE: src/api/modules/comment.ts
type SearchCommentReq (line 5) | interface SearchCommentReq {
type CommentSaveReq (line 17) | interface CommentSaveReq {
type CommentAdminDTO (line 25) | interface CommentAdminDTO {
type CommentPageDTO (line 45) | interface CommentPageDTO {
FILE: src/api/modules/sensitive.ts
type SensitiveWordHitDTO (line 5) | interface SensitiveWordHitDTO {
type SensitiveWordConfigDTO (line 10) | interface SensitiveWordConfigDTO {
type SensitiveWordConfigReq (line 18) | interface SensitiveWordConfigReq {
type SensitiveWordHitPageReq (line 24) | interface SensitiveWordHitPageReq {
type SensitiveWordHitPageDTO (line 29) | interface SensitiveWordHitPageDTO {
FILE: src/api/modules/user.ts
type UserLoginAuditItem (line 4) | interface UserLoginAuditItem {
type UserSessionItem (line 23) | interface UserSessionItem {
type UserShareRiskItem (line 43) | interface UserShareRiskItem {
type LoginAuditQuery (line 60) | interface LoginAuditQuery {
type UserShareRiskQuery (line 71) | interface UserShareRiskQuery {
type UserSessionQuery (line 81) | interface UserSessionQuery {
type UserForbidReq (line 91) | interface UserForbidReq {
type UserUnforbidReq (line 97) | interface UserUnforbidReq {
type PageResult (line 101) | interface PageResult<T> {
FILE: src/api/modules/wxMenu.ts
type WxMenuButton (line 4) | interface WxMenuButton {
type WxMenuTree (line 16) | interface WxMenuTree {
type WxMenuReplyArticle (line 20) | interface WxMenuReplyArticle {
type WxMenuReply (line 27) | interface WxMenuReply {
type WxMenuKeywordReply (line 33) | interface WxMenuKeywordReply {
type WxMenuClickReply (line 43) | interface WxMenuClickReply {
type WxMenuAiProviderOption (line 49) | interface WxMenuAiProviderOption {
type WxMenuConfig (line 56) | interface WxMenuConfig {
type WxMenuDetail (line 70) | interface WxMenuDetail {
type WxMenuSaveReq (line 96) | interface WxMenuSaveReq {
type WxMenuValidateReq (line 109) | interface WxMenuValidateReq {
type WxMenuValidateRes (line 121) | interface WxMenuValidateRes {
type WxMenuPreviewMatchReq (line 130) | interface WxMenuPreviewMatchReq extends WxMenuSaveReq {
type WxMenuPreviewMatchRes (line 136) | interface WxMenuPreviewMatchRes {
type WxMenuPreviewAiReq (line 146) | interface WxMenuPreviewAiReq {
type WxMenuPreviewAiRes (line 153) | interface WxMenuPreviewAiRes {
type WxMenuPublishReq (line 160) | interface WxMenuPublishReq {
type WxMenuPublishRes (line 164) | interface WxMenuPublishRes {
FILE: src/components/common-wrap/index.tsx
type IProps (line 5) | interface IProps {
FILE: src/components/svgIcon/index.tsx
type SvgProps (line 1) | interface SvgProps {
function SvgIcon (line 8) | function SvgIcon(props: SvgProps) {
FILE: src/config/config.ts
constant HOME_URL (line 4) | const HOME_URL: string = "/statistics/index";
constant LOGIN_URL (line 7) | const LOGIN_URL: string = "/login";
constant TABS_BLACK_LIST (line 10) | const TABS_BLACK_LIST: string[] = ["/403", "/404", "/500", "/layout", "/...
constant MAP_KEY (line 13) | const MAP_KEY: string = "";
FILE: src/enums/common.ts
type UpdateEnum (line 1) | enum UpdateEnum {
type IPagination (line 6) | interface IPagination {
type PushStatusEnum (line 18) | enum PushStatusEnum {
FILE: src/enums/httpEnum.ts
type ResultEnum (line 5) | enum ResultEnum {
type RequestEnum (line 17) | enum RequestEnum {
type ContentTypeEnum (line 28) | enum ContentTypeEnum {
FILE: src/layouts/components/Header/components/AvatarIcon.tsx
type ModalProps (line 22) | interface ModalProps {
FILE: src/layouts/components/Header/components/InfoModal.tsx
type Props (line 4) | interface Props {
FILE: src/layouts/components/Header/components/PasswordModal.tsx
type Props (line 4) | interface Props {
FILE: src/layouts/components/Menu/index.tsx
type MenuItem (line 41) | type MenuItem = Required<MenuProps>["items"][number];
FILE: src/redux/index.ts
type RootState (line 28) | type RootState = ReturnType<typeof reducer>;
type AppThunk (line 30) | type AppThunk<ReturnType = void> = ThunkAction<
type AppDispatch (line 38) | type AppDispatch = ThunkDispatch<RootState, unknown, Action<string>>;
FILE: src/redux/interface/index.ts
type ThemeConfigProp (line 6) | interface ThemeConfigProp {
type GlobalState (line 16) | interface GlobalState {
type MenuState (line 24) | interface MenuState {
type TabsState (line 30) | interface TabsState {
type BreadcrumbState (line 36) | interface BreadcrumbState {
type AuthState (line 43) | interface AuthState {
type DiscState (line 51) | interface DiscState {
FILE: src/redux/modules/menu/action.ts
type MenuProps (line 19) | interface MenuProps {
FILE: src/redux/mutation-types.ts
constant UPDATE_COLLAPSE (line 2) | const UPDATE_COLLAPSE = "UPDATE_ASIDE_COLLAPSE";
constant SET_MENU_LIST (line 4) | const SET_MENU_LIST = "SET_MENU_LIST";
constant SET_TABS_LIST (line 6) | const SET_TABS_LIST = "SET_TABS_LIST";
constant SET_TABS_ACTIVE (line 8) | const SET_TABS_ACTIVE = "SET_TABS_ACTIVE";
constant SET_BREADCRUMB_LIST (line 10) | const SET_BREADCRUMB_LIST = "SET_BREADCRUMB_LIST";
constant SET_AUTH_BUTTONS (line 12) | const SET_AUTH_BUTTONS = "SET_AUTH_BUTTONS";
constant SET_AUTH_ROUTER (line 14) | const SET_AUTH_ROUTER = "SET_AUTH_ROUTER";
constant SET_TOKEN (line 16) | const SET_TOKEN = "SET_TOKEN";
constant USER_INFO (line 18) | const USER_INFO = "USER_INFO";
constant SET_ASSEMBLY_SIZE (line 20) | const SET_ASSEMBLY_SIZE = "SET_ASSEMBLY_SIZE";
constant SET_THEME_CONFIG (line 22) | const SET_THEME_CONFIG = "SET_THEME_CONFIG";
constant UPDATE_DISC (line 24) | const UPDATE_DISC = "UPDATE_DISC";
FILE: src/routers/interface/index.ts
type MetaProps (line 1) | interface MetaProps {
type RouteObject (line 8) | interface RouteObject {
FILE: src/typings/common.ts
type MapItem (line 1) | interface MapItem {
FILE: src/typings/global.d.ts
type MenuOptions (line 3) | interface MenuOptions {
type Recordable (line 14) | type Recordable<T = any> = Record<string, T>;
type ViteEnv (line 16) | interface ViteEnv {
type MenuInfo (line 29) | interface MenuInfo {
FILE: src/typings/window.d.ts
type Window (line 3) | interface Window {
type Navigator (line 6) | interface Navigator {
FILE: src/utils/getEnv.ts
function isDevFn (line 5) | function isDevFn(mode: string): boolean {
function isProdFn (line 9) | function isProdFn(mode: string): boolean {
function isReportMode (line 16) | function isReportMode(): boolean {
function wrapperEnv (line 21) | function wrapperEnv(envConf: Recordable): ViteEnv {
function getEnvConfig (line 49) | function getEnvConfig(match = "VITE_GLOB_", confFiles = [".env", ".env.p...
function getRootPath (line 73) | function getRootPath(...dir: string[]) {
FILE: src/utils/is/index.ts
function is (line 6) | function is(val: unknown, type: string) {
function isFunction (line 13) | function isFunction<T = Function>(val: unknown): val is T {
function isDate (line 37) | function isDate(val: unknown): val is Date {
function isNumber (line 44) | function isNumber(val: unknown): val is number {
function isAsyncFunction (line 51) | function isAsyncFunction<T = any>(val: unknown): val is Promise<T> {
function isPromise (line 58) | function isPromise<T = any>(val: unknown): val is Promise<T> {
function isString (line 65) | function isString(val: unknown): val is string {
function isBoolean (line 72) | function isBoolean(val: unknown): val is boolean {
function isArray (line 79) | function isArray(val: any): val is Array<any> {
function isImageDom (line 104) | function isImageDom(o: Element) {
function isNull (line 108) | function isNull(val: unknown): val is null {
function isNullAndUnDef (line 112) | function isNullAndUnDef(val: unknown): val is null | undefined {
function isNullOrUnDef (line 116) | function isNullOrUnDef(val: unknown): val is null | undefined {
FILE: src/utils/util.ts
function handleRouter (line 139) | function handleRouter(routerList: Menu.MenuOptions[], newArr: string[] =...
function randomNum (line 186) | function randomNum(min: number, max: number): number {
FILE: src/views/aiConfig/index.tsx
type AiConfigFormValues (line 34) | interface AiConfigFormValues {
constant AI_SOURCE_OPTIONS (line 71) | const AI_SOURCE_OPTIONS: Array<{ label: string; value: AISourceValue }> = [
constant REMOVED_SOURCE_VALUES (line 80) | const REMOVED_SOURCE_VALUES: AISourceValue[] = ["CHAT_GPT_3_5", "CHAT_GP...
constant AI_SOURCE_LABEL_MAP (line 81) | const AI_SOURCE_LABEL_MAP = AI_SOURCE_OPTIONS.reduce<Record<AISourceValu...
constant AI_TEST_PROMPT (line 85) | const AI_TEST_PROMPT = "你正在执行后台 AI 配置连通性测试。请忽略其他上下文,只输出“连接成功”。";
FILE: src/views/article/components/debounceselect/index.tsx
type DebounceSelectProps (line 14) | interface DebounceSelectProps<ValueType = any> extends Omit<SelectProps<...
type IPagination (line 18) | interface IPagination {
type IFormType (line 30) | interface IFormType {
type TagValue (line 44) | interface TagValue {
function DebounceSelect (line 50) | function DebounceSelect<ValueType extends { key?: string; label: React.R...
FILE: src/views/article/components/search/index.tsx
type IProps (line 11) | interface IProps {
FILE: src/views/article/edit/index.tsx
method viewerEffect (line 38) | viewerEffect({ markdownBody }: { markdownBody: HTMLElement }) {
method editorEffect (line 76) | editorEffect({ editor }: { editor: any }) {
method viewerEffect (line 79) | viewerEffect({ markdownBody }: { markdownBody: HTMLElement }) {
method render (line 165) | render(moveable: any, React: any) {
type IProps (line 270) | interface IProps {}
type TagValue (line 272) | interface TagValue {
type ImageInfo (line 277) | interface ImageInfo {
type IFormType (line 284) | interface IFormType {
type UploadTask (line 939) | interface UploadTask {
function transformElement (line 1379) | function transformElement(element: any): any {
function transformDocument (line 1400) | function transformDocument(document: any) {
FILE: src/views/article/edit/search/index.tsx
type IProps (line 11) | interface IProps {
FILE: src/views/article/list/index.tsx
type DataType (line 20) | interface DataType {
type IProps (line 35) | interface IProps {}
type IInitForm (line 38) | interface IInitForm {
type ISearchForm (line 47) | interface ISearchForm {
method render (line 391) | render(value, item) {
method render (line 441) | render(value) {
method render (line 453) | render(_, item) {
method render (line 469) | render(_, item) {
method render (line 484) | render(_, item) {
FILE: src/views/author/loginAudit/index.tsx
type LoginAuditFilterForm (line 21) | interface LoginAuditFilterForm {
type ShareRiskFilterForm (line 28) | interface ShareRiskFilterForm {
FILE: src/views/author/whitelist/index.tsx
type DataType (line 17) | interface DataType {
type IProps (line 25) | interface IProps {}
type IFormType (line 27) | interface IFormType {
type DataIndex (line 82) | type DataIndex = keyof DataType;
method render (line 240) | render(value) {
FILE: src/views/author/zsxqlist/components/search/index.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/author/zsxqlist/index.tsx
type DataType (line 18) | interface DataType {
type IProps (line 33) | interface IProps {}
type ISearchForm (line 36) | interface ISearchForm {
type IInitForm (line 46) | interface IInitForm {
method render (line 329) | render(value, item) {
method render (line 348) | render(value) {
method render (line 367) | render(value) {
method render (line 382) | render(value) {
method render (line 393) | render(value) {
method render (line 406) | render(value) {
method render (line 417) | render(_, item) {
FILE: src/views/category/components/search/index.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/category/index.tsx
type DataType (line 16) | interface DataType {
type IProps (line 22) | interface IProps {}
type IFormType (line 24) | interface IFormType {
method render (line 184) | render(status, item) {
FILE: src/views/column/article/components/debounceselect/DebounceSelect.tsx
type DebounceSelectProps (line 8) | interface DebounceSelectProps<ValueType = any> extends Omit<SelectProps<...
function DebounceSelect (line 13) | function DebounceSelect<ValueType extends { key?: string; label: React.R...
FILE: src/views/column/article/components/search/index.tsx
type IProps (line 12) | interface IProps {
FILE: src/views/column/article/components/tableselect/TableSelect.tsx
type DataType (line 14) | interface DataType {
type ValueType (line 21) | interface ValueType {
type IProps (line 27) | interface IProps {
type ISearchArticleForm (line 37) | interface ISearchArticleForm {
method render (line 124) | render(value, item) {
method render (line 138) | render(value) {
FILE: src/views/column/article/index.tsx
type IProps (line 21) | interface IProps {}
type DataType (line 24) | interface DataType {
type ISearchForm (line 35) | interface ISearchForm {
type IFormType (line 40) | interface IFormType {
type ColumnValue (line 67) | interface ColumnValue {
function fetchColumnList (line 234) | async function fetchColumnList(key: string): Promise<ColumnValue[]> {
method render (line 266) | render(value, item) {
method render (line 281) | render(value, item) {
FILE: src/views/column/setting/articlesort/index.tsx
type IProps (line 51) | interface IProps {}
type IGroupFormType (line 53) | interface IGroupFormType {
type GroupData (line 62) | interface GroupData {
type DataType (line 72) | interface DataType {
type ISearchForm (line 86) | interface ISearchForm {
type IFormType (line 91) | interface IFormType {
type IArticleSortFormType (line 103) | interface IArticleSortFormType {
type RowProps (line 187) | interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
method render (line 470) | render(value, item) {
method render (line 500) | render(value, item) {
FILE: src/views/column/setting/articlesort/search.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/column/setting/components/authorselect/index.tsx
type IProps (line 14) | interface IProps {
FILE: src/views/column/setting/components/imgupload/index.tsx
type IProps (line 11) | interface IProps {
FILE: src/views/column/setting/components/search/index.tsx
type IProps (line 11) | interface IProps {
FILE: src/views/column/setting/groups/index.tsx
type IProps (line 27) | interface IProps {}
type GroupData (line 29) | interface GroupData {
type DataType (line 40) | interface DataType {
type IMoveType (line 52) | interface IMoveType {
type IFormType (line 61) | interface IFormType {
FILE: src/views/column/setting/index.tsx
type DataType (line 42) | interface DataType {
type IProps (line 55) | interface IProps {}
type IFormType (line 57) | interface IFormType {
type ISearchForm (line 90) | interface ISearchForm {
method render (line 349) | render(value) {
method render (line 362) | render(value, item) {
method render (line 375) | render(value) {
method render (line 389) | render(type) {
method render (line 397) | render(state) {
FILE: src/views/comment/components/search/index.tsx
type IProps (line 9) | interface IProps {
FILE: src/views/comment/index.tsx
type ModalMode (line 22) | type ModalMode = "create" | "reply" | "edit";
type SearchFormState (line 24) | interface SearchFormState {
type FormState (line 32) | interface FormState {
FILE: src/views/config/components/imgupload/ImgCropUpload.tsx
type ImgCropUploadProps (line 7) | interface ImgCropUploadProps {
FILE: src/views/config/components/imgupload/index.tsx
type IProps (line 11) | interface IProps {
FILE: src/views/config/components/search/index.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/config/index.tsx
type DataType (line 38) | interface DataType {
type IProps (line 49) | interface IProps {}
type IFormType (line 52) | interface IFormType {
type ISearchFormType (line 64) | interface ISearchFormType {
method render (line 291) | render(name, item) {
method render (line 304) | render(type) {
method render (line 312) | render(status, item) {
FILE: src/views/global/components/search/index.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/global/index.tsx
type DataType (line 16) | interface DataType {
type IProps (line 23) | interface IProps {}
type IFormType (line 25) | interface IFormType {
FILE: src/views/resume/components/search/index.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/resume/index.tsx
type DataType (line 19) | interface DataType {
type IProps (line 34) | interface IProps {}
type IFormType (line 36) | interface IFormType {
FILE: src/views/sensitive/index.tsx
type IProps (line 22) | interface IProps {}
type SensitiveWordFormValues (line 24) | interface SensitiveWordFormValues {
FILE: src/views/statistics/index.tsx
type IProps (line 17) | interface IProps {}
FILE: src/views/tag/components/search/index.tsx
type IProps (line 10) | interface IProps {
FILE: src/views/tag/index.tsx
type DataType (line 16) | interface DataType {
type IProps (line 22) | interface IProps {}
type IFormType (line 24) | interface IFormType {
method render (line 181) | render(status, item) {
FILE: src/views/wxMenu/index.tsx
constant DEFAULT_DRAFT_COMMENT (line 50) | const DEFAULT_DRAFT_COMMENT = "微信自定义菜单草稿";
constant DEFAULT_FALLBACK_STRATEGY (line 51) | const DEFAULT_FALLBACK_STRATEGY = "fixed_reply";
constant LOCAL_DRAFT_CACHE_KEY (line 52) | const LOCAL_DRAFT_CACHE_KEY = "paicoding-admin:wx-menu:draft-cache";
constant REPLY_TYPE_OPTIONS (line 54) | const REPLY_TYPE_OPTIONS = [
constant MATCH_TYPE_OPTIONS (line 59) | const MATCH_TYPE_OPTIONS = [
constant FALLBACK_STRATEGY_OPTIONS (line 65) | const FALLBACK_STRATEGY_OPTIONS = [
constant PREVIEW_EVENT_OPTIONS (line 71) | const PREVIEW_EVENT_OPTIONS = [
constant AI_PROVIDER_CATALOG (line 77) | const AI_PROVIDER_CATALOG: Array<{
type AiProviderOptionViewModel (line 91) | interface AiProviderOptionViewModel {
constant DEFAULT_MENU_TEMPLATE (line 96) | const DEFAULT_MENU_TEMPLATE = JSON.stringify(
constant SUPPORTED_TYPES (line 125) | const SUPPORTED_TYPES = [
constant MATCH_TYPE_LABEL_MAP (line 141) | const MATCH_TYPE_LABEL_MAP: Record<string, string> = {
constant MATCH_TYPE_HINT_MAP (line 147) | const MATCH_TYPE_HINT_MAP: Record<string, string> = {
constant FALLBACK_STRATEGY_DESC_MAP (line 153) | const FALLBACK_STRATEGY_DESC_MAP: Record<string, string> = {
type LocalDraftCache (line 254) | interface LocalDraftCache {
type SectionTitleProps (line 491) | interface SectionTitleProps {
type WechatReplyPreviewProps (line 507) | interface WechatReplyPreviewProps {
type WechatMenuPreviewProps (line 557) | interface WechatMenuPreviewProps {
type MatchPreviewMetaProps (line 606) | interface MatchPreviewMetaProps {
type ReplyEditorProps (line 654) | interface ReplyEditorProps {
type KeywordRuleCardProps (line 771) | interface KeywordRuleCardProps {
FILE: vite.config.ts
constant DEFAULT_BACKEND_ORIGIN (line 16) | const DEFAULT_BACKEND_ORIGIN = "http://127.0.0.1:8080";
constant BACKEND_PROBE_PATHS (line 17) | const BACKEND_PROBE_PATHS = ["/api/admin/probe", "/api/admin/isLogined"];
constant BACKEND_PROBE_SIGNATURE (line 18) | const BACKEND_PROBE_SIGNATURE = "paicoding-port-for-admin";
constant BACKEND_PROBE_TIMEOUT (line 19) | const BACKEND_PROBE_TIMEOUT = 1200;
constant WELL_KNOWN_BACKEND_PORTS (line 20) | const WELL_KNOWN_BACKEND_PORTS = [8080, 9201, 8081, 8082, 8083, 8084, 80...
function normalizeLocalOrigin (line 22) | function normalizeLocalOrigin(value?: string) {
function getListeningPorts (line 36) | function getListeningPorts() {
function parseProbeResponse (line 73) | function parseProbeResponse(body: string, fallbackOrigin: string) {
function requestProbe (line 95) | function requestProbe(url: URL, fallbackOrigin: string): Promise<string> {
function probeBackendOrigin (line 139) | async function probeBackendOrigin(origin: string) {
function resolveBackendOrigin (line 150) | async function resolveBackendOrigin(configuredDomain: string) {
Condensed preview — 225 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (762K chars).
[
{
"path": ".editorconfig",
"chars": 376,
"preview": "# @see: http://editorconfig.org\n\nroot = true\n\n[*] # 表示所有文件适用\ncharset = utf-8 # 设置文件字符集为 utf-8\nend_of_line = lf # 控制换行类型("
},
{
"path": ".eslintignore",
"chars": 129,
"preview": "*.sh\nnode_modules\n*.md\n*.woff\n*.ttf\n.vscode\n.idea\ndist\n/public\n/docs\n.husky\n.local\n/bin\n.eslintrc.js\n.prettierrc.js\n/src"
},
{
"path": ".eslintrc.js",
"chars": 2918,
"preview": "// @see: http://eslint.cn\n\nmodule.exports = {\n\tsettings: {\n\t\treact: {\n\t\t\tversion: \"detect\"\n\t\t}\n\t},\n\troot: true,\n\tenv: {\n"
},
{
"path": ".gitattributes",
"chars": 407,
"preview": "# Set the default behavior, in case people don't have core.autocrlf set.\n* text=auto\n\n# Explicitly declare text files yo"
},
{
"path": ".gitignore",
"chars": 343,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": ".husky/commit-msg",
"chars": 91,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit $1\n"
},
{
"path": ".husky/pre-commit",
"chars": 78,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpm run lint:lint-staged\n"
},
{
"path": ".prettierignore",
"chars": 61,
"preview": "/dist/*\n.local\n/node_modules/**\n\n**/*.svg\n**/*.sh\n\n/public/*\n"
},
{
"path": ".prettierrc.js",
"chars": 1112,
"preview": "// @see: https://www.prettier.cn\n\nmodule.exports = {\n\t// 超过最大值换行\n\tprintWidth: 130,\n\t// 缩进字节数\n\ttabWidth: 2,\n\t// 使用制表符而不是空"
},
{
"path": ".qoder/settings.json",
"chars": 377,
"preview": "{\n \"permissions\": {\n \"ask\": [\n \"Read(!/Users/itwanger/Documents/GitHub/paicoding-admin/**)\",\n \"Edit(!/User"
},
{
"path": ".stylelintignore",
"chars": 27,
"preview": "/dist/*\n/public/*\npublic/*\n"
},
{
"path": ".stylelintrc.js",
"chars": 783,
"preview": "module.exports = {\n\t// 引入标准配置文件和scss配置扩展\n\textends: [\"stylelint-config-standard\", \"stylelint-config-recommended-scss\"],\n\t"
},
{
"path": ".trae/documents/在文章编辑页集成 Moveable.js 实现图片缩放.md",
"chars": 837,
"preview": "## 实施计划:集成 react-moveable 实现图片缩放\n\n### 1. 安装依赖\n- 安装 `react-moveable` 库,用于提供图片的拖拽和缩放功能。\n\n### 2. 修改文章编辑页面 ([index.tsx](file"
},
{
"path": ".trae/documents/文章编辑页:导入 Markdown + 修复 Word 图片清晰度.md",
"chars": 2134,
"preview": "## 目标\n- 在 `#/article/edit/index` 增加“导入Markdown”功能:读取本地 `.md/.markdown/.txt` 内容并写入编辑器。\n- 导入时不上传图片;图片仍走现有“转链”按钮统一处理外链图片。\n-"
},
{
"path": ".vscode/extensions.json",
"chars": 184,
"preview": "{\n\t\"recommendations\": [\n\t\t\"dsznajder.es7-react-js-snippets\",\n\t\t\"stylelint.vscode-stylelint\",\n\t\t\"dbaeumer.vscode-eslint\","
},
{
"path": "AGENTS.md",
"chars": 4905,
"preview": "# AGENTS.md\n\nThis file provides guidance to Qoder (qoder.com) when working with code in this repository.\n\n## Project Ove"
},
{
"path": "CHANGELOG.md",
"chars": 274,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github."
},
{
"path": "CLAUDE.md",
"chars": 6963,
"preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2022 SpicyBoy\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 8214,
"preview": "# paicoding-admin 🚀\n\n## 介绍 📖\n\n<p align=\"center\">\n <a href=\"https://paicoding.com/\">\n <img src=\"https://cdn.tobebette"
},
{
"path": "commitlint.config.js",
"chars": 4701,
"preview": "// @see: https://cz-git.qbenben.com/zh/guide\n/** @type {import('cz-git').UserConfig} */\n\nmodule.exports = {\n\tignores: [c"
},
{
"path": "deploy-front.sh",
"chars": 3323,
"preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$SCRIPT_DIR"
},
{
"path": "index.html",
"chars": 2070,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<link rel=\"icon\" type=\"image/svg+xml\" href=\"./favi"
},
{
"path": "launch.sh",
"chars": 2758,
"preview": "#!/usr/bin/env bash\n\n# file to upload\nWEB_PKG=\"dist.tar.gz\"\nWEB_PKG_BK=\"dist_bk.tar.gz\"\nTMP_WEB_PKG=\"tmp.tar.gz\"\n\nDEPLOY"
},
{
"path": "lint-staged.config.js",
"chars": 309,
"preview": "module.exports = {\n\t\"*.{js,jsx,ts,tsx}\": [\"eslint --fix\", \"prettier --write\"],\n\t\"{!(package)*.json,*.code-snippets,.!(br"
},
{
"path": "package.json",
"chars": 3493,
"preview": "{\n\t\"name\": \"react\",\n\t\"private\": true,\n\t\"version\": \"0.0.1\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite\",\n\t\t\"serve\": \"vite\",\n\t\t\"build:de"
},
{
"path": "postcss.config.js",
"chars": 56,
"preview": "module.exports = {\n\tplugins: {\n\t\tautoprefixer: {}\n\t}\n};\n"
},
{
"path": "src/App.tsx",
"chars": 773,
"preview": "import { connect } from \"react-redux\";\nimport { HashRouter } from \"react-router-dom\";\nimport { ConfigProvider } from \"an"
},
{
"path": "src/api/config/servicePort.ts",
"chars": 37,
"preview": "// 后端微服务端口名\nexport const PORT1 = \"\";\n"
},
{
"path": "src/api/helper/axiosCancel.ts",
"chars": 1388,
"preview": "import axios, { AxiosRequestConfig, Canceler } from \"axios\";\nimport qs from \"qs\";\n\nimport { isFunction } from \"@/utils/i"
},
{
"path": "src/api/helper/checkStatus.ts",
"chars": 756,
"preview": "import { message } from \"antd\";\n\n/**\n * @description: 校验网络请求状态码\n * @param {Number} status\n * @return void\n */\nexport con"
},
{
"path": "src/api/index.ts",
"chars": 4303,
"preview": "import { message } from \"antd\";\nimport axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from \"axi"
},
{
"path": "src/api/interface/index.ts",
"chars": 824,
"preview": "// * 请求响应参数(不包含data)\nexport interface Result {\n\tcode: string;\n\tmsg: string;\n}\n\n// * 请求响应参数(包含data)\nexport interface Resu"
},
{
"path": "src/api/modules/aiConfig.ts",
"chars": 2053,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/article.ts",
"chars": 1781,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/author.ts",
"chars": 1396,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/category.ts",
"chars": 794,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/column.ts",
"chars": 2697,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/comment.ts",
"chars": 1623,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface\";\n\nex"
},
{
"path": "src/api/modules/common.ts",
"chars": 601,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/config.ts",
"chars": 895,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/global.ts",
"chars": 645,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/login.ts",
"chars": 812,
"preview": "import qs from \"qs\";\n\nimport http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from "
},
{
"path": "src/api/modules/resume.ts",
"chars": 776,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/sensitive.ts",
"chars": 1304,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/statistics.ts",
"chars": 464,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\n\n/**\n * @name 数据统计模块\n */\n\nexport const getAl"
},
{
"path": "src/api/modules/tag.ts",
"chars": 704,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\nimport { Login } from \"@/api/interface/index"
},
{
"path": "src/api/modules/user.ts",
"chars": 2759,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\n\nexport interface UserLoginAuditItem {\n\tid: "
},
{
"path": "src/api/modules/wxMenu.ts",
"chars": 4464,
"preview": "import http from \"@/api\";\nimport { PORT1 } from \"@/api/config/servicePort\";\n\nexport interface WxMenuButton {\n\ttype?: str"
},
{
"path": "src/assets/fonts/font.less",
"chars": 206,
"preview": "@font-face {\n\tfont-family: YouSheBiaoTiHei;\n\tsrc: url(\"./YouSheBiaoTiHei.ttf\");\n}\n@font-face {\n\tfont-family: MetroDF;\n\ts"
},
{
"path": "src/assets/iconfont/iconfont.less",
"chars": 496,
"preview": "@font-face {\n\tfont-family: iconfont;\n\tsrc: url(\"iconfont.ttf?t=1648886414212\") format(\"truetype\");\n}\n.iconfont {\n\tfont-f"
},
{
"path": "src/components/ErrorMessage/403.tsx",
"chars": 507,
"preview": "import { useNavigate } from \"react-router-dom\";\nimport { Button, Result } from \"antd\";\n\nimport { HOME_URL } from \"@/conf"
},
{
"path": "src/components/ErrorMessage/404.tsx",
"chars": 502,
"preview": "import { useNavigate } from \"react-router-dom\";\nimport { Button, Result } from \"antd\";\n\nimport { HOME_URL } from \"@/conf"
},
{
"path": "src/components/ErrorMessage/500.tsx",
"chars": 491,
"preview": "import { useNavigate } from \"react-router-dom\";\nimport { Button, Result } from \"antd\";\n\nimport { HOME_URL } from \"@/conf"
},
{
"path": "src/components/ErrorMessage/index.less",
"chars": 157,
"preview": ".ant-result {\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\theight: 100%;\n\t."
},
{
"path": "src/components/Loading/index.less",
"chars": 398,
"preview": "/* 请求 Loading 样式 */\n.request-loading {\n\t.ant-spin-text {\n\t\tmargin-top: 5px;\n\t\tfont-size: 18px;\n\t\tcolor: #509ff1;\n\t}\n\t.an"
},
{
"path": "src/components/Loading/index.tsx",
"chars": 212,
"preview": "import { Spin } from \"antd\";\n\nimport \"./index.less\";\n\nconst Loading = ({ tip = \"Loading\" }: { tip?: string }) => {\n\tretu"
},
{
"path": "src/components/SwitchDark/index.tsx",
"chars": 670,
"preview": "import { connect } from \"react-redux\";\nimport { Switch } from \"antd\";\n\nimport { setThemeConfig } from \"@/redux/modules/g"
},
{
"path": "src/components/common-wrap/index.scss",
"chars": 654,
"preview": ".content-wrap {\n height: 100%;\n padding: 10px 8px;\n display: flex;\n flex-direction: column;\n box-sizing: border-box"
},
{
"path": "src/components/common-wrap/index.tsx",
"chars": 486,
"preview": "import React, { ReactNode } from \"react\";\n\nimport \"./index.scss\";\n\ninterface IProps {\n\tchildren: ReactNode;\n\tclassName?:"
},
{
"path": "src/components/second-sure-modal/index.tsx",
"chars": 843,
"preview": "import React, { useState } from \"react\";\nimport { Button, Modal } from \"antd\";\n\nconst SecondSureModal: React.FC = () => "
},
{
"path": "src/components/svgIcon/index.tsx",
"chars": 467,
"preview": "interface SvgProps {\n\tname: string; // 图标的名称 ==> 必传\n\tcolor?: string; //图标的颜色 ==> 非必传\n\tprefix?: string; // 图标的前缀 ==> 非必传("
},
{
"path": "src/config/config.ts",
"chars": 337,
"preview": "// ? 全局不动配置项 只做导出不做修改\n\n// * 首页地址(默认)\nexport const HOME_URL: string = \"/statistics/index\";\n\n// * 登录地址\nexport const LOGIN_"
},
{
"path": "src/config/nprogress.ts",
"chars": 265,
"preview": "import NProgress from \"nprogress\";\n\nimport \"nprogress/nprogress.css\";\n\nNProgress.configure({\n\teasing: \"ease\", // 动画方式\n\ts"
},
{
"path": "src/config/serviceLoading.tsx",
"chars": 674,
"preview": "import ReactDOM from \"react-dom/client\";\n\nimport Loading from \"@/components/Loading\";\n\nlet needLoadingRequestCount = 0;\n"
},
{
"path": "src/enums/common.ts",
"chars": 452,
"preview": "export enum UpdateEnum {\n\tSave = 0,\n\tEdit\n}\n\nexport interface IPagination {\n\tcurrent: number;\n\tpageSize: number;\n\ttotal?"
},
{
"path": "src/enums/httpEnum.ts",
"chars": 637,
"preview": "// * 请求枚举配置\n/**\n * @description:请求配置\n */\nexport enum ResultEnum {\n\tSUCCESS = 0,\n\tERROR = 500,\n\tOVERDUE = 599,\n\tTIMEOUT ="
},
{
"path": "src/hooks/useTheme.ts",
"chars": 1153,
"preview": "import { ThemeConfigProp } from \"@/redux/interface\";\nimport darkTheme from \"@/styles/theme/theme-dark.less\";\nimport defa"
},
{
"path": "src/index.scss",
"chars": 59,
"preview": ".ant-table-tbody > tr > td {\n padding: 16px !important;\n}\n"
},
{
"path": "src/layouts/components/Footer/index.less",
"chars": 241,
"preview": ".footer {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\theight: 30px;\n\tborder-top: 1px solid #e4e7ed;"
},
{
"path": "src/layouts/components/Footer/index.tsx",
"chars": 592,
"preview": "import { connect } from \"react-redux\";\n\nimport { baseDomain } from \"@/utils/util\";\n\nimport \"./index.less\";\n\nconst Layout"
},
{
"path": "src/layouts/components/Header/components/AssemblySize.tsx",
"chars": 1057,
"preview": "import { connect } from \"react-redux\";\nimport { Dropdown, Menu } from \"antd\";\n\nimport { setAssemblySize } from \"@/redux/"
},
{
"path": "src/layouts/components/Header/components/AvatarIcon.tsx",
"chars": 2615,
"preview": "/* eslint-disable prettier/prettier */\nimport { useRef } from \"react\";\nimport { connect, useDispatch } from \"react-redux"
},
{
"path": "src/layouts/components/Header/components/BreadcrumbNav.tsx",
"chars": 807,
"preview": "import { connect } from \"react-redux\";\nimport { useLocation } from \"react-router-dom\";\nimport { Breadcrumb } from \"antd\""
},
{
"path": "src/layouts/components/Header/components/CollapseIcon.tsx",
"chars": 655,
"preview": "import { connect } from \"react-redux\";\nimport { MenuFoldOutlined, MenuUnfoldOutlined } from \"@ant-design/icons\";\n\nimport"
},
{
"path": "src/layouts/components/Header/components/Fullscreen.tsx",
"chars": 725,
"preview": "import { useEffect, useState } from \"react\";\nimport { message } from \"antd\";\nimport screenfull from \"screenfull\";\n\nconst"
},
{
"path": "src/layouts/components/Header/components/InfoModal.tsx",
"chars": 1585,
"preview": "import { Ref, useImperativeHandle, useState } from \"react\";\nimport { Avatar, message, Modal } from \"antd\";\n\ninterface Pr"
},
{
"path": "src/layouts/components/Header/components/PasswordModal.tsx",
"chars": 854,
"preview": "import { Ref, useImperativeHandle, useState } from \"react\";\nimport { message, Modal } from \"antd\";\n\ninterface Props {\n\ti"
},
{
"path": "src/layouts/components/Header/components/Theme.tsx",
"chars": 2826,
"preview": "import { useState } from \"react\";\nimport { connect } from \"react-redux\";\nimport { FireOutlined, SettingOutlined } from \""
},
{
"path": "src/layouts/components/Header/index.less",
"chars": 943,
"preview": ".ant-layout-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tborder-bottom: 1px solid #f"
},
{
"path": "src/layouts/components/Header/index.tsx",
"chars": 2146,
"preview": "import { useEffect } from \"react\";\nimport { connect } from \"react-redux\";\nimport { Layout } from \"antd\";\n\nimport { login"
},
{
"path": "src/layouts/components/Menu/components/Logo.tsx",
"chars": 461,
"preview": "import { connect } from \"react-redux\";\n\nimport logo from \"@/assets/images/logo.svg\";\nimport logoMd from \"@/assets/images"
},
{
"path": "src/layouts/components/Menu/index.css",
"chars": 1110,
"preview": ".menu {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n height: 100%;\n /* 去除菜单 Loading 遮"
},
{
"path": "src/layouts/components/Menu/index.less",
"chars": 852,
"preview": ".menu {\n\tdisplay: flex;\n\tflex-direction: column;\n\tjustify-content: space-between;\n\theight: 100%;\n\t.logo-box {\n\t\tdisplay:"
},
{
"path": "src/layouts/components/Menu/index.tsx",
"chars": 3650,
"preview": "/**\n * 菜单控制\n */\nimport React, { useEffect, useState } from \"react\";\nimport { connect } from \"react-redux\";\nimport { useL"
},
{
"path": "src/layouts/components/Tabs/components/MoreButton.tsx",
"chars": 1365,
"preview": "import { useTranslation } from \"react-i18next\";\nimport { useLocation, useNavigate } from \"react-router-dom\";\nimport { Do"
},
{
"path": "src/layouts/components/Tabs/index.less",
"chars": 1535,
"preview": ".tabs {\n\tposition: relative;\n\tborder-bottom: 1px solid #e4e7ed;\n\t.ant-tabs {\n\t\tpadding: 0 90px 0 13px;\n\t\t.ant-tabs-nav {"
},
{
"path": "src/layouts/components/Tabs/index.tsx",
"chars": 2659,
"preview": "import { useEffect, useState } from \"react\";\nimport { connect } from \"react-redux\";\nimport { useLocation, useNavigate } "
},
{
"path": "src/layouts/index.less",
"chars": 720,
"preview": ".container {\n\tdisplay: flex;\n\tmin-width: 950px;\n\theight: 100%;\n\tmax-width: 100%;\n\toverflow-x: hidden;\n\tbox-sizing: borde"
},
{
"path": "src/layouts/index.tsx",
"chars": 1485,
"preview": "import { useEffect } from \"react\";\nimport { connect } from \"react-redux\";\nimport { Outlet } from \"react-router-dom\";\nimp"
},
{
"path": "src/main.tsx",
"chars": 622,
"preview": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { Provider } from \"react-redux\";\nimport { Per"
},
{
"path": "src/redux/index.ts",
"chars": 1675,
"preview": "/* eslint-disable simple-import-sort/imports */\n/* eslint-disable prettier/prettier */\nimport { Action, combineReducers,"
},
{
"path": "src/redux/interface/index.ts",
"chars": 923,
"preview": "import type { SizeType } from \"antd/lib/config-provider/SizeContext\";\n\nimport { MapItem } from \"@/typings/common\";\n\n/* t"
},
{
"path": "src/redux/modules/auth/action.ts",
"chars": 321,
"preview": "import * as types from \"@/redux/mutation-types\";\n\n// * setAuthButtons\nexport const setAuthButtons = (authButtons: { [pro"
},
{
"path": "src/redux/modules/auth/reducer.ts",
"chars": 617,
"preview": "import produce from \"immer\";\nimport { AnyAction } from \"redux\";\n\nimport { AuthState } from \"@/redux/interface\";\nimport *"
},
{
"path": "src/redux/modules/breadcrumb/action.ts",
"chars": 212,
"preview": "import * as types from \"@/redux/mutation-types\";\n\n// * setBreadcrumbList\nexport const setBreadcrumbList = (breadcrumbLis"
},
{
"path": "src/redux/modules/breadcrumb/reducer.ts",
"chars": 571,
"preview": "import produce from \"immer\";\nimport { AnyAction } from \"redux\";\n\nimport { BreadcrumbState } from \"@/redux/interface\";\nim"
},
{
"path": "src/redux/modules/disc/action.ts",
"chars": 1168,
"preview": "import { toPairs } from \"lodash\";\nimport { Dispatch } from \"redux\";\n\nimport { getDiscListApi } from \"@/api/modules/commo"
},
{
"path": "src/redux/modules/disc/reducer.ts",
"chars": 489,
"preview": "import produce from \"immer\";\nimport { AnyAction } from \"redux\";\n\nimport { DiscState } from \"@/redux/interface\";\nimport *"
},
{
"path": "src/redux/modules/global/action.ts",
"chars": 622,
"preview": "import { ThemeConfigProp } from \"@/redux/interface/index\";\nimport * as types from \"@/redux/mutation-types\";\nimport { Map"
},
{
"path": "src/redux/modules/global/reducer.ts",
"chars": 1061,
"preview": "import produce from \"immer\";\nimport { AnyAction } from \"redux\";\n\nimport { GlobalState } from \"@/redux/interface\";\nimport"
},
{
"path": "src/redux/modules/menu/action.ts",
"chars": 1185,
"preview": "import { Dispatch } from \"react\";\n\nimport { getMenuList } from \"@/api/modules/login\";\nimport * as types from \"@/redux/mu"
},
{
"path": "src/redux/modules/menu/reducer.ts",
"chars": 608,
"preview": "import produce from \"immer\";\nimport { AnyAction } from \"redux\";\n\nimport { MenuState } from \"@/redux/interface\";\nimport *"
},
{
"path": "src/redux/modules/tabs/action.ts",
"chars": 295,
"preview": "import * as types from \"@/redux/mutation-types\";\n\n// * setTabsList\nexport const setTabsList = (tabsList: Menu.MenuOption"
},
{
"path": "src/redux/modules/tabs/reducer.ts",
"chars": 725,
"preview": "import produce from \"immer\";\nimport { AnyAction } from \"redux\";\n\nimport { HOME_URL } from \"@/config/config\";\nimport { Ta"
},
{
"path": "src/redux/mutation-types.ts",
"chars": 773,
"preview": "// 更新 menu 折叠状态\nexport const UPDATE_COLLAPSE = \"UPDATE_ASIDE_COLLAPSE\";\n// 设置 menuList\nexport const SET_MENU_LIST = \"SET"
},
{
"path": "src/routers/constant.tsx",
"chars": 123,
"preview": "import Layout from \"@/layouts/index\";\n/**\n * @description: default layout\n */\nexport const LayoutIndex = () => <Layout /"
},
{
"path": "src/routers/index.tsx",
"chars": 1011,
"preview": "import { Navigate, useRoutes } from \"react-router-dom\";\n\nimport { RouteObject } from \"@/routers/interface\";\nimport Login"
},
{
"path": "src/routers/interface/index.ts",
"chars": 293,
"preview": "export interface MetaProps {\n\tkeepAlive?: boolean;\n\trequiresAuth?: boolean;\n\ttitle: string;\n\tkey?: string;\n}\n\nexport int"
},
{
"path": "src/routers/modules/aiConfig.tsx",
"chars": 412,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport AiConfigPage"
},
{
"path": "src/routers/modules/article.tsx",
"chars": 1113,
"preview": "import React from \"react\";\n\nimport { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/int"
},
{
"path": "src/routers/modules/author.tsx",
"chars": 1106,
"preview": "import React from \"react\";\n\nimport { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/int"
},
{
"path": "src/routers/modules/category.tsx",
"chars": 406,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Sort from \"@"
},
{
"path": "src/routers/modules/column.tsx",
"chars": 1354,
"preview": "import React from \"react\";\n\nimport { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/int"
},
{
"path": "src/routers/modules/config.tsx",
"chars": 412,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Banner from "
},
{
"path": "src/routers/modules/error.tsx",
"chars": 742,
"preview": "import React from \"react\";\n\nimport { RouteObject } from \"@/routers/interface\";\nimport lazyLoad from \"@/routers/utils/laz"
},
{
"path": "src/routers/modules/global.tsx",
"chars": 412,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Banner from "
},
{
"path": "src/routers/modules/home.tsx",
"chars": 406,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Home from \"@"
},
{
"path": "src/routers/modules/resume.tsx",
"chars": 408,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Label from \""
},
{
"path": "src/routers/modules/sensitive.tsx",
"chars": 408,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Sensitive fr"
},
{
"path": "src/routers/modules/statistics.tsx",
"chars": 442,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Statistics f"
},
{
"path": "src/routers/modules/tag.tsx",
"chars": 399,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport Label from \""
},
{
"path": "src/routers/modules/wxMenu.tsx",
"chars": 396,
"preview": "import { LayoutIndex } from \"@/routers/constant\";\nimport { RouteObject } from \"@/routers/interface\";\nimport WxMenuPage f"
},
{
"path": "src/routers/route.tsx",
"chars": 2234,
"preview": "/* eslint-disable prettier/prettier */\nimport {\n\tAlertOutlined,\n\tApiOutlined,\n\tBarsOutlined,\n\tCalendarOutlined,\n\tFileAdd"
},
{
"path": "src/routers/utils/authRouter.tsx",
"chars": 1327,
"preview": "// 路由权限控制\n\nimport { Navigate, useLocation } from \"react-router-dom\";\n\nimport { AxiosCanceler } from \"@/api/helper/axiosC"
},
{
"path": "src/routers/utils/lazyLoad.tsx",
"chars": 497,
"preview": "import React, { Suspense } from \"react\";\nimport { Spin } from \"antd\";\n\n/**\n * @description 路由懒加载\n * @param {Element} Com"
},
{
"path": "src/styles/common.less",
"chars": 1510,
"preview": "/* 常用 flex */\n.flx-center {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n.flx-justify-between {\n\tdi"
},
{
"path": "src/styles/reset.less",
"chars": 1076,
"preview": "/* Reset style sheet */\nhtml,\nbody,\ndiv,\nspan,\napplet,\nobject,\niframe,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\np,\nblockquote,\npre,\na,\nab"
},
{
"path": "src/styles/theme/theme-dark.less",
"chars": 2156,
"preview": "\n\n/* 自定义 antd 暗黑模式样式 */\n@dark-main-bg-color: #141414;\n@dark-bg-color: #1f1f1f;\n@dark-border-color: #414243;\n@dark-text-c"
},
{
"path": "src/styles/theme/theme-default.less",
"chars": 2272,
"preview": "\n\n/* 自定义 antd 默认样式 */\n@light-bg-color: #ffffff;\n@light-main-bg-color: #f0f2f5;\n@light-border-color: #e4e7ed;\n@light-bord"
},
{
"path": "src/styles/var.less",
"chars": 54,
"preview": "/* Global definition less */\n@primary-color: #1890ff;\n"
},
{
"path": "src/typings/common.ts",
"chars": 50,
"preview": "export interface MapItem {\n\t[key: string]: any;\n}\n"
},
{
"path": "src/typings/global.d.ts",
"chars": 773,
"preview": "// * Menu\ndeclare namespace Menu {\n\tinterface MenuOptions {\n\t\tpath: string;\n\t\ttitle: string;\n\t\ticon?: string;\n\t\tisLink?:"
},
{
"path": "src/typings/plugins.d.ts",
"chars": 115,
"preview": "declare module \"qs\";\ndeclare module \"nprogress\";\ndeclare module \"js-md5\";\ndeclare module \"react-transition-group\";\n"
},
{
"path": "src/typings/window.d.ts",
"chars": 196,
"preview": "// * global\ndeclare global {\n\tinterface Window {\n\t\t__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;\n\t}\n\tinterface Navigator {\n"
},
{
"path": "src/utils/getEnv.ts",
"chars": 1816,
"preview": "import dotenv from \"dotenv\";\nimport fs from \"fs\";\nimport path from \"path\";\n\nexport function isDevFn(mode: string): boole"
},
{
"path": "src/utils/is/index.ts",
"chars": 2458,
"preview": "const toString = Object.prototype.toString;\n\n/**\n * @description: 判断值是否未某个类型\n */\nexport function is(val: unknown, type: "
},
{
"path": "src/utils/util.ts",
"chars": 4890,
"preview": "import { RouteObject } from \"@/routers/interface\";\n\n/**\n * @description 获取当前域名\n */\nexport const baseDomain = import.meta"
},
{
"path": "src/views/aiConfig/index.scss",
"chars": 1242,
"preview": ".ai-config-page {\n\t&__toolbar {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: space-between;\n\t\tgap: 12px;\n\t"
},
{
"path": "src/views/aiConfig/index.tsx",
"chars": 14236,
"preview": "import { FC, useEffect, useMemo, useState } from \"react\";\nimport { ReloadOutlined, SaveOutlined } from \"@ant-design/icon"
},
{
"path": "src/views/article/components/debounceselect/index.scss",
"chars": 0,
"preview": ""
},
{
"path": "src/views/article/components/debounceselect/index.tsx",
"chars": 5010,
"preview": "/* eslint-disable prettier/prettier */\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport"
},
{
"path": "src/views/article/components/search/index.scss",
"chars": 332,
"preview": ".article-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;\n }\n\n &__s"
},
{
"path": "src/views/article/components/search/index.tsx",
"chars": 3341,
"preview": "/* eslint-disable prettier/prettier */\nimport React, { FC } from \"react\";\nimport { useNavigate } from \"react-router-dom\""
},
{
"path": "src/views/article/edit/index.scss",
"chars": 1363,
"preview": ".bytemd {\n height: calc(100vh - 220px);\n}\n\n/* CustomRadioGroup.css */\n.custom-radio-group .ant-radio-button-wrapper {\n "
},
{
"path": "src/views/article/edit/index.tsx",
"chars": 71553,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nim"
},
{
"path": "src/views/article/edit/search/index.scss",
"chars": 337,
"preview": ".article-edit-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;\n }\n\n "
},
{
"path": "src/views/article/edit/search/index.tsx",
"chars": 1826,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC } from \"react\";\nimport { ArrowLeftOutlined, FileTextOutlined, FileWor"
},
{
"path": "src/views/article/list/index.scss",
"chars": 363,
"preview": ".cell-text {\n /* stylelint-disable-next-line value-no-vendor-prefix */\n display: -webkit-box;\n -webkit-box-orient: ve"
},
{
"path": "src/views/article/list/index.tsx",
"chars": 16138,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useState } from \"react\";\nimport { connect } "
},
{
"path": "src/views/author/loginAudit/index.scss",
"chars": 653,
"preview": ".login-audit {\n\t&__meta {\n\t\tmargin-bottom: 16px;\n\t}\n\n\t&__card {\n\t\tmargin-bottom: 16px;\n\t\tborder-radius: 16px;\n\t\tbox-shad"
},
{
"path": "src/views/author/loginAudit/index.tsx",
"chars": 17290,
"preview": "import { FC, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { SearchOutlined } from \"@ant-desi"
},
{
"path": "src/views/author/whitelist/index.scss",
"chars": 355,
"preview": ".author-whitelist-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;\n "
},
{
"path": "src/views/author/whitelist/index.tsx",
"chars": 8285,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useRef, useState } from \"react\";\nimport High"
},
{
"path": "src/views/author/zsxqlist/components/search/index.scss",
"chars": 2102,
"preview": ".zsxq-white-list-search {\n\tmargin-bottom: 16px;\n\n\t&__wrap {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\tpadding"
},
{
"path": "src/views/author/zsxqlist/components/search/index.tsx",
"chars": 2910,
"preview": "/* eslint-disable prettier/prettier */\nimport React, { FC } from \"react\";\nimport { SearchOutlined } from \"@ant-design/ic"
},
{
"path": "src/views/author/zsxqlist/index.scss",
"chars": 4007,
"preview": "// 移动端卡片列表(默认隐藏)\n.mobile-card-list {\n\tdisplay: none;\n\tmax-width: 100%;\n\toverflow-x: hidden;\n}\n\n// 桌面端表格(默认显示)\n.desktop-t"
},
{
"path": "src/views/author/zsxqlist/index.tsx",
"chars": 17195,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useState } from \"react\";\nimport { connect } "
},
{
"path": "src/views/category/components/search/index.scss",
"chars": 324,
"preview": ".category-search {\n\tmargin-bottom: 16px;\n\n\t&__wrap {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\tpadding-left: "
},
{
"path": "src/views/category/components/search/index.tsx",
"chars": 1457,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC } from \"react\";\nimport { PlusOutlined, SearchOutlined } from \"@ant-de"
},
{
"path": "src/views/category/index.css",
"chars": 26,
"preview": ".sort {\n height: 100%;\n}\n"
},
{
"path": "src/views/category/index.scss",
"chars": 1,
"preview": "\n"
},
{
"path": "src/views/category/index.tsx",
"chars": 7034,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useState } from \"react\";\nimport { connect } "
},
{
"path": "src/views/column/article/components/DatePicker.tsx",
"chars": 242,
"preview": "import { DatePicker } from \"antd\";\nimport type { Dayjs } from \"dayjs\";\nimport dayjsGenerateConfig from \"rc-picker/lib/ge"
},
{
"path": "src/views/column/article/components/debounceselect/DebounceSelect.tsx",
"chars": 1989,
"preview": "import { useMemo, useRef, useState } from \"react\";\nimport { Select, SelectProps, Spin } from \"antd\";\nimport { debounce }"
},
{
"path": "src/views/column/article/components/debounceselect/index.scss",
"chars": 0,
"preview": ""
},
{
"path": "src/views/column/article/components/search/index.scss",
"chars": 327,
"preview": ".column-article-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;\n "
},
{
"path": "src/views/column/article/components/search/index.tsx",
"chars": 2222,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC } from \"react\";\nimport { PlusOutlined, SearchOutlined } from \"@ant-de"
},
{
"path": "src/views/column/article/components/tableselect/TableSelect.tsx",
"chars": 6218,
"preview": "/* eslint-disable prettier/prettier */\nimport React, { FC, useEffect, useState } from \"react\";\nimport { PoweroffOutlined"
},
{
"path": "src/views/column/article/index.scss",
"chars": 750,
"preview": "// 增加封面图的样式\n.cover {\n\twidth: 70px;\n\t// height = width * 156 / 110\n\theight: 100px;\n\tbackground-size: cover;\n\tbackground-p"
},
{
"path": "src/views/column/article/index.tsx",
"chars": 10605,
"preview": "/* eslint-disable react/jsx-no-comment-textnodes */\n/* eslint-disable prettier/prettier */\nimport { FC, useCallback, use"
},
{
"path": "src/views/column/setting/articlesort/index.scss",
"chars": 1522,
"preview": ".rotated-icon {\n\ttransform: rotate(90deg); /* 或任何您需要的角度 */\n}\n\n.group-tree-container {\n\tmargin: 20px;\n\tpadding: 20px;\n\tba"
},
{
"path": "src/views/column/setting/articlesort/index.tsx",
"chars": 25050,
"preview": "/* eslint-disable react/jsx-no-comment-textnodes */\n/* eslint-disable prettier/prettier */\nimport { FC, useCallback, use"
},
{
"path": "src/views/column/setting/articlesort/search.scss",
"chars": 308,
"preview": ".column-article-sort-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;"
},
{
"path": "src/views/column/setting/articlesort/search.tsx",
"chars": 1613,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC } from \"react\";\nimport { ArrowLeftOutlined, PlusOutlined, SearchOutli"
},
{
"path": "src/views/column/setting/components/authorselect/index.scss",
"chars": 100,
"preview": "// 增加 avatar 的样式\n.avatar {\n\twidth: 30px;\n\theight: 30px;\n\tborder-radius: 50%;\n\tmargin-right: 16px;\n}\n"
},
{
"path": "src/views/column/setting/components/authorselect/index.tsx",
"chars": 2911,
"preview": "/* eslint-disable prettier/prettier */\n/* eslint-disable react/prop-types */\n// 这是一个上传图片的组件,使用的是antd的Upload组件\n\nimport { "
},
{
"path": "src/views/column/setting/components/imgupload/index.tsx",
"chars": 2357,
"preview": "/* eslint-disable react/prop-types */\n// 这是一个上传图片的组件,使用的是antd的Upload组件\n\nimport { FC } from \"react\";\nimport { UploadOutli"
},
{
"path": "src/views/column/setting/components/search/index.scss",
"chars": 353,
"preview": ".column-setting-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;\n "
},
{
"path": "src/views/column/setting/components/search/index.tsx",
"chars": 1504,
"preview": "/* eslint-disable prettier/prettier */\nimport React, { FC } from \"react\";\nimport { PlusOutlined, SearchOutlined } from \""
},
{
"path": "src/views/column/setting/groups/index.scss",
"chars": 815,
"preview": ".rotated-icon {\n\ttransform: rotate(90deg); /* 或任何您需要的角度 */\n}\n\n.group-tree {\n\tmargin: 20px;\n\tpadding: 20px;\n\tbackground-c"
},
{
"path": "src/views/column/setting/groups/index.tsx",
"chars": 15579,
"preview": "/* eslint-disable react/jsx-no-comment-textnodes */\n/* eslint-disable prettier/prettier */\nimport { FC, useCallback, use"
},
{
"path": "src/views/column/setting/index.css",
"chars": 26,
"preview": ".sort {\n height: 100%;\n}\n"
},
{
"path": "src/views/column/setting/index.scss",
"chars": 315,
"preview": ".cell-text {\n overflow: hidden;\n text-overflow: ellipsis;\n display: box;\n -webkit-line-clamp: 2; /* 控制显示的行数 */\n -we"
},
{
"path": "src/views/column/setting/index.tsx",
"chars": 15715,
"preview": "import { FC, useCallback, useEffect, useState } from \"react\";\nimport { connect } from \"react-redux\";\nimport { DeleteOutl"
},
{
"path": "src/views/comment/components/search/index.scss",
"chars": 291,
"preview": ".comment-search {\n\tmargin-bottom: 16px;\n\n\t&__wrap {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t&__search {\n"
},
{
"path": "src/views/comment/components/search/index.tsx",
"chars": 2427,
"preview": "import { FC } from \"react\";\nimport { PlusCircleOutlined, SearchOutlined } from \"@ant-design/icons\";\nimport { Button, Inp"
},
{
"path": "src/views/comment/index.scss",
"chars": 1057,
"preview": ".comment-page {\n\t&__table {\n\t\t.cell-link {\n\t\t\tdisplay: inline-block;\n\t\t\tmax-width: 260px;\n\t\t\toverflow: hidden;\n\t\t\ttext-o"
},
{
"path": "src/views/comment/index.tsx",
"chars": 11256,
"preview": "import { FC, useCallback, useEffect, useMemo, useState } from \"react\";\nimport { DeleteOutlined, EditOutlined, MessageOut"
},
{
"path": "src/views/config/components/imgupload/ImgCropUpload.tsx",
"chars": 1569,
"preview": "/* eslint-disable prettier/prettier */\nimport React, { useState } from \"react\";\nimport { Upload } from \"antd\";\nimport ty"
},
{
"path": "src/views/config/components/imgupload/index.tsx",
"chars": 2417,
"preview": "/* eslint-disable react/prop-types */\n// 这是一个上传图片的组件,使用的是antd的Upload组件\n\nimport { FC } from \"react\";\nimport { UploadOutli"
},
{
"path": "src/views/config/components/search/index.scss",
"chars": 322,
"preview": ".config-search {\n\tmargin-bottom: 16px;\n\n\t&__wrap {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t\tpadding-left: 48"
},
{
"path": "src/views/config/components/search/index.tsx",
"chars": 2119,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC } from \"react\";\nimport { PlusOutlined, ReloadOutlined, SearchOutlined"
},
{
"path": "src/views/config/index.css",
"chars": 26,
"preview": ".sort {\n height: 100%;\n}\n"
},
{
"path": "src/views/config/index.scss",
"chars": 49,
"preview": "// 没有边距\n.container {\n\tpadding: 0;\n\tmargin: 0;\n}\n\n"
},
{
"path": "src/views/config/index.tsx",
"chars": 12287,
"preview": "/* eslint-disable react/jsx-no-undef */\n/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useS"
},
{
"path": "src/views/global/components/search/index.scss",
"chars": 352,
"preview": ".global-config-search {\n margin-bottom: 16px;\n\n &__wrap {\n display: flex;\n justify-content: space-between;\n p"
},
{
"path": "src/views/global/components/search/index.tsx",
"chars": 1835,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC } from \"react\";\nimport { PlusOutlined, SearchOutlined } from \"@ant-de"
},
{
"path": "src/views/global/index.scss",
"chars": 0,
"preview": ""
},
{
"path": "src/views/global/index.tsx",
"chars": 7029,
"preview": "/* eslint-disable prettier/prettier */\nimport { FC, useCallback, useEffect, useState } from \"react\";\nimport { connect } "
}
]
// ... and 25 more files (download for full content)
About this extraction
This page contains the full source code of the itwanger/paicoding-admin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 225 files (591.4 KB), approximately 188.0k tokens, and a symbol index with 305 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.