├── .browserslistrc ├── .env ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc.mjs ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── biome.json ├── cliff.toml ├── create-tags.sh ├── favicon.png ├── index.html ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── polyfills └── packages │ └── path │ ├── index.js │ └── package.json ├── postcss.config.js ├── readme.md ├── release.sh ├── renovate.json ├── scripts ├── clean-page-tag.sh ├── empty-project │ └── package.json └── netlify.sh ├── src ├── App.tsx ├── auto-import.d.ts ├── components │ ├── ai │ │ └── ai-helper.tsx │ ├── avatar │ │ ├── index.tsx │ │ └── index.vue │ ├── button │ │ ├── parallax-button.vue │ │ └── rounded-button.tsx │ ├── code-highlight │ │ └── index.tsx │ ├── config-form │ │ ├── index.tsx │ │ └── mock.json │ ├── directives │ │ └── if.tsx │ ├── drawer │ │ ├── components │ │ │ ├── image-detail-section.tsx │ │ │ └── json-editor.tsx │ │ └── text-base-drawer.tsx │ ├── editor │ │ ├── codemirror │ │ │ ├── codemirror.css │ │ │ ├── codemirror.tsx │ │ │ ├── extension.ts │ │ │ ├── syntax-highlight.ts │ │ │ ├── use-auto-fonts.ts │ │ │ ├── use-auto-theme.ts │ │ │ └── use-codemirror.ts │ │ ├── monaco │ │ │ ├── theme │ │ │ │ ├── dark.json │ │ │ │ └── light.json │ │ │ └── use-define-theme.ts │ │ ├── plain │ │ │ └── plain.tsx │ │ └── universal │ │ │ ├── constants.ts │ │ │ ├── editor-config.ts │ │ │ ├── editor.module.css │ │ │ ├── index.css │ │ │ ├── index.tsx │ │ │ ├── props.ts │ │ │ ├── reset-icon-button.tsx │ │ │ ├── types.tsx │ │ │ └── use-editor-setting.tsx │ ├── function-editor │ │ ├── index.tsx │ │ └── libs │ │ │ ├── lib.declare.ts │ │ │ ├── node.declare.ts │ │ │ └── node │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── assert.d.ts │ │ │ ├── async_hooks.d.ts │ │ │ ├── base.d.ts │ │ │ ├── buffer.d.ts │ │ │ ├── child_process.d.ts │ │ │ ├── cluster.d.ts │ │ │ ├── constants.d.ts │ │ │ ├── crypto.d.ts │ │ │ ├── dgram.d.ts │ │ │ ├── dns.d.ts │ │ │ ├── domain.d.ts │ │ │ ├── fs.d.ts │ │ │ ├── globals.d.ts │ │ │ ├── globals.global.d.ts │ │ │ ├── index.d.ts │ │ │ ├── inspector.d.ts │ │ │ ├── net.d.ts │ │ │ ├── os.d.ts │ │ │ ├── package.json │ │ │ ├── path.d.ts │ │ │ ├── perf_hooks.d.ts │ │ │ ├── punycode.d.ts │ │ │ ├── querystring.d.ts │ │ │ ├── readline.d.ts │ │ │ ├── stream.d.ts │ │ │ ├── string_decoder.d.ts │ │ │ ├── timers.d.ts │ │ │ ├── trace_events.d.ts │ │ │ ├── url.d.ts │ │ │ ├── util.d.ts │ │ │ ├── wasi.d.ts │ │ │ └── zlib.d.ts │ ├── icons │ │ └── index.tsx │ ├── input │ │ ├── base.tsx │ │ ├── material-input.tsx │ │ ├── material.module.css │ │ ├── underline-input.tsx │ │ └── underline.module.css │ ├── ip-info │ │ └── index.tsx │ ├── json-highlight │ │ └── index.tsx │ ├── k-bar │ │ └── index.tsx │ ├── kv-editor │ │ └── index.tsx │ ├── link │ │ └── title-link.tsx │ ├── location │ │ ├── get-location-button.tsx │ │ └── search-button.tsx │ ├── output-modal │ │ ├── normal.tsx │ │ └── xterm.tsx │ ├── preview │ │ └── index.tsx │ ├── shorthand │ │ └── index.tsx │ ├── sidebar │ │ ├── hooks.ts │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── uwu.png │ ├── special-button │ │ ├── copy-text-button.tsx │ │ ├── delete-confirm.tsx │ │ ├── fetch-github-repo.tsx │ │ ├── iframe-preview.tsx │ │ ├── parse-content.tsx │ │ └── preview.tsx │ ├── spin │ │ └── index.tsx │ ├── table │ │ ├── edit-column.tsx │ │ ├── index.module.css │ │ └── index.tsx │ ├── time │ │ └── relative-time.tsx │ ├── upload │ │ └── index.tsx │ ├── xlog-connect │ │ ├── class.ts │ │ └── index.tsx │ └── xterm │ │ └── index.tsx ├── configs.ts ├── constants │ ├── env.ts │ ├── kaomoji.ts │ ├── keys.ts │ ├── note.ts │ └── social.ts ├── external │ ├── api │ │ ├── github-check-update.ts │ │ ├── github-mx-snippets.ts │ │ ├── github-repo.ts │ │ ├── hitokoto.ts │ │ ├── jinrishici.ts │ │ ├── npm.ts │ │ └── octokit.ts │ └── types │ │ └── npm-pkg.ts ├── hooks │ ├── use-async-monaco.ts │ ├── use-auto-save.ts │ ├── use-lifecycle.ts │ ├── use-memo-fetch-data-list.ts │ ├── use-parse-payload.ts │ ├── use-portal-element.ts │ ├── use-save-confirm.ts │ ├── use-storage.ts │ ├── use-store-ref.ts │ └── use-table.ts ├── index.css ├── layouts │ ├── app-layout.tsx │ ├── content │ │ ├── index.module.css │ │ └── index.tsx │ ├── router-view.tsx │ ├── setup-view.vue │ ├── sidebar │ │ ├── index.module.css │ │ └── index.tsx │ └── two-col.tsx ├── main.ts ├── models │ ├── activity.ts │ ├── ai.ts │ ├── amap.ts │ ├── analyze.ts │ ├── authn.ts │ ├── base.ts │ ├── category.ts │ ├── comment.ts │ ├── link.ts │ ├── note.ts │ ├── options.ts │ ├── page.ts │ ├── post.ts │ ├── project.ts │ ├── recently.ts │ ├── say.ts │ ├── snippet.ts │ ├── stat.ts │ ├── subscribe.ts │ ├── system.ts │ ├── token.ts │ ├── topic.ts │ ├── user.ts │ └── wehbook.ts ├── monaco.ts ├── router │ ├── guard.ts │ ├── index.ts │ ├── name.ts │ ├── route.tsx │ └── router.ts ├── shared │ └── types │ │ └── base.ts ├── socket │ ├── index.ts │ ├── socket-client.tsx │ └── types.ts ├── stores │ ├── app.ts │ ├── category.ts │ ├── index.ts │ ├── ui.ts │ └── user.ts ├── types.d.ts ├── utils │ ├── auth.ts │ ├── authjs │ │ ├── auth.ts │ │ └── session.ts │ ├── authn.ts │ ├── build-menus.ts │ ├── color.ts │ ├── confetti.ts │ ├── endpoint.ts │ ├── event-bus.ts │ ├── image.ts │ ├── index.ts │ ├── is-init.ts │ ├── json.ts │ ├── markdown-parser.ts │ ├── markdown.ts │ ├── notification.ts │ ├── number.ts │ ├── rest.ts │ ├── time.ts │ └── word.ts ├── views │ ├── ai │ │ └── summary.tsx │ ├── analyze │ │ ├── components │ │ │ ├── analyze-data-table.tsx │ │ │ ├── guest-activity.tsx │ │ │ └── reading-rank.tsx │ │ ├── index.tsx │ │ └── types.tsx │ ├── comments │ │ ├── index.tsx │ │ └── markdown-render.tsx │ ├── dashboard │ │ ├── badge.tsx │ │ ├── card.tsx │ │ ├── index.tsx │ │ ├── statistic.tsx │ │ └── update-panel.tsx │ ├── debug │ │ ├── authn │ │ │ └── index.tsx │ │ ├── events │ │ │ └── index.tsx │ │ └── serverless │ │ │ └── index.tsx │ ├── extra-features │ │ ├── assets │ │ │ └── template │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── ejs-render.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── tabs │ │ │ │ ├── email.tsx │ │ │ │ └── markdown.tsx │ │ ├── markdown-helper.tsx │ │ ├── snippets │ │ │ ├── components │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── import-snippets-button.tsx │ │ │ │ ├── install-dep-button.tsx │ │ │ │ ├── install-dep-xterm.tsx │ │ │ │ └── update-deps-button.tsx │ │ │ ├── index.tsx │ │ │ ├── interfaces │ │ │ │ └── snippet-group.ts │ │ │ └── tabs │ │ │ │ ├── for-edit.tsx │ │ │ │ └── for-list.tsx │ │ ├── subscribe │ │ │ ├── constants.ts │ │ │ └── index.tsx │ │ └── webhook │ │ │ └── index.tsx │ ├── login │ │ ├── index.module.css │ │ └── index.tsx │ ├── maintenance │ │ ├── backup.tsx │ │ ├── cron.tsx │ │ ├── log-view │ │ │ ├── index.tsx │ │ │ └── tabs │ │ │ │ ├── log-list.tsx │ │ │ │ └── realtime-log.tsx │ │ └── pty │ │ │ └── index.tsx │ ├── manage-files │ │ └── index.tsx │ ├── manage-friends │ │ ├── components │ │ │ ├── avatar.tsx │ │ │ ├── fallback.jpg │ │ │ └── reason-modal.tsx │ │ ├── index.tsx │ │ └── url-components.tsx │ ├── manage-notes │ │ ├── components │ │ │ ├── topic-detail.tsx │ │ │ └── topic-modal.tsx │ │ ├── hooks │ │ │ └── use-memo-note-list.ts │ │ ├── list.tsx │ │ ├── topic.tsx │ │ └── write.tsx │ ├── manage-pages │ │ ├── list.tsx │ │ └── write.tsx │ ├── manage-posts │ │ ├── category.tsx │ │ ├── components │ │ │ └── ask-ai.tsx │ │ ├── hooks │ │ │ └── use-memo-post-list.ts │ │ ├── list.tsx │ │ └── write.tsx │ ├── manage-project │ │ ├── edit.tsx │ │ └── list.tsx │ ├── manage-says │ │ ├── edit.tsx │ │ └── list.tsx │ ├── reader │ │ └── index.tsx │ ├── setting │ │ ├── index.tsx │ │ └── tabs │ │ │ ├── auth.tsx │ │ │ ├── providers │ │ │ └── oauth.ts │ │ │ ├── sections │ │ │ └── oauth.tsx │ │ │ ├── security.tsx │ │ │ ├── system.tsx │ │ │ ├── user.module.css │ │ │ └── user.tsx │ ├── setup-api │ │ └── index.tsx │ ├── setup │ │ ├── index.module.css │ │ └── index.tsx │ └── shorthand │ │ ├── index.module.css │ │ └── index.tsx └── vue-app-env.d.ts ├── theme.config.ts ├── tsconfig.json ├── vite.config.mts └── windi.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions 2 | safari>=14 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_LOGIN_BG=https://fastly.jsdelivr.net/gh/mx-space/docs-images@master/images/chichi-1.jpeg 2 | VITE_APP_BASE_API=http://localhost:2333 3 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # VITE_APP_BASE_API=https://api.innei.ren/v2 2 | # VITE_APP_WEB_URL=https://innei.ren 3 | # VITE_APP_GATEWAY=https://api.innei.ren 4 | # # VITE_APP_PUBLIC_URL=https://fastly.jsdelivr.net/gh/mx-space/admin-next@gh-pages/ 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/views/dev 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | root: true, 4 | parserOptions: { 5 | parser: '@typescript-eslint/parser', 6 | ecmaVersion: 2020, 7 | sourceType: 'module', 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | }, 12 | extends: ['plugin:vue/vue3-recommended', '@innei/eslint-config-ts'], 13 | overrides: [ 14 | { 15 | files: [ 16 | '*.config.[tj]s', 17 | 'pages/**/*.[tj]sx', 18 | 'src/pages/**/*.[tj]sx', 19 | 'src/views/**/*.[tj]sx', 20 | 'src/layouts/**/*.[tj]sx', 21 | ], 22 | rules: { 23 | 'import/no-default-export': 'off', 24 | }, 25 | }, 26 | ], 27 | rules: { 28 | '@typescript-eslint/ban-types': 'off', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | '@typescript-eslint/no-unused-vars': 31 | process.env.NODE_ENV === 'development' ? 'warning' : 'off', 32 | 'vue/one-component-per-file': 'off', 33 | '@typescript-eslint/ban-ts-comment': 'off', 34 | 'vue/require-default-prop': 'off', 35 | 'vue/no-mutating-props': 'off', 36 | '@typescript-eslint/no-namespace': 'off', 37 | '@typescript-eslint/no-non-null-assertion': 0, 38 | 'vue/html-self-closing': 'error', 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.tsx linguist-language=Vue 2 | * -text 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [innei] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://afdian.com/@Innei'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: 9 | - '**' 10 | pull_request: 11 | branches: [master] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Cache pnpm modules 29 | uses: actions/cache@v4 30 | env: 31 | cache-name: cache-pnpm-modules 32 | with: 33 | path: ~/.pnpm-store 34 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 35 | restore-keys: | 36 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | 38 | - uses: pnpm/action-setup@v3.0.0 39 | with: 40 | version: 8.x.x 41 | run_install: true 42 | - run: | 43 | export NODE_OPTIONS=--max-old-space-size=32768 44 | pnpm run build 45 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Deploy 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Cache pnpm modules 27 | uses: actions/cache@v4 28 | env: 29 | cache-name: cache-pnpm-modules 30 | with: 31 | path: ~/.pnpm-store 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 35 | 36 | - uses: pnpm/action-setup@v3.0.0 37 | with: 38 | version: 8.x.x 39 | run_install: true 40 | - run: | 41 | export NODE_OPTIONS=--max-old-space-size=32768 42 | pnpm run build 43 | 44 | - run: echo '22333322.xyz' > dist/CNAME 45 | - name: Prepare tag 46 | id: prepare_tag 47 | if: startsWith(github.ref, 'refs/tags/') 48 | run: | 49 | TAG_NAME="${GITHUB_REF##refs/tags/}" 50 | echo "::set-output name=tag_name::${TAG_NAME}" 51 | echo "::set-output name=deploy_tag_name::page_${TAG_NAME}" 52 | - name: Archive Version Tag 53 | uses: peaceiris/actions-gh-pages@v4 54 | with: 55 | github_token: ${{ secrets.GITHUB_TOKEN }} 56 | publish_dir: ./dist 57 | tag_name: ${{ steps.prepare_tag.outputs.deploy_tag_name }} 58 | tag_message: 'Store ${{ steps.prepare_tag.outputs.tag_name }}' 59 | force_orphan: true 60 | env: 61 | CI: true 62 | # VITE_APP_PUBLIC_URL: ${{ secrets.VITE_APP_PUBLIC_URL }} 63 | VITE_APP_PUBLIC_URL: https://fastly.jsdelivr.net/gh/mx-space/mx-admin@gh-pages/ 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Release 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 18.x 23 | 24 | - name: Cache pnpm modules 25 | uses: actions/cache@v4 26 | env: 27 | cache-name: cache-pnpm-modules 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | restore-keys: | 32 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | 34 | - uses: pnpm/action-setup@v3.0.0 35 | with: 36 | version: 8.x.x 37 | run_install: true 38 | - name: Build project # This would actually build your project, using zip for an example artifact 39 | run: | 40 | sh release.sh 41 | - name: Create Release 42 | id: create_release 43 | uses: actions/create-release@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | tag_name: ${{ github.ref }} 48 | release_name: Release ${{ github.ref }} 49 | draft: false 50 | prerelease: false 51 | # - run: npx changelogithub 52 | # continue-on-error: true 53 | # env: 54 | # GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 55 | - name: Upload Release Asset 56 | id: upload-release-asset 57 | uses: actions/upload-release-asset@v1 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | with: 61 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 62 | asset_path: ./release.zip 63 | asset_name: release.zip 64 | asset_content_type: application/zip 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | bundle-analyze 7 | stats.html 8 | .env.development 9 | 10 | /src/views/dev 11 | g.d.ts -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | public-hoist-pattern[]=*eslint* 3 | public-hoist-pattern[]=*types* 4 | public-hoist-pattern[]=*@prettier/plugin-* 5 | public-hoist-pattern[]=*prettier-plugin-* 6 | 7 | public-hoist-pattern[]=*@codemirror* 8 | 9 | registry=https://registry.npmjs.org 10 | 11 | # https://zenn.dev/haxibami/scraps/083718c1beec04 12 | strict-peer-dependencies=false 13 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | import config from '@innei/prettier' 2 | 3 | export default config 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "hitokoto", 4 | "vditor" 5 | ], 6 | // Note that `javascript.preferences.autoImportFileExcludePatterns` can be specified for JavaScript too. 7 | "typescript.preferences.autoImportFileExcludePatterns": [ 8 | "**/node_modules/@types/node", 9 | "**/node_modules/@vicons/**" 10 | ], 11 | "typescript.tsserver.log": "verbose" 12 | } -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://tera.netlify.app/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | upper_first }} 19 | {% for commit in commits %} 20 | - {{ commit.message | upper_first }}\ 21 | {% endfor %} 22 | {% endfor %}\n 23 | """ 24 | # remove the leading and trailing whitespaces from the template 25 | trim = true 26 | # changelog footer 27 | footer = """ 28 | 29 | """ 30 | 31 | [git] 32 | # allow only conventional commits 33 | # https://www.conventionalcommits.org 34 | conventional_commits = true 35 | # regex for parsing and grouping commits 36 | commit_parsers = [ 37 | { message = "^feat", group = "Features"}, 38 | { message = "^fix", group = "Bug Fixes"}, 39 | { message = "^doc", group = "Documentation"}, 40 | { message = "^perf", group = "Performance"}, 41 | { message = "^refactor", group = "Refactor"}, 42 | { message = "^style", group = "Styling"}, 43 | { message = "^test", group = "Testing"}, 44 | { message = "^chore\\(release\\): prepare for", skip = true}, 45 | { message = "^chore", group = "Miscellaneous Tasks"}, 46 | { body = ".*security", group = "Security"}, 47 | ] 48 | # filter out the commits that are not matched by commit parsers 49 | filter_commits = false 50 | # glob pattern for matching git tags 51 | tag_pattern = "v[0-9]*" 52 | # regex for skipping tags 53 | skip_tags = "v0.1.0-beta.1" 54 | -------------------------------------------------------------------------------- /create-tags.sh: -------------------------------------------------------------------------------- 1 | tag=v$(json -f package.json version) 2 | git add . 3 | git commit -a -m "release: $tag" &>/dev/null 4 | git push 5 | git tag -a "$tag" -m "Release $tag" &>/dev/null 6 | git push --tags 7 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-space/mx-admin/7c1b853374ea4ace29ce4184c6e8e86b797cd9e6/favicon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 18 | Mx Space Admin Vue 3 v2 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "bash ./scripts/netlify.sh" 3 | [build.environment] 4 | NPM_FLAGS="--prefix=./scripts/empty-project/" -------------------------------------------------------------------------------- /polyfills/packages/path/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "path", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-nested': {}, 4 | 'postcss-preset-env': {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # MX Space Admin Vue 3 v2 2 | 3 | 嘿,这是一个为 Mix Space Server 打造的后台管理。使用 Vue 3 + Naive UI 编写。添加了许多有趣的小玩意。一起来玩吗? 4 | 5 | 嘿!现在可以使用共享面板了,立即前往: 6 | 7 | 8 | ``` 9 | git clone --single-branch -b master https://github.com/mx-space/mx-admin.git 10 | ``` 11 | 12 | ## Build 13 | 14 | ``` 15 | pnpm i 16 | pnpm build 17 | ``` 18 | 19 | ## Preview 20 | 21 | ![CleanShot 2024-05-04 at 11  18 54@2x](https://github.com/mx-space/mx-admin/assets/41265413/6522e6ba-73dc-4214-9999-ad8d88272c2e) 22 | ![CleanShot 2024-05-04 at 11  19 15@2x](https://github.com/mx-space/mx-admin/assets/41265413/44f05e84-c9ed-4e54-b6ed-432a42fb163b) 23 | ![CleanShot 2024-05-04 at 11  19 35@2x](https://github.com/mx-space/mx-admin/assets/41265413/df54ed83-dad0-4bea-a156-320f21108c90) 24 | ![CleanShot 2024-05-04 at 11  20 40@2x](https://github.com/mx-space/mx-admin/assets/41265413/57670e28-5898-4ff9-9fd2-9fda5fb9c21b) 25 | ![CleanShot 2024-05-04 at 11  20 56@2x](https://github.com/mx-space/mx-admin/assets/41265413/5042c5ef-388c-4fa8-bed4-b7758e285875) 26 | ![CleanShot 2024-05-04 at 11  22 13@2x](https://github.com/mx-space/mx-admin/assets/41265413/d1f9d840-5458-4950-997f-638b51fcbeb7) 27 | 28 | 29 | ## Version 30 | 31 | - v4.0 for Mix Space Server v5.0 32 | 33 | ## License 34 | 35 | MIT. © 2021-present Mix Space & Innei 36 | 37 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | export NODE_OPTIONS=--max-old-space-size=32768 2 | 3 | pnpm i 4 | pnpm build 5 | zip -r release.zip dist/* 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "schedule:weekly" 5 | ], 6 | "labels": [ 7 | "dependencies" 8 | ], 9 | "rangeStrategy": "bump", 10 | "packageRules": [ 11 | { 12 | "depTypeList": [ 13 | "peerDependencies" 14 | ], 15 | "enabled": false 16 | } 17 | ], 18 | "ignoreDeps": [ 19 | "@codemirror/commands", 20 | "@codemirror/lang-markdown", 21 | "@codemirror/language", 22 | "@codemirror/language-data", 23 | "@codemirror/state", 24 | "@codemirror/theme-one-dark", 25 | "@codemirror/view", 26 | "@ddietr/codemirror-themes", 27 | "@lezer/highlight", 28 | "eslint" 29 | ] 30 | } -------------------------------------------------------------------------------- /scripts/clean-page-tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取所有 pag_va.b.c 格式的标签,按版本号排序 4 | tags=$(git tag -l "page_v*" | sort -Vr) 5 | 6 | # 初始化变量来保留最新的标签 7 | latest_tag="" 8 | 9 | # 遍历标签,删除除了最新之外的所有标签 10 | for tag in $tags; do 11 | if [[ "$latest_tag" == "" ]]; then 12 | # 保留第一个标签(最新的标签) 13 | latest_tag=$tag 14 | echo "Keeping latest tag: $latest_tag" 15 | else 16 | # 删除本地标签 17 | git tag -d $tag 18 | echo "Deleted local tag: $tag" 19 | 20 | # 删除远程标签,假设远程名称为 origin 21 | git push origin --delete $tag 22 | echo "Deleted remote tag: $tag" 23 | fi 24 | done 25 | 26 | # 运行 git gc 来优化本地仓库 27 | git gc --prune=now --aggressive 28 | echo "Completed git garbage collection" 29 | -------------------------------------------------------------------------------- /scripts/empty-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This is an empty project used as the destination of netlify npm install." 3 | } 4 | -------------------------------------------------------------------------------- /scripts/netlify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | test "$CI" = true || exit 1 4 | npx pnpm install -r --store-dir=node_modules/.pnpm-store 5 | npm run build 6 | -------------------------------------------------------------------------------- /src/components/ai/ai-helper.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NIcon, NTooltip } from 'naive-ui' 2 | 3 | import { OpenAIIcon } from '~/components/icons' 4 | import { RESTManager } from '~/utils' 5 | 6 | enum AiQueryType { 7 | TitleSlug = 'title-slug', 8 | Title = 'title', 9 | } 10 | 11 | export const AiHelperButton = defineComponent({ 12 | props: { 13 | reactiveData: { 14 | type: Object as PropType<{ 15 | title: string 16 | text: string 17 | slug?: string 18 | }>, 19 | required: true, 20 | }, 21 | }, 22 | setup(props) { 23 | const loading = ref(false) 24 | 25 | const callApi = async () => { 26 | const { title, text } = props.reactiveData 27 | 28 | if (!text && !title) { 29 | return 30 | } 31 | 32 | const hasSlug = 'slug' in props.reactiveData 33 | 34 | loading.value = true 35 | if (title && hasSlug) { 36 | if (!hasSlug) { 37 | return 38 | } 39 | const { slug } = await RESTManager.api.ai.writer.generate 40 | .post<{ 41 | slug: string 42 | }>({ 43 | data: { 44 | type: AiQueryType.Title, 45 | title, 46 | }, 47 | }) 48 | .finally(() => { 49 | loading.value = false 50 | }) 51 | 52 | props.reactiveData.slug = slug 53 | } else if (text) { 54 | const aiResult = await RESTManager.api.ai.writer.generate 55 | .post<{ 56 | slug: string 57 | title: string 58 | }>({ 59 | data: { 60 | type: AiQueryType.TitleSlug, 61 | text, 62 | }, 63 | }) 64 | .finally(() => { 65 | loading.value = false 66 | }) 67 | 68 | props.reactiveData.title = aiResult.title 69 | 70 | if (hasSlug) props.reactiveData.slug = aiResult.slug 71 | } 72 | } 73 | return () => { 74 | return ( 75 | 76 | {{ 77 | default() { 78 | return 'AI 生成标题或者 Slug' 79 | }, 80 | trigger() { 81 | return ( 82 | 93 | {!loading.value && } 94 | 95 | ) 96 | }, 97 | }} 98 | 99 | ) 100 | } 101 | }, 102 | }) 103 | -------------------------------------------------------------------------------- /src/components/avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import Avatar from './index.vue' 2 | 3 | // eslint-disable-next-line import/no-default-export 4 | export { Avatar, Avatar as default } 5 | -------------------------------------------------------------------------------- /src/components/avatar/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 53 | 54 | 72 | -------------------------------------------------------------------------------- /src/components/button/rounded-button.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NPopover } from 'naive-ui' 2 | import { defineComponent } from 'vue' 3 | import { RouterLink } from 'vue-router' 4 | import { Icon } from '@vicons/utils' 5 | import type { ButtonHTMLAttributes, PropType } from 'vue' 6 | import type { RouteLocationRaw } from 'vue-router' 7 | 8 | export type ButtonType = PropType< 9 | 'primary' | 'info' | 'success' | 'warning' | 'error' 10 | > 11 | 12 | export const baseButtonProps = { 13 | variant: { 14 | type: String as ButtonType, 15 | default: 'primary', 16 | }, 17 | color: { 18 | type: String, 19 | }, 20 | onClick: { 21 | type: Function as any as PropType, 22 | }, 23 | disabled: { 24 | type: Boolean, 25 | }, 26 | } 27 | export const RoundedButton = defineComponent({ 28 | props: baseButtonProps, 29 | setup(props, { slots }) { 30 | return () => { 31 | return ( 32 | 39 | {slots} 40 | 41 | ) 42 | } 43 | }, 44 | }) 45 | 46 | export const HeaderActionButton = defineComponent({ 47 | props: { 48 | ...baseButtonProps, 49 | to: { 50 | type: [Object, String] as PropType, 51 | }, 52 | name: { 53 | type: String, 54 | }, 55 | icon: { 56 | type: Object as PropType, 57 | required: true, 58 | }, 59 | }, 60 | setup(props) { 61 | const Inner = () => ( 62 | 69 | {props.icon} 70 | 71 | ) 72 | const WrapInfo = () => 73 | props.name ? ( 74 | 75 | {{ 76 | trigger() { 77 | return 78 | }, 79 | default() { 80 | return props.name 81 | }, 82 | }} 83 | 84 | ) : ( 85 | 86 | ) 87 | return () => 88 | props.to ? ( 89 | 90 | 91 | 92 | ) : ( 93 | 94 | ) 95 | }, 96 | }) 97 | -------------------------------------------------------------------------------- /src/components/code-highlight/index.tsx: -------------------------------------------------------------------------------- 1 | export const CodeHighlight = defineComponent({ 2 | props: { 3 | language: { 4 | type: String, 5 | required: true, 6 | }, 7 | code: { 8 | type: String, 9 | required: true, 10 | }, 11 | }, 12 | setup(props) { 13 | const $ref = ref() 14 | 15 | onMounted(() => { 16 | import('monaco-editor').then((mo) => { 17 | mo.editor 18 | .colorize(props.code, props.language, { 19 | tabSize: 2, 20 | }) 21 | .then((res) => { 22 | $ref.value!.innerHTML = res 23 | }) 24 | }) 25 | }) 26 | return () =>
{props.code}
27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /src/components/directives/if.tsx: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | 3 | export const If = defineComponent({ 4 | props: { 5 | condition: { 6 | type: [Boolean, Function] as PropType< 7 | Boolean | ((...args: any[]) => boolean) 8 | >, 9 | required: true, 10 | }, 11 | }, 12 | setup(props, { slots }) { 13 | const render = () => { 14 | const condition = 15 | typeof props.condition === 'function' 16 | ? props.condition() 17 | : props.condition 18 | if (condition) { 19 | return slots.default?.() 20 | } 21 | } 22 | return () => render() 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/drawer/components/json-editor.tsx: -------------------------------------------------------------------------------- 1 | import { useAsyncLoadMonaco } from '~/hooks/use-async-monaco' 2 | import { NButton } from 'naive-ui' 3 | import type { PropType } from 'vue' 4 | 5 | const JSONEditorProps = { 6 | value: { 7 | type: String, 8 | required: true, 9 | }, 10 | 11 | onFinish: { 12 | type: Function as PropType<(s: string) => void>, 13 | required: true, 14 | }, 15 | } as const 16 | export const JSONEditor = defineComponent({ 17 | props: JSONEditorProps, 18 | 19 | setup(props) { 20 | const htmlRef = ref() 21 | const refValue = ref(props.value) 22 | const editor = useAsyncLoadMonaco( 23 | htmlRef, 24 | refValue, 25 | (val) => { 26 | refValue.value = val 27 | }, 28 | { 29 | language: 'json', 30 | }, 31 | ) 32 | const handleFinish = () => { 33 | props.onFinish(refValue.value) 34 | } 35 | return () => { 36 | const { Snip } = editor 37 | return ( 38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 | 提交 46 | 47 |
48 |
49 | ) 50 | } 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /src/components/editor/codemirror/codemirror.css: -------------------------------------------------------------------------------- 1 | .cm-editor { 2 | @apply bg-transparent; 3 | 4 | & .cm-gutters { 5 | @apply bg-transparent; 6 | } 7 | 8 | & { 9 | @apply h-full; 10 | } 11 | 12 | .cm-editor { 13 | .cm-gutters, 14 | .cm-activeLineGutter, 15 | .cm-activeLine { 16 | @apply transition-colors duration-500; 17 | } 18 | } 19 | 20 | &.cm-focused { 21 | @apply !outline-none; 22 | } 23 | 24 | & .cm-gutters, 25 | & .cm-gutter { 26 | @apply !min-w-[3rem]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/editor/codemirror/codemirror.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/no-setup-props-destructure */ 2 | import { useSaveConfirm } from '~/hooks/use-save-confirm' 3 | import { defineComponent } from 'vue' 4 | import type { EditorState } from '@codemirror/state' 5 | import type { PropType } from 'vue' 6 | 7 | import styles from '../universal/editor.module.css' 8 | import { editorBaseProps } from '../universal/props' 9 | 10 | import './codemirror.css' 11 | 12 | import { useCodeMirror } from './use-codemirror' 13 | 14 | export const CodemirrorEditor = defineComponent({ 15 | name: 'CodemirrorEditor', 16 | props: { 17 | ...editorBaseProps, 18 | onStateChange: { 19 | type: Function as PropType<(state: EditorState) => void>, 20 | required: false, 21 | }, 22 | className: { 23 | type: String, 24 | }, 25 | }, 26 | setup(props, { expose }) { 27 | const [refContainer, editorView] = useCodeMirror({ 28 | initialDoc: props.text, 29 | onChange: (state) => { 30 | props.onChange(state.doc.toString()) 31 | props.onStateChange?.(state) 32 | }, 33 | }) 34 | 35 | watch( 36 | () => props.text, 37 | (n) => { 38 | const editor = editorView.value 39 | 40 | if (editor && n != editor.state.doc.toString()) { 41 | editor.dispatch({ 42 | changes: { from: 0, to: editor.state.doc.length, insert: n }, 43 | }) 44 | } 45 | }, 46 | ) 47 | 48 | expose({ 49 | setValue: (value: string) => { 50 | const editor = editorView.value 51 | if (editor) { 52 | editor.dispatch({ 53 | changes: { from: 0, to: editor.state.doc.length, insert: value }, 54 | }) 55 | } 56 | }, 57 | }) 58 | 59 | const memoedText = props.text 60 | 61 | useSaveConfirm( 62 | props.unSaveConfirm, 63 | () => memoedText === editorView.value?.state.doc.toString(), 64 | ) 65 | 66 | return () => ( 67 |
68 | ) 69 | }, 70 | }) 71 | -------------------------------------------------------------------------------- /src/components/editor/codemirror/extension.ts: -------------------------------------------------------------------------------- 1 | import type { Extension } from '@codemirror/state' 2 | 3 | import { markdown, markdownLanguage } from '@codemirror/lang-markdown' 4 | import { languages } from '@codemirror/language-data' 5 | import { Compartment } from '@codemirror/state' 6 | 7 | const extensionMap = { 8 | theme: new Compartment(), 9 | language: new Compartment(), 10 | fonts: new Compartment(), 11 | } 12 | 13 | export const codemirrorReconfigureExtension: Extension[] = [ 14 | extensionMap.theme.of([]), 15 | extensionMap.language.of( 16 | markdown({ 17 | base: markdownLanguage, 18 | codeLanguages: languages, 19 | addKeymap: true, 20 | }), 21 | ), 22 | extensionMap.fonts.of([]), 23 | ] 24 | 25 | export { extensionMap as codemirrorReconfigureExtensionMap } 26 | -------------------------------------------------------------------------------- /src/components/editor/codemirror/syntax-highlight.ts: -------------------------------------------------------------------------------- 1 | import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' 2 | import { EditorView } from '@codemirror/view' 3 | import { tags } from '@lezer/highlight' 4 | 5 | import { monospaceFonts } from './use-auto-fonts' 6 | import type { Extension } from '@codemirror/state' 7 | 8 | export const syntaxHighlightingStyle = HighlightStyle.define([ 9 | { 10 | tag: tags.heading1, 11 | fontSize: '1.4em', 12 | fontWeight: 'bold', 13 | }, 14 | { 15 | tag: tags.heading2, 16 | fontSize: '1.3em', 17 | fontWeight: 'bold', 18 | }, 19 | { 20 | tag: tags.heading3, 21 | fontSize: '1.2em', 22 | fontWeight: 'bold', 23 | }, 24 | { 25 | tag: tags.heading4, 26 | fontSize: '1.1em', 27 | fontWeight: 'bold', 28 | }, 29 | { 30 | tag: tags.heading5, 31 | fontSize: '1.1em', 32 | fontWeight: 'bold', 33 | }, 34 | { 35 | tag: tags.heading6, 36 | fontSize: '1.1em', 37 | fontWeight: 'bold', 38 | }, 39 | { tag: tags.strong, fontWeight: 'bold' }, 40 | { tag: tags.emphasis, fontStyle: 'italic' }, 41 | { tag: tags.deleted, textDecoration: 'line-through' }, 42 | { 43 | tag: tags.url, 44 | fontWeight: 'bold', 45 | textDecoration: 'underline', 46 | }, 47 | { 48 | tag: tags.link, 49 | textDecoration: 'underline', 50 | fontWeight: '500', 51 | }, 52 | ]) 53 | 54 | export const syntaxTheme: Extension = [ 55 | EditorView.theme({ 56 | '.cm-scroller': { 57 | fontFamily: monospaceFonts, 58 | }, 59 | }), 60 | syntaxHighlighting(syntaxHighlightingStyle), 61 | ] 62 | -------------------------------------------------------------------------------- /src/components/editor/codemirror/use-auto-fonts.ts: -------------------------------------------------------------------------------- 1 | import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' 2 | import { tags } from '@lezer/highlight' 3 | 4 | import { useEditorConfig } from '../universal/use-editor-setting' 5 | import { codemirrorReconfigureExtensionMap } from './extension' 6 | import type { EditorView } from '@codemirror/view' 7 | 8 | export const monospaceFonts = `"OperatorMonoSSmLig Nerd Font","Cascadia Code PL","FantasqueSansMono Nerd Font","operator mono","Fira code Retina","Fira code","Consolas", Monaco, "Hannotate SC", monospace, -apple-system` 9 | 10 | const markdownTags = [ 11 | tags.heading1, 12 | tags.heading2, 13 | tags.heading3, 14 | tags.heading4, 15 | tags.heading5, 16 | tags.heading6, 17 | tags.strong, 18 | tags.emphasis, 19 | tags.deleted, 20 | tags.content, 21 | tags.url, 22 | tags.link, 23 | ] 24 | 25 | export const useCodeMirrorConfigureFonts = ( 26 | editorView: Ref, 27 | ) => { 28 | const { general } = useEditorConfig() 29 | 30 | watch( 31 | () => [general.setting.fontFamily, editorView.value], 32 | ([fontFamily]) => { 33 | if (!editorView.value) return 34 | const sansFonts = fontFamily || 'var(--sans-font)' 35 | 36 | const fontStyles = HighlightStyle.define([ 37 | { 38 | tag: [tags.processingInstruction, tags.monospace], 39 | fontFamily: monospaceFonts, 40 | }, 41 | { tag: markdownTags, fontFamily: sansFonts }, 42 | ]) 43 | 44 | editorView.value.dispatch({ 45 | effects: [ 46 | codemirrorReconfigureExtensionMap.fonts.reconfigure([ 47 | syntaxHighlighting(fontStyles), 48 | ]), 49 | ], 50 | }) 51 | }, 52 | { 53 | immediate: true, 54 | flush: 'post', 55 | }, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/editor/codemirror/use-auto-theme.ts: -------------------------------------------------------------------------------- 1 | import { useStoreRef } from '~/hooks/use-store-ref' 2 | import { UIStore } from '~/stores/ui' 3 | 4 | import { oneDark } from '@codemirror/theme-one-dark' 5 | import { githubLight } from '@ddietr/codemirror-themes/theme/github-light' 6 | 7 | import { codemirrorReconfigureExtensionMap } from './extension' 8 | import type { Ref } from 'vue' 9 | import type { EditorView } from '@codemirror/view/dist' 10 | 11 | export const useCodeMirrorAutoToggleTheme = ( 12 | view: Ref, 13 | ) => { 14 | const { isDark } = useStoreRef(UIStore) 15 | watch( 16 | [isDark, view], 17 | ([isDark]) => { 18 | if (!view.value) { 19 | return 20 | } 21 | 22 | if (isDark) { 23 | view.value.dispatch({ 24 | effects: [ 25 | codemirrorReconfigureExtensionMap.theme.reconfigure(oneDark), 26 | ], 27 | }) 28 | } else { 29 | view.value.dispatch({ 30 | effects: [ 31 | codemirrorReconfigureExtensionMap.theme.reconfigure(githubLight), 32 | ], 33 | }) 34 | } 35 | }, 36 | 37 | { immediate: true, flush: 'post' }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/editor/monaco/theme/light.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "vs", 3 | "inherit": true, 4 | "rules": [ 5 | { 6 | "background": "FFFFFF", 7 | "token": "" 8 | }, 9 | { 10 | "foreground": "008e00", 11 | "token": "comment" 12 | }, 13 | { 14 | "foreground": "7d4726", 15 | "token": "meta.preprocessor" 16 | }, 17 | { 18 | "foreground": "7d4726", 19 | "token": "keyword.control.import" 20 | }, 21 | { 22 | "foreground": "df0002", 23 | "token": "string" 24 | }, 25 | { 26 | "foreground": "3a00dc", 27 | "token": "constant.numeric" 28 | }, 29 | { 30 | "foreground": "c800a4", 31 | "token": "constant.language" 32 | }, 33 | { 34 | "foreground": "275a5e", 35 | "token": "constant.character" 36 | }, 37 | { 38 | "foreground": "275a5e", 39 | "token": "constant.other" 40 | }, 41 | { 42 | "foreground": "c800a4", 43 | "token": "variable.language" 44 | }, 45 | { 46 | "foreground": "c800a4", 47 | "token": "variable.other" 48 | }, 49 | { 50 | "foreground": "c800a4", 51 | "token": "keyword" 52 | }, 53 | { 54 | "foreground": "c900a4", 55 | "token": "storage" 56 | }, 57 | { 58 | "foreground": "438288", 59 | "token": "entity.name.class" 60 | }, 61 | { 62 | "foreground": "790ead", 63 | "token": "entity.name.tag" 64 | }, 65 | { 66 | "foreground": "450084", 67 | "token": "entity.other.attribute-name" 68 | }, 69 | { 70 | "foreground": "450084", 71 | "token": "support.function" 72 | }, 73 | { 74 | "foreground": "450084", 75 | "token": "support.constant" 76 | }, 77 | { 78 | "foreground": "790ead", 79 | "token": "support.type" 80 | }, 81 | { 82 | "foreground": "790ead", 83 | "token": "support.class" 84 | }, 85 | { 86 | "foreground": "790ead", 87 | "token": "support.other.variable" 88 | } 89 | ], 90 | "colors": { 91 | "editor.foreground": "#000000", 92 | "editor.background": "#FFFFFF00", 93 | "editor.selectionBackground": "#B5D5FF", 94 | "editor.lineHighlightBackground": "#00000012", 95 | "editorCursor.foreground": "#000000", 96 | "editorWhitespace.foreground": "#BFBFBF" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/editor/monaco/use-define-theme.ts: -------------------------------------------------------------------------------- 1 | import { useStoreRef } from '~/hooks/use-store-ref' 2 | import { UIStore } from '~/stores/ui' 3 | 4 | import Dark from './theme/dark.json' 5 | import Light from './theme/light.json' 6 | 7 | const set = new Set() 8 | export const useDefineTheme = ( 9 | theme: string, 10 | json: any, 11 | cb: (m: any) => any, 12 | ) => { 13 | if (set.has(theme)) { 14 | return 15 | } 16 | 17 | onMounted(() => { 18 | import('monaco-editor').then((monaco) => { 19 | monaco.editor.defineTheme(theme, json) 20 | set.add(theme) 21 | 22 | cb(monaco) 23 | }) 24 | }) 25 | } 26 | 27 | export const useDefineMyThemes = () => { 28 | const ui = useStoreRef(UIStore) 29 | const isDark = ui.isDark 30 | const cb = (monaco: any) => { 31 | if (isDark.value) { 32 | monaco.editor.setTheme('dark') 33 | } else { 34 | monaco.editor.setTheme('light') 35 | } 36 | } 37 | useDefineTheme('light', Light, cb) 38 | useDefineTheme('dark', Dark, cb) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/editor/plain/plain.tsx: -------------------------------------------------------------------------------- 1 | import { useSaveConfirm } from '~/hooks/use-save-confirm' 2 | import { NInput } from 'naive-ui' 3 | import { defineComponent, ref, toRaw, watch } from 'vue' 4 | 5 | import { editorBaseProps } from '../universal/props' 6 | import { useEditorConfig } from '../universal/use-editor-setting' 7 | import type { HTMLAttributes, PropType } from 'vue' 8 | 9 | export const PlainEditor = defineComponent({ 10 | props: { 11 | ...editorBaseProps, 12 | wrapperProps: { 13 | type: Object as PropType, 14 | required: false, 15 | }, 16 | }, 17 | setup(props) { 18 | const textRef = ref() 19 | 20 | let memoInitialValue: string = toRaw(props.text) 21 | 22 | watch( 23 | () => props.text, 24 | (n) => { 25 | if (!memoInitialValue && n) { 26 | memoInitialValue = n 27 | } 28 | }, 29 | ) 30 | 31 | useSaveConfirm(props.unSaveConfirm, () => memoInitialValue === props.text) 32 | const { general } = useEditorConfig() 33 | const handleKeydown = (e: KeyboardEvent) => { 34 | const autocorrect = general.setting.autocorrect 35 | if (!autocorrect) { 36 | return 37 | } 38 | if (e.key === 'Enter') { 39 | import('@huacnlee/autocorrect') 40 | .then(({ format }) => { 41 | const newValue = format((e.target as HTMLInputElement).value) 42 | 43 | props.onChange(newValue) 44 | }) 45 | .catch(() => { 46 | console.log('not support wasm') 47 | }) 48 | } 49 | } 50 | return () => ( 51 |
52 | void props.onChange(e)} 57 | value={props.text} 58 | class="h-full" 59 | > 60 |
61 | ) 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/editor/universal/constants.ts: -------------------------------------------------------------------------------- 1 | export const EditorStorageKeys = { 2 | editor: 'editor-pref', 3 | general: 'editor-general', 4 | } as const 5 | 6 | export enum Editor { 7 | codemirror = 'codemirror', 8 | 9 | plain = 'plain', 10 | } 11 | -------------------------------------------------------------------------------- /src/components/editor/universal/editor-config.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsInt, IsString } from 'class-validator' 2 | 3 | export class GeneralSettingDto { 4 | @IsInt() 5 | fontSize = 14 6 | 7 | @IsString() 8 | fontFamily = 9 | '"Helvetica Neue","Luxi Sans","DejaVu Sans","Hiragino Sans GB","Microsoft Yahei",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji","Segoe UI Symbol","Android Emoji","EmojiSymbols"' 10 | @IsBoolean() 11 | autocorrect = false 12 | } 13 | -------------------------------------------------------------------------------- /src/components/editor/universal/editor.module.css: -------------------------------------------------------------------------------- 1 | .editor.loading { 2 | @apply flex w-full items-center justify-center; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/editor/universal/index.css: -------------------------------------------------------------------------------- 1 | .editor-wrapper { 2 | height: calc(100vh - 18.8rem); 3 | @apply relative; 4 | 5 | > div { 6 | @apply h-full; 7 | } 8 | 9 | textarea, 10 | .n-input__placeholder { 11 | font-size: var(--editor-font-size) !important; 12 | font-family: var(--editor-font-family) !important; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/editor/universal/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | 3 | export const editorBaseProps = { 4 | text: { 5 | type: String, 6 | required: true, 7 | }, 8 | onChange: { 9 | type: Function as PropType<(value: string) => void>, 10 | required: true, 11 | }, 12 | 13 | unSaveConfirm: { 14 | type: Boolean, 15 | default: true, 16 | }, 17 | } as const 18 | -------------------------------------------------------------------------------- /src/components/editor/universal/reset-icon-button.tsx: -------------------------------------------------------------------------------- 1 | import { RefreshCircle } from '~/components/icons' 2 | import { NButton, NPopover } from 'naive-ui' 3 | 4 | import { Icon } from '@vicons/utils' 5 | 6 | export const ResetIconButton = defineComponent({ 7 | props: { 8 | resetFn: { 9 | type: Function, 10 | required: true, 11 | }, 12 | }, 13 | setup(props) { 14 | return () => ( 15 | 16 | {{ 17 | trigger() { 18 | return ( 19 | props.resetFn()}> 20 | 24 | 25 | 26 | 27 | ) 28 | }, 29 | default() { 30 | return '将会重置这些设定' 31 | }, 32 | }} 33 | 34 | ) 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/editor/universal/types.tsx: -------------------------------------------------------------------------------- 1 | export type EditorRef = { 2 | setValue: (value: string) => void 3 | } 4 | -------------------------------------------------------------------------------- /src/components/editor/universal/use-editor-setting.tsx: -------------------------------------------------------------------------------- 1 | import { useStorageObject } from '~/hooks/use-storage' 2 | import { NFormItem, NH5, NInput, NInputNumber, NSwitch } from 'naive-ui' 3 | 4 | import { GeneralSettingDto } from './editor-config' 5 | import { ResetIconButton } from './reset-icon-button' 6 | 7 | const StorageKeys = { 8 | editor: 'editor-pref', 9 | general: 'editor-general', 10 | } as const 11 | export const useEditorConfig = () => { 12 | const { 13 | storage: generalSetting, 14 | reset: resetGeneralSetting, 15 | destory: generalDestory, 16 | } = useStorageObject(GeneralSettingDto, StorageKeys.general) 17 | 18 | const destory = () => { 19 | generalDestory() 20 | } 21 | 22 | const GeneralSetting = defineComponent(() => { 23 | return () => ( 24 | 25 | 26 | 通用设置 27 | 28 | 29 | 30 | void (generalSetting.fontFamily = e)} 32 | value={generalSetting.fontFamily} 33 | /> 34 | 35 | 36 | void (generalSetting.fontSize = e ?? 14)} 38 | value={generalSetting.fontSize} 39 | /> 40 | 41 | 42 | 43 | void (generalSetting.autocorrect = e)} 46 | /> 47 | 48 | 49 | ) 50 | }) 51 | 52 | return { 53 | general: { 54 | setting: generalSetting, 55 | resetSetting: resetGeneralSetting, 56 | Panel: GeneralSetting, 57 | }, 58 | 59 | destory, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/lib.declare.ts: -------------------------------------------------------------------------------- 1 | export namespace globalTypeDeclare { 2 | export const libSource = ` 3 | `.trim() 4 | 5 | export const libUri = 'ts:filename/extends.d.ts' 6 | } 7 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node.declare.ts: -------------------------------------------------------------------------------- 1 | const nodeDeclare = import.meta.glob('./node/*.d.ts', { 2 | as: 'raw', 3 | }) 4 | 5 | const urlSourceMap: Record = {} 6 | for (const path in nodeDeclare) { 7 | const module = await nodeDeclare[path]() 8 | urlSourceMap[`ts:node/${path.split('/').pop()}`] = module 9 | } 10 | 11 | export { urlSourceMap as NodeDeclare } 12 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > `npm install --save @types/node` 4 | 5 | # Summary 6 | 7 | This package contains type definitions for Node.js (http://nodejs.org/). 8 | 9 | # Details 10 | 11 | Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node/v14. 12 | 13 | ### Additional Details 14 | 15 | - Last updated: Wed, 12 May 2021 19:31:27 GMT 16 | - Dependencies: none 17 | - Global values: `Buffer`, `__dirname`, `__filename`, `clearImmediate`, `clearInterval`, `clearTimeout`, `console`, `exports`, `global`, `module`, `process`, `queueMicrotask`, `require`, `setImmediate`, `setInterval`, `setTimeout` 18 | 19 | # Credits 20 | 21 | These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [DefinitelyTyped](https://github.com/DefinitelyTyped), [Alberto Schiabel](https://github.com/jkomyno), [Alvis HT Tang](https://github.com/alvis), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [Chigozirim C.](https://github.com/smac89), [David Junger](https://github.com/touffy), [Deividas Bakanas](https://github.com/DeividasBakanas), [Eugene Y. Q. Shen](https://github.com/eyqs), [Hannes Magnusson](https://github.com/Hannes-Magnusson-CK), [Hoàng Văn Khải](https://github.com/KSXGitHub), [Huw](https://github.com/hoo29), [Kelvin Jin](https://github.com/kjin), [Klaus Meinhardt](https://github.com/ajafff), [Lishude](https://github.com/islishude), [Mariusz Wiktorczyk](https://github.com/mwiktorczyk), [Mohsen Azimi](https://github.com/mohsen1), [Nicolas Even](https://github.com/n-e), [Nikita Galkin](https://github.com/galkin), [Parambir Singh](https://github.com/parambirs), [Sebastian Silbermann](https://github.com/eps1lon), [Simon Schick](https://github.com/SimonSchick), [Thomas den Hollander](https://github.com/ThomasdenH), [Wilco Bakker](https://github.com/WilcoBakker), [wwwy3y3](https://github.com/wwwy3y3), [Samuel Ainsworth](https://github.com/samuela), [Kyle Uehlein](https://github.com/kuehlein), [Thanik Bhongbhibhat](https://github.com/bhongy), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Minh Son Nguyen](https://github.com/nguymin4), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Surasak Chaisurin](https://github.com/Ryan-Willpower), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Jason Kwok](https://github.com/JasonHK), [Victor Perin](https://github.com/victorperin), and [Yongsheng Zhang](https://github.com/ZYSzys). 22 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/base.d.ts: -------------------------------------------------------------------------------- 1 | // NOTE: These definitions support NodeJS and TypeScript 3.7. 2 | 3 | // NOTE: TypeScript version-specific augmentations can be found in the following paths: 4 | // - ~/base.d.ts - Shared definitions common to all TypeScript versions 5 | // - ~/index.d.ts - Definitions specific to TypeScript 2.1 6 | // - ~/ts3.7/base.d.ts - Definitions specific to TypeScript 3.7 7 | // - ~/ts3.7/index.d.ts - Definitions specific to TypeScript 3.7 with assert pulled in 8 | 9 | // Reference required types from the default lib: 10 | /// 11 | /// 12 | /// 13 | /// 14 | 15 | // Base definitions for all NodeJS modules that are not specific to any version of TypeScript: 16 | /// 17 | 18 | // TypeScript 3.7-specific augmentations: 19 | /// 20 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/buffer.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'buffer' { 2 | export const INSPECT_MAX_BYTES: number; 3 | export const kMaxLength: number; 4 | export const kStringMaxLength: number; 5 | export const constants: { 6 | MAX_LENGTH: number; 7 | MAX_STRING_LENGTH: number; 8 | }; 9 | const BuffType: typeof Buffer; 10 | 11 | export type TranscodeEncoding = "ascii" | "utf8" | "utf16le" | "ucs2" | "latin1" | "binary"; 12 | 13 | export function transcode(source: Uint8Array, fromEnc: TranscodeEncoding, toEnc: TranscodeEncoding): Buffer; 14 | 15 | export const SlowBuffer: { 16 | /** @deprecated since v6.0.0, use `Buffer.allocUnsafeSlow()` */ 17 | new(size: number): Buffer; 18 | prototype: Buffer; 19 | }; 20 | 21 | export { BuffType as Buffer }; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/constants.d.ts: -------------------------------------------------------------------------------- 1 | /** @deprecated since v6.3.0 - use constants property exposed by the relevant module instead. */ 2 | declare module '~/constants' { 3 | import { constants as osConstants, SignalConstants } from 'os' 4 | import { constants as cryptoConstants } from 'crypto' 5 | import { constants as fsConstants } from 'fs' 6 | 7 | const exp: typeof osConstants.errno & 8 | typeof osConstants.priority & 9 | SignalConstants & 10 | typeof cryptoConstants & 11 | typeof fsConstants 12 | export = exp 13 | } 14 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/domain.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'domain' { 2 | import EventEmitter = require('events') 3 | 4 | global { 5 | namespace NodeJS { 6 | interface Domain extends EventEmitter { 7 | run(fn: (...args: any[]) => T, ...args: any[]): T 8 | add(emitter: EventEmitter | Timer): void 9 | remove(emitter: EventEmitter | Timer): void 10 | bind(cb: T): T 11 | intercept(cb: T): T 12 | } 13 | } 14 | } 15 | 16 | interface Domain extends NodeJS.Domain {} 17 | class Domain extends EventEmitter { 18 | members: Array 19 | enter(): void 20 | exit(): void 21 | } 22 | 23 | function create(): Domain 24 | } 25 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/globals.global.d.ts: -------------------------------------------------------------------------------- 1 | declare var global: NodeJS.Global & typeof globalThis 2 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/querystring.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'querystring' { 2 | interface StringifyOptions { 3 | encodeURIComponent?: (str: string) => string 4 | } 5 | 6 | interface ParseOptions { 7 | maxKeys?: number 8 | decodeURIComponent?: (str: string) => string 9 | } 10 | 11 | interface ParsedUrlQuery extends NodeJS.Dict {} 12 | 13 | interface ParsedUrlQueryInput 14 | extends NodeJS.Dict< 15 | | string 16 | | number 17 | | boolean 18 | | ReadonlyArray 19 | | ReadonlyArray 20 | | ReadonlyArray 21 | | null 22 | > {} 23 | 24 | function stringify( 25 | obj?: ParsedUrlQueryInput, 26 | sep?: string, 27 | eq?: string, 28 | options?: StringifyOptions, 29 | ): string 30 | function parse( 31 | str: string, 32 | sep?: string, 33 | eq?: string, 34 | options?: ParseOptions, 35 | ): ParsedUrlQuery 36 | /** 37 | * The querystring.encode() function is an alias for querystring.stringify(). 38 | */ 39 | const encode: typeof stringify 40 | /** 41 | * The querystring.decode() function is an alias for querystring.parse(). 42 | */ 43 | const decode: typeof parse 44 | function escape(str: string): string 45 | function unescape(str: string): string 46 | } 47 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/string_decoder.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'string_decoder' { 2 | class StringDecoder { 3 | constructor(encoding?: BufferEncoding) 4 | write(buffer: Buffer): string 5 | end(buffer?: Buffer): string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/timers.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'timers' { 2 | function setTimeout( 3 | callback: (...args: any[]) => void, 4 | ms?: number, 5 | ...args: any[] 6 | ): NodeJS.Timeout 7 | namespace setTimeout { 8 | function __promisify__(ms: number): Promise 9 | function __promisify__(ms: number, value: T): Promise 10 | } 11 | function clearTimeout(timeoutId: NodeJS.Timeout): void 12 | function setInterval( 13 | callback: (...args: any[]) => void, 14 | ms?: number, 15 | ...args: any[] 16 | ): NodeJS.Timeout 17 | function clearInterval(intervalId: NodeJS.Timeout): void 18 | function setImmediate( 19 | callback: (...args: any[]) => void, 20 | ...args: any[] 21 | ): NodeJS.Immediate 22 | namespace setImmediate { 23 | function __promisify__(): Promise 24 | function __promisify__(value: T): Promise 25 | } 26 | function clearImmediate(immediateId: NodeJS.Immediate): void 27 | } 28 | -------------------------------------------------------------------------------- /src/components/function-editor/libs/node/trace_events.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'trace_events' { 2 | /** 3 | * The `Tracing` object is used to enable or disable tracing for sets of 4 | * categories. Instances are created using the 5 | * `trace_events.createTracing()` method. 6 | * 7 | * When created, the `Tracing` object is disabled. Calling the 8 | * `tracing.enable()` method adds the categories to the set of enabled trace 9 | * event categories. Calling `tracing.disable()` will remove the categories 10 | * from the set of enabled trace event categories. 11 | */ 12 | interface Tracing { 13 | /** 14 | * A comma-separated list of the trace event categories covered by this 15 | * `Tracing` object. 16 | */ 17 | readonly categories: string 18 | 19 | /** 20 | * Disables this `Tracing` object. 21 | * 22 | * Only trace event categories _not_ covered by other enabled `Tracing` 23 | * objects and _not_ specified by the `--trace-event-categories` flag 24 | * will be disabled. 25 | */ 26 | disable(): void 27 | 28 | /** 29 | * Enables this `Tracing` object for the set of categories covered by 30 | * the `Tracing` object. 31 | */ 32 | enable(): void 33 | 34 | /** 35 | * `true` only if the `Tracing` object has been enabled. 36 | */ 37 | readonly enabled: boolean 38 | } 39 | 40 | interface CreateTracingOptions { 41 | /** 42 | * An array of trace category names. Values included in the array are 43 | * coerced to a string when possible. An error will be thrown if the 44 | * value cannot be coerced. 45 | */ 46 | categories: string[] 47 | } 48 | 49 | /** 50 | * Creates and returns a Tracing object for the given set of categories. 51 | */ 52 | function createTracing(options: CreateTracingOptions): Tracing 53 | 54 | /** 55 | * Returns a comma-separated list of all currently-enabled trace event 56 | * categories. The current set of enabled trace event categories is 57 | * determined by the union of all currently-enabled `Tracing` objects and 58 | * any categories enabled using the `--trace-event-categories` flag. 59 | */ 60 | function getEnabledCategories(): string | undefined 61 | } 62 | -------------------------------------------------------------------------------- /src/components/input/base.tsx: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | 3 | export const inputBaseProps = { 4 | type: { 5 | type: String, 6 | }, 7 | value: { 8 | type: String, 9 | required: true, 10 | }, 11 | placeholder: { 12 | type: String, 13 | }, 14 | onChange: { 15 | type: Function as PropType<(value: string) => void>, 16 | required: true, 17 | }, 18 | } as const 19 | -------------------------------------------------------------------------------- /src/components/input/material-input.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, onMounted, ref } from 'vue' 2 | 3 | import { inputBaseProps } from './base' 4 | import styles from './material.module.css' 5 | 6 | export const MaterialInput = defineComponent({ 7 | props: { 8 | ...inputBaseProps, 9 | label: { 10 | type: String, 11 | required: true, 12 | }, 13 | }, 14 | emits: ['compositionend', 'compositionstart'], 15 | setup(props, { emit }) { 16 | const inputRef = ref() 17 | 18 | onMounted(() => { 19 | if (!inputRef.value) { 20 | return 21 | } 22 | inputRef.value.addEventListener('compositionstart', () => { 23 | emit('compositionstart') 24 | }) 25 | 26 | inputRef.value.addEventListener('compositionend', () => { 27 | emit('compositionend') 28 | }) 29 | }) 30 | 31 | return () => ( 32 |
33 | props.onChange((e.target as any).value)} 39 | /> 40 | 41 | 42 |
43 | ) 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/input/material.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | * { 3 | box-sizing: border-box; 4 | } 5 | &.group { 6 | position: relative; 7 | width: 100%; 8 | } 9 | input { 10 | @apply bg-transparent; 11 | 12 | font-size: 18px; 13 | padding: 10px 10px 10px 5px; 14 | display: block; 15 | width: 100%; 16 | border: none; 17 | border-bottom: 1px solid theme(colors.primary.default); 18 | } 19 | input:focus { 20 | outline: none; 21 | } 22 | 23 | /* LABEL ======================================= */ 24 | label { 25 | color: #999; 26 | font-size: 18px; 27 | font-weight: normal; 28 | position: absolute; 29 | pointer-events: none; 30 | left: 5px; 31 | top: 10px; 32 | transition: 0.2s ease all; 33 | -moz-transition: 0.2s ease all; 34 | -webkit-transition: 0.2s ease all; 35 | } 36 | 37 | /* active state */ 38 | input:focus ~ label, 39 | input:valid ~ label { 40 | @apply text-primary-deep; 41 | 42 | top: -20px; 43 | font-size: 16px; 44 | } 45 | 46 | /* BOTTOM BARS ================================= */ 47 | .bar { 48 | position: relative; 49 | display: block; 50 | width: 100%; 51 | } 52 | .bar:before, 53 | .bar:after { 54 | @apply bg-primary-default; 55 | 56 | content: ''; 57 | height: 1px; 58 | width: 0; 59 | bottom: 1px; 60 | position: absolute; 61 | transition: 0.2s ease all; 62 | -moz-transition: 0.2s ease all; 63 | -webkit-transition: 0.2s ease all; 64 | } 65 | .bar:before { 66 | left: 50%; 67 | } 68 | .bar:after { 69 | right: 50%; 70 | } 71 | 72 | /* active state */ 73 | input:focus ~ .bar:before, 74 | input:focus ~ .bar:after { 75 | width: 50%; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/input/underline-input.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | 3 | import { inputBaseProps } from './base' 4 | import styles from './underline.module.css' 5 | 6 | export const UnderlineInput = defineComponent({ 7 | props: { ...inputBaseProps, autoShrink: { type: Boolean, default: true } }, 8 | setup(props) { 9 | return () => ( 10 |
16 | { 22 | props.onChange((e.target as any).value) 23 | }} 24 | /> 25 | {props.value}  26 |
27 | ) 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/input/underline.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply relative inline-flex items-center; 3 | 4 | line-height: 1.4; 5 | transition: border 0.5s; 6 | box-sizing: border-box; 7 | 8 | > span { 9 | @apply whitespace-nowrap; 10 | } 11 | 12 | > input { 13 | box-sizing: border-box; 14 | 15 | @apply w-full bg-transparent; 16 | outline: 0; 17 | display: inline-block; 18 | border-bottom: 1px solid currentColor; 19 | &:hover, 20 | &:focus { 21 | border-bottom: 1px solid theme(colors.primary.default); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ip-info/index.tsx: -------------------------------------------------------------------------------- 1 | import { NPopover } from 'naive-ui' 2 | import { RESTManager } from '~/utils' 3 | import { defineComponent, ref } from 'vue' 4 | import type { PopoverTrigger } from 'naive-ui' 5 | import type { PropType } from 'vue' 6 | 7 | const ipLocationCacheMap = new Map() 8 | 9 | export const IpInfoPopover = defineComponent({ 10 | props: { 11 | ip: { 12 | type: String, 13 | required: true, 14 | }, 15 | triggerEl: { 16 | type: [Object, Function] as PropType<(() => VNode) | VNode>, 17 | required: true, 18 | }, 19 | trigger: { 20 | type: String as PropType, 21 | default: 'click', 22 | }, 23 | }, 24 | setup(props) { 25 | const ipInfoText = ref('获取中..') 26 | const setIpInfoText = (info: IP) => { 27 | ipInfoText.value = `IP: ${info.ip}
28 | 城市:${ 29 | [info.countryName, info.regionName, info.cityName] 30 | .filter(Boolean) 31 | .join(' - ') || 'N/A' 32 | }
33 | ISP: ${info.ispDomain || 'N/A'}
34 | 组织:${info.ownerDomain || 'N/A'}
35 | 范围:${info.range ? Object.values(info.range).join(' - ') : 'N/A'} 36 | ` 37 | } 38 | const resetIpInfoText = () => (ipInfoText.value = '获取中..') 39 | 40 | const onIpInfoShow = async (show: boolean, ip: string) => { 41 | if (!ip) { 42 | return 43 | } 44 | if (show) { 45 | if (ipLocationCacheMap.has(ip)) { 46 | const ipInfo = ipLocationCacheMap.get(ip)! 47 | setIpInfoText(ipInfo) 48 | return 49 | } 50 | 51 | const data: any = await RESTManager.api.fn('built-in').ip.get({ 52 | params: { 53 | ip, 54 | }, 55 | }) 56 | 57 | setIpInfoText(data) 58 | ipLocationCacheMap.set(ip, data) 59 | } else { 60 | resetIpInfoText() 61 | } 62 | } 63 | 64 | return () => ( 65 | { 69 | if (!props.ip) { 70 | return 71 | } 72 | await onIpInfoShow(show, props.ip) 73 | }} 74 | > 75 | {{ 76 | trigger() { 77 | return typeof props.triggerEl == 'function' 78 | ? props.triggerEl() 79 | : props.triggerEl 80 | }, 81 | default() { 82 | return
83 | }, 84 | }} 85 |
86 | ) 87 | }, 88 | }) 89 | 90 | interface IP { 91 | ip: string 92 | countryName: string 93 | regionName: string 94 | cityName: string 95 | ownerDomain: string 96 | ispDomain: string 97 | range?: { 98 | from: string 99 | to: string 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/components/json-highlight/index.tsx: -------------------------------------------------------------------------------- 1 | import hljs from 'highlight.js/lib/core' 2 | import json from 'highlight.js/lib/languages/json' 3 | 4 | import 'highlight.js/styles/atom-one-dark.css' 5 | 6 | hljs.registerLanguage('json', json) 7 | 8 | export const JSONHighlight = defineComponent({ 9 | props: { 10 | code: { 11 | type: String, 12 | required: true, 13 | }, 14 | }, 15 | setup(props) { 16 | const $ref = ref() 17 | 18 | const highlight = () => { 19 | const result = hljs.highlight('json', props.code) 20 | if (!$ref.value) return 21 | 22 | $ref.value.innerHTML = result.value 23 | } 24 | onMounted(() => { 25 | highlight() 26 | }) 27 | watch( 28 | () => props.code, 29 | () => { 30 | highlight() 31 | }, 32 | ) 33 | 34 | return () => { 35 | return ( 36 |

43 |       )
44 |     }
45 |   },
46 | })
47 | 


--------------------------------------------------------------------------------
/src/components/location/get-location-button.tsx:
--------------------------------------------------------------------------------
 1 | import { LocationIcon } from '~/components/icons'
 2 | import { NButton, useMessage } from 'naive-ui'
 3 | import { RESTManager } from '~/utils/rest'
 4 | import { defineComponent, ref } from 'vue'
 5 | import { Icon } from '@vicons/utils'
 6 | import type { Amap, Regeocode } from '~/models/amap'
 7 | import type { PropType } from 'vue'
 8 | 
 9 | export const GetLocationButton = defineComponent({
10 |   props: {
11 |     onChange: {
12 |       type: Function as PropType<
13 |         (amap: Regeocode, coordinates: readonly [number, number]) => any
14 |       >,
15 |       required: true,
16 |     },
17 |   },
18 |   setup(props) {
19 |     const message = useMessage()
20 |     const loading = ref(false)
21 |     const handleGetLocation = async () => {
22 |       const GetGeo = () =>
23 |         new Promise((r, j) => {
24 |           navigator.geolocation.getCurrentPosition(
25 |             (pos) => {
26 |               loading.value = true
27 |               r(pos)
28 |               loading.value = false
29 |             },
30 |             (err) => {
31 |               loading.value = false
32 |               j(err)
33 |             },
34 |           )
35 |         })
36 |       if (navigator.geolocation) {
37 |         try {
38 |           const coordinates = await GetGeo()
39 |           // console.log(coordinates)
40 | 
41 |           const {
42 |             coords: { latitude, longitude },
43 |           } = coordinates
44 | 
45 |           const coo = [longitude, latitude] as const
46 |           const res = await RESTManager.api
47 |             .fn('built-in')
48 |             .geocode_location.get({
49 |               params: {
50 |                 longitude,
51 |                 latitude,
52 |               },
53 |             })
54 | 
55 |           props.onChange(res.regeocode, coo)
56 |         } catch (error: any) {
57 |           console.error(error)
58 | 
59 |           if (error.code == 2) {
60 |             message.error('获取定位失败,连接超时')
61 |           } else {
62 |             message.error('定位权限未打开')
63 |           }
64 |         }
65 |       } else {
66 |         message.error('浏览器不支持定位')
67 |       }
68 |     }
69 |     return () => (
70 |       
77 |         {{
78 |           icon() {
79 |             return (
80 |               
81 |                 
82 |               
83 |             )
84 |           },
85 |           default() {
86 |             return '定位'
87 |           },
88 |         }}
89 |       
90 |     )
91 |   },
92 | })
93 | 


--------------------------------------------------------------------------------
/src/components/output-modal/normal.tsx:
--------------------------------------------------------------------------------
 1 | // transform asni to html output
 2 | import { AnsiUp } from 'ansi_up'
 3 | import { EventSourcePolyfill } from 'event-source-polyfill'
 4 | import { NCard, NModal } from 'naive-ui'
 5 | import { getToken } from '~/utils'
 6 | 
 7 | const ansi_up = new AnsiUp()
 8 | export const ShellOutputNormal = defineComponent({
 9 |   props: {
10 |     onClose: {
11 |       type: Function,
12 |     },
13 |   },
14 |   setup(props, { expose }) {
15 |     const logViewOpen = ref(false)
16 | 
17 |     const shellOutput = ref('')
18 | 
19 |     const $output = ref()
20 | 
21 |     expose({
22 |       run(ssePath: string, onFinish?: () => any) {
23 |         logViewOpen.value = true
24 |         const event = new EventSourcePolyfill(ssePath, {
25 |           headers: {
26 |             Authorization: getToken()!,
27 |           },
28 |         })
29 | 
30 |         event.onmessage = (e) => {
31 |           if (!e) {
32 |             event.close()
33 |             onFinish?.()
34 |             shellOutput.value += 'Done.'
35 |             return
36 |           }
37 |           shellOutput.value += e.data
38 | 
39 |           requestAnimationFrame(() => {
40 |             if (!$output.value) {
41 |               return
42 |             }
43 |             // scroll to el end
44 |             $output.value.scrollTop = $output.value.scrollHeight
45 |           })
46 |         }
47 |         event.onerror = (e: any) => {
48 |           event.close()
49 |           if (e?.data) {
50 |             message.error(e.data)
51 |           } else {
52 |             console.error(e)
53 |           }
54 |         }
55 |       },
56 |     })
57 | 
58 |     watch(
59 |       () => logViewOpen.value,
60 |       (open) => {
61 |         if (!open) {
62 |           shellOutput.value = ''
63 |         }
64 |       },
65 |     )
66 | 
67 |     return () => (
68 |        {
71 |           logViewOpen.value = s
72 | 
73 |           if (!s) {
74 |             props.onClose?.()
75 |           }
76 |         }}
77 |         transformOrigin="center"
78 |       >
79 |         
80 |           
81 |
89 |
90 |
91 |
92 | ) 93 | }, 94 | }) 95 | -------------------------------------------------------------------------------- /src/components/output-modal/xterm.tsx: -------------------------------------------------------------------------------- 1 | // 展示 Shell 输出的 xterm 2 | import { Xterm } from '~/components/xterm' 3 | import { EventSourcePolyfill } from 'event-source-polyfill' 4 | import { NCard, NModal, NSpin } from 'naive-ui' 5 | import { getToken } from '~/utils' 6 | import type { Terminal } from '@xterm/xterm' 7 | 8 | export const ShellOutputXterm = defineComponent({ 9 | setup(_, { expose }) { 10 | // wait for modal transition done 11 | const wait = ref(true) 12 | const logViewOpen = ref(false) 13 | watch( 14 | () => logViewOpen.value, 15 | () => { 16 | if (logViewOpen.value) { 17 | wait.value = true 18 | } else { 19 | wait.value = false 20 | } 21 | 22 | setTimeout(() => { 23 | wait.value = false 24 | }, 1000) 25 | }, 26 | ) 27 | 28 | const xtermData = ref(['']) 29 | 30 | expose({ 31 | run(ssePath: string, onFinish?: () => any) { 32 | logViewOpen.value = true 33 | const event = new EventSourcePolyfill(ssePath, { 34 | headers: { 35 | Authorization: getToken()!, 36 | }, 37 | }) 38 | 39 | event.onmessage = (e) => { 40 | if (!e) { 41 | event.close() 42 | onFinish?.() 43 | xtermData.value.push('Done.') 44 | return 45 | } 46 | xtermData.value.push(e.data) 47 | } 48 | event.onerror = (e: any) => { 49 | console.log(e.eventPhase) 50 | 51 | event.close() 52 | if (e?.data) { 53 | message.error(e.data) 54 | } else { 55 | console.error(e) 56 | } 57 | } 58 | }, 59 | }) 60 | 61 | const XtermRef = ref() 62 | watch( 63 | // @ts-expect-error 64 | () => [xtermData.value, XtermRef.value], 65 | ([dataArr, XtermRef]: [string[], Terminal]) => { 66 | if (!XtermRef) { 67 | return 68 | } 69 | if (dataArr.length > 0) { 70 | dataArr.forEach((data) => XtermRef.write(data)) 71 | xtermData.value.length = 0 72 | } 73 | }, 74 | 75 | { 76 | deep: true, 77 | }, 78 | ) 79 | 80 | return () => ( 81 | { 84 | logViewOpen.value = s 85 | }} 86 | transformOrigin="center" 87 | > 88 | 89 |
90 | {wait.value ? ( 91 |
92 | 93 |
94 | ) : ( 95 | { 98 | xtermData.value.forEach((data) => { 99 | xterm.write(data) 100 | }) 101 | 102 | xtermData.value.length = 0 103 | XtermRef.value = xterm 104 | }} 105 | > 106 | )} 107 |
108 |
109 |
110 | ) 111 | }, 112 | }) 113 | -------------------------------------------------------------------------------- /src/components/preview/index.tsx: -------------------------------------------------------------------------------- 1 | export const ArticlePreview = defineComponent({ 2 | props: { 3 | url: { 4 | type: String, 5 | required: true, 6 | }, 7 | }, 8 | setup(props) { 9 | return () => ( 10 | 11 | ) 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /src/components/shorthand/index.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NInput, NSpace, useDialog } from 'naive-ui' 2 | import { RESTManager } from '~/utils' 3 | import { ref } from 'vue' 4 | import type { RecentlyModel } from '~/models/recently' 5 | 6 | export const useShorthand = () => { 7 | const modal = useDialog() 8 | const quickHandText = ref('') 9 | 10 | return { 11 | create() { 12 | return new Promise((resolve, reject) => { 13 | const dialog = modal.create({ 14 | title: '速记', 15 | type: 'success', 16 | 17 | content() { 18 | return ( 19 | { 23 | quickHandText.value = e 24 | }} 25 | value={quickHandText.value} 26 | class="h-[300px] max-h-[80vh]" 27 | /> 28 | ) 29 | }, 30 | action() { 31 | return ( 32 | 33 | { 37 | RESTManager.api.recently 38 | .post({ 39 | data: { 40 | content: quickHandText.value, 41 | }, 42 | }) 43 | .then((res) => { 44 | quickHandText.value = '' 45 | message.success('记录成功') 46 | dialog.destroy() 47 | resolve(res) 48 | }) 49 | .catch((error) => { 50 | reject(error) 51 | }) 52 | }} 53 | > 54 | 记好了 55 | 56 | { 59 | void dialog.destroy() 60 | resolve(null) 61 | }} 62 | > 63 | 不想记了 64 | 65 | 66 | ) 67 | }, 68 | }) 69 | }) 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/sidebar/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type { ComputedGetter, InjectionKey } from 'vue' 3 | 4 | type TSidebarCollapseStatus = 5 | | 'collapsed' 6 | | 'expanded' 7 | | 'collapsing' 8 | | 'expanding' 9 | const useSidebarStatusImpl = (collapseValueRef: ComputedGetter) => { 10 | let prevValue = 11 | typeof collapseValueRef === 'function' 12 | ? collapseValueRef() 13 | : collapseValueRef 14 | 15 | const statusRef = ref( 16 | prevValue ? 'collapsed' : 'expanded', 17 | ) 18 | 19 | const collapseComputedRef = computed(collapseValueRef) 20 | watch( 21 | () => collapseComputedRef.value, 22 | () => { 23 | if (prevValue !== collapseComputedRef.value) { 24 | statusRef.value = collapseComputedRef.value ? 'collapsing' : 'expanding' 25 | prevValue = collapseComputedRef.value 26 | } 27 | }, 28 | ) 29 | 30 | const onTransitionEnd = () => { 31 | statusRef.value = collapseComputedRef.value ? 'collapsed' : 'expanded' 32 | } 33 | 34 | return { 35 | onTransitionEnd, 36 | statusRef, 37 | } as const 38 | } 39 | 40 | const injectionKey = Symbol() as InjectionKey<{ 41 | status: TSidebarCollapseStatus 42 | }> 43 | 44 | export const useSidebarStatusInjection = ( 45 | collapseValueRef: ComputedGetter, 46 | ) => { 47 | const { onTransitionEnd, statusRef } = useSidebarStatusImpl(collapseValueRef) 48 | 49 | provide(injectionKey, { 50 | status: statusRef.value, 51 | }) 52 | return { 53 | onTransitionEnd, 54 | statusRef, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/sidebar/uwu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-space/mx-admin/7c1b853374ea4ace29ce4184c6e8e86b797cd9e6/src/components/sidebar/uwu.png -------------------------------------------------------------------------------- /src/components/special-button/copy-text-button.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NIcon } from 'naive-ui' 2 | 3 | import { MingcuteCopy2Line } from '../icons' 4 | 5 | export const CopyTextButton = defineComponent({ 6 | props: { 7 | text: { 8 | type: String, 9 | required: true, 10 | }, 11 | }, 12 | setup(props) { 13 | return () => ( 14 | { 17 | navigator.clipboard.writeText(props.text) 18 | message.success('Copied to clipboard') 19 | }} 20 | text 21 | class={'ml-2'} 22 | > 23 | 24 | 25 | 26 | 27 | ) 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/special-button/delete-confirm.tsx: -------------------------------------------------------------------------------- 1 | import { HeaderActionButton } from '~/components/button/rounded-button' 2 | import { DeleteIcon } from '~/components/icons' 3 | import { useDialog, useMessage } from 'naive-ui' 4 | import type { PropType, Ref } from 'vue' 5 | 6 | type AccpetType = string[] | Set 7 | 8 | /** 9 | * 删除之后 onDelete 提示 `删除成功` 10 | */ 11 | export const DeleteConfirmButton = defineComponent({ 12 | props: { 13 | checkedRowKeys: { 14 | type: Object as PropType>, 15 | }, 16 | onDelete: { 17 | type: Function as PropType<(checkedRowKeys?: AccpetType) => any>, 18 | required: true, 19 | }, 20 | 21 | message: { type: String }, 22 | 23 | customIcon: { 24 | type: Object as PropType, 25 | }, 26 | 27 | showSuccessMessage: { 28 | type: Boolean, 29 | default: true, 30 | }, 31 | 32 | customSuccessMessage: { 33 | type: String, 34 | }, 35 | customButtonTip: { 36 | type: String, 37 | }, 38 | }, 39 | setup(props) { 40 | const dialog = useDialog() 41 | const message = useMessage() 42 | 43 | return () => { 44 | const { 45 | customIcon, 46 | checkedRowKeys: _checkedRowKeys, 47 | onDelete, 48 | message: content, 49 | customSuccessMessage, 50 | showSuccessMessage, 51 | customButtonTip, 52 | } = props 53 | 54 | const checkedRowKeys = isRef(_checkedRowKeys) 55 | ? _checkedRowKeys.value 56 | : _checkedRowKeys 57 | 58 | const size = !checkedRowKeys 59 | ? 0 60 | : Array.isArray(checkedRowKeys) 61 | ? checkedRowKeys.length 62 | : checkedRowKeys.size 63 | const disabled = !checkedRowKeys ? false : size === 0 64 | return ( 65 | { 70 | dialog.warning({ 71 | title: '警告', 72 | content: 73 | content ?? `你确定要删除${size > 1 ? '多条' : '这条'}数据?`, 74 | positiveText: 'はい', 75 | negativeText: '达咩', 76 | onPositiveClick: async () => { 77 | await onDelete(checkedRowKeys) 78 | showSuccessMessage && 79 | message.success(customSuccessMessage ?? '删除成功') 80 | }, 81 | }) 82 | }} 83 | icon={customIcon ?? } 84 | /> 85 | ) 86 | } 87 | }, 88 | }) 89 | -------------------------------------------------------------------------------- /src/components/special-button/iframe-preview.tsx: -------------------------------------------------------------------------------- 1 | import { MagnifyIcon } from '~/components/icons' 2 | import { ArticlePreview } from '~/components/preview' 3 | import { NButton, NPopover } from 'naive-ui' 4 | 5 | import { Icon } from '@vicons/utils' 6 | 7 | export const IframePreviewButton = defineComponent({ 8 | props: { 9 | path: { 10 | type: String, 11 | required: true, 12 | }, 13 | }, 14 | setup(props) { 15 | return () => ( 16 | 17 | {{ 18 | default() { 19 | return 20 | }, 21 | trigger() { 22 | return ( 23 | { 32 | e.stopPropagation() 33 | }} 34 | > 35 | 36 | 37 | 38 | 39 | ) 40 | }, 41 | }} 42 | 43 | ) 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/spin/index.tsx: -------------------------------------------------------------------------------- 1 | import { NP, NSpace, NSpin } from 'naive-ui' 2 | 3 | export const CenterSpin = (props: { description?: string }) => ( 4 |
5 | 6 | 7 | {props.description && {props.description}} 8 | 9 |
10 | ) 11 | 12 | CenterSpin.props = ['description'] 13 | -------------------------------------------------------------------------------- /src/components/table/index.module.css: -------------------------------------------------------------------------------- 1 | /* /// table style */ 2 | 3 | .table-row { 4 | @apply py-7 leading-normal; 5 | 6 | font-size: 12px; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/time/relative-time.tsx: -------------------------------------------------------------------------------- 1 | import { NPopover } from 'naive-ui' 2 | import { parseDate, relativeTimeFromNow } from '~/utils/time' 3 | import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue' 4 | 5 | const _RelativeTime = defineComponent({ 6 | props: { 7 | time: { 8 | type: [String, Date], 9 | required: true, 10 | }, 11 | }, 12 | setup(props) { 13 | const time = ref(relativeTimeFromNow(props.time)) 14 | let timer: ReturnType | undefined | void 15 | onMounted(() => { 16 | if (!props.time) { 17 | return 18 | } 19 | timer = setInterval(() => { 20 | time.value = relativeTimeFromNow(props.time) 21 | }, 1000) 22 | }) 23 | 24 | onBeforeUnmount(() => { 25 | timer && (timer = clearInterval(timer)) 26 | }) 27 | 28 | return () => time.value 29 | }, 30 | }) 31 | 32 | export const RelativeTime = defineComponent({ 33 | props: { 34 | time: { 35 | type: [String, Date], 36 | required: true, 37 | }, 38 | showPopoverInfoAbsoluteTime: { 39 | type: Boolean, 40 | default: true, 41 | }, 42 | }, 43 | setup(props) { 44 | return () => 45 | props.showPopoverInfoAbsoluteTime ? ( 46 | 47 | {{ 48 | trigger() { 49 | return ( 50 | 51 | <_RelativeTime time={props.time} /> 52 | 53 | ) 54 | }, 55 | default() { 56 | return props.time 57 | ? parseDate(props.time, 'yyyy 年 M 月 d 日 HH:mm:ss') 58 | : '此内容自发布以来没有被修改过' 59 | }, 60 | }} 61 | 62 | ) : ( 63 | <_RelativeTime time={props.time} /> 64 | ) 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /src/components/upload/index.tsx: -------------------------------------------------------------------------------- 1 | import { NUpload, UploadOnFinish } from 'naive-ui' 2 | import { OnError } from 'naive-ui/es/upload/src/interface' 3 | import type { PropType } from 'vue' 4 | 5 | import { getToken, RESTManager } from '~/utils' 6 | 7 | export const UploadWrapper = defineComponent({ 8 | props: { 9 | onFinish: { 10 | type: Function as PropType, 11 | }, 12 | onError: { 13 | type: Function as PropType, 14 | }, 15 | type: { 16 | type: String, 17 | required: true, 18 | }, 19 | }, 20 | setup(props, { slots }) { 21 | return () => { 22 | const { onFinish, onError, type, ...rest } = props 23 | 24 | return ( 25 | { 35 | message.error('上传失败') 36 | return e.file 37 | }) 38 | } 39 | onFinish={onFinish} 40 | {...Object.fromEntries( 41 | Object.entries(rest).filter(([k, v]) => typeof v !== 'undefined'), 42 | )} 43 | > 44 | {slots.default?.()} 45 | 46 | ) 47 | } 48 | }, 49 | }) 50 | 51 | UploadWrapper.props = [...Array.from(Object.keys(NUpload.props)), 'type'] 52 | -------------------------------------------------------------------------------- /src/components/xlog-connect/index.tsx: -------------------------------------------------------------------------------- 1 | import { RESTManager } from '~/utils' 2 | 3 | export const CrossBellConnectorIndirector = defineComponent({ 4 | setup() { 5 | const siteId = ref('') 6 | 7 | RESTManager.api.options.thirdPartyServiceIntegration 8 | .get<{ 9 | data: { xLogSiteId: string } 10 | }>() 11 | .then(async ({ data }) => { 12 | const { xLogSiteId } = data 13 | siteId.value = xLogSiteId 14 | 15 | const CrossBellConnector = await import('./class').then( 16 | (mo) => mo.CrossBellConnector, 17 | ) 18 | 19 | CrossBellConnector.setSiteId(xLogSiteId) 20 | }) 21 | 22 | return () => null 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/configs.ts: -------------------------------------------------------------------------------- 1 | export const configs = { 2 | title: window.injectData.TITLE || '静かな森', 3 | } 4 | -------------------------------------------------------------------------------- /src/constants/env.ts: -------------------------------------------------------------------------------- 1 | export const WEB_URL: string = 2 | window.injectData.WEB_URL || 3 | (import.meta.env.VITE_APP_WEB_URL as string) || 4 | 'http://localhost:2323' 5 | 6 | export const bgUrl = 7 | window.injectData.LOGIN_BG || 8 | (import.meta.env.VITE_APP_LOGIN_BG as string) || 9 | localStorage.getItem('LOGIN_BG') || 10 | 'https://fastly.jsdelivr.net/gh/mx-space/docs-images@master/images/chichi-1.jpeg' 11 | 12 | export const API_URL = transformUrl( 13 | sessionStorage.getItem('__api') || 14 | localStorage.getItem('__api') || 15 | window.injectData.BASE_API || 16 | (import.meta.env.VITE_APP_BASE_API as string) || 17 | '', 18 | ) 19 | 20 | export const GATEWAY_URL = transformUrl( 21 | sessionStorage.getItem('__gateway') || 22 | localStorage.getItem('__gateway') || 23 | window.injectData.GATEWAY || 24 | import.meta.env.VITE_APP_GATEWAY || 25 | '', 26 | ) 27 | 28 | function transformUrl(url: string) { 29 | if (url === '/') return location.origin 30 | if (url.startsWith('/')) { 31 | return location.origin + url 32 | } 33 | 34 | return url.endsWith('/') ? url.slice(0, -1) : url 35 | } 36 | -------------------------------------------------------------------------------- /src/constants/keys.ts: -------------------------------------------------------------------------------- 1 | export const enum EmitKeyMap { 2 | EditDataUpdate = 'editDataUpdate', 3 | } 4 | 5 | export const SESSION_WITH_LOGIN = 'session-with-login' 6 | -------------------------------------------------------------------------------- /src/constants/note.ts: -------------------------------------------------------------------------------- 1 | export const MOOD_SET = [ 2 | '开心', 3 | '伤心', 4 | '决心', 5 | '坚定', 6 | '痛恨', 7 | '生气', 8 | '悲哀', 9 | '痛苦', 10 | '可怕', 11 | '不快', 12 | '可恶', 13 | '担心', 14 | '绝望', 15 | '焦虑', 16 | '激动', 17 | ] as const 18 | export const WEATHER_SET = ['晴', '多云', '雨', '阴', '雪', '雷雨'] as const 19 | -------------------------------------------------------------------------------- /src/constants/social.ts: -------------------------------------------------------------------------------- 1 | export const socialKeyMap = { 2 | GitHub: 'github', 3 | Weibo: 'weibo', 4 | 网易云: 'netease', 5 | 哔哩哔哩: 'bilibili', 6 | } 7 | -------------------------------------------------------------------------------- /src/external/api/github-check-update.ts: -------------------------------------------------------------------------------- 1 | import PKG from '../../../package.json' 2 | import { octokit } from './octokit' 3 | 4 | export const checkUpdateFromGitHub = async () => { 5 | const { data: system } = await octokit.rest.repos.getLatestRelease({ 6 | owner: 'mx-space', 7 | repo: 'mx-server', 8 | }) 9 | 10 | const { data: dashboard } = await octokit.rest.repos.getLatestRelease({ 11 | owner: 'mx-space', 12 | repo: PKG.name, 13 | }) 14 | 15 | return { 16 | system: system.tag_name.replace(/^v/, ''), 17 | dashboard: dashboard.tag_name.replace(/^v/, ''), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/external/api/github-mx-snippets.ts: -------------------------------------------------------------------------------- 1 | import { octokit } from './octokit' 2 | 3 | export namespace GitHubSnippetRepo { 4 | export async function fetchRepo() { 5 | const repo = await octokit.rest.repos.get({ 6 | owner: 'mx-space', 7 | repo: 'snippets', 8 | }) 9 | return repo.data 10 | } 11 | 12 | export async function fetchFileTree(path = '') { 13 | const tree = await octokit.rest.repos.getContent({ 14 | owner: 'mx-space', 15 | repo: 'snippets', 16 | path, 17 | }) 18 | 19 | return tree.data 20 | } 21 | 22 | export async function searchFile(path = '') { 23 | const tree = await octokit.rest.search.code({ 24 | q: `repo:mx-space/snippets in:path ${path}`, 25 | sort: 'indexed', 26 | order: 'desc', 27 | }) 28 | return tree.data 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/external/api/hitokoto.ts: -------------------------------------------------------------------------------- 1 | export enum SentenceType { 2 | '动画' = 'a', 3 | '漫画' = 'b', 4 | '游戏' = 'c', 5 | '文学' = 'd', 6 | '原创' = 'e', 7 | '来自网络' = 'f', 8 | '其他' = 'g', 9 | '影视' = 'h', 10 | '诗词' = 'i', 11 | '网易云' = 'j', 12 | '哲学' = 'k', 13 | '抖机灵' = 'l', 14 | } 15 | export const fetchHitokoto = async ( 16 | type: SentenceType[] | SentenceType = SentenceType.文学, 17 | ) => { 18 | const json = await fetch( 19 | `https://v1.hitokoto.cn/${ 20 | Array.isArray(type) 21 | ? `?${type.map((t) => `c=${t}`).join('&')}` 22 | : `?c=${type}` 23 | }`, 24 | ) 25 | const data = (await (json.json() as unknown)) as { 26 | id: number 27 | hitokoto: string 28 | type: string 29 | from: string 30 | from_who: string 31 | creator: string 32 | creator_uid: number 33 | reviewer: number 34 | uuid: string 35 | created_at: string 36 | } 37 | 38 | return data 39 | } 40 | -------------------------------------------------------------------------------- /src/external/api/jinrishici.ts: -------------------------------------------------------------------------------- 1 | export interface ShiJuData { 2 | id: number 3 | content: string 4 | origin: { 5 | title: string 6 | dynasty: string 7 | author: string 8 | content: string[] 9 | matchTags: string[] 10 | } 11 | } 12 | export const getJinRiShiCiOne = async () => { 13 | const res = await fetch('https://v2.jinrishici.com/one.json') 14 | const json = await res.json() 15 | return json.data as ShiJuData 16 | } 17 | -------------------------------------------------------------------------------- /src/external/api/npm.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'umi-request' 2 | import type { NpmPKGInfo } from '~/external/types/npm-pkg' 3 | 4 | export const getNpmPKGLatest = async (name: string) => { 5 | return extend({}).get(`https://registry.npmjs.org/${name}/latest`) 6 | } 7 | -------------------------------------------------------------------------------- /src/external/api/octokit.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from 'octokit' 2 | 3 | const getGHTokenFromLocalStorage = () => { 4 | const token = localStorage.getItem('ghToken') 5 | if (token) { 6 | return token 7 | } 8 | return null 9 | } 10 | 11 | const octokit = new Octokit( 12 | getGHTokenFromLocalStorage() 13 | ? { 14 | auth: getGHTokenFromLocalStorage(), 15 | } 16 | : {}, 17 | ) 18 | 19 | octokit.hook.error('request', async (error, options) => { 20 | try { 21 | message.error(error.message) 22 | } catch {} 23 | throw error 24 | }) 25 | 26 | export { octokit } 27 | -------------------------------------------------------------------------------- /src/external/types/npm-pkg.ts: -------------------------------------------------------------------------------- 1 | export interface NpmPKGInfo { 2 | name: string 3 | description: string 4 | keywords: string[] 5 | version: string 6 | homepage: string 7 | bugs: Bugs 8 | license: string 9 | main: string 10 | exports: any 11 | repository?: Repository 12 | engines?: Engines 13 | dependencies?: Record 14 | 15 | gitHead: string 16 | } 17 | interface Bugs { 18 | url: string 19 | } 20 | 21 | interface Repository { 22 | type: string 23 | url: string 24 | directory: string 25 | } 26 | interface Engines { 27 | node: string 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/use-auto-save.ts: -------------------------------------------------------------------------------- 1 | import { IsISO8601, IsString } from 'class-validator' 2 | import { toRaw } from 'vue' 3 | import { onBeforeRouteLeave } from 'vue-router' 4 | 5 | import { useStorageObject } from './use-storage' 6 | 7 | class SaveDto { 8 | @IsISO8601() 9 | savedTime!: string 10 | 11 | @IsString() 12 | text!: string 13 | 14 | @IsString() 15 | title!: string 16 | } 17 | // TODO 18 | export const useAutoSave = ( 19 | cacheKey: string, 20 | interval: number, 21 | getSaveData: () => Omit, 22 | ) => { 23 | let timer: any 24 | const key = `auto-save-${cacheKey}` 25 | const { storage, reset, clear, destory } = useStorageObject( 26 | SaveDto, 27 | key, 28 | false, 29 | ) 30 | let memoPreviousValue = getSaveData() 31 | const save = () => { 32 | const { text, title } = getSaveData() 33 | if (!text && !title) { 34 | return 35 | } 36 | if (memoPreviousValue.text == text && memoPreviousValue.title == title) { 37 | return 38 | } else { 39 | memoPreviousValue = { text, title } 40 | } 41 | Object.assign(storage, { 42 | savedTime: new Date().toISOString(), 43 | text, 44 | title, 45 | } as SaveDto) 46 | 47 | console.log('saved data:', storage) 48 | } 49 | 50 | function disposer() { 51 | clearInterval(timer) 52 | } 53 | 54 | onUnmounted(() => { 55 | destory() 56 | }) 57 | return { 58 | reset, 59 | getPrevSaved() { 60 | return { ...toRaw(storage) } 61 | }, 62 | save, 63 | track() { 64 | disposer() 65 | save() 66 | timer = setInterval(save, interval) 67 | }, 68 | disposer, 69 | clearSaved: clear, 70 | } 71 | } 72 | 73 | export const useAutoSaveInEditor = ( 74 | data: T, 75 | hook: ReturnType, 76 | ) => { 77 | const { disposer, clearSaved, getPrevSaved, save, track } = hook 78 | 79 | const dialog = window.dialog 80 | 81 | const check = async () => { 82 | const prevSaved = getPrevSaved() 83 | 84 | console.log('prev saved:', prevSaved) 85 | 86 | if ( 87 | (prevSaved.text || prevSaved.title) && 88 | (prevSaved.text !== data.text || prevSaved.title !== data.title) 89 | ) { 90 | requestAnimationFrame(() => { 91 | dialog.info({ 92 | title: '发现有未保存的内容,是否还原?', 93 | negativeText: '清除', 94 | positiveText: '嗯', 95 | onNegativeClick() { 96 | clearSaved() 97 | }, 98 | onPositiveClick() { 99 | Object.assign(data, { 100 | text: prevSaved.text, 101 | title: prevSaved.title, 102 | }) 103 | }, 104 | }) 105 | }) 106 | } 107 | } 108 | 109 | // const initialSaved = getPrevSaved() 110 | onBeforeRouteLeave(() => { 111 | save() 112 | // if (initialSaved.text == data.text && initialSaved.title == data.title) { 113 | // clearSaved() 114 | // } 115 | disposer() 116 | }) 117 | 118 | return { 119 | ...hook, 120 | enable() { 121 | check() 122 | track() 123 | }, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/hooks/use-lifecycle.ts: -------------------------------------------------------------------------------- 1 | export const useMountAndUnmount = (callback: () => any) => { 2 | onMounted(() => { 3 | const res = callback() 4 | onBeforeUnmount(() => { 5 | if (res && typeof res === 'function') { 6 | res() 7 | } 8 | }) 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/use-memo-fetch-data-list.ts: -------------------------------------------------------------------------------- 1 | import type { PaginateResult } from '@mx-space/api-client' 2 | import type { Ref } from 'vue' 3 | 4 | export const createMemoDataListFetchHook = < 5 | T extends { id: string }, 6 | FetchDataType extends T, 7 | >( 8 | fetchFn: (page?: number) => Promise>, 9 | ) => { 10 | return createGlobalState(() => { 11 | const datalist: Ref = ref([]) 12 | const idSet = new Set() 13 | let currentPage = 0 14 | let isEnd = false 15 | 16 | const loading = ref(true) 17 | const fetch = async (page = 1) => { 18 | loading.value = true 19 | const { data, pagination } = await fetchFn(page) 20 | 21 | datalist.value.push(...data.filter((dataItem) => !idSet.has(dataItem.id))) 22 | loading.value = false 23 | data.forEach((i) => idSet.add(i.id)) 24 | 25 | currentPage = pagination.currentPage 26 | if (!pagination.hasNextPage) { 27 | isEnd = true 28 | } 29 | } 30 | 31 | return { 32 | loading, 33 | datalist, 34 | append(data: T[]) { 35 | for (const item of data) { 36 | if (!idSet.has(item.id)) { 37 | idSet.add(item.id) 38 | datalist.value.push({ ...item }) 39 | } 40 | } 41 | }, 42 | fetchNext: () => { 43 | if (isEnd) { 44 | return 45 | } 46 | fetch(currentPage + 1) 47 | }, 48 | refresh() { 49 | this.reset() 50 | 51 | nextTick(() => { 52 | this.fetchNext() 53 | }) 54 | }, 55 | reset() { 56 | currentPage = 0 57 | isEnd = false 58 | datalist.value = [] 59 | idSet.clear() 60 | }, 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/hooks/use-parse-payload.ts: -------------------------------------------------------------------------------- 1 | import { toRaw } from 'vue' 2 | 3 | export const useParsePayloadIntoData = 4 | (data: any) => 5 | (payload: T) => { 6 | const raw = toRaw(data) 7 | const keys = Object.keys(raw) 8 | for (const k in payload) { 9 | if (keys.includes(k)) { 10 | data[k] = payload[k] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/use-portal-element.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue' 2 | 3 | type CleanFn = () => void 4 | export const PortalInjectKey: InjectionKey<{ 5 | setElement: (el: VNode | null) => CleanFn 6 | }> = Symbol() 7 | export const usePortalElement = () => { 8 | const ctx = inject(PortalInjectKey)! 9 | 10 | return ctx.setElement 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/use-save-confirm.ts: -------------------------------------------------------------------------------- 1 | import { useDialog } from 'naive-ui' 2 | import { onBeforeUnmount, onMounted } from 'vue' 3 | import { onBeforeRouteLeave } from 'vue-router' 4 | 5 | /** 6 | * 7 | * @param enable 8 | * @param comparedFn true: 不提示,false: 提示 9 | */ 10 | export const useSaveConfirm = ( 11 | enable: boolean, 12 | comparedFn: () => boolean, 13 | message = '文章未保存是否确定离开?', 14 | ): void => { 15 | const beforeUnloadHandler = (event) => { 16 | if (comparedFn()) { 17 | return 18 | } 19 | event.preventDefault() 20 | 21 | // Chrome requires returnValue to be set. 22 | event.returnValue = message 23 | return false 24 | } 25 | 26 | onMounted(() => { 27 | if (enable) { 28 | window.addEventListener('beforeunload', beforeUnloadHandler) 29 | } 30 | }) 31 | onBeforeUnmount(() => { 32 | if (enable) { 33 | window.removeEventListener('beforeunload', beforeUnloadHandler) 34 | } 35 | }) 36 | 37 | const dialog = useDialog() 38 | 39 | onBeforeRouteLeave(async (to, _, next) => { 40 | if (!enable) { 41 | next() 42 | return 43 | } 44 | if (comparedFn()) { 45 | next() 46 | return 47 | } 48 | 49 | // HACK 50 | if (to.hash == '|publish') { 51 | next() 52 | return 53 | } 54 | 55 | const confirm = new Promise((r, j) => { 56 | dialog.warning({ 57 | title: message, 58 | negativeText: '嗯', 59 | positiveText: '手抖了啦', 60 | onNegativeClick() { 61 | r(true) 62 | }, 63 | onPositiveClick() { 64 | r(false) 65 | }, 66 | }) 67 | }) 68 | 69 | const res = await Promise.resolve(confirm) 70 | 71 | if (res) { 72 | next() 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/hooks/use-storage.ts: -------------------------------------------------------------------------------- 1 | import { instanceToPlain, plainToInstance } from 'class-transformer' 2 | import { validateSync } from 'class-validator' 3 | import { throttle } from 'es-toolkit/compat' 4 | import { reactive, watch } from 'vue' 5 | 6 | const key2reactive = new Map() 7 | export const useStorageObject = ( 8 | DTO: Class, 9 | storageKey: string, 10 | fixWrongPropertyData = true, 11 | ) => { 12 | const getObjectStorage = () => { 13 | const saved = localStorage.getItem(storageKey) 14 | if (!saved) { 15 | console.debug(storageKey, ': no saved data') 16 | return null 17 | } 18 | try { 19 | const parsed = JSON.parse(saved) 20 | const classify = plainToInstance(DTO, parsed) 21 | const err = validateSync(classify) 22 | if (err.length > 0) { 23 | if (fixWrongPropertyData) { 24 | const instanceDto = new DTO() 25 | err.forEach((e) => { 26 | const propertyName = e.property 27 | parsed[propertyName] = instanceDto[propertyName] 28 | 29 | localStorage.setItem(storageKey, JSON.stringify(parsed)) 30 | }) 31 | } 32 | if (__DEV__) { 33 | console.log(err) 34 | console.log( 35 | 'wrong property key:', 36 | err.map((e) => e.property).toString(), 37 | ) 38 | fixWrongPropertyData && 39 | console.log('after fix wrong property:', parsed) 40 | } 41 | return fixWrongPropertyData ? parsed : null 42 | } 43 | return parsed 44 | } catch (error) { 45 | console.log(error) 46 | 47 | return null 48 | } 49 | } 50 | const storedReactive = key2reactive.get(storageKey) 51 | const objectStorage: U = 52 | storedReactive ?? 53 | reactive(getObjectStorage() ?? instanceToPlain(new DTO())) 54 | if (!storedReactive) { 55 | key2reactive.set(storageKey, objectStorage) 56 | } 57 | watch( 58 | () => objectStorage, 59 | throttle( 60 | (n) => { 61 | localStorage.setItem(storageKey, JSON.stringify(n)) 62 | }, 63 | 400, 64 | { trailing: true }, 65 | ), 66 | { deep: true }, 67 | ) 68 | 69 | onBeforeMount(() => { 70 | localStorage.setItem(storageKey, JSON.stringify(objectStorage)) 71 | }) 72 | 73 | return { 74 | storage: objectStorage as U, 75 | reset: () => { 76 | Object.assign(objectStorage, instanceToPlain(new DTO())) 77 | }, 78 | clear() { 79 | localStorage.removeItem(storageKey) 80 | }, 81 | destory() { 82 | key2reactive.delete(storageKey) 83 | }, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/hooks/use-store-ref.ts: -------------------------------------------------------------------------------- 1 | import type { StoreGeneric } from 'pinia' 2 | 3 | export const useStoreRef = (store: () => SS) => 4 | Object.assign({}, store(), storeToRefs(store())) 5 | -------------------------------------------------------------------------------- /src/hooks/use-table.ts: -------------------------------------------------------------------------------- 1 | import qs from 'qs' 2 | import { reactive, ref } from 'vue' 3 | import type { Pager } from '~/models/base' 4 | import type { Ref } from 'vue' 5 | import type { LocationQueryValue } from 'vue-router' 6 | 7 | export type fetchDataFn = ( 8 | page?: string | number | LocationQueryValue[], 9 | size?: number, 10 | db_query?: string | undefined, 11 | ) => Promise 12 | export const useDataTableFetch = ( 13 | fetchDataFn: (data: Ref, pager: Ref) => fetchDataFn, 14 | ) => { 15 | const data: Ref = ref([]) as any 16 | const pager = ref({} as any) 17 | const sortProps = reactive({ 18 | sortBy: '', 19 | sortOrder: 0, 20 | }) 21 | const checkedRowKeys = ref([]) 22 | const loading = ref(false) 23 | return { 24 | data, 25 | pager, 26 | sortProps, 27 | checkedRowKeys, 28 | loading, 29 | fetchDataFn: async ( 30 | page?: number, 31 | size?: number, 32 | db_query?: Record, 33 | ) => { 34 | loading.value = true 35 | await fetchDataFn(data, pager)( 36 | page, 37 | size, 38 | db_query ? qs.stringify(db_query) : undefined, 39 | ) 40 | loading.value = false 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:ital,wght@0,300;1,300&display=swap'); 2 | 3 | body, 4 | html { 5 | margin: 0; 6 | font-size: 14px !important; 7 | padding: 0; 8 | 9 | --sans-font: 'Segoe UI', 'Helvetica Neue', sans-serif, -apple-system, 10 | system-ui; 11 | 12 | font-family: var(--sans-font); 13 | 14 | @apply dark:bg-gray-900; 15 | } 16 | 17 | body { 18 | min-height: 100vh; 19 | 20 | @apply overflow-hidden; 21 | } 22 | 23 | body a { 24 | @apply text-primary-default transition-colors; 25 | text-decoration: none; 26 | } 27 | 28 | body a:hover { 29 | @apply text-primary-shallow; 30 | } 31 | 32 | button:focus, 33 | button { 34 | outline: unset; 35 | } 36 | 37 | *::-webkit-scrollbar { 38 | width: 10px; 39 | height: 10px; 40 | } 41 | 42 | *::-webkit-scrollbar-thumb, 43 | *::-webkit-scrollbar-thumb:hover { 44 | @apply bg-primary-shallow; 45 | 46 | background-clip: padding-box; 47 | border: 3px solid transparent; 48 | border-radius: 5px; 49 | } 50 | 51 | *::-webkit-scrollbar-corner { 52 | background: var(--light-bg); 53 | } 54 | 55 | .n-scrollbar-rail { 56 | @apply z-100; 57 | } 58 | 59 | .n-message { 60 | @apply !rounded-full; 61 | } 62 | 63 | .n-notification-main-footer { 64 | @apply !block; 65 | } 66 | 67 | @layer components { 68 | .modal-card { 69 | @apply !w-[60rem] !max-w-[90vw]; 70 | 71 | &.sm { 72 | @apply !w-[40rem]; 73 | } 74 | 75 | &.xs { 76 | @apply !w-[30rem]; 77 | } 78 | } 79 | } 80 | 81 | * { 82 | -webkit-tap-highlight-color: transparent; 83 | -webkit-text-size-adjust: 100%; 84 | } 85 | 86 | .n-list-item { 87 | .n-list-item__prefix { 88 | align-self: flex-start; 89 | } 90 | 91 | .n-thing-avatar { 92 | @apply self-center; 93 | } 94 | } 95 | 96 | .n-tabs-tab { 97 | padding-top: 10px !important; 98 | } 99 | 100 | .n-data-table-base-table-header { 101 | @apply overflow-hidden; 102 | } 103 | 104 | .n-upload-trigger, 105 | .n-upload-file-list { 106 | @apply w-full; 107 | } 108 | 109 | .p0 .n-upload-dragger { 110 | @apply flex p-0; 111 | } 112 | 113 | /* monaco */ 114 | .monaco-editor { 115 | @apply !border-0 !outline-none; 116 | 117 | & .sticky-widget { 118 | background-color: var(--n-color) !important; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/layouts/app-layout.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router' 2 | 3 | import { RouteName } from '~/router/name' 4 | import { useUserStore } from '~/stores/user' 5 | 6 | import { SidebarLayout } from './sidebar' 7 | 8 | export const AppLayout = defineComponent({ 9 | setup() { 10 | const { fetchUser } = useUserStore() 11 | const router = useRouter() 12 | fetchUser().then(() => { 13 | const toSetting = localStorage.getItem('to-setting') 14 | if (toSetting === 'true') { 15 | router.push({ 16 | name: RouteName.Setting, 17 | params: { 18 | type: 'user', 19 | }, 20 | }) 21 | localStorage.removeItem('to-setting') 22 | } 23 | }) 24 | 25 | return () => 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/layouts/content/index.module.css: -------------------------------------------------------------------------------- 1 | .sticky-header { 2 | @apply sticky left-0 right-0 top-[-2.5rem] z-20 bg-transparent p-12 pb-6; 3 | 4 | &::before { 5 | @apply absolute inset-0 content-['']; 6 | 7 | background: var(--n-color); 8 | mask-image: linear-gradient( 9 | rgb(255, 255, 255) 80%, 10 | rgba(255, 255, 255, 0) 100% 11 | ); 12 | } 13 | 14 | @screen phone { 15 | @apply px-6; 16 | } 17 | } 18 | .header { 19 | display: flex; 20 | justify-content: space-between; 21 | 22 | @apply relative z-10 select-none; 23 | 24 | & .title { 25 | @apply flex items-center; 26 | 27 | font-size: 1.73rem; 28 | } 29 | } 30 | 31 | .main { 32 | @apply p-12; 33 | 34 | padding-top: 6px; 35 | 36 | @screen phone { 37 | & { 38 | @apply p-4; 39 | } 40 | } 41 | } 42 | 43 | .buttons { 44 | position: fixed; 45 | bottom: 3rem; 46 | right: 3rem; 47 | display: flex; 48 | flex-direction: column; 49 | z-index: 30; 50 | 51 | button { 52 | @apply flex items-center justify-center; 53 | 54 | z-index: 3; 55 | box-sizing: border-box; 56 | width: 2.8em; 57 | height: 2.8em; 58 | color: #795548; 59 | margin-top: 0.5em; 60 | background: #fff; 61 | border-radius: 66%; 62 | transition: 63 | transform 0.3s, 64 | background 0.3s, 65 | color 0.3s; 66 | animation: show 0.5s both; 67 | box-shadow: 68 | 0 0 10px rgba(0, 0, 0, 0.1), 69 | 0 5px 20px rgba(0, 0, 0, 0.2); 70 | &:not(:first-child) { 71 | margin-top: 0.8rem; 72 | } 73 | } 74 | 75 | @screen phone { 76 | & { 77 | @apply bottom-[1.5rem] right-[1.5rem] flex-row items-center justify-center; 78 | } 79 | 80 | & button { 81 | @apply !mt-0 ml-3; 82 | } 83 | } 84 | } 85 | 86 | .header-actions { 87 | @apply phone:mr-0 relative bottom-0 right-0 top-0 mr-12 flex items-center; 88 | 89 | & > button, 90 | & > a { 91 | @apply flex h-12 w-12 items-center justify-center shadow-2xl; 92 | } 93 | 94 | a > button { 95 | @apply h-full w-full; 96 | } 97 | } 98 | 99 | :global(html.dark) { 100 | .buttons button { 101 | @apply bg-gray-600 text-blue-300; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/layouts/router-view.tsx: -------------------------------------------------------------------------------- 1 | import { NSpin } from 'naive-ui' 2 | import { Suspense, defineComponent } from 'vue' 3 | import { RouterView } from 'vue-router' 4 | import type { VNode } from 'vue' 5 | import type { RouteLocation } from 'vue-router' 6 | 7 | const $RouterView = defineComponent({ 8 | setup() { 9 | return () => ( 10 | 11 | {{ 12 | default({ Component }: { Component: VNode; route: RouteLocation }) { 13 | return ( 14 | 15 | {{ 16 | default: () => Component, 17 | 18 | fallback() { 19 | return ( 20 |
21 | 22 |
23 | ) 24 | }, 25 | }} 26 |
27 | ) 28 | }, 29 | }} 30 |
31 | ) 32 | }, 33 | }) 34 | export default $RouterView 35 | -------------------------------------------------------------------------------- /src/layouts/setup-view.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /src/layouts/sidebar/index.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | .content { 3 | left: var(--w); 4 | 5 | @apply transition-all duration-500; 6 | @apply fixed inset-0 overflow-hidden; 7 | } 8 | 9 | @screen phone { 10 | .content { 11 | left: var(--sidebar-collapse-width) !important; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/layouts/two-col.tsx: -------------------------------------------------------------------------------- 1 | import { NGrid } from 'naive-ui' 2 | 3 | export const TwoColGridLayout = defineComponent({ 4 | setup(props, { slots }) { 5 | return () => ( 6 | 7 | {slots?.default?.()} 8 | 9 | ) 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './monaco' 2 | import 'reflect-metadata' 3 | 4 | import { piniaStore } from '~/stores' 5 | import { bus } from '~/utils/event-bus' 6 | 7 | import 'virtual:windi.css' 8 | 9 | import { createApp } from 'vue' 10 | 11 | import App from './App' 12 | 13 | import './index.css' 14 | 15 | import { router } from './router' 16 | import { attachTokenFromQuery, RESTManager } from './utils' 17 | 18 | attachTokenFromQuery() 19 | 20 | const app = createApp(App) 21 | 22 | app.use(router) 23 | app.use(piniaStore) 24 | app.mount('#app') 25 | 26 | if (__DEV__) { 27 | window.app = app 28 | window.bus = bus 29 | } 30 | 31 | // cjs webpack compatibility 32 | // @ts-ignore 33 | window.global = window 34 | // @ts-ignore 35 | window.process = { 36 | env: {}, 37 | } 38 | // @ts-ignore 39 | window.module = { 40 | exports: {}, 41 | } 42 | 43 | declare global { 44 | interface JSON { 45 | safeParse: typeof JSON.parse 46 | } 47 | } 48 | JSON.safeParse = (...rest) => { 49 | try { 50 | return JSON.parse(...rest) 51 | } catch { 52 | return null 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/models/activity.ts: -------------------------------------------------------------------------------- 1 | export interface ActivityReadDurationType { 2 | id: string 3 | type: number 4 | payload: RoomPayload 5 | created: string 6 | refId: string 7 | } 8 | export interface RoomPayload { 9 | connectedAt: number 10 | operationTime: number 11 | updatedAt: number 12 | position: number 13 | roomName: string 14 | ip: string 15 | joinedAt?: number 16 | displayName?: string 17 | identity: string 18 | } 19 | -------------------------------------------------------------------------------- /src/models/ai.ts: -------------------------------------------------------------------------------- 1 | export interface AISummaryModel { 2 | id: string 3 | created: string 4 | summary: string 5 | hash: string 6 | refId: string 7 | lang: string 8 | } 9 | -------------------------------------------------------------------------------- /src/models/amap.ts: -------------------------------------------------------------------------------- 1 | export interface Amap { 2 | status: string 3 | regeocode: Regeocode 4 | info: string 5 | infocode: string 6 | } 7 | 8 | export interface Regeocode { 9 | addressComponent: AddressComponent 10 | formattedAddress: string 11 | } 12 | 13 | export interface AddressComponent { 14 | city: string 15 | province: string 16 | adcode: string 17 | district: string 18 | towncode: string 19 | streetNumber: StreetNumber 20 | country: string 21 | township: string 22 | businessAreas: BusinessArea[] 23 | building: Building 24 | neighborhood: Building 25 | citycode: string 26 | } 27 | 28 | export interface Building { 29 | name: any[] 30 | type: any[] 31 | } 32 | 33 | export interface BusinessArea { 34 | location: string 35 | name: string 36 | id: string 37 | } 38 | 39 | export interface StreetNumber { 40 | number: string 41 | location: string 42 | direction: string 43 | distance: string 44 | street: string 45 | } 46 | 47 | export interface AMapSearch { 48 | suggestion: Suggestion 49 | count: string 50 | infocode: string 51 | pois: Pois[] 52 | status: string 53 | info: string 54 | } 55 | 56 | export interface Pois { 57 | parent: any[] | string 58 | address: string 59 | distance: any[] 60 | pname: string 61 | importance: any[] 62 | bizEXT: BizEXT 63 | bizType: any[] 64 | cityname: string 65 | type: string 66 | photos: any[] 67 | typecode: string 68 | shopinfo: string 69 | poiweight: any[] 70 | childtype: any[] | string 71 | adname: string 72 | name: string 73 | location: string 74 | tel: any[] 75 | shopid: any[] 76 | id: string 77 | } 78 | 79 | export interface BizEXT { 80 | cost: any[] 81 | rating: any[] 82 | } 83 | 84 | export interface Suggestion { 85 | keywords: any[] 86 | cities: any[] 87 | } 88 | -------------------------------------------------------------------------------- /src/models/analyze.ts: -------------------------------------------------------------------------------- 1 | export declare namespace UA { 2 | export interface Browser { 3 | name: string 4 | version: string 5 | major: string 6 | } 7 | 8 | export interface Engine { 9 | name: string 10 | version: string 11 | } 12 | 13 | export interface Os { 14 | name: string 15 | version: string 16 | } 17 | 18 | export interface Ua { 19 | ua: string 20 | browser?: Browser 21 | engine?: Engine 22 | os?: Os 23 | } 24 | export interface Root { 25 | id: string 26 | ip?: string 27 | ua: Ua 28 | timestamp: Date 29 | path?: string 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/models/authn.ts: -------------------------------------------------------------------------------- 1 | export interface AuthnModel { 2 | name: string 3 | 4 | credentialID: string 5 | credentialPublicKey: string 6 | counter: number 7 | credentialDeviceType: 'singleDevice' | 'multiDevice' 8 | credentialBackedUp: boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/models/base.ts: -------------------------------------------------------------------------------- 1 | import type { Pager, PaginateResult } from '@mx-space/api-client' 2 | 3 | export { Pager, PaginateResult } 4 | export interface Count { 5 | read: number 6 | like: number 7 | } 8 | 9 | export interface Image { 10 | height: number 11 | width: number 12 | type: string 13 | accent?: string 14 | src: string 15 | blurHash?: string 16 | } 17 | 18 | export class BaseModel { 19 | created?: Date 20 | id?: string 21 | } 22 | -------------------------------------------------------------------------------- /src/models/category.ts: -------------------------------------------------------------------------------- 1 | import type { PostModel } from './post' 2 | 3 | export enum CategoryType { 4 | Category, 5 | Tag, 6 | } 7 | 8 | export interface CategoryModel { 9 | type: CategoryType 10 | count: number 11 | id: string 12 | created: string 13 | slug: string 14 | name: string 15 | modified: string 16 | } 17 | 18 | export interface CategoryResponse { 19 | data: CategoryModel[] 20 | } 21 | 22 | export type CategoryWithChildrenModel = CategoryModel & { 23 | children: PickedPostModelInCategoryChildren[] 24 | } 25 | 26 | export type PickedPostModelInCategoryChildren = Pick< 27 | PostModel, 28 | 'id' | 'title' | 'slug' | 'modified' | 'created' 29 | > 30 | 31 | export interface TagModel { 32 | count: number 33 | name: string 34 | } 35 | -------------------------------------------------------------------------------- /src/models/comment.ts: -------------------------------------------------------------------------------- 1 | import type { Pager } from './base' 2 | 3 | export interface CommentModel { 4 | refType: string 5 | state: number 6 | children: CommentModel[] 7 | commentsIndex: number 8 | id: string 9 | author: string 10 | text: string 11 | mail: string 12 | url: string 13 | ip: string 14 | agent: string 15 | key: string 16 | pid: string 17 | created: string 18 | modified: string 19 | avatar: string 20 | isWhispers?: boolean 21 | } 22 | 23 | export interface CommentsResponse { 24 | data: CommentModel[] 25 | pagination: Pager 26 | } 27 | 28 | export enum CommentState { 29 | Unread, 30 | Read, 31 | Junk, 32 | } 33 | -------------------------------------------------------------------------------- /src/models/link.ts: -------------------------------------------------------------------------------- 1 | import type { LinkModel, LinkState, PaginateResult } from '@mx-space/api-client' 2 | 3 | export { LinkType, LinkState } from '@mx-space/api-client' 4 | 5 | export { LinkModel } 6 | 7 | export type LinkResponse = PaginateResult 8 | 9 | export type LinkStateCount = { 10 | audit: number 11 | collection: number 12 | friends: number 13 | outdate: number 14 | banned: number 15 | reject: number 16 | } 17 | 18 | export const LinkStateNameMap: Record = { 19 | Audit: '待审核', 20 | Pass: '通过', 21 | Outdate: '过时', 22 | Banned: '屏蔽', 23 | Reject: '拒绝', 24 | } 25 | -------------------------------------------------------------------------------- /src/models/note.ts: -------------------------------------------------------------------------------- 1 | export interface NoteModel { 2 | id: string 3 | hide: boolean 4 | allowComment: boolean 5 | count: { 6 | read: number 7 | like: number 8 | } 9 | title: string 10 | text: string 11 | mood?: string 12 | weather?: string 13 | bookmark?: boolean 14 | created: string 15 | modified: string 16 | publicAt?: Date 17 | password?: string | null 18 | nid: number 19 | 20 | location?: string 21 | 22 | coordinates?: Coordinate 23 | 24 | meta?: any 25 | } 26 | 27 | export interface Coordinate { 28 | latitude: number 29 | longitude: number 30 | } 31 | -------------------------------------------------------------------------------- /src/models/options.ts: -------------------------------------------------------------------------------- 1 | export module MxServerOptions { 2 | export interface SeoOption { 3 | title: string 4 | description: string 5 | keywords: string[] 6 | } 7 | 8 | export interface UrlOption { 9 | webUrl: string 10 | adminUrl: string 11 | serverUrl: string 12 | wsUrl: string 13 | } 14 | 15 | export interface MailOption { 16 | port: number 17 | host: string 18 | secure: boolean 19 | } 20 | 21 | export interface MailOptionsOption { 22 | enable: boolean 23 | user: string 24 | pass: string 25 | options: MailOption 26 | } 27 | 28 | export interface CommentOptionsOption { 29 | antiSpam: boolean 30 | disableComment: boolean 31 | spamKeywords: string[] 32 | blockIps: string[] 33 | disableNoChinese: boolean 34 | commentShouldAudit: boolean 35 | recordIpLocation: boolean 36 | } 37 | 38 | export interface BackupOptionsOption { 39 | enable: boolean 40 | endpoint: string 41 | secretId: string 42 | secretKey: string 43 | bucket: string 44 | region: string 45 | } 46 | 47 | export interface BaiduSearchOptionsOption { 48 | enable: boolean 49 | token: string 50 | } 51 | 52 | export interface AlgoliaSearchOptionsOption { 53 | enable: boolean 54 | apiKey: string 55 | appId: string 56 | indexName: string 57 | } 58 | 59 | export interface AdminExtraOption { 60 | enableAdminProxy: boolean 61 | background: string 62 | gaodemapKey: string 63 | } 64 | 65 | export interface FriendLinkOptionsOption { 66 | allowApply: boolean 67 | } 68 | 69 | export interface TextOptionsOption { 70 | macros: boolean 71 | } 72 | 73 | export interface BarkOptionsOption { 74 | enable: boolean 75 | key: string 76 | serverUrl: string 77 | enableComment: boolean 78 | } 79 | 80 | export interface FeatureListOption { 81 | emailSubscribe: boolean 82 | } 83 | 84 | export interface ThirdPartyServiceIntegrationOption { 85 | xLogSiteId: string 86 | } 87 | 88 | export interface AuthSecurityOption { 89 | disablePasswordLogin: boolean 90 | } 91 | 92 | export interface AIOption { 93 | openAiKey: string 94 | openAiEndpoint: string 95 | openAiPreferredModel: string 96 | enableSummary: boolean 97 | enableAutoGenerateSummary: boolean 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/models/page.ts: -------------------------------------------------------------------------------- 1 | import type { Pager } from './base' 2 | 3 | export enum EnumPageType { 4 | 'md' = 'md', 5 | 'html' = 'html', 6 | 'frame' = 'frame', 7 | } 8 | export interface PageModel { 9 | created: string 10 | modified: string 11 | allowComment: boolean 12 | id: string 13 | /** Slug */ 14 | slug: string 15 | 16 | /** Title */ 17 | title: string 18 | 19 | /** SubTitle */ 20 | subtitle?: string 21 | 22 | /** Order */ 23 | order?: number 24 | 25 | /** Text */ 26 | text: string 27 | 28 | /** Type (MD | html | frame) */ 29 | type?: EnumPageType 30 | 31 | /** Other Options */ 32 | options?: object 33 | } 34 | 35 | export interface PageResponse { 36 | data: PageModel[] 37 | pagination: Pager 38 | } 39 | -------------------------------------------------------------------------------- /src/models/post.ts: -------------------------------------------------------------------------------- 1 | import type { Count, Image, Pager } from './base' 2 | 3 | export interface PostResponse { 4 | data: PostModel[] 5 | pagination: Pager 6 | } 7 | 8 | export interface PostModel { 9 | commentsIndex: number 10 | allowComment: boolean 11 | copyright: boolean 12 | tags: string[] 13 | count: Count 14 | id: string 15 | text: string 16 | title: string 17 | slug: string 18 | categoryId: string 19 | images: Image[] 20 | modified: string 21 | created: string 22 | category: Category 23 | pin?: string | null 24 | pinOrder?: number 25 | related?: Pick[] 26 | meta?: any 27 | } 28 | 29 | export interface Category { 30 | type: number 31 | count: number 32 | id: string 33 | name: string 34 | slug: string 35 | created: Date 36 | categoryId: string 37 | } 38 | -------------------------------------------------------------------------------- /src/models/project.ts: -------------------------------------------------------------------------------- 1 | import type { Pager } from './base' 2 | 3 | export interface ProjectModel { 4 | id: string 5 | name: string 6 | previewUrl?: string 7 | docUrl?: string 8 | projectUrl?: string 9 | images?: string[] 10 | description: string 11 | avatar?: string 12 | text: string 13 | created: string 14 | modified?: string 15 | } 16 | 17 | export type ProjectResponse = { 18 | data: ProjectModel[] 19 | pagination: Pager 20 | } 21 | -------------------------------------------------------------------------------- /src/models/recently.ts: -------------------------------------------------------------------------------- 1 | export interface RecentlyModel { 2 | content: string 3 | created: string 4 | id: string 5 | up: number 6 | down: number 7 | } 8 | -------------------------------------------------------------------------------- /src/models/say.ts: -------------------------------------------------------------------------------- 1 | import type { Pager } from './base' 2 | 3 | export interface SayModel { 4 | id?: string 5 | text: string 6 | source?: string 7 | author?: string 8 | } 9 | 10 | export interface SayResponse { 11 | data: SayModel[] 12 | pagination: Pager 13 | } 14 | -------------------------------------------------------------------------------- /src/models/snippet.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel } from './base' 2 | 3 | export const defaultServerlessFunction = ` 4 | export default async function handler(ctx: Context) { 5 | return 'pong'; 6 | } 7 | `.trimStart() 8 | export enum SnippetType { 9 | JSON = 'json', 10 | JSON5 = 'json5', 11 | Function = 'function', 12 | Text = 'text', 13 | YAML = 'yaml', 14 | } 15 | 16 | export enum SnippetTypeToLanguage { 17 | json = 'json', 18 | json5 = 'plaintext', 19 | function = 'typescript', 20 | text = 'markdown', 21 | yaml = 'yaml', 22 | } 23 | export class SnippetModel extends BaseModel { 24 | type = SnippetType.JSON 25 | private = false 26 | raw = '{}' 27 | name = '' 28 | reference = 'root' 29 | comment?: string 30 | metatype?: string 31 | schema?: string 32 | 33 | // for serverless function 34 | enable?: boolean 35 | method?: string 36 | secret?: Record 37 | 38 | builtIn = false 39 | } 40 | -------------------------------------------------------------------------------- /src/models/stat.ts: -------------------------------------------------------------------------------- 1 | export interface Stat { 2 | allComments: number 3 | categories: number 4 | comments: number 5 | linkApply: number 6 | links: number 7 | notes: number 8 | pages: number 9 | posts: number 10 | says: number 11 | recently: number 12 | unreadComments: number 13 | callTime: number 14 | uv: number 15 | todayIpAccessCount: number 16 | online: number 17 | 18 | todayMaxOnline: number 19 | todayOnlineTotal: number 20 | } 21 | -------------------------------------------------------------------------------- /src/models/subscribe.ts: -------------------------------------------------------------------------------- 1 | import type { Pager } from './base' 2 | 3 | export interface SubscribeResponse { 4 | data: SubscribeModel[] 5 | pagination: Pager 6 | } 7 | 8 | export interface SubscribeModel { 9 | id: string 10 | email: string 11 | cancel_token: string 12 | subscribe: number 13 | created: string 14 | } 15 | -------------------------------------------------------------------------------- /src/models/system.ts: -------------------------------------------------------------------------------- 1 | interface AppInfo { 2 | name: string 3 | version: string 4 | } 5 | -------------------------------------------------------------------------------- /src/models/token.ts: -------------------------------------------------------------------------------- 1 | export interface TokenModel { 2 | created: string 3 | 4 | token: string 5 | 6 | expired?: Date 7 | 8 | name: string 9 | 10 | id: string 11 | } 12 | -------------------------------------------------------------------------------- /src/models/topic.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export interface TopicModel extends BaseModel { 4 | description?: string 5 | introduce: string 6 | name: string 7 | slug: string 8 | icon?: string 9 | } 10 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | export interface UserModel { 2 | ok: number 3 | id: string 4 | introduce: string 5 | mail: string 6 | url: string 7 | name: string 8 | socialIds?: Record 9 | username: string 10 | created: Date 11 | modified: Date 12 | v: number 13 | lastLoginTime: string 14 | lastLoginIp?: string 15 | avatar: string 16 | postID: string 17 | } 18 | -------------------------------------------------------------------------------- /src/models/wehbook.ts: -------------------------------------------------------------------------------- 1 | export interface WebhookModel { 2 | payloadUrl: string 3 | events: string[] 4 | enabled: boolean 5 | id: string 6 | secret: string 7 | 8 | scope: number 9 | } 10 | 11 | type JSON = string 12 | export declare class WebhookEventModel { 13 | headers: JSON 14 | payload: JSON 15 | event: string 16 | response: JSON 17 | success: boolean 18 | hookId: Ref 19 | status: number 20 | id: string 21 | timestamp: string 22 | } 23 | 24 | export const EventScope = { 25 | TO_VISITOR: 1 << 0, 26 | TO_ADMIN: 1 << 1, 27 | TO_SYSTEM: 1 << 2, 28 | // TO_VISITOR_ADMIN: (1 << 0) | (1 << 1), 29 | // TO_SYSTEM_VISITOR: (1 << 0) | (1 << 2), 30 | // TO_SYSTEM_ADMIN: (1 << 1) | (1 << 2), 31 | ALL: (1 << 0) | (1 << 1) | (1 << 2), 32 | } 33 | -------------------------------------------------------------------------------- /src/monaco.ts: -------------------------------------------------------------------------------- 1 | import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' 2 | import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker' 3 | import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker' 4 | import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' 5 | import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' 6 | 7 | self.MonacoEnvironment = { 8 | getWorker(_, label) { 9 | if (label === 'json') { 10 | return new jsonWorker() 11 | } 12 | if (label === 'css' || label === 'scss' || label === 'less') { 13 | return new cssWorker() 14 | } 15 | if (label === 'html' || label === 'handlebars' || label === 'razor') { 16 | return new htmlWorker() 17 | } 18 | if (label === 'typescript' || label === 'javascript') { 19 | return new tsWorker() 20 | } 21 | 22 | return new editorWorker() 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /src/router/guard.ts: -------------------------------------------------------------------------------- 1 | import { API_URL, GATEWAY_URL } from '~/constants/env' 2 | import QProgress from 'qier-progress' 3 | import { removeToken, setToken } from '~/utils/auth' 4 | import { checkIsInit } from '~/utils/is-init' 5 | 6 | import { SESSION_WITH_LOGIN } from '~/constants/keys' 7 | import { getTokenIsUpstream } from '~/stores/user' 8 | 9 | import { configs } from '../configs' 10 | import { RESTManager } from '../utils/rest' 11 | import { router } from './router' 12 | 13 | export const progress = new QProgress({ colorful: false, color: '#1a9cf3' }) 14 | const title = configs.title 15 | 16 | let loginWithTokenOnce = false 17 | let lastCheckedLogAt = 0 18 | 19 | router.beforeEach(async (to) => { 20 | if (to.path === '/setup-api') { 21 | return 22 | } 23 | 24 | if (!API_URL || !GATEWAY_URL) { 25 | console.log( 26 | 'missing api url or gateway url', 27 | API_URL, 28 | GATEWAY_URL, 29 | ', redirect to /setup-api', 30 | ) 31 | return '/setup-api' 32 | } 33 | 34 | progress.start() 35 | // guard for setup route 36 | 37 | if (to.path === '/setup') { 38 | // if (__DEV__) { 39 | // return 40 | // } 41 | const isInit = await checkIsInit() 42 | console.log('[isInit]', isInit) 43 | if (isInit) { 44 | return '/' 45 | } 46 | } 47 | 48 | if (to.meta.isPublic || to.fullPath.startsWith('/dev')) { 49 | return 50 | } else { 51 | const now = Date.now() 52 | if (now - lastCheckedLogAt < 1000 * 60 * 5) { 53 | return 54 | } 55 | const { ok } = await RESTManager.api('master')('check_logged').get<{ 56 | ok: number 57 | }>() 58 | lastCheckedLogAt = now 59 | if (!ok) { 60 | return `/login?from=${encodeURI(to.fullPath)}` 61 | } else { 62 | import('~/socket').then((mo) => { 63 | mo.socket.initIO() 64 | }) 65 | 66 | const sessionWithLogin = sessionStorage.getItem(SESSION_WITH_LOGIN) 67 | if (sessionWithLogin) return 68 | // login with token only 69 | if (loginWithTokenOnce || getTokenIsUpstream()) { 70 | return 71 | } else { 72 | await RESTManager.api.master.login 73 | .put<{ token: string }>() 74 | .then((res) => { 75 | loginWithTokenOnce = true 76 | removeToken() 77 | setToken(res.token) 78 | 79 | import('~/socket').then((mo) => { 80 | mo.socket.initIO() 81 | }) 82 | }) 83 | .catch(() => { 84 | console.log('登陆失败') 85 | location.reload() 86 | }) 87 | } 88 | } 89 | } 90 | }) 91 | 92 | router.afterEach((to, _) => { 93 | document.title = getPageTitle(to?.meta.title as any) 94 | progress.finish() 95 | }) 96 | 97 | // HACK editor save 98 | router.afterEach((to) => { 99 | if (to.hash == '|publish') { 100 | router.replace({ ...to, hash: '' }) 101 | } 102 | }) 103 | 104 | router.onError((err) => { 105 | progress.finish() 106 | if (err == '网络错误') { 107 | return router.push('/setup-api') 108 | } 109 | }) 110 | 111 | function getPageTitle(pageTitle?: string | null) { 112 | if (pageTitle) { 113 | return `${pageTitle} - ${title}` 114 | } 115 | return `${title}` 116 | } 117 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import './guard' 2 | 3 | import { routeForMenu } from './route' 4 | 5 | import './router' 6 | 7 | export { router } from './router' 8 | export { routeForMenu } 9 | -------------------------------------------------------------------------------- /src/router/name.ts: -------------------------------------------------------------------------------- 1 | export enum RouteName { 2 | Dashboard = 'dashboard', 3 | Post = 'post', 4 | ViewPost = 'view-posts', 5 | EditPost = 'edit-posts', 6 | EditCategory = 'edit-category', 7 | Note = 'note', 8 | ViewNote = 'view-notes', 9 | EditNote = 'edit-notes', 10 | Topic = 'topic', 11 | Comment = 'comment', 12 | Page = 'page', 13 | 14 | ListPage = 'page-list', 15 | EditPage = 'page-edit', 16 | Say = 'say', 17 | ListSay = 'say-list', 18 | EditSay = 'say-edit', 19 | Project = 'project', 20 | ListProject = 'project-list', 21 | 22 | EditProject = 'project-edit', 23 | Friend = 'friends', 24 | File = 'files', 25 | Analyze = 'analyze', 26 | Setting = 'setting', 27 | Profile = 'setting-profile', 28 | System = 'setting-system', 29 | Security = 'setting-security', 30 | Reset = 'reset', 31 | Backup = 'backup', 32 | Markdown = 'markdown', 33 | Subscribe = 'subscribe', 34 | Cron = 'cron', 35 | Log = 'log', 36 | Optimize = 'optimize', 37 | Login = 'login', 38 | Home = 'home', 39 | Setup = 'setup', 40 | ListShortHand = 'shorthand', 41 | Snippet = 'snippet', 42 | Webhook = 'webhook', 43 | 44 | AssetTemplate = 'asset-template', 45 | Pty = 'pty', 46 | 47 | Ai = 'ai', 48 | AiSummary = 'ai-summary', 49 | 50 | Maintain = 'maintain', 51 | Other = 'other', 52 | Reader = 'reader', 53 | } 54 | -------------------------------------------------------------------------------- /src/router/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | import { routes } from './route' 4 | 5 | export const router = createRouter({ 6 | history: createWebHashHistory(), 7 | 8 | routes, 9 | }) 10 | -------------------------------------------------------------------------------- /src/shared/types/base.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from '~/models/base' 2 | 3 | export type WriteBaseType = { 4 | title: string 5 | text: string 6 | allowComment: boolean 7 | 8 | id?: string 9 | images: Image[] 10 | created?: string 11 | modified?: string 12 | 13 | meta?: any 14 | } 15 | -------------------------------------------------------------------------------- /src/socket/index.ts: -------------------------------------------------------------------------------- 1 | import { SocketClient } from './socket-client' 2 | 3 | const client = new SocketClient() 4 | window.socket = client 5 | export { client as socket } 6 | -------------------------------------------------------------------------------- /src/socket/types.ts: -------------------------------------------------------------------------------- 1 | export enum EventTypes { 2 | GATEWAY_CONNECT = 'GATEWAY_CONNECT', 3 | GATEWAY_DISCONNECT = 'GATEWAY_DISCONNECT', 4 | 5 | VISITOR_ONLINE = 'VISITOR_ONLINE', 6 | VISITOR_OFFLINE = 'VISITOR_OFFLINE', 7 | 8 | AUTH_FAILED = 'AUTH_FAILED', 9 | 10 | COMMENT_CREATE = 'COMMENT_CREATE', 11 | 12 | POST_CREATE = 'POST_CREATE', 13 | POST_UPDATE = 'POST_UPDATE', 14 | POST_DELETE = 'POST_DELETE', 15 | 16 | NOTE_CREATE = 'NOTE_CREATE', 17 | NOTE_UPDATE = 'NOTE_UPDATE', 18 | NOTE_DELETE = 'NOTE_DELETE', 19 | 20 | PAGE_UPDATED = 'PAGE_UPDATED', 21 | 22 | SAY_CREATE = 'SAY_CREATE', 23 | SAY_DELETE = 'SAY_DELETE', 24 | SAY_UPDATE = 'SAY_UPDATE', 25 | 26 | LINK_APPLY = 'LINK_APPLY', 27 | 28 | DANMAKU_CREATE = 'DANMAKU_CREATE', 29 | // util 30 | CONTENT_REFRESH = 'CONTENT_REFRESH', // 内容更新或重置 页面需要重载 31 | // for admin 32 | IMAGE_REFRESH = 'IMAGE_REFRESH', 33 | IMAGE_FETCH = 'IMAGE_FETCH', 34 | 35 | ADMIN_NOTIFICATION = 'ADMIN_NOTIFICATION', 36 | STDOUT = 'STDOUT', 37 | 38 | PTY = 'pty', 39 | 40 | PTY_MESSAGE = 'pty_message', 41 | } 42 | 43 | export type NotificationTypes = 'error' | 'warn' | 'success' | 'info' 44 | -------------------------------------------------------------------------------- /src/stores/app.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Innei 3 | * @Date: 2021-03-22 11:41:32 4 | * @LastEditTime: 2021-03-22 11:41:32 5 | * @LastEditors: Innei 6 | * @FilePath: /admin-next/src/stores/ui.ts 7 | * Mark: Coding with Love 8 | */ 9 | import { RESTManager } from '~/utils' 10 | import { onMounted, ref } from 'vue' 11 | 12 | export interface ViewportRecord { 13 | w: number 14 | h: number 15 | mobile: boolean 16 | pad: boolean 17 | hpad: boolean 18 | wider: boolean 19 | widest: boolean 20 | phone: boolean 21 | } 22 | 23 | export const useAppStore = defineStore('app', () => { 24 | const app = ref() 25 | onMounted(() => { 26 | RESTManager.api.get().then((res) => { 27 | app.value = res 28 | }) 29 | }) 30 | return { 31 | app, 32 | } 33 | }) 34 | 35 | export { useAppStore as AppStore } 36 | -------------------------------------------------------------------------------- /src/stores/category.ts: -------------------------------------------------------------------------------- 1 | import { RESTManager } from '~/utils/rest' 2 | import { computed, ref } from 'vue' 3 | import type { CategoryModel, CategoryResponse } from '~/models/category' 4 | 5 | export const useCategoryStore = defineStore('category', () => { 6 | const data = ref() 7 | 8 | const map = computed( 9 | () => new Map(data.value?.map((i) => [i.id, i])) || new Map(), 10 | ) 11 | 12 | return { 13 | data, 14 | map, 15 | get(id: string) { 16 | return map.value.get(id) 17 | }, 18 | async fetch(force?: boolean) { 19 | if (!data.value || force) { 20 | const response = (await RESTManager.api.categories.get({ 21 | params: { 22 | type: 'Category', 23 | }, 24 | })) as CategoryResponse 25 | 26 | data.value = response.data 27 | } else { 28 | return data.value 29 | } 30 | }, 31 | } 32 | }) 33 | 34 | export { useCategoryStore as CategoryStore } 35 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from './app' 2 | import { useCategoryStore } from './category' 3 | import { useUIStore } from './ui' 4 | import { useUserStore } from './user' 5 | 6 | ;([useUserStore, useAppStore, useCategoryStore, useUIStore] as const).forEach( 7 | (store: any) => { 8 | if (import.meta.hot) 9 | import.meta.hot.accept(acceptHMRUpdate(store, import.meta.hot)) 10 | }, 11 | ) 12 | 13 | export const piniaStore = createPinia() 14 | -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { useRouter } from 'vue-router' 3 | 4 | import { getToken, setToken } from '../utils/auth' 5 | import { RESTManager } from '../utils/rest' 6 | import type { UserModel } from '../models/user' 7 | 8 | let tokenIsUpstream = false 9 | 10 | export const setTokenIsUpstream = (isUpstream: boolean) => { 11 | tokenIsUpstream = isUpstream 12 | } 13 | 14 | export const getTokenIsUpstream = () => { 15 | return tokenIsUpstream 16 | } 17 | 18 | export const useUserStore = defineStore('user', () => { 19 | const user = ref(null) 20 | const token = ref('') 21 | 22 | const $token = getToken() 23 | if ($token) { 24 | token.value = $token 25 | } 26 | const router = useRouter() 27 | return { 28 | user, 29 | token, 30 | 31 | async fetchUser() { 32 | try { 33 | const $user = await RESTManager.api.master.get() 34 | user.value = $user 35 | } catch (error: any) { 36 | if (error.data?.message == '没有完成初始化!') { 37 | router.replace('/setup') 38 | } 39 | } 40 | }, 41 | 42 | updateToken($token: string) { 43 | if ($token) { 44 | setToken($token) 45 | } 46 | 47 | token.value = $token 48 | }, 49 | } 50 | }) 51 | 52 | export { useUserStore as UserStore } 53 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { MessageApi, useDialog, useNotification } from 'naive-ui' 2 | import type { VNodeProps } from 'vue' 3 | 4 | declare global { 5 | export interface Window { 6 | message: MessageApi 7 | notification: ReturnType 8 | dialog: ReturnType 9 | injectData: { 10 | BASE_API: null | string 11 | WEB_URL: null | string 12 | GATEWAY: null | string 13 | LOGIN_BG: null | string 14 | TITLE: null | string 15 | 16 | INIT: null | boolean 17 | 18 | PAGE_PROXY: boolean 19 | } 20 | 21 | [K: string]: any 22 | } 23 | 24 | export const message: Omit & { 25 | error: (content: ContentType, options?: MessageOptions) => never 26 | } 27 | export const dialog: ReturnType 28 | export const notification: ReturnType 29 | 30 | export const Fragment: { 31 | new (): { 32 | $props: VNodeProps 33 | } 34 | __isFragment: true 35 | } 36 | 37 | export const __DEV__: boolean 38 | export type KV = Record 39 | 40 | export type Class = new (...args: any[]) => T 41 | } 42 | 43 | export {} 44 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | import { router } from '~/router' 4 | import { setTokenIsUpstream } from '~/stores/user' 5 | 6 | export const TokenKey = 'mx-token' 7 | 8 | /** 9 | * 带了 bearer 10 | */ 11 | export function getToken(): string | null { 12 | const token = Cookies.get(TokenKey) 13 | return token ? `bearer ${token}` : null 14 | } 15 | 16 | export function setToken(token: string) { 17 | if (typeof token !== 'string') { 18 | return 19 | } 20 | return Cookies.set(TokenKey, token, { 21 | expires: 14, 22 | }) 23 | } 24 | 25 | export function removeToken() { 26 | return Cookies.remove(TokenKey) 27 | } 28 | export const attachTokenFromQuery = () => { 29 | const token = new URLSearchParams(window.location.search).get('token') 30 | if (token) { 31 | setToken(token) 32 | setTokenIsUpstream(true) 33 | 34 | router.isReady().then(() => { 35 | const parsedUrl = new URL(window.location.href) 36 | parsedUrl.searchParams.delete('token') 37 | 38 | // Vue router 在 hash 模式无法解决这个问题 39 | history.replaceState({}, '', parsedUrl.href) 40 | 41 | const query = {} as any 42 | for (const [key, value] of parsedUrl.searchParams.entries()) { 43 | query[key] = value 44 | } 45 | 46 | router.replace({ 47 | path: parsedUrl.pathname, 48 | query, 49 | }) 50 | }) 51 | } else { 52 | // hash mode 53 | 54 | const hash = window.location.hash.slice(1) 55 | 56 | const parsedUrl = new URL(hash, window.location.origin) 57 | const token = parsedUrl.searchParams.get('token') 58 | if (token) { 59 | setToken(token) 60 | setTokenIsUpstream(true) 61 | parsedUrl.searchParams.delete('token') 62 | 63 | router.isReady().then(() => { 64 | const query = {} as any 65 | for (const [key, value] of parsedUrl.searchParams.entries()) { 66 | query[key] = value 67 | } 68 | 69 | router.replace({ 70 | path: parsedUrl.pathname, 71 | query, 72 | }) 73 | }) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/authjs/auth.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from 'better-auth/client' 2 | 3 | import { API_URL } from '~/constants/env' 4 | 5 | export const authClient = createAuthClient({ 6 | baseURL: API_URL + '/auth', 7 | fetchOptions: { 8 | credentials: 'include', 9 | }, 10 | }) 11 | 12 | export type AuthSocialProviders = 13 | | 'apple' 14 | | 'discord' 15 | | 'facebook' 16 | | 'github' 17 | | 'google' 18 | | 'microsoft' 19 | | 'spotify' 20 | | 'twitch' 21 | | 'twitter' 22 | | 'dropbox' 23 | | 'linkedin' 24 | | 'gitlab' 25 | -------------------------------------------------------------------------------- /src/utils/authjs/session.ts: -------------------------------------------------------------------------------- 1 | import type { authClient } from './auth' 2 | 3 | import { RESTManager } from '../rest' 4 | 5 | type Session = typeof authClient.$Infer.Session & { 6 | isOwner: boolean 7 | } 8 | export const getSession = async () => { 9 | return RESTManager.api.auth.session.get() 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/authn.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AuthenticationResponseJSON, 3 | RegistrationResponseJSON, 4 | } from '@simplewebauthn/types' 5 | 6 | import { startAuthentication, startRegistration } from '@simplewebauthn/browser' 7 | 8 | import { RESTManager } from './rest' 9 | 10 | class AuthnUtilsStatic { 11 | async createPassKey(name: string) { 12 | const registrationOptions = 13 | await RESTManager.api.passkey.register.post() 14 | let attResp: RegistrationResponseJSON 15 | try { 16 | // Pass the options to the authenticator and wait for a response 17 | attResp = await startRegistration(registrationOptions) 18 | } catch (error: any) { 19 | // Some basic error handling 20 | if (error.name === 'InvalidStateError') { 21 | message.error( 22 | 'Error: Authenticator was probably already registered by user', 23 | ) 24 | } else { 25 | message.error(error.message) 26 | } 27 | } 28 | 29 | try { 30 | Object.assign(attResp, { 31 | name, 32 | }) 33 | const verificationResp = 34 | await RESTManager.api.passkey.register.verify.post({ 35 | data: attResp, 36 | }) 37 | if (verificationResp.verified) { 38 | message.success('Successfully registered authenticator') 39 | } else { 40 | message.error('Error: Could not verify authenticator') 41 | } 42 | } catch { 43 | message.error('Error: Could not verify authenticator') 44 | } 45 | } 46 | 47 | async validate(test?: boolean) { 48 | const registrationOptions = 49 | await RESTManager.api.passkey.authentication.post() 50 | let attResp: AuthenticationResponseJSON 51 | try { 52 | // Pass the options to the authenticator and wait for a response 53 | attResp = await startAuthentication(registrationOptions) 54 | } catch (error: any) { 55 | // Some basic error handling 56 | 57 | message.error(error.message) 58 | } 59 | 60 | if (test) { 61 | Object.assign(attResp, { test: true }) 62 | } 63 | try { 64 | const verificationResp = 65 | await RESTManager.api.passkey.authentication.verify.post<{ 66 | verified: boolean 67 | token?: string 68 | }>({ 69 | data: attResp, 70 | }) 71 | if (verificationResp.verified) { 72 | message.success('Successfully authentication by passkey') 73 | } else { 74 | message.error('Error: Could not verify authenticator') 75 | } 76 | return verificationResp 77 | } catch (error: any) { 78 | message.error(error.message) 79 | } 80 | } 81 | } 82 | 83 | export const AuthnUtils = new AuthnUtilsStatic() 84 | -------------------------------------------------------------------------------- /src/utils/build-menus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | import type { RouteRecordNormalized } from 'vue-router' 3 | 4 | type TRouteRecordNormalized = Omit & { 5 | meta?: { 6 | query?: KV 7 | params?: KV 8 | icon: VNode 9 | title?: string 10 | hide?: boolean 11 | [key: string]: any 12 | } 13 | } 14 | export interface MenuModel { 15 | title: string 16 | path: string 17 | icon: any 18 | subItems?: Array 19 | hasParent: boolean 20 | fullPath: string 21 | query?: any 22 | } 23 | 24 | const parsePath = (path: string, params?: KV) => { 25 | // 1. add slash 26 | let n = /^\//.test(path) ? path : `/${path}` 27 | 28 | // 2. replace default params into path 29 | const hasParams = n.match(/(\/?:)/) 30 | if (!hasParams) { 31 | return n 32 | } 33 | if (!params || Object.prototype.toString.call(params) !== '[object Object]') { 34 | throw new TypeError('params must be object') 35 | } 36 | for (const paramKey in params) { 37 | n = n.replaceAll(`:${paramKey}`, params[paramKey]) 38 | } 39 | return n 40 | } 41 | 42 | const buildModel = ( 43 | item: TRouteRecordNormalized, 44 | hasParent: boolean, 45 | prevPath: string, 46 | ): MenuModel => { 47 | const path = parsePath(item.path, item.meta?.params) 48 | 49 | const fullPath = `${prevPath}/${path}` 50 | 51 | return { 52 | title: (item.meta?.title as string) || item.name?.toString() || path, 53 | path, 54 | icon: item.meta?.icon as any, 55 | subItems: buildSubMenus(item, fullPath), 56 | hasParent, 57 | fullPath: fullPath.replaceAll('//', '/'), 58 | query: item.meta?.query, 59 | } 60 | } 61 | function buildSubMenus(route: TRouteRecordNormalized, prevPath = '') { 62 | if (Array.isArray(route.children)) { 63 | return route.children 64 | .filter((item) => { 65 | if (!item.meta) { 66 | return true 67 | } 68 | return item.meta.hide !== true 69 | }) 70 | .map((item) => { 71 | return buildModel(item as TRouteRecordNormalized, true, prevPath) 72 | }) 73 | } else { 74 | return [] 75 | } 76 | } 77 | 78 | export const buildMenus = ( 79 | routes: Array, 80 | ): MenuModel[] => 81 | ( 82 | routes.find((item) => item.name === 'home' && item.path === '/') as any 83 | ).children 84 | .filter( 85 | (item: TRouteRecordNormalized) => 86 | item.path !== '*' && item.meta?.hide !== true, 87 | ) 88 | .map((item: TRouteRecordNormalized) => { 89 | return buildModel(item, false, '') 90 | }) 91 | 92 | export { buildModel as buildMenuModel } 93 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import tinycolor from 'tinycolor2' 2 | 3 | import { ThemeColorConfig } from '~/../theme.config' 4 | 5 | export const colorRef = useStorage('mx-admin-color', ThemeColorConfig) 6 | 7 | interface Colors { 8 | primaryColor: string 9 | primaryColorHover: string 10 | primaryColorPressed: string 11 | primaryColorSuppl: string 12 | } 13 | 14 | export function defineColors(baseColor: string): Colors { 15 | const base = tinycolor(baseColor) 16 | 17 | const primaryColor = base.toHexString() 18 | const primaryColorHover = base.brighten(10).toHexString() 19 | const primaryColorPressed = base.darken(10).toHexString() 20 | const primaryColorSuppl = base.darken(15).toHexString() 21 | 22 | return { 23 | primaryColor, 24 | primaryColorHover, 25 | primaryColorPressed, 26 | primaryColorSuppl, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/confetti.ts: -------------------------------------------------------------------------------- 1 | import confetti from 'canvas-confetti' 2 | 3 | export function showConfetti() { 4 | const end = Date.now() + 100 5 | const config: confetti.Options = { 6 | particleCount: 25, 7 | startVelocity: 90, 8 | angle: 60, 9 | spread: 60, 10 | origin: { x: 0, y: 1 }, 11 | zIndex: 300, 12 | gravity: 1.5, 13 | colors: ['#6AD991', '#F6C549', '#E65040', '#5B89F7', '#9688F2'], 14 | } 15 | 16 | ;(function frame() { 17 | confetti({ 18 | ...config, 19 | angle: 60, 20 | origin: { x: 0, y: 1 }, 21 | }) 22 | 23 | confetti({ 24 | ...config, 25 | angle: 120, 26 | origin: { x: 1, y: 1 }, 27 | }) 28 | 29 | if (Date.now() < end) { 30 | requestAnimationFrame(frame) 31 | } 32 | })() 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/endpoint.ts: -------------------------------------------------------------------------------- 1 | import { RESTManager, getToken } from '~/utils' 2 | 3 | export const buildMarkdownRenderUrl = (id: string, withToken?: boolean) => { 4 | const endpoint = RESTManager.endpoint 5 | const url = new URL(endpoint) 6 | return `${url.protocol}//${url.host}/render/markdown/${id}${ 7 | withToken ? `?token=${getToken()}` : '' 8 | }` 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/event-bus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | 3 | /* 4 | * @Author: Innei 5 | * @Date: 2020-05-23 14:31:11 6 | * @LastEditTime: 2021-01-09 12:37:37 7 | * @LastEditors: Innei 8 | * @FilePath: /web/utils/observable.ts 9 | * @MIT 10 | */ 11 | import type { EventTypes } from '~/socket/types' 12 | 13 | type IDisposable = () => void 14 | export class EventBus { 15 | private observers: Record = {} 16 | 17 | on(event: EventTypes, handler: any): IDisposable 18 | on(event: string, handler: any): IDisposable 19 | on(event: string, handler: (...rest: any) => void): IDisposable { 20 | const queue = this.observers[event] 21 | 22 | const disposer = () => { 23 | this.off(event, handler) 24 | } 25 | if (!queue) { 26 | this.observers[event] = [handler] 27 | return disposer 28 | } 29 | const isExist = queue.includes(handler) 30 | if (!isExist) { 31 | this.observers[event].push(handler) 32 | } 33 | 34 | return disposer 35 | } 36 | 37 | emit(event: string, payload?: any, ...args: any[]): void 38 | emit(event: EventTypes, payload?: any, ...args: any[]): void 39 | emit(event: EventTypes, payload?: any, ...args: any[]) { 40 | const queue = this.observers[event] 41 | if (!queue) { 42 | return 43 | } 44 | for (const func of queue) { 45 | func.call(this, payload, ...args) 46 | } 47 | } 48 | 49 | off(event: string, handler?: (...rest: any) => void) { 50 | const queue = this.observers[event] 51 | if (!queue) { 52 | return 53 | } 54 | 55 | if (handler) { 56 | const index = queue.indexOf(handler) 57 | if (index !== -1) { 58 | queue.splice(index, 1) 59 | } 60 | } else { 61 | queue.length = 0 62 | } 63 | } 64 | } 65 | export const bus = new EventBus() 66 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { isEqual, isObject } from 'es-toolkit/compat' 2 | import transform from 'lodash.transform' 3 | import { toRaw } from 'vue' 4 | 5 | export * from './auth' 6 | export * from './build-menus' 7 | export * from './rest' 8 | export * from './time' 9 | 10 | /** 11 | * diff 两层,Object 浅层比较,引用不一致返回整个不一样的 Object 12 | * @param origin 13 | * @param newObject 14 | * @returns 15 | */ 16 | export const shallowDiff = ( 17 | origin: T, 18 | newObject: T, 19 | ): Partial => { 20 | const diff = {} as Partial 21 | 22 | for (const key in newObject) { 23 | if (isObject(newObject[key])) { 24 | const insideObject = newObject[key] 25 | const originInsideObject = origin[key] 26 | // shallow compare, 2 层 27 | Object.keys(toRaw(insideObject)).map((key$) => { 28 | if (isObject(insideObject[key$])) { 29 | const insideObject$ = insideObject[key$] 30 | for (const k in insideObject$) { 31 | if (insideObject$[k] !== originInsideObject[key$][k]) { 32 | diff[key] = insideObject 33 | 34 | break 35 | } 36 | } 37 | } else if (insideObject[key$] !== originInsideObject[key$]) { 38 | diff[key] = insideObject 39 | } 40 | }) 41 | } else if (newObject[key] !== origin[key]) { 42 | diff[key] = newObject[key] 43 | } 44 | } 45 | 46 | return diff 47 | } 48 | 49 | /** 50 | * 深层 diff, 返回不一致的 KV 51 | * @param base 52 | * @param object 53 | * @returns 54 | */ 55 | export function deepDiff(base: T, object: T): Partial { 56 | function changes(object: any, base: any) { 57 | return transform(object, (result: any, value, key) => { 58 | if (!isEqual(value, base?.[key])) { 59 | result[key] = 60 | isObject(value) && isObject(base?.[key]) 61 | ? changes(value, base?.[key]) 62 | : value 63 | } 64 | }) 65 | } 66 | return changes(object, base) 67 | } 68 | 69 | export function responseBlobToFile(response: any, filename: string): void { 70 | const url = window.URL.createObjectURL(new Blob([response as any])) 71 | const link = document.createElement('a') 72 | link.href = url 73 | link.setAttribute('download', filename) 74 | document.body.append(link) 75 | link.click() 76 | } 77 | 78 | export function toPascalCase(string: string) { 79 | return `${string}` 80 | .replaceAll(new RegExp(/[_-]+/, 'g'), ' ') 81 | .replaceAll(new RegExp(/[^\s\w]/, 'g'), '') 82 | .replaceAll( 83 | new RegExp(/\s+(.)(\w*)/, 'g'), 84 | ($1, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}`, 85 | ) 86 | .replace(new RegExp(/\w/), (s) => s.toUpperCase()) 87 | } 88 | 89 | export function uuid() { 90 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(/[xy]/g, (c) => { 91 | const r = (Math.random() * 16) | 0, 92 | v = c == 'x' ? r : (r & 0x3) | 0x8 93 | return v.toString(16) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /src/utils/is-init.ts: -------------------------------------------------------------------------------- 1 | import { router } from '~/router' 2 | 3 | import { RESTManager } from './rest' 4 | 5 | export const checkIsInit = async (): Promise => { 6 | try { 7 | // FIXME 8 | return ( 9 | window.injectData?.INIT ?? 10 | ( 11 | await RESTManager.api.init 12 | .get<{ isInit: boolean }>({ 13 | errorHandler(e) { 14 | if (e?.response.status == 404 || e?.response.status === 403) { 15 | return { isInit: true } 16 | } 17 | 18 | throw e 19 | }, 20 | }) 21 | .then((res) => { 22 | if (typeof res !== 'object' || (res && !('isInit' in res))) { 23 | router.push('/setup-api') 24 | message.error('api error') 25 | } 26 | return res 27 | }) 28 | ).isInit === true 29 | ) 30 | } catch (error) { 31 | console.error(error) 32 | return false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/json.ts: -------------------------------------------------------------------------------- 1 | export const JSONSafeParse: typeof JSON.parse = (...rest) => { 2 | try { 3 | return JSON.parse(...rest) 4 | } catch { 5 | return null 6 | } 7 | } 8 | 9 | export const JSONParseReturnOriginal: typeof JSON.parse = (...rest) => { 10 | try { 11 | return JSON.parse(...rest) 12 | } catch { 13 | return rest[0] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/markdown-parser.ts: -------------------------------------------------------------------------------- 1 | import { load } from 'js-yaml' 2 | 3 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 4 | export class ParseMarkdownYAML { 5 | constructor(private strList: string[]) { 6 | this.strList = strList 7 | } 8 | 9 | parse(str: string) { 10 | const raw = str 11 | 12 | // 增加对windows CRLF的兼容 13 | const parts = /-{3,}\r?\n(.*?)-{3,}\r?\n*(.*)$/gms.exec(raw) 14 | if (!parts) { 15 | return { text: raw } 16 | } 17 | 18 | const text = parts.pop() 19 | const parseYAML = load(parts[1]) 20 | const meta: Partial> = {} 21 | const { categories, tags, date, updated, created, title } = parseYAML as any 22 | 23 | if (date || created) meta.date = new Date(date || created).toISOString() 24 | if (updated) meta.updated = new Date(updated).toISOString() 25 | 26 | meta.categories = categories 27 | meta.tags = tags 28 | meta.title = title 29 | 30 | Object.keys(meta).forEach((key) => { 31 | const value = meta[key] 32 | if (typeof value === 'undefined') delete meta[key] 33 | }) 34 | 35 | return { meta, text } as ParsedModel 36 | } 37 | 38 | start() { 39 | const files = this.strList 40 | const contents = [] as ParsedModel[] 41 | for (const [idx, file] of files.entries()) { 42 | try { 43 | contents.push(this.parse(file)) 44 | } catch (err) { 45 | throw { 46 | idx, 47 | err, 48 | } 49 | } 50 | } 51 | return contents 52 | } 53 | } 54 | 55 | export interface ParsedModel { 56 | meta?: { 57 | title: string 58 | updated: string 59 | date: string 60 | categories: Array 61 | tags: Array 62 | slug: string 63 | } 64 | text: string 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked' 2 | 3 | export const pickImagesFromMarkdown = (text: string) => { 4 | const ast = marked.lexer(text) 5 | const images = new Set() 6 | function pickImage(node: any) { 7 | if (node.type === 'image') { 8 | images.add(node.href) 9 | return 10 | } 11 | if (node.tokens && Array.isArray(node.tokens)) { 12 | return node.tokens.forEach(pickImage) 13 | } 14 | } 15 | ast.forEach(pickImage) 16 | 17 | return [...images.values()] 18 | } 19 | 20 | const videoExts = ['mp4', 'webm', 'ogg', 'avi', 'mov', 'flv', 'wmv', 'mkv'] 21 | 22 | export const isVideoExt = (ext: string) => { 23 | return videoExts.includes(ext) 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/notification.ts: -------------------------------------------------------------------------------- 1 | export class BrowserNotification { 2 | constructor() { 3 | this.initNotice() 4 | } 5 | 6 | initNotice(): Promise { 7 | return new Promise((r, j) => { 8 | if (!('Notification' in window)) { 9 | j(new Error('浏览器不支持发送通知')) 10 | } else if (Notification.permission !== 'denied') { 11 | Notification.requestPermission().then((p) => 12 | p === 'granted' ? r(true) : j(new Error('已拒绝通知')), 13 | ) 14 | } else if (Notification.permission === 'denied') { 15 | return j(new Error('已拒绝通知')) 16 | } else { 17 | j(true) 18 | } 19 | }) 20 | } 21 | 22 | notice( 23 | title: string, 24 | body: string, 25 | options: Omit = {}, 26 | ): Promise { 27 | return new Promise((r) => { 28 | this.initNotice().then((b) => { 29 | if (b && !document.hasFocus()) { 30 | const notification = new Notification(title, { body, ...options }) 31 | r(notification) 32 | } 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 100 => 100 3 | * 1100 => 1.1K 4 | * 11000 => 10K 5 | * 1100000 => 1.1M 6 | * 1000000000 => 1B 7 | * 1100000000 => 1.1B 8 | */ 9 | export const formatNumber = (number: number) => { 10 | const len = String(number).length 11 | 12 | if (len < 4) { 13 | return number 14 | } 15 | 16 | if (len < 7) { 17 | return `${(number / 1000).toFixed(1)}K` 18 | } 19 | 20 | if (len < 10) { 21 | return `${(number / 1000000).toFixed(1)}M` 22 | } 23 | 24 | return `${(number / 1000000000).toFixed(1)}B` 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 2 | import { format as f } from 'date-fns' 3 | 4 | export enum DateFormat { 5 | 'yyyy 年 M 月 d 日', 6 | 'yyyy 年 M 月 d 日 HH:mm:ss', 7 | 'HH:mm', 8 | 9 | 'H:mm:ss A', 10 | 'M-d HH:mm:ss', 11 | } 12 | 13 | export const parseDate = ( 14 | time: string | Date, 15 | format: keyof typeof DateFormat = 'yyyy 年 M 月 d 日', 16 | ) => { 17 | const date = new Date(time) 18 | if (isNaN(date as any)) return 'N/A' 19 | return f(date, format) 20 | } 21 | 22 | export const relativeTimeFromNow = ( 23 | time: Date | string, 24 | current = new Date(), 25 | ) => { 26 | if (!time) { 27 | return '-' 28 | } 29 | time = new Date(time) 30 | const msPerMinute = 60 * 1000 31 | const msPerHour = msPerMinute * 60 32 | const msPerDay = msPerHour * 24 33 | const msPerMonth = msPerDay * 30 34 | const msPerYear = msPerDay * 365 35 | 36 | const elapsed = +current - +time 37 | 38 | if (elapsed < msPerMinute) { 39 | const gap = Math.ceil(elapsed / 1000) 40 | return gap <= 0 ? '刚刚' : `${gap} 秒前` 41 | } else if (elapsed < msPerHour) { 42 | return `${Math.round(elapsed / msPerMinute)} 分钟前` 43 | } else if (elapsed < msPerDay) { 44 | return `${Math.round(elapsed / msPerHour)} 小时前` 45 | } else if (elapsed < msPerMonth) { 46 | return `${Math.round(elapsed / msPerDay)} 天前` 47 | } else if (elapsed < msPerYear) { 48 | return `${Math.round(elapsed / msPerMonth)} 个月前` 49 | } else { 50 | return `${Math.round(elapsed / msPerYear)} 年前` 51 | } 52 | } 53 | 54 | export const getDayOfYear = (date = new Date()) => { 55 | const now = date 56 | const start = new Date(now.getFullYear(), 0, 0) 57 | const diff = now.getTime() - start.getTime() 58 | const oneDay = 1000 * 60 * 60 * 24 59 | const day = Math.floor(diff / oneDay) 60 | 61 | return day 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/word.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 2022 xxxx -> 2022 3 | * 月色真美! -> 月 4 | */ 5 | export const textToBigCharOrWord = (name: string | undefined) => { 6 | if (!name) { 7 | return '' 8 | } 9 | const splitOnce = name.split(' ')[0] 10 | const bigChar = splitOnce.length > 4 ? name[0] : splitOnce 11 | return bigChar 12 | } 13 | -------------------------------------------------------------------------------- /src/views/analyze/types.tsx: -------------------------------------------------------------------------------- 1 | export interface IPAggregate { 2 | today: Today[] 3 | weeks: Week[] 4 | months: Month[] 5 | paths: Path[] 6 | total: Total 7 | todayIps: string[] 8 | } 9 | export interface Month { 10 | date: string 11 | key: Key 12 | value: number 13 | } 14 | export interface Path { 15 | count: number 16 | path: string 17 | } 18 | export interface Today { 19 | hour: string 20 | key: Key 21 | value: number 22 | } 23 | export interface Total { 24 | callTime: number 25 | uv: number 26 | } 27 | export interface Week { 28 | day: string 29 | key: Key 30 | value: number 31 | } 32 | 33 | export enum Key { 34 | IP = 'ip', 35 | PV = 'pv', 36 | } 37 | -------------------------------------------------------------------------------- /src/views/comments/markdown-render.tsx: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked' 2 | import xss from 'xss' 3 | 4 | marked.use({ 5 | renderer: { 6 | html({ text }) { 7 | return xss(text) 8 | }, 9 | }, 10 | }) 11 | const render = (text: string) => 12 | marked.parse(text, { 13 | gfm: true, 14 | }) 15 | 16 | export const CommentMarkdownRender = defineComponent({ 17 | props: { 18 | text: { 19 | type: String, 20 | required: true, 21 | }, 22 | }, 23 | setup(props) { 24 | return () =>
25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/views/dashboard/badge.tsx: -------------------------------------------------------------------------------- 1 | import { NBadge } from 'naive-ui' 2 | import { defineComponent } from 'vue' 3 | 4 | const Badge = defineComponent({ 5 | props: { processing: Boolean, value: [String, Number] }, 6 | setup(props, ctx) { 7 | return () => ( 8 | 9 | {props.value === 'N/A' ? ( 10 | ctx.slots 11 | ) : ( 12 | {ctx.slots} 13 | )} 14 | 15 | ) 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/views/dashboard/card.tsx: -------------------------------------------------------------------------------- 1 | import { NBadge, NButton, NCard, NSpace, NThing } from 'naive-ui' 2 | import type { PropType, VNode } from 'vue' 3 | 4 | import { Icon } from '@vicons/utils' 5 | 6 | import { Statistic } from './statistic' 7 | 8 | export interface CardProps { 9 | label: string 10 | value: number | string 11 | icon: VNode | (() => VNode) 12 | actions?: { 13 | name: string 14 | onClick: () => void 15 | primary?: boolean 16 | showBadage?: boolean 17 | }[] 18 | } 19 | 20 | export const Card = defineComponent({ 21 | props: { 22 | label: String, 23 | value: [Number, String], 24 | icon: Function as PropType<() => VNode>, 25 | actions: { 26 | type: Array as PropType< 27 | { 28 | name: string 29 | onClick: () => void 30 | primary?: boolean 31 | showBadge?: { type: Boolean; default: false } 32 | }[] 33 | >, 34 | default: () => [], 35 | }, 36 | }, 37 | 38 | setup(props: CardProps) { 39 | return () => ( 40 | <> 41 | 42 | 43 | {{ 44 | header() { 45 | return 46 | }, 47 | ['header-extra']: function () { 48 | return ( 49 | 50 | {typeof props.icon == 'function' 51 | ? props.icon() 52 | : props.icon} 53 | 54 | ) 55 | }, 56 | 57 | action() { 58 | if (!props.actions) { 59 | return null 60 | } 61 | return ( 62 | 63 | {props.actions.map((i) => { 64 | const Inner = () => 65 | i.primary ? ( 66 | 67 | {i.name} 68 | 69 | ) : ( 70 | 71 | {i.name} 72 | 73 | ) 74 | return i.showBadage && 75 | props.value && 76 | props.value !== 'N/A' ? ( 77 | 78 | 79 | 80 | ) : ( 81 | 82 | ) 83 | })} 84 | 85 | ) 86 | }, 87 | }} 88 | 89 | 90 | 91 | ) 92 | }, 93 | }) 94 | -------------------------------------------------------------------------------- /src/views/dashboard/statistic.tsx: -------------------------------------------------------------------------------- 1 | import { NSkeleton, NSpace, NStatistic } from 'naive-ui' 2 | import { defineComponent } from 'vue' 3 | 4 | export const Statistic = defineComponent({ 5 | props: { label: String, value: [String, Number] }, 6 | setup(props) { 7 | return () => { 8 | const value = props.value 9 | return ( 10 | 11 | {props.value === 'N/A' ? ( 12 | 13 | 14 | 17 | 18 | ) : ( 19 | 30 | )} 31 | 32 | ) 33 | } 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /src/views/dashboard/update-panel.tsx: -------------------------------------------------------------------------------- 1 | import { ShellOutputNormal } from '~/components/output-modal/normal' 2 | import { usePortalElement } from '~/hooks/use-portal-element' 3 | import { RESTManager } from '~/utils' 4 | 5 | export const UpdatePanel = defineComponent({ 6 | setup(_) { 7 | const $shellRef = ref() 8 | const handleUpdate = () => { 9 | $shellRef.value.run(`${RESTManager.endpoint}/update/upgrade/dashboard`) 10 | } 11 | 12 | const portal = usePortalElement() 13 | 14 | onMounted(() => { 15 | nextTick(() => { 16 | handleUpdate() 17 | }) 18 | }) 19 | 20 | return () => ( 21 | { 24 | setTimeout(() => { 25 | portal(null) 26 | }, 1000) 27 | }} 28 | /> 29 | ) 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/views/debug/serverless/index.tsx: -------------------------------------------------------------------------------- 1 | import { HeaderActionButton } from '~/components/button/rounded-button' 2 | import { FunctionCodeEditor } from '~/components/function-editor' 3 | import { CheckCircleOutlinedIcon } from '~/components/icons' 4 | import { ContentLayout } from '~/layouts/content' 5 | import { TwoColGridLayout } from '~/layouts/two-col' 6 | import { defaultServerlessFunction } from '~/models/snippet' 7 | import { NGi, useMessage } from 'naive-ui' 8 | import { RESTManager } from '~/utils' 9 | 10 | import { useLocalStorage } from '@vueuse/core' 11 | 12 | export default defineComponent({ 13 | setup() { 14 | const value = useLocalStorage('debug-serverless', defaultServerlessFunction) 15 | 16 | const message = useMessage() 17 | const previewRef = ref() 18 | const errorMsg = ref('') 19 | const runTest = async () => { 20 | try { 21 | const res = await RESTManager.api.debug.function.post({ 22 | data: { 23 | function: value.value, 24 | }, 25 | errorHandler: (err) => { 26 | errorMsg.value = `Error: ${err.data.message}` 27 | message.error(err.data.message) 28 | }, 29 | }) 30 | 31 | import('monaco-editor').then((mo) => { 32 | mo.editor 33 | .colorize(JSON.stringify(res.data, null, 2), 'typescript', { 34 | tabSize: 2, 35 | }) 36 | .then((res) => { 37 | previewRef.value!.innerHTML = res 38 | }) 39 | .catch(() => { 40 | previewRef.value!.innerHTML = JSON.stringify(res, null, 2) 41 | }) 42 | }) 43 | } catch {} 44 | } 45 | return () => ( 46 | 49 | } 51 | onClick={runTest} 52 | > 53 | 54 | } 55 | > 56 | 57 | 58 |
59 | 60 |
61 |
62 | 63 |
67 |               {errorMsg.value}
68 |             
69 |
70 |
71 |
72 | ) 73 | }, 74 | }) 75 | -------------------------------------------------------------------------------- /src/views/extra-features/assets/template/code-editor.tsx: -------------------------------------------------------------------------------- 1 | import { CenterSpin } from '~/components/spin' 2 | import { 3 | useAsyncLoadMonaco, 4 | usePropsValueToRef, 5 | } from '~/hooks/use-async-monaco' 6 | import type { PropType } from 'vue' 7 | 8 | export const CodeEditorForTemplateEditing = defineComponent({ 9 | props: { 10 | value: { 11 | type: String, 12 | required: true, 13 | }, 14 | onChange: { 15 | type: Function as PropType<(str: string) => void>, 16 | required: true, 17 | }, 18 | }, 19 | setup(props) { 20 | const editorRef = ref() 21 | const value = usePropsValueToRef(props) 22 | 23 | const obj = useAsyncLoadMonaco(editorRef, value, props.onChange, { 24 | language: 'html', 25 | }) 26 | 27 | return () => ( 28 |
29 |
30 | {!obj.loaded.value && ( 31 | 32 | )} 33 |
34 | ) 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/views/extra-features/assets/template/ejs-render.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'ejs' 2 | import type { PropType } from 'vue' 3 | 4 | export const EJSRender = defineComponent({ 5 | props: { 6 | template: { 7 | type: String, 8 | required: true, 9 | }, 10 | data: { 11 | type: Object, 12 | required: true, 13 | }, 14 | onError: { 15 | type: Function as PropType<(err: Error) => void>, 16 | }, 17 | }, 18 | setup(props) { 19 | const html = ref('') 20 | watch( 21 | () => props.template, 22 | async () => { 23 | html.value = await render(props.template, props.data, { 24 | async: true, 25 | }).catch((error) => { 26 | props.onError?.(error) 27 | 28 | console.error(error) 29 | 30 | return html.value 31 | }) 32 | }, 33 | { immediate: true }, 34 | ) 35 | 36 | return () => ( 37 |
38 |
39 |
40 | ) 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /src/views/extra-features/assets/template/index.tsx: -------------------------------------------------------------------------------- 1 | import { ContentLayout } from '~/layouts/content' 2 | import { NTabPane, NTabs } from 'naive-ui' 3 | 4 | import { EmailTab } from './tabs/email' 5 | import { MarkdownTab } from './tabs/markdown' 6 | 7 | export default defineComponent({ 8 | setup() { 9 | const tab = ref('1') 10 | return () => ( 11 | 12 | { 16 | tab.value = tabvalue 17 | }} 18 | > 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/views/extra-features/assets/template/tabs/markdown.tsx: -------------------------------------------------------------------------------- 1 | export const MarkdownTab = defineComponent({ 2 | setup() { 3 | return () =>
即将推出
4 | }, 5 | }) 6 | -------------------------------------------------------------------------------- /src/views/extra-features/snippets/components/code-editor.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionCodeEditor } from '~/components/function-editor' 2 | import { usePropsValueToRef } from '~/hooks/use-async-monaco' 3 | import type { PropType } from 'vue' 4 | 5 | export const CodeEditorForSnippet = defineComponent({ 6 | props: { 7 | onSave: { 8 | type: Function as PropType<() => any>, 9 | }, 10 | 11 | value: { 12 | type: String, 13 | required: true, 14 | }, 15 | onChange: { 16 | type: Function as PropType<(str: string) => void>, 17 | required: true, 18 | }, 19 | language: { 20 | type: String, 21 | required: true, 22 | }, 23 | }, 24 | setup(props) { 25 | const value = usePropsValueToRef(props) 26 | 27 | const editorRef = ref() 28 | 29 | watch( 30 | () => value.value, 31 | () => { 32 | if (!editorRef.value) { 33 | return 34 | } 35 | if (editorRef.value.loaded) { 36 | props.onChange?.(value.value) 37 | } 38 | }, 39 | ) 40 | 41 | return () => ( 42 |
43 | 49 |
50 | ) 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /src/views/extra-features/snippets/components/install-dep-button.tsx: -------------------------------------------------------------------------------- 1 | import { HeaderActionButton } from '~/components/button/rounded-button' 2 | import { DownloadOutlined } from '~/components/icons' 3 | import { NButton, NCard, NForm, NFormItem, NInput, NModal } from 'naive-ui' 4 | 5 | import { InstallDepsXterm } from './install-dep-xterm' 6 | 7 | export const InstallDependencyButton = defineComponent({ 8 | setup() { 9 | const modalShow = ref(false) 10 | 11 | const $installDepsComponent = ref>() 12 | 13 | const input = ref('') 14 | 15 | const ok = (e?: Event) => { 16 | e?.preventDefault() 17 | // @ts-expect-error 18 | $installDepsComponent.value?.install(input.value) 19 | } 20 | return () => { 21 | return ( 22 | <> 23 | } 25 | name="安装依赖" 26 | color="#FADC" 27 | onClick={() => { 28 | modalShow.value = true 29 | }} 30 | /> 31 | 32 | { 35 | modalShow.value = show 36 | }} 37 | > 38 | 39 | 40 | 41 | { 44 | input.value = value 45 | }} 46 | placeholder="E.g. qs" 47 | /> 48 | 49 |
50 | 51 | 安装 52 | 53 |
54 |
55 |
56 |
57 | 58 | 59 | ) 60 | } 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /src/views/extra-features/snippets/components/install-dep-xterm.tsx: -------------------------------------------------------------------------------- 1 | import { ShellOutputXterm } from '~/components/output-modal/xterm' 2 | import { RESTManager } from '~/utils' 3 | 4 | export const InstallDepsXterm = defineComponent({ 5 | setup(_, { expose }) { 6 | const $shell = ref() 7 | expose({ 8 | install(pkg: string | string[], onFinish?: () => any) { 9 | $shell.value.run( 10 | `${RESTManager.endpoint}/dependencies/install_deps?packageNames=${ 11 | Array.isArray(pkg) ? pkg.join(',') : pkg 12 | }`, 13 | onFinish, 14 | ) 15 | }, 16 | }) 17 | 18 | return () => 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /src/views/extra-features/snippets/index.tsx: -------------------------------------------------------------------------------- 1 | import { ContentLayout } from '~/layouts/content' 2 | import { NTabPane, NTabs } from 'naive-ui' 3 | import { useRoute, useRouter } from 'vue-router' 4 | 5 | import { Tab2ForEdit } from './tabs/for-edit' 6 | import { Tab1ForList } from './tabs/for-list' 7 | 8 | export default defineComponent({ 9 | name: 'SnippetView', 10 | setup() { 11 | const route = useRoute() 12 | const router = useRouter() 13 | const currentTab = computed(() => route.query.tab || '0') 14 | 15 | return () => ( 16 | 17 | { 21 | router.push({ 22 | query: { 23 | tab: e, 24 | }, 25 | }) 26 | }} 27 | > 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/views/extra-features/snippets/interfaces/snippet-group.ts: -------------------------------------------------------------------------------- 1 | export interface SnippetGroup { 2 | reference: string 3 | count: number 4 | } 5 | -------------------------------------------------------------------------------- /src/views/extra-features/subscribe/constants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SubscribeNoteCreateBit, 3 | SubscribePostCreateBit, 4 | SubscribeRecentCreateBit, 5 | SubscribeSayCreateBit, 6 | } from '@mx-space/api-client' 7 | 8 | const bit2TextMap = new Map([ 9 | [SubscribePostCreateBit, '博文'], 10 | [SubscribeNoteCreateBit, '手记'], 11 | [SubscribeRecentCreateBit, '速记'], 12 | [SubscribeSayCreateBit, '说说'], 13 | ]) 14 | 15 | export { bit2TextMap as SubscribeBit2TextMap } 16 | -------------------------------------------------------------------------------- /src/views/login/index.module.css: -------------------------------------------------------------------------------- 1 | .r { 2 | :global { 3 | .bg { 4 | @apply fixed bottom-0 left-0 right-0 top-0; 5 | @apply -m-4 bg-gray-600 bg-cover bg-center bg-no-repeat transition-opacity duration-700 ease-linear; 6 | 7 | filter: blur(5px); 8 | } 9 | 10 | .wrapper { 11 | @apply absolute flex flex-col items-center justify-center; 12 | 13 | top: 50%; 14 | left: 50%; 15 | width: 300px; 16 | transform: translate(-50%, -50%); 17 | } 18 | 19 | .input-wrap { 20 | position: relative; 21 | overflow: hidden; 22 | margin: 2rem 0; 23 | & input { 24 | position: relative; 25 | -webkit-text-fill-color: #eee; 26 | color: #eee; 27 | 28 | @apply rounded-3xl bg-white bg-opacity-20 tracking-wider; 29 | 30 | padding: 3px 14px; 31 | line-height: 1.8; 32 | backdrop-filter: blur(24px); 33 | } 34 | 35 | & * { 36 | outline: none; 37 | } 38 | } 39 | 40 | form { 41 | @apply flex flex-col items-center justify-center; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/maintenance/log-view/index.tsx: -------------------------------------------------------------------------------- 1 | import { ContentLayout } from '~/layouts/content' 2 | import { NTabPane, NTabs } from 'naive-ui' 3 | import { useRoute, useRouter } from 'vue-router' 4 | 5 | import { LogListView } from './tabs/log-list' 6 | import { RealtimeLogPipeline } from './tabs/realtime-log' 7 | 8 | export default defineComponent({ 9 | setup() { 10 | const route = useRoute() 11 | const tabIndex = computed(() => route.query.tab?.toString() || '0') 12 | const router = useRouter() 13 | 14 | return () => ( 15 | 16 | { 20 | router.replace({ 21 | ...route, 22 | query: { 23 | ...route.query, 24 | tab, 25 | }, 26 | }) 27 | }} 28 | > 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/views/maintenance/log-view/tabs/realtime-log.tsx: -------------------------------------------------------------------------------- 1 | import { Xterm } from '~/components/xterm' 2 | import { socket } from '~/socket' 3 | import { EventTypes } from '~/socket/types' 4 | import { bus } from '~/utils/event-bus' 5 | import { useMountAndUnmount } from '~/hooks/use-lifecycle' 6 | import type { Terminal } from '@xterm/xterm' 7 | 8 | export const RealtimeLogPipeline = defineComponent({ 9 | setup() { 10 | const listen = (prevLog = true) => { 11 | socket.socket.emit('log', { prevLog }) 12 | } 13 | 14 | let term: Terminal 15 | const messageQueue: string[] = [] 16 | const logHandler = (e) => { 17 | if (!term) { 18 | messageQueue.push(e) 19 | } else { 20 | if (messageQueue.length > 0) { 21 | emptyQueue(term) 22 | } 23 | term.write(e) 24 | } 25 | } 26 | 27 | const emptyQueue = (term: Terminal) => { 28 | while (messageQueue.length) { 29 | const message = messageQueue.shift() 30 | 31 | term.write(message!) 32 | } 33 | } 34 | 35 | tryOnMounted(() => { 36 | listen() 37 | 38 | bus.on(EventTypes.STDOUT, logHandler) 39 | }) 40 | 41 | useMountAndUnmount(() => { 42 | const handler = () => { 43 | listen(false) 44 | } 45 | socket.socket.io.on('open', handler) 46 | 47 | return () => { 48 | socket.socket.io.off('open', handler) 49 | } 50 | }) 51 | 52 | onBeforeUnmount(() => { 53 | socket.socket.emit('unlog') 54 | 55 | bus.off(EventTypes.STDOUT, logHandler) 56 | }) 57 | 58 | return () => ( 59 | { 62 | term = _term 63 | 64 | emptyQueue(term) 65 | }} 66 | /> 67 | ) 68 | }, 69 | }) 70 | -------------------------------------------------------------------------------- /src/views/manage-friends/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { NAvatar } from 'naive-ui' 2 | 3 | import FallbackAvatar from './fallback.jpg' 4 | 5 | export const Avatar = defineComponent<{ avatar: string; name: string }>( 6 | (props) => { 7 | const $ref = ref() 8 | 9 | const inView = ref(false) 10 | const observer = useIntersectionObserver($ref, (intersection) => { 11 | if (intersection[0].isIntersecting) { 12 | inView.value = true 13 | observer.stop() 14 | } 15 | }) 16 | return () => ( 17 |
18 | {props.avatar ? ( 19 | inView.value ? ( 20 | { 24 | console.log(FallbackAvatar) 25 | ;(e.target as HTMLImageElement).src = FallbackAvatar 26 | }} 27 | > 28 | ) : ( 29 | {props.name.charAt(0)} 30 | ) 31 | ) : ( 32 | {props.name.charAt(0)} 33 | )} 34 |
35 | ) 36 | }, 37 | ) 38 | ;(Avatar as any).props = ['avatar', 'name'] 39 | -------------------------------------------------------------------------------- /src/views/manage-friends/components/fallback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mx-space/mx-admin/7c1b853374ea4ace29ce4184c6e8e86b797cd9e6/src/views/manage-friends/components/fallback.jpg -------------------------------------------------------------------------------- /src/views/manage-friends/components/reason-modal.tsx: -------------------------------------------------------------------------------- 1 | import { LinkState, LinkStateNameMap } from '~/models/link' 2 | import { NButton, NForm, NFormItem, NInput, NSelect } from 'naive-ui' 3 | import type { SelectMixedOption } from 'naive-ui/es/select/src/interface' 4 | import type { PropType } from 'vue' 5 | 6 | export const LinkAuditModal = defineComponent({ 7 | props: { 8 | onCallback: { 9 | type: Function as PropType<(state: LinkState, reason: string) => void>, 10 | required: true, 11 | }, 12 | }, 13 | setup(props) { 14 | const reason = ref('') 15 | const linkState = ref(LinkState.Pass) 16 | const stateOptions: SelectMixedOption[] = Object.entries(LinkStateNameMap) 17 | .filter(([key]) => key !== 'Audit') 18 | .map(([key, label]) => { 19 | return { 20 | value: LinkState[key], 21 | key, 22 | label, 23 | } 24 | }) 25 | 26 | const handleValidateButtonClick = () => { 27 | props.onCallback(linkState.value, reason.value) 28 | } 29 | 30 | return () => ( 31 | 32 | 33 | (linkState.value = val)} 36 | options={stateOptions} 37 | /> 38 | 39 | 40 | (reason.value = val)} 44 | placeholder="请输入原因" 45 | maxlength={200} 46 | autosize={{ 47 | maxRows: 4, 48 | minRows: 2, 49 | }} 50 | > 51 | 52 | 53 |
54 | 55 | 发送 56 | 57 |
58 |
59 | ) 60 | }, 61 | }) 62 | -------------------------------------------------------------------------------- /src/views/manage-friends/url-components.tsx: -------------------------------------------------------------------------------- 1 | import { NPopover } from 'naive-ui' 2 | 3 | export const UrlComponent = defineComponent({ 4 | props: { 5 | url: String, 6 | errorMessage: String, 7 | status: [String, Number], 8 | }, 9 | setup(props) { 10 | return () => ( 11 |
12 | 13 | {props.url} 14 | 15 | 16 | {typeof props.status !== 'undefined' && 17 | (props.errorMessage ? ( 18 | 19 | {{ 20 | trigger() { 21 | return
22 | }, 23 | default() { 24 | return props.errorMessage 25 | }, 26 | }} 27 |
28 | ) : ( 29 |
30 | ))} 31 |
32 | ) 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /src/views/manage-notes/hooks/use-memo-note-list.ts: -------------------------------------------------------------------------------- 1 | import { createMemoDataListFetchHook } from '~/hooks/use-memo-fetch-data-list' 2 | import { RESTManager } from '~/utils' 3 | import type { NoteModel, PaginateResult } from '@mx-space/api-client' 4 | 5 | export const useMemoNoteList = createMemoDataListFetchHook< 6 | { id: string; title: string; nid: number }, 7 | NoteModel 8 | >((page) => 9 | RESTManager.api.notes.get>({ 10 | params: { 11 | page, 12 | size: 50, 13 | select: 'nid title _id id', 14 | }, 15 | }), 16 | ) 17 | -------------------------------------------------------------------------------- /src/views/manage-posts/hooks/use-memo-post-list.ts: -------------------------------------------------------------------------------- 1 | import { RESTManager } from '~/utils' 2 | import { createMemoDataListFetchHook } from '../../../hooks/use-memo-fetch-data-list' 3 | import type { Category, PostModel } from '~/models/post' 4 | 5 | export const useMemoPostList = createMemoDataListFetchHook< 6 | { 7 | id: string 8 | title: string 9 | slug: string 10 | category: Category 11 | }, 12 | PostModel 13 | >((page) => 14 | RESTManager.api.posts.get({ 15 | params: { 16 | page, 17 | size: 50, 18 | select: 'id title nid _id slug category categoryId', 19 | }, 20 | }), 21 | ) 22 | -------------------------------------------------------------------------------- /src/views/reader/index.tsx: -------------------------------------------------------------------------------- 1 | import { NList, NListItem } from 'naive-ui' 2 | import useSWRV from 'swrv' 3 | 4 | import { GithubIcon, MingcuteUserStarFill } from '~/components/icons' 5 | import { ContentLayout } from '~/layouts/content' 6 | import { RESTManager } from '~/utils' 7 | 8 | type ReaderModel = { 9 | id: string 10 | provider: string 11 | type: string 12 | name: string 13 | email: string 14 | image: string 15 | isOwner: boolean 16 | } 17 | 18 | const ReaderView = defineComponent({ 19 | setup() { 20 | const { data } = useSWRV('reader', () => 21 | RESTManager.api.readers.get<{ data: ReaderModel[] }>(), 22 | ) 23 | return () => ( 24 | 25 | 26 | {data.value?.data.map((reader) => ( 27 | 28 | {{ 29 | prefix() { 30 | return ( 31 | 36 | {reader.name} 41 | 42 | ) 43 | }, 44 | default() { 45 | return ( 46 |
47 |

48 | 49 | 50 | {reader.name} 51 | 52 | 53 | {reader.isOwner && } 54 | 55 |

56 |

{reader.email}

57 |
58 | ) 59 | }, 60 | }} 61 |
62 | ))} 63 |
64 |
65 | ) 66 | }, 67 | }) 68 | 69 | export default ReaderView 70 | 71 | const ProviderIcon = defineComponent({ 72 | props: { 73 | provider: { 74 | type: String, 75 | required: true, 76 | }, 77 | }, 78 | setup(props) { 79 | return () => { 80 | switch (props.provider) { 81 | case 'github': 82 | return ( 83 | 84 | 85 | 86 | ) 87 | 88 | default: 89 | return ( 90 | 94 | ) 95 | } 96 | } 97 | }, 98 | }) 99 | -------------------------------------------------------------------------------- /src/views/setting/index.tsx: -------------------------------------------------------------------------------- 1 | import { NTabPane, NTabs } from 'naive-ui' 2 | import { defineComponent, ref, watch } from 'vue' 3 | import { useRoute, useRouter } from 'vue-router' 4 | 5 | import { ContentLayout } from '~/layouts/content' 6 | 7 | import { TabAuth } from './tabs/auth' 8 | import { TabSecurity } from './tabs/security' 9 | import { TabSystem } from './tabs/system' 10 | import { TabUser } from './tabs/user' 11 | 12 | enum SettingTab { 13 | User = 'user', 14 | System = 'system', 15 | Security = 'security', 16 | Auth = 'auth', 17 | } 18 | export default defineComponent({ 19 | setup() { 20 | const route = useRoute() 21 | const router = useRouter() 22 | const tabValue = ref(route.params.type as string) 23 | 24 | watch( 25 | () => route.params.type, 26 | (n) => { 27 | if (!n) { 28 | return 29 | } 30 | tabValue.value = n as any 31 | }, 32 | ) 33 | const headerActionsEl = ref(null) 34 | return () => ( 35 | 36 | { 39 | // @ts-expect-error 40 | router.replace({ ...route, params: { ...route.params, type: e } }) 41 | }} 42 | > 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ) 60 | }, 61 | }) 62 | -------------------------------------------------------------------------------- /src/views/setting/tabs/providers/oauth.ts: -------------------------------------------------------------------------------- 1 | export interface OauthData { 2 | providers: ProvidersItem[] 3 | public: { 4 | github: Github 5 | } 6 | } 7 | interface ProvidersItem { 8 | type: 'github' 9 | enabled: boolean 10 | } 11 | interface Github { 12 | clientId: string 13 | } 14 | 15 | export interface FlatOauthData { 16 | github: Github & { enabled: boolean; type: 'github' } 17 | } 18 | 19 | export function flattenOauthData(data: OauthData): FlatOauthData { 20 | const flatData: FlatOauthData = {} as any 21 | 22 | for (const provider of data.providers) { 23 | const providerType = provider.type 24 | if (providerType in data.public) { 25 | flatData[providerType] = { 26 | ...data.public[providerType], 27 | enabled: provider.enabled, 28 | type: providerType, 29 | } 30 | } else { 31 | console.warn( 32 | `Provider type ${providerType} found in providers but not in public data.`, 33 | ) 34 | } 35 | } 36 | 37 | return flatData 38 | } 39 | 40 | export const OauthDataInjectKey = Symbol.for('OauthDataInjectKey') 41 | 42 | export const useProvideOauthData = () => (data: Ref) => { 43 | return provide(OauthDataInjectKey, data) 44 | } 45 | 46 | export const useInjectOauthData = () => 47 | inject(OauthDataInjectKey) as Ref 48 | -------------------------------------------------------------------------------- /src/views/setting/tabs/user.module.css: -------------------------------------------------------------------------------- 1 | .tab-user { 2 | @apply !m-auto !max-w-[800px]; 3 | & .avatar { 4 | @apply relative; 5 | @apply flex items-center justify-center; 6 | 7 | &::before { 8 | @apply -translate-x-1/2 -translate-y-1/2; 9 | @apply border-gray$-default absolute content-['']; 10 | @apply rounded-full border-[108px]; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/views/setup/index.module.css: -------------------------------------------------------------------------------- 1 | .full { 2 | @apply relative h-[100vh] w-full overflow-auto; 3 | @apply flex items-center justify-center; 4 | 5 | & :global { 6 | .n-step-content__description { 7 | font-size: 10px; 8 | } 9 | .form-card { 10 | /* @apply !mt-8; */ 11 | } 12 | .bg-image { 13 | @apply fixed bottom-0 left-0 right-0 top-0 overflow-hidden; 14 | 15 | &::before { 16 | background-image: var(--bg); 17 | content: ''; 18 | 19 | @apply bottom-0 left-0 right-0 top-0; 20 | @apply bg-cover bg-center; 21 | @apply blur-md; 22 | @apply m-[-10px]; 23 | @apply absolute bottom-0 left-0 right-0 top-0; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/views/shorthand/index.module.css: -------------------------------------------------------------------------------- 1 | .timeline-grid { 2 | @apply grid; 3 | 4 | grid-template-columns: auto 50px; 5 | grid-gap: 0.5rem; 6 | 7 | &:hover { 8 | :global { 9 | .action { 10 | @apply opacity-100; 11 | } 12 | } 13 | } 14 | 15 | :global { 16 | .action { 17 | @apply relative flex opacity-0 transition-opacity; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/vue-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue' 6 | const component: DefineComponent<{}, {}, any> 7 | // eslint-disable-next-line import/no-default-export 8 | export default component 9 | } 10 | -------------------------------------------------------------------------------- /theme.config.ts: -------------------------------------------------------------------------------- 1 | export const ThemeColorConfig = { 2 | primaryColor: '#02AED2', // 基准中性蓝 3 | primaryColorHover: '#2FBEDF', // 较亮的中性蓝 4 | primaryColorPressed: '#0199AC', // 较暗的中性蓝 5 | primaryColorSuppl: '#016B7F', // 最暗的中性蓝,用于补充色 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "esModuleInterop": true, 13 | "noImplicitAny": false, 14 | "baseUrl": "./", 15 | "allowJs": true, 16 | "skipLibCheck": true, 17 | "jsxImportSource": "vue", 18 | "noEmit": true, 19 | "skipDefaultLibCheck": true, 20 | "lib": [ 21 | "esnext", 22 | "dom", 23 | "dom.iterable" 24 | ], 25 | "types": [], 26 | "paths": { 27 | "~": [ 28 | "./src" 29 | ], 30 | "~/*": [ 31 | "./src/*" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "src/**/*.ts", 37 | "src/**/*.d.ts", 38 | "src/**/*.tsx", 39 | "src/**/*.vue", 40 | ], 41 | "exclude": [ 42 | "node_modules", 43 | "assets", 44 | "src/components/function-editor/libs/node/**/*", 45 | "dist/**" 46 | ] 47 | } -------------------------------------------------------------------------------- /windi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'windicss/helpers' 2 | import lineClamp from 'windicss/plugin/line-clamp' 3 | 4 | import { ThemeColorConfig } from './theme.config' 5 | 6 | export default defineConfig({ 7 | content: ['./src/**/*.html', './src/**/*.vue', './src/**/*.tsx'], 8 | darkMode: 'class', 9 | 10 | theme: { 11 | extend: { 12 | screens: { 13 | 'light-mode': { raw: '(prefers-color-scheme: light)' }, 14 | 'dark-mode': { raw: '(prefers-color-scheme: dark)' }, 15 | phone: { raw: '(max-width: 768px)' }, 16 | desktop: { raw: '(min-width: 1024px)' }, 17 | tablet: { raw: '(max-width: 1023px)' }, 18 | }, 19 | zIndex: { 20 | '-10': -10, 21 | '-1': -1, 22 | 0: 0, 23 | 1: 1, 24 | 10: 10, 25 | 20: 20, 26 | 30: 30, 27 | 40: 40, 28 | 50: 50, 29 | 60: 60, 30 | 70: 70, 31 | 80: 80, 32 | 90: 90, 33 | 100: 100, 34 | auto: 'auto', 35 | }, 36 | colors: { 37 | primary: { 38 | default: 'var(--color-primary)', 39 | deep: 'var(--color-primary-deep)', 40 | shallow: 'var(--color-primary-shallow)', 41 | }, 42 | gray$: { 43 | default: '#ddd', 44 | }, 45 | }, 46 | }, 47 | }, 48 | 49 | plugins: [lineClamp], 50 | }) 51 | --------------------------------------------------------------------------------