├── .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 | 
22 | 
23 | 
24 | 
25 | 
26 | 
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 |
48 |
49 |
![]()
50 |
一个头像
51 |
52 |
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 |
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 |
25 |
33 |
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 |
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 |
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 |
--------------------------------------------------------------------------------