├── .dockerignore ├── .env.development ├── .env.production ├── .eslintrc ├── .gitea └── workflows │ └── deploy.yaml ├── .gitignore ├── .prettierrc ├── LICENSE ├── Makefile ├── README.md ├── app ├── components │ ├── ActionMenu │ │ └── index.tsx │ ├── CodeEditor │ │ └── index.tsx │ ├── DebounceSelect │ │ └── index.tsx │ ├── GoogleAd │ │ ├── index.tsx │ │ └── script.tsx │ ├── GrayControl │ │ └── index.tsx │ ├── IssueEditor │ │ └── index.tsx │ ├── IssueLabel │ │ └── index.tsx │ ├── MarkdownEditor │ │ ├── index.client.tsx │ │ └── index.tsx │ ├── MarkdownView │ │ └── index.tsx │ ├── NavigationProcess │ │ └── NavigationProcess.tsx │ ├── Search │ │ ├── SearchList │ │ │ ├── index.tsx │ │ │ └── item.tsx │ │ └── index.tsx │ ├── TextArea │ │ └── index.tsx │ ├── UpdateScript │ │ └── index.tsx │ └── layout │ │ └── MainLayout │ │ └── index.tsx ├── context-manager.ts ├── entry.client.tsx ├── entry.server.tsx ├── i18n.ts ├── i18next.server.ts ├── root.tsx ├── routes │ ├── $lng │ │ ├── index.tsx │ │ ├── invite-confirm │ │ │ └── index.tsx │ │ ├── post-script.tsx │ │ ├── script-show-page │ │ │ ├── $id.tsx │ │ │ └── $id │ │ │ │ ├── code.tsx │ │ │ │ ├── comment.tsx │ │ │ │ ├── diff.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── issue.tsx │ │ │ │ ├── issue │ │ │ │ ├── $issueId │ │ │ │ │ └── comment.tsx │ │ │ │ ├── create.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── manage.tsx │ │ │ │ ├── permission.tsx │ │ │ │ ├── permission │ │ │ │ ├── accessRole.tsx │ │ │ │ ├── inviteModal.tsx │ │ │ │ ├── userGroup.tsx │ │ │ │ └── userModal.tsx │ │ │ │ ├── statistic.tsx │ │ │ │ ├── statistic │ │ │ │ ├── advanced.tsx │ │ │ │ └── index.tsx │ │ │ │ ├── update.tsx │ │ │ │ └── version.tsx │ │ ├── search.tsx │ │ ├── search │ │ │ └── index.tsx │ │ ├── sitemap.tsx │ │ ├── users.notify.tsx │ │ ├── users.tsx │ │ ├── users.webhook.tsx │ │ └── users │ │ │ └── $id.tsx │ ├── api.tsx │ └── index.tsx ├── services │ ├── http.ts │ ├── scripts │ │ ├── api.ts │ │ ├── issues │ │ │ ├── api.ts │ │ │ └── types.ts │ │ └── types.ts │ ├── users │ │ ├── api.ts │ │ └── types.ts │ ├── utils.ts │ └── utils │ │ ├── api.ts │ │ └── type.ts ├── styles │ ├── app.css │ ├── github-markdown-css.css │ └── invite-confirm.css └── utils │ ├── cookie.ts │ ├── errors.ts │ ├── i18n.ts │ ├── icon.tsx │ └── utils.ts ├── crowdin.yml ├── deploy ├── docker │ └── Dockerfile ├── gateway │ ├── Chart.yaml │ ├── README.md │ ├── _helpers.tpl │ ├── templates │ │ └── gateway.yaml │ └── values.yaml └── helm │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml │ └── values.yaml ├── package-lock.json ├── package.json ├── public ├── ads.txt ├── assets │ └── logo.png ├── baidu_verify_codeva-aj6KRqtFfK.html ├── favicon.ico ├── locales │ ├── ach-UG │ │ └── common.json │ ├── en │ │ └── common.json │ ├── zh-CN │ │ └── common.json │ └── zh-TW │ │ └── common.json └── robots.txt ├── remix.config.cjs ├── remix.env.d.ts ├── scripts └── prebuild.tsx ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── types └── monaco.d.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | # /.cache 3 | # /build 4 | # /public/build 5 | 6 | # app/styles/*.css 7 | 8 | /.idea 9 | /.vscode 10 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | APP_API_URL = 'https://test.scriptcat.org/api/v2' 2 | APP_API_PROXY = 'https://test.scriptcat.org/api/v2' 3 | APP_BBS_OAUTH_CLIENT = 'dC37Fgznr5aAFZU' 4 | 5 | APP_ENV=development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | APP_API_URL = 'https://scriptcat.org/api/v2' 2 | APP_API_PROXY = 'https://scriptcat.org/api/v2' 3 | APP_BBS_OAUTH_CLIENT = '80mfto0y3b8v' -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@remix-run/eslint-config", 4 | "@remix-run/eslint-config/node" 5 | ], 6 | "rules": { 7 | "react/jsx-no-target-blank": [ 8 | false 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /.gitea/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/* 8 | - test/* 9 | 10 | env: 11 | APP_NAME: ${{ github.event.repository.name }} 12 | NAMESPACE: app 13 | REGISTRY: ${{ secrets.DOCKER_REGISTRY && secrets.DOCKER_REGISTRY || 'docker.io' }} 14 | REGISTRY_MIRROR: ${{ secrets.DOCKER_REGISTRY_MIRROR && secrets.DOCKER_REGISTRY_MIRROR || 'docker.io' }} 15 | REPOSITORY: ${{ github.repository }} 16 | DOMAIN: scriptcat.org 17 | ENV: ${{ startsWith(github.ref, 'refs/heads/release/') && 'pre' || startsWith(github.ref, 'refs/heads/test/') && 'test' || github.ref=='refs/heads/main' && 'prod' }} 18 | RUNNER_TOOL_CACHE: /toolcache 19 | BASEIMAGE: ${{ secrets.BASEIMAGE && secrets.BASEIMAGE || '' }} 20 | 21 | 22 | jobs: 23 | deploy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: debug 29 | run: | 30 | echo $ACTIONS_CACHE_URL 31 | 32 | - name: Use Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: "20.x" 36 | cache: 'npm' 37 | 38 | - name: Build 39 | run: | 40 | npm ci 41 | npm run build 42 | 43 | - name: Set up QEMU 44 | # uses: docker/setup-qemu-action@v3 45 | uses: actions/setup-qemu-action@v3 46 | 47 | - name: Set up Docker Buildx 48 | # uses: docker/setup-buildx-action@v3 49 | uses: actions/setup-buildx-action@v3 50 | with: 51 | driver-opts: | 52 | image=${{ env.REGISTRY_MIRROR }}/moby/buildkit:buildx-stable-1 53 | 54 | - name: Login to Docker Hub 55 | # uses: docker/login-action@v3 56 | uses: actions/login-action@v3 57 | with: 58 | registry: ${{ env.REGISTRY }} 59 | username: ${{ secrets.DOCKER_USERNAME }} 60 | password: ${{ secrets.DOCKER_TOKEN }} 61 | 62 | - name: Set outputs 63 | id: vars 64 | run: | 65 | echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 66 | 67 | - name: Docker build and push 68 | # use: docker/build-push-action@v5 69 | uses: actions/build-push-action@v6 70 | with: 71 | push: true 72 | file: deploy/docker/Dockerfile 73 | tags: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.ENV }}.${{ steps.vars.outputs.sha_short }} 74 | context: . 75 | build-args: | 76 | BASEIMAGE=${{ env.BASEIMAGE }}/node:20-alpine3.16 77 | 78 | - name: Set up kubeconfig 79 | # uses: azure/k8s-set-context@v3 80 | uses: actions/k8s-set-context@v4 81 | with: 82 | method: kubeconfig 83 | kubeconfig: ${{ secrets.KUBE_CONFIG }} 84 | context: k8s-context 85 | 86 | - name: Set up Helm 87 | # uses: azure/setup-helm@v3 88 | uses: actions/setup-helm@v4 89 | with: 90 | version: 'v3.13.1' # default is latest (stable) 91 | 92 | - name: Deploy ${{ env.ENV }} 93 | env: 94 | APP_NAME: ${{ env.ENV=='prod' && env.APP_NAME || format('{0}-{1}', env.APP_NAME, env.ENV) }} 95 | DOMAIN: ${{ env.ENV=='prod' && env.DOMAIN || format('{0}.{1}', env.ENV, env.DOMAIN) }} 96 | RESOURCE_CPU: ${{ env.ENV=='prod' && '500m' || '100m' }} 97 | RESOURCE_MEMORY: ${{ env.ENV=='prod' && '512Mi' || '128Mi' }} 98 | INNER_DOMAIN: ${{ env.ENV=='prod' && 'scriptlist-cago' || format('scriptlist-{0}-cago', env.ENV) }} 99 | APP_BBS_OAUTH_CLIENT: ${{ env.ENV=='prod' && '80mfto0y3b8v' || env.ENV=='pre' && '5uk70yummcoe' || 'sxIv1i8H1ZwnSAH' }} 100 | AUTO_SCALING: ${{ env.ENV=='prod' && 'true' || 'false' }} 101 | AUTO_SCALING_MIN_REPLICAS: ${{ env.ENV=='prod' && '2' || '1' }} 102 | run: | 103 | cd deploy/helm 104 | helm upgrade --install \ 105 | --namespace $NAMESPACE $APP_NAME . -f values.yaml \ 106 | --set image.tag=${{ env.ENV }}.${{ steps.vars.outputs.sha_short }} --set image.repository=$REGISTRY/$REPOSITORY \ 107 | --set env[0].value=https://$DOMAIN/api/v2 \ 108 | --set env[1].value=http://$INNER_DOMAIN.app.svc.cluster.local/api/v2 \ 109 | --set env[2].value=$APP_BBS_OAUTH_CLIENT \ 110 | --set ingress.hosts[0].host=$DOMAIN \ 111 | --set ingress.tls[0].hosts[0]=$DOMAIN \ 112 | --set resources.requests.cpu=$RESOURCE_CPU \ 113 | --set resources.requests.memory=$RESOURCE_MEMORY \ 114 | --set autoscaling.enabled=$AUTO_SCALING \ 115 | --set autoscaling.minReplicas=$AUTO_SCALING_MIN_REPLICAS 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | /public/styles/* 7 | /public/assets/monaco-editor/* 8 | .env 9 | 10 | /.idea 11 | /.vscode 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | prod: 3 | npm run build 4 | APP_API_URL=https://scriptcat.org/api/v2 APP_API_PROXY=https://scriptcat.org/api/v2 APP_BBS_OAUTH_CLIENT=80mfto0y3b8v npm run start 5 | 6 | 7 | docker: 8 | docker build -t scriptcat-list-frontend -f deploy/docker/Dockerfile . 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scriptlist 2 | > 脚本猫,脚本站 3 | 4 | ## 运行 5 | 6 | ```bash 7 | npm i 8 | # 复制环境变量, 如果你不开发后端的话, 直接复制 .env.production 即可 9 | cp .env.production .env 10 | # dev 11 | npm run dev 12 | # build 13 | npm run build 14 | ``` 15 | -------------------------------------------------------------------------------- /app/components/ActionMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DeleteOutlined, 3 | ExclamationCircleOutlined, 4 | FileExclamationOutlined, 5 | } from '@ant-design/icons'; 6 | import { Dropdown, Menu, Modal, Select, Space } from 'antd'; 7 | import { useContext } from 'react'; 8 | import { useTranslation } from 'react-i18next'; 9 | import { UserContext } from '~/context-manager'; 10 | import TextArea from '../TextArea'; 11 | 12 | const { Option } = Select; 13 | export type MenuItemKey = 'delete'; 14 | 15 | export type DeleteLevel = 'admin' | 'super_moderator' | 'moderator'; 16 | export type ActionMenuProps = { 17 | children: React.ReactNode; 18 | uid: number | number[]; 19 | deleteLevel: DeleteLevel; // 删除等级 管理员 超级版主 版主 20 | allowSelfDelete: boolean; // 允许自己删除 21 | onDeleteClick: () => void; 22 | // 处罚 23 | punish?: boolean; 24 | onPunishClick?: () => void; 25 | }; 26 | 27 | const ActionMenu: React.FC = ({ 28 | children, 29 | deleteLevel, 30 | allowSelfDelete, 31 | uid, 32 | onDeleteClick, 33 | punish, 34 | onPunishClick, 35 | }) => { 36 | const user = useContext(UserContext); 37 | const authorMap = new Map(); 38 | const { t } = useTranslation(); 39 | 40 | const [modal, contextHolder] = Modal.useModal(); 41 | if (uid instanceof Array) { 42 | uid.forEach((v) => authorMap.set(v, true)); 43 | } else { 44 | authorMap.set(uid, true); 45 | } 46 | let items = []; 47 | if (user.user) { 48 | // 判断用户等级是否为管理员 或者 允许作者删除 49 | if ( 50 | (user.user.is_admin > 0 && 51 | (user.user.is_admin === 1 || 52 | (deleteLevel === 'super_moderator' && user.user.is_admin <= 2) || 53 | (deleteLevel === 'moderator' && user.user.is_admin <= 3))) || 54 | (allowSelfDelete && authorMap.has(user.user.user_id)) 55 | ) { 56 | items.push({ 57 | label: ( 58 | 59 | 60 | {t('delete')} 61 | 62 | ), 63 | key: 'delete', 64 | }); 65 | if (punish) { 66 | items.push({ 67 | label: ( 68 | 69 | 70 | {t('punish')} 71 | 72 | ), 73 | key: 'punish', 74 | }); 75 | } 76 | } 77 | } 78 | items.push({ 79 | label: ( 80 | 81 | 82 | {t('report')} 83 | 84 | ), 85 | key: 'report', 86 | }); 87 | return ( 88 |
89 | {contextHolder} 90 | { 94 | if (value.key === 'report') { 95 | window.open( 96 | 'https://bbs.tampermonkey.net.cn/forum-75-1.html', 97 | '_blank' 98 | ); 99 | } else if (value.key === 'punish') { 100 | modal.confirm({ 101 | title: t('confirm_punish'), 102 | content: ( 103 | 109 | {t('select_punish_option')} 110 | 118 | {t('punish_reason')} 119 |