├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── biome.json ├── http.md ├── package.json ├── packages ├── client │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.png │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── CharacterForm │ │ │ │ └── index.tsx │ │ │ ├── Layout │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ ├── ListForm │ │ │ │ └── index.tsx │ │ │ ├── Loading │ │ │ │ └── index.tsx │ │ │ └── result │ │ │ │ └── error.tsx │ │ ├── config.ts │ │ ├── http │ │ │ ├── api.ts │ │ │ ├── http.ts │ │ │ └── index.ts │ │ ├── i18n │ │ │ ├── index.ts │ │ │ └── locales.ts │ │ ├── main.tsx │ │ ├── routes │ │ │ └── index.tsx │ │ ├── store │ │ │ ├── adminReducer.ts │ │ │ ├── index.ts │ │ │ └── settingsReducer.ts │ │ ├── styles │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── utils │ │ │ └── index.ts │ │ ├── views │ │ │ ├── 404 │ │ │ │ └── index.tsx │ │ │ ├── Admin │ │ │ │ ├── Create │ │ │ │ │ └── index.tsx │ │ │ │ ├── Edit │ │ │ │ │ └── index.tsx │ │ │ │ ├── Imgs │ │ │ │ │ └── index.tsx │ │ │ │ ├── List │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.css │ │ │ │ ├── Login │ │ │ │ │ └── index.tsx │ │ │ │ ├── Password │ │ │ │ │ └── index.tsx │ │ │ │ ├── Settings │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Character │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ ├── Home │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── Photos │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── common │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── schema │ │ │ └── index.ts │ │ └── types │ │ │ ├── api.ts │ │ │ └── data.ts │ └── tsconfig.json ├── core │ ├── README.md │ ├── data.sql │ ├── package.json │ ├── prisma │ │ └── schema.prisma │ ├── public │ │ ├── favicon.png │ │ └── imgs │ │ │ └── .gitkeep │ ├── src │ │ ├── app │ │ │ ├── error.ts │ │ │ └── index.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── container │ │ │ ├── index.ts │ │ │ ├── modules.ts │ │ │ └── symbols.ts │ │ ├── index.ts │ │ ├── router │ │ │ ├── controller │ │ │ │ ├── character.controller.ts │ │ │ │ ├── collection.controller.ts │ │ │ │ ├── series.controller.ts │ │ │ │ ├── settings.controller.ts │ │ │ │ └── tag.controller.ts │ │ │ └── service │ │ │ │ ├── character.service.ts │ │ │ │ ├── collection.service.ts │ │ │ │ ├── series.service.ts │ │ │ │ ├── settings.service.ts │ │ │ │ └── tag.service.ts │ │ └── utils │ │ │ ├── auth │ │ │ └── index.ts │ │ │ ├── bot │ │ │ └── index.ts │ │ │ ├── db │ │ │ └── index.ts │ │ │ └── logger │ │ │ └── index.ts │ └── tsconfig.json └── data │ ├── AliceBedford.json │ ├── ArimuraRomi.json │ ├── HonmaMisuzu.json │ ├── NatsumeAi.json │ ├── screenboot-0.png │ ├── screenboot-1.png │ └── screenboot-2.png ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── request.http ├── scripts └── release.ts ├── tsconfig.base.json ├── tsconfig.json └── tsup.config.ts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | docs: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v2 25 | with: 26 | version: 8 27 | run_install: true 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: 18 33 | cache: pnpm 34 | 35 | - name: Build VitePress site 36 | run: pnpm build 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | lib 4 | .husky/_ 5 | 6 | .vscode/* 7 | .vs/* 8 | !.vscode/extensions.json 9 | 10 | *.tgz 11 | *.log 12 | tsconfig.tsbuildinfo 13 | 14 | .env 15 | 16 | migrations 17 | 18 | packages/core/public/imgs/**/*.png 19 | packages/core/public/imgs/**/*.jpg 20 | packages/core/public/imgs/**/*.jpeg 21 | 22 | packages/core/public/assets/* 23 | packages/core/public/*.html -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.1](https://github.com/BIYUEHU/moehub/compare/v1.0.0...v1.0.1) (2024-08-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * birthday reminder and login reloction ([b82ee8a](https://github.com/BIYUEHU/moehub/commit/b82ee8ac13290db31bb395098ee44705e118f8fe)) 7 | 8 | 9 | 10 | # [1.0.0](https://github.com/BIYUEHU/moehub/compare/82fa140206b09213840d21d5d95df57217693410...v1.0.0) (2024-08-15) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * birthday and tags commit bug ([d65a65d](https://github.com/BIYUEHU/moehub/commit/d65a65dd2792d489e149684d2752775a387bed1e)) 16 | * character submit problems ([c745f01](https://github.com/BIYUEHU/moehub/commit/c745f01a41fd5b0a68ebfe7cb4dcc8532a6930b7)) 17 | * some styles ([efb3c94](https://github.com/BIYUEHU/moehub/commit/efb3c94c0caaf7daa467afd107261856166416c1)) 18 | 19 | 20 | ### Features 21 | 22 | * auth service, password 404 pages ([35cedff](https://github.com/BIYUEHU/moehub/commit/35cedff71ded2c0682cf3c651161b355665f2ff5)) 23 | * bot module, redux store and settings page ([2abd7f0](https://github.com/BIYUEHU/moehub/commit/2abd7f05aef25268a6650f1e018894a9c49db15c)) 24 | * character page ([99c7339](https://github.com/BIYUEHU/moehub/commit/99c733964918db7b7827ec835b6ada9c45300446)) 25 | * frontend web base on react and vite ([891bf17](https://github.com/BIYUEHU/moehub/commit/891bf17920112c9e5049ba43ec03a46bf63a153c)) 26 | * hash router ([16465d1](https://github.com/BIYUEHU/moehub/commit/16465d1d37936e6faf675187f4414c7a2d335aa6)) 27 | * Ioc, Di, structure and character api ([3144781](https://github.com/BIYUEHU/moehub/commit/314478181ef82b663f50e4d3e2399fd8314fd77d)) 28 | * login,admin,edit,create pages ([fb64472](https://github.com/BIYUEHU/moehub/commit/fb64472026d9bfb664fbd38c6e27c036f340512e)) 29 | * login,admin,edit,create pages ([e52fdba](https://github.com/BIYUEHU/moehub/commit/e52fdba4befc4adb85da27a8148fa2792d97b203)) 30 | * photos and i18n ([bd9d8f1](https://github.com/BIYUEHU/moehub/commit/bd9d8f1e1ef234fdcf390c215c19a8973357c3f3)) 31 | * photos and images ([6ea2083](https://github.com/BIYUEHU/moehub/commit/6ea208388388cfa399f39942281f86408e8c2b25)) 32 | * settings and pages layout change ([ff88eec](https://github.com/BIYUEHU/moehub/commit/ff88eec67e011d7ad879a8c9e4f6ed1ebd4772d5)) 33 | * setup prsima,database and koajs ([82fa140](https://github.com/BIYUEHU/moehub/commit/82fa140206b09213840d21d5d95df57217693410)) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/core/README.md -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [ ] Static webpage version 4 | - [ ] Supports character collections 5 | - [ ] Support next.js version 6 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "space", 7 | "indentWidth": 2, 8 | "lineEnding": "lf", 9 | "lineWidth": 120, 10 | "attributePosition": "auto" 11 | }, 12 | "linter": { 13 | "enabled": true, 14 | "rules": {}, 15 | "ignore": ["**/*.js", "**/*.d.ts"] 16 | }, 17 | "javascript": { 18 | "formatter": { 19 | "enabled": true, 20 | "jsxQuoteStyle": "double", 21 | "quoteProperties": "asNeeded", 22 | "trailingCommas": "none", 23 | "semicolons": "asNeeded", 24 | "arrowParentheses": "always", 25 | "bracketSpacing": true, 26 | "bracketSameLine": false, 27 | "quoteStyle": "single", 28 | "attributePosition": "auto" 29 | }, 30 | "parser": { 31 | "unsafeParameterDecoratorsEnabled": true 32 | } 33 | }, 34 | "overrides": [ 35 | { 36 | "include": ["*.ts", "*.tsx"] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /http.md: -------------------------------------------------------------------------------- 1 | # HTTP状态码和请求方式 2 | 3 | HTTP状态码提供了关于请求结果的重要信息。以下是一些常见HTTP状态码及其使用场景和请求方式的详细说明: 4 | 5 | ## 1xx - 信息性状态码 6 | 7 | - **100 Continue**:表明到目前为止所有都是正常的,客户端应该继续其请求。通常用于发送大文件时,客户端在发送完请求头后会等待此状态码,以确认服务器准备好接收请求体。 8 | 9 | ## 2xx - 成功状态码 10 | 11 | - **200 OK**:请求已成功,且请求的网页已返回。 12 | - **201 Created**:请求成功,并且服务器创建了新的资源。通常用于POST请求后,资源被成功创建。 13 | - **202 Accepted**:服务器已接受请求,但尚未处理。这表示异步操作已经开始,但尚未完成。 14 | - **204 No Content**:服务器成功处理了请求,但没有返回任何内容。这通常用于DELETE请求。 15 | 16 | ## 3xx - 重定向状态码 17 | 18 | - **301 Moved Permanently**:请求的网页已永久移动到新位置。服务器返回此响应时,会提供新的URL。 19 | - **302 Found**:请求的网页临时移动到另一个URL。与301不同,302表示资源的移动是临时的。 20 | - **303 See Other**:服务器指示请求应该使用GET方法重定向到另一个URL。 21 | - **304 Not Modified**:自从上次请求后,请求的网页未修改过。这是缓存相关的响应,用于减少不必要的数据传输。 22 | 23 | ## 4xx - 客户端错误状态码 24 | 25 | - **400 Bad Request**:请求有语法错误或请求无法处理。 26 | - **401 Unauthorized**:请求需要用户的身份验证。通常需要提供认证信息,如用户名和密码。 27 | - **403 Forbidden**:服务器理解请求客户端的请求,但是拒绝执行此请求。这表示访问被禁止。 28 | - **404 Not Found**:服务器找不到请求的网页。 29 | - **405 Method Not Allowed**:请求行中指定的请求方法不能被用于请求网页。 30 | - **406 Not Acceptable**:服务器无法根据客户端请求的内容特性完成请求。 31 | - **407 Proxy Authentication Required**:类似于401,但是请求必须通过代理服务器进行认证。 32 | - **408 Request Timeout**:请求超时,客户端没有在服务器准备等待的时间内完成请求。 33 | - **409 Conflict**:服务器在尝试处理请求时遇到了冲突。 34 | - **410 Gone**:请求的资源不再可用,且没有任何重定向地址。 35 | - **411 Length Required**:服务器拒绝在没有定义Content-Length头的情况下处理请求。 36 | - **412 Precondition Failed**:请求头中设置的条件失败。 37 | - **413 Payload Too Large**:请求体过大,超出了服务器愿意或能够处理的范围。 38 | - **414 URI Too Long**:请求的URI过长。 39 | - **415 Unsupported Media Type**:请求的媒体类型不被服务器支持。 40 | - **416 Range Not Satisfiable**:请求的范围无法满足。 41 | - **417 Expectation Failed**:服务器无法满足请求头Expect中指定的期望值。 42 | - **422 Unprocessable Entity**:请求格式正确,但服务器无法理解请求体中的语义错误。 43 | - **429 Too Many Requests**:用户发送了太多的请求,超出了服务器的处理能力。 44 | 45 | ## 5xx - 服务器错误状态码 46 | 47 | - **500 Internal Server Error**:服务器遇到了一个未曾预料的状况,无法完成对请求的处理。 48 | - **501 Not Implemented**:服务器不支持请求的功能,无法完成请求。 49 | - **502 Bad Gateway**:服务器作为网关或代理,收到了一个无效的响应。 50 | - **503 Service Unavailable**:服务器目前无法使用,可能是由于超载或停机维护。 51 | - **504 Gateway Timeout**:服务器作为网关或代理,但是没有及时从上游服务器接收请求。 52 | - **505 HTTP Version Not Supported**:服务器不支持请求中使用的HTTP协议版本。 53 | - **511 Network Authentication Required**:表明客户端需要进行网络认证。 54 | 55 | ## 请求方式 56 | 57 | - **GET**:请求指定的资源。请求可以被缓存,且只有幀定的URL参数。 58 | - **POST**:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。 59 | - **PUT**:请求将指定资源更新为请求体中给出的表示。 60 | - **DELETE**:请求删除指定的资源。 61 | - **HEAD**:与GET方法相同,但是不返回请求体,只返回头部。 62 | - **PATCH**:对资源进行部分修改。 63 | - **OPTIONS**:返回服务器支持的通信选项。 64 | 65 | > 了解这些状态码和请求方式可以帮助开发者更好地设计和实现HTTP API,同时也能更准确地处理客户端和服务器之间的交互。 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moehub/root", 3 | "description": "Anime character collection gallery", 4 | "version": "1.0.1", 5 | "private": true, 6 | "license": "GPL-3.0", 7 | "author": "Romi ", 8 | "scripts": { 9 | "serve": "pnpm core serve", 10 | "core": "pnpm --filter moehub", 11 | "client": "pnpm --filter @moehub/client", 12 | "common": "pnpm --filter @moehub/common", 13 | "dev:core": "nodemon --watch", 14 | "dev:client": "pnpm --filter @moehub/client dev", 15 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 16 | "release": "tsx scripts/release", 17 | "build": "pnpm -r build" 18 | }, 19 | "devDependencies": { 20 | "@biomejs/biome": "^1.8.3", 21 | "@types/shelljs": "^0.8.15", 22 | "conventional-changelog-cli": "^4.1.0", 23 | "nodemon": "^3.1.3", 24 | "prettier": "^3.3.0", 25 | "tsup": "^8.2.4", 26 | "tsx": "^4.11.2", 27 | "typescript": "5.5.3" 28 | }, 29 | "packageManager": "pnpm@8.7.4+", 30 | "engines": { 31 | "node": ">=17.9.0" 32 | }, 33 | "nodemonConfig": { 34 | "exec": "pnpm common exec tsup && tsx packages/core/src", 35 | "ext": "ts", 36 | "ignore": ["packages/common", "packages/client"] 37 | }, 38 | "dependencies": { 39 | "@types/node": "^20.14.15", 40 | "shelljs": "^0.8.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | -------------------------------------------------------------------------------- /packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | MoeHub 15 | 19 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@moehub/client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "pnpm --filter @moehub/common build && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "files": ["package.json"], 12 | "dependencies": { 13 | "@ant-design/icons": "^5.3.7", 14 | "@kotori-bot/core": "1.6.0-rc.1", 15 | "@kotori-bot/i18n": "^1.3.2", 16 | "@moehub/common": "workspace:^", 17 | "@reduxjs/toolkit": "^2.2.6", 18 | "antd": "^5.18.1", 19 | "axios": "^1.7.2", 20 | "dayjs": "^1.11.11", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-helmet-async": "^2.0.5", 24 | "react-masonry-css": "^1.0.16", 25 | "react-redux": "^9.1.2", 26 | "react-router-dom": "^6.23.1", 27 | "redux-persist": "^6.0.0", 28 | "swr": "^2.2.5", 29 | "tailwindcss": "^3.4.4" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^18.2.66", 33 | "@types/react-dom": "^18.2.22", 34 | "@vitejs/plugin-react-swc": "^3.5.0", 35 | "typescript": "^5.5.3", 36 | "vite": "^5.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/client/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BIYUEHU/moehub/516715a7d823a3d6ad0982fb259f89cd4abc034f/packages/client/public/favicon.png -------------------------------------------------------------------------------- /packages/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { HashRouter as Router, Route, Routes } from 'react-router-dom' 2 | import Layout from '@/components/Layout' 3 | import routes from './routes' 4 | import { useDispatch } from 'react-redux' 5 | import useSWR from 'swr' 6 | import { getSettings } from './http' 7 | import { useEffect } from 'react' 8 | import ErrorResult from './components/result/error' 9 | import Loading from './components/Loading' 10 | import { loadSettings } from './store/settingsReducer' 11 | import { getLanguage } from './store/adminReducer' 12 | import store from './store' 13 | import i18n from './i18n' 14 | 15 | function App() { 16 | const language = getLanguage(store.getState()) 17 | 18 | i18n.set(language) 19 | 20 | const dispatch = useDispatch() 21 | const { data, error } = useSWR('/api/settings', getSettings) 22 | 23 | useEffect(() => { 24 | if (data) dispatch(loadSettings(data)) 25 | }, [data, dispatch]) 26 | 27 | if (error) return 28 | if (!data) return 29 | 30 | return ( 31 | 32 | 33 | {routes.map((route) => ( 34 | } 38 | /> 39 | ))} 40 | 41 | 42 | ) 43 | } 44 | 45 | export default App 46 | -------------------------------------------------------------------------------- /packages/client/src/components/CharacterForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ColorPicker, DatePicker, Form, Input, InputNumber, Radio, Select, Space, Switch, Tabs } from 'antd' 2 | import type { MoehubDataCharacter, MoehubDataCharacterSubmit } from '@moehub/common' 3 | import dayjs from 'dayjs' 4 | import { getTags } from '@/http' 5 | import useSWR from 'swr' 6 | import { useEffect } from 'react' 7 | import ListForm from '../ListForm' 8 | import { t } from '@/i18n' 9 | 10 | export type MoehubDataCharacterHandle = Omit & { 11 | birthday?: dayjs.Dayjs 12 | color?: { toHex(): string; cleared: false | string } 13 | } 14 | 15 | export function handleMoehubDataCharacter(values: MoehubDataCharacterHandle): MoehubDataCharacterSubmit { 16 | // console.log(values.color) 17 | return { 18 | ...values, 19 | color: values.color 20 | ? typeof values.color === 'string' 21 | ? values.color 22 | : values.color.cleared === false 23 | ? values.color.toHex() 24 | : '' 25 | : undefined, 26 | birthday: values.birthday ? new Date(values.birthday.toString()).getTime() : undefined 27 | } 28 | } 29 | 30 | interface CharacterFormProps { 31 | onSubmit: (values: MoehubDataCharacterHandle) => void 32 | data?: MoehubDataCharacter 33 | } 34 | 35 | const items = (isDisabled: boolean, tags?: { label: string; value: string }[]) => [ 36 | { 37 | key: '1', 38 | label: t`com.characterForm.label.1`, 39 | children: ( 40 | <> 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {t`com.characterForm.gender.male`} 50 | {t`com.characterForm.gender.female`} 51 | {t`com.characterForm.gender.other`} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {t`com.characterForm.seriesGenre.anime`} 60 | {t`com.characterForm.seriesGenre.comic`} 61 | {t`com.characterForm.seriesGenre.galgame`} 62 | {t`com.characterForm.seriesGenre.game`} 63 | {t`com.characterForm.seriesGenre.novel`} 64 | {t`com.characterForm.seriesGenre.other`} 65 | 66 | 67 | 68 | ) 69 | }, 70 | { 71 | key: '2', 72 | label: t`com.characterForm.label.2`, 73 | children: ( 74 | <> 75 | 76 | 83 | 84 | )} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | {t`com.characterForm.bloodType.A`} 146 | {t`com.characterForm.bloodType.B`} 147 | {t`com.characterForm.bloodType.AB`} 148 | {t`com.characterForm.bloodType.O`} 149 | 150 | 151 | 152 | } /> 47 | 48 | 49 | } type="password" /> 50 | 51 |
52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | export default LoginView 65 | -------------------------------------------------------------------------------- /packages/client/src/views/Admin/Password/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Flex, Form, Input, notification, Space } from 'antd' 2 | import type { MoehubDataUpdateLoginSubmit } from '@moehub/common' 3 | import { useDispatch } from 'react-redux' 4 | import { updateLogin } from '@/http' 5 | import { useNavigate } from 'react-router-dom' 6 | import { logout } from '@/store/adminReducer' 7 | import { t } from '@/i18n' 8 | 9 | const PasswordView: React.FC = () => { 10 | const dispatch = useDispatch() 11 | const navigate = useNavigate() 12 | 13 | async function onSubmit(values: MoehubDataUpdateLoginSubmit & { confirmPassword: string }) { 14 | if (values.newPassword !== values.confirmPassword) { 15 | notification.warning({ message: t`view.password.passwordMismatch` }) 16 | return 17 | } 18 | try { 19 | await updateLogin(values.newPassword, values.oldPassword) 20 | notification.success({ message: t`view.password.updateSuccess` }) 21 | dispatch(logout()) 22 | setTimeout(() => { 23 | navigate(0) 24 | }, 500) 25 | } catch { 26 | notification.error({ message: t`view.password.updateFailure` }) 27 | } 28 | } 29 | 30 | return ( 31 |
32 |

{t`view.password.changePassword`}

33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 51 | 52 | 53 |
54 |
55 |
56 |
57 | ) 58 | } 59 | 60 | export default PasswordView 61 | -------------------------------------------------------------------------------- /packages/client/src/views/Admin/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Button, Card, Flex, Form, Input, InputNumber, notification, Space, Switch, Tabs } from 'antd' 2 | import type { MoehubDataSettings, MoehubDataSettingsSubmit } from '@moehub/common' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { getSettings, loadSettings } from '@/store/settingsReducer' 5 | import { useEffect } from 'react' 6 | import { postEmail, updateSettings } from '@/http' 7 | import { useNavigate } from 'react-router-dom' 8 | import ListForm from '@/components/ListForm' 9 | import { t } from '@/i18n' 10 | 11 | const items = (isSame: boolean, testEmail: () => void) => [ 12 | { 13 | key: '1', 14 | label: t`view.settings.websiteSettings`, 15 | children: ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {isSame ? null : ( 28 | <> 29 | 30 |
31 | 32 | )} 33 | 34 | 35 | 36 | 37 | 38 | {(name) => ( 39 | 40 | 41 | 42 | )} 43 | 44 | 45 | 46 | ) 47 | }, 48 | { 49 | key: '2', 50 | label: t`view.settings.homepageSettings`, 51 | children: ( 52 | <> 53 | 54 | 55 | 56 | 57 | 58 | {(key) => ( 59 | <> 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | )} 68 | 69 | 70 | 71 | 72 | {(key) => ( 73 | <> 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | )} 82 | 83 | 84 | 85 | 86 | 87 | 88 | ) 89 | }, 90 | { 91 | key: '3', 92 | label: t`view.settings.advancedSettings`, 93 | children: ( 94 | <> 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | ) 112 | }, 113 | { 114 | key: '4', 115 | label: t`view.settings.emailSettings`, 116 | children: ( 117 | <> 118 | 119 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 149 | 150 | ) 151 | } 152 | ] 153 | 154 | const SettingsView: React.FC = () => { 155 | const [form] = Form.useForm() 156 | const settings = useSelector(getSettings) 157 | const dispatch = useDispatch() 158 | const navigate = useNavigate() 159 | 160 | useEffect(() => form.setFieldsValue(settings)) 161 | 162 | async function onSubmit(values: MoehubDataSettingsSubmit) { 163 | const handler = (items: ([string, string] | { 0: string; 1: string })[]) => 164 | items.map((item) => [item[0], item[1]]) as [string, string][] 165 | 166 | const data = { 167 | ...values, 168 | home_buttons: values.home_buttons ? handler(values.home_buttons) : undefined, 169 | home_timeline: values.home_timeline ? handler(values.home_timeline) : undefined 170 | } 171 | 172 | await updateSettings(data) 173 | notification.success({ message: t`view.settings.saveSuccess` }) 174 | dispatch(loadSettings(data)) 175 | setTimeout(() => { 176 | navigate(0) 177 | }, 500) 178 | } 179 | 180 | async function testEmail() { 181 | await postEmail() 182 | notification.success({ message: t`view.settings.testEmailSuccess` }) 183 | } 184 | 185 | return ( 186 |
187 |

{t`view.settings.systemSettings`}

188 | 189 | 190 |
191 | 192 |
193 | 194 | 195 | 198 | 199 | 200 | 201 |
202 |
203 |
204 | ) 205 | } 206 | 207 | export default SettingsView 208 | -------------------------------------------------------------------------------- /packages/client/src/views/Admin/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Col, Flex, Modal, Row, Statistic } from 'antd' 2 | import { Link, useNavigate } from 'react-router-dom' 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { getSettings } from '@/store/settingsReducer' 5 | import { logout } from '@/store/adminReducer' 6 | import { useEffect, useState } from 'react' 7 | import { BookOutlined, FireOutlined, StarOutlined, TagsOutlined } from '@ant-design/icons' 8 | import useSWR from 'swr' 9 | import { getCharacters, getCollections } from '@/http' 10 | import ErrorResult from '@/components/result/error' 11 | import Loading from '@/components/Loading' 12 | import { f, t } from '@/i18n' 13 | 14 | const AdminView: React.FC = () => { 15 | const [isModalOpen, setIsModalOpen] = useState(false) 16 | const { admin_username } = useSelector(getSettings) 17 | const navigate = useNavigate() 18 | const dispatch = useDispatch() 19 | const { data: characters, error: errorCharacters } = useSWR('/api/characters', getCharacters) 20 | const { data: collections, error: errorCollections } = useSWR('/api/collections', getCollections) 21 | const [characterTotal, setCharacterTotal] = useState() 22 | const [tagsTotal, setTagsTotal] = useState() 23 | const [seriesTotal, setSeriesTotal] = useState() 24 | const [collectionTotal, setCollectionTotal] = useState() 25 | 26 | useEffect(() => { 27 | if (!characters || !collections) return 28 | setCharacterTotal(characters.length) 29 | const series = new Set() 30 | const tags = new Set() 31 | 32 | for (const character of characters) { 33 | series.add(character.series) 34 | if (!character.tags) continue 35 | for (const tag of character.tags) tags.add(tag) 36 | } 37 | 38 | setSeriesTotal(series.size) 39 | setTagsTotal(tags.size) 40 | setCollectionTotal(collections.length) 41 | }, [characters, collections]) 42 | 43 | function logoutOk() { 44 | setIsModalOpen(false) 45 | dispatch(logout()) 46 | navigate('/admin/login') 47 | } 48 | 49 | return ( 50 |
51 |

{t`view.admin.title`}

52 | 53 | 54 | {/* biome-ignore lint: */} 55 |

56 |

{t`view.admin.canDo`}

57 | 58 | 59 | 62 | 63 | 64 | 67 | 68 | 69 | 72 | 73 | 74 | 77 | 78 | 79 | 82 | 83 | 86 | setIsModalOpen(false)} 91 | okText={t`view.admin.modal.ok`} 92 | cancelText={t`view.admin.modal.cancel`} 93 | > 94 |

{t`view.admin.modal.content`}

95 |
96 |
97 |
98 | 99 |

{t`view.admin.dashboard.title`}

100 | 101 | {characters && collections ? ( 102 | <> 103 | {' '} 104 | 105 | } 109 | /> 110 | 111 | 112 | } /> 113 | 114 | 115 | } 119 | /> 120 | 121 | 122 | } 126 | /> 127 | 128 | 129 | ) : errorCharacters || errorCollections ? ( 130 | 131 | ) : ( 132 | 133 | )} 134 | 135 |
136 |
137 |
138 | ) 139 | } 140 | 141 | export default AdminView 142 | -------------------------------------------------------------------------------- /packages/client/src/views/Character/index.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Carousel, Descriptions, Flex, Image, Tag } from 'antd' 2 | import { useParams } from 'react-router-dom' 3 | import Loading from '@/components/Loading' 4 | import ErrorResult from '@/components/result/error' 5 | import styles from './styles.module.css' 6 | import useSWR from 'swr' 7 | import { getCharacter } from '@/http' 8 | import { useSelector } from 'react-redux' 9 | import { getSettings } from '@/store/settingsReducer' 10 | import { useEffect } from 'react' 11 | import i18n, { t } from '@/i18n' 12 | 13 | interface InfoCardProps { 14 | children: React.ReactNode 15 | title: string 16 | } 17 | 18 | function getRandomColor() { 19 | const list = ['magenta', 'red', 'volcano', 'orange', 'gold', 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple'] 20 | return list[Math.floor(Math.random() * list.length)] 21 | } 22 | 23 | const GenderReflect = { 24 | MALE: t`view.character.gender.male`, 25 | OTHER: t`view.character.gender.other` 26 | } 27 | 28 | const SeriesGenreReflect = { 29 | ANIME: t`view.character.seriesGenre.anime`, 30 | COMIC: t`view.character.seriesGenre.comic`, 31 | GALGAME: t`view.character.seriesGenre.galgame`, 32 | GAME: t`view.character.seriesGenre.game`, 33 | NOVEL: t`view.character.seriesGenre.novel`, 34 | OTHER: t`view.character.seriesGenre.other` 35 | } 36 | 37 | const InfoCard: React.FC = ({ title, children }) => ( 38 | 39 | {children} 40 | 41 | ) 42 | 43 | const CharacterView: React.FC = () => { 44 | const { id: characterId } = useParams() 45 | const { data, error, isLoading } = useSWR(`/api/character/${characterId}`, () => getCharacter(Number(characterId))) 46 | const { site_title } = useSelector(getSettings) 47 | 48 | useEffect(() => { 49 | if (data) 50 | document.title = `${['ja_JP', 'zh_CN', 'zh_TW'].includes(i18n.get()) ? data.name : data.romaji} - ${site_title}` 51 | }, [data, site_title]) 52 | 53 | if (isLoading) return 54 | if (error || !data) return 55 | 56 | return ( 57 |
58 |

{t`view.character.title`}

59 | 60 | 61 | {data.hitokoto ? ( 62 |
63 | 『{data.hitokoto}』 64 |
65 | ) : null} 66 | {data.songId ? ( 67 | <> 68 |
69 |