[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nmax_line_length = 80\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Enforce Unix newlines\n* text=auto eol=lf\n\npublic/mockServiceWorker.js linguist-vendored=true\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: pnpm\n\n      - name: Install\n        run: pnpm install\n\n      - name: Lint\n        run: pnpm run lint\n\n  typecheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: pnpm\n\n      - name: Install\n        run: pnpm install\n\n      - name: Typecheck\n        run: pnpm run typecheck\n\n  test:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        node-version: [20.x]\n        os: [ubuntu-latest]\n      fail-fast: false\n\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: pnpm install\n      - run: pnpm run test:unit\n\n  test-e2e:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x]\n        os: [ubuntu-latest]\n      fail-fast: false\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'pnpm'\n      - name: Cypress\n        uses: cypress-io/github-action@v6\n        with:\n          install-command: pnpm install\n          build: pnpm run build\n          start: pnpm run preview\n          record: true\n          command-prefix: '--'\n        env:\n          # pass the Dashboard record key as an environment variable\n          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n          # pass GitHub token to allow accurately detecting a build vs a re-run build\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          # pass the project ID from the secrets through environment variable\n          CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n*.local\n*.log\n.idea\n/.vite-inspect\n\n/coverage\n/public/docs\n/cypress/videos/*\n"
  },
  {
    "path": ".npmrc",
    "content": "auto-install-peers=true\n"
  },
  {
    "path": ".prettierignore",
    "content": "pnpm-lock.yaml\nsrc/auto-imports.d.ts\npublic/mockServiceWorker.js\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"vue.volar\",\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\",\n    \"lokalise.i18n-ally\",\n    \"lukas-tr.materialdesignicons-intellisense\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"name\": \"Debug Current Vitest File\",\n      \"autoAttachChildProcesses\": true,\n      \"skipFiles\": [\"<node_internals>/**\", \"**/node_modules/**\"],\n      \"program\": \"${workspaceRoot}/node_modules/vitest/vitest.mjs\",\n      \"args\": [\"run\", \"${relativeFile}\"],\n      \"smartStep\": true,\n      \"console\": \"integratedTerminal\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"i18n-ally.localesPaths\": [\"src/locales\"],\n  \"i18n-ally.keystyle\": \"nested\",\n  \"i18n-ally.enabledFrameworks\": [\"vue-sfc\", \"vue\"],\n  \"json.schemas\": [\n    {\n      \"fileMatch\": [\"/.prettierrc\"],\n      \"url\": \"https://json.schemastore.org/prettierrc.json\"\n    }\n  ],\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022-present Yue JIN & NuStar Nuclear\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img alt=\"Vitify - Opinionated Vuetify Admin Starter Template\" src=\"public/favicon.svg\" width=200px/>\n</p>\n<h1 align=\"center\">Vitify Admin</h1>\n\n<p align=\"center\">\n  <a href=\"https://github.com/vuejs/vue\">\n    <img src=\"https://img.shields.io/badge/vue-2.7.16-brightgreen.svg\" alt=\"vue\">\n  </a>\n  <a href=\"https://github.com/vuetifyjs/vuetify\">\n    <img src=\"https://img.shields.io/badge/vuetify-2.7.2-blue.svg\" alt=\"vuetify\">\n  </a>\n  <a href=\"https://github.com/kingyue737/vitify-admin/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/mashape/apistatus.svg\" alt=\"license\">\n  </a>\n</p>\n\n<p align='center'>\n<b>Vite</b> + <b>Vuetify</b>, Opinionated Admin Starter Template<br><br>\n</p>\n\n<p align='center'>\n<a href=\"https://vitify-admin.netlify.app/\">Live Demo<br><br></a>\n<a href=\"https://kingyue737.github.io/vitify-docs/\">Documentation<br><br></a>\n</p>\n\n## Variants\n\n- [vitify-nuxt](https://github.com/kingyue737/vitify-nuxt) - with Nuxt 3, the best DX 🔥🔥🔥\n\n- [vitify-next](https://github.com/kingyue737/vitify-next) - Lightweight Vue 3 version of this template\n- [vitify-electron](https://github.com/kingyue737/vitify-electron) - Vuetify 3 + Electron starter\n\n## Features\n\n- 🦾 Full [TypeScript Support and intellisense](https://github.com/vuetifyjs/vuetify/issues/14798#issuecomment-1139788615) for [Vuetify 2](https://vuetifyjs.com/) components, powered by [Volar](https://github.com/johnsoncodehk/volar/tree/master/extensions/vscode-vue-language-features)\n\n- 🖖 [Vue 2.7](https://github.com/vuejs/vue) - Composition API and `<script setup>`\n\n- ⚡️ [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [ESBuild](https://github.com/evanw/esbuild) - born with fastness\n\n- 🗂️ [File based routing](./src/pages)\n\n- 📑 [Layout system](./src/layouts)\n\n- 🍍 [State Management via Pinia](https://pinia.vuejs.org/)\n\n- 🌍 [I18n ready](./locales)\n\n- 📥 [APIs auto importing](https://github.com/antfu/unplugin-auto-import) - use Composition API and others directly\n\n- ☁️ Deploy on [Netlify](https://www.netlify.com/), zero-config\n\n- 🧪 Unit/Component Testing with [Vitest](https://github.com/vitest-dev/vitest) + [Testing Library](https://github.com/testing-library/vue-testing-library), E2E Testing with [Cypress](https://cypress.io/) on [GitHub Actions](https://github.com/features/actions)\n\n<br>\n\n### Admin Starter Template\n\n- 🪟 Layout with drawer, header, footer(status bar) and login page\n\n- 🧭 Auto generated navigation drawer and breadcrumbs based on routes\n\n- 🤡 Mock API in dev and testing with [Mock Service Worker](https://github.com/mswjs/msw)\n\n- 🔔 Notification store\n\n- 🧑‍💼 Route authority based on user role\n\n- 📉 Data visualization with [vue-echarts](https://github.com/ecomfe/vue-echarts)\n\n- 🔗 Communicate with backend with REST API powered by [axios](https://github.com/axios/axios)\n\n- 🎨 Theme color customization and dark mode\n\n- 📱 Responsive layout\n\n## Pre-packed\n\n### UI Frameworks\n\n- [Vuetify 2](https://vuetifyjs.com/) - Material Design Framework\n\n### Plugins\n\n- [Vue Router](https://github.com/vuejs/vue-router)\n  - [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) - File system based routing\n  - [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) - Layouts for pages\n- [Pinia](https://pinia.esm.dev) - Intuitive, type safe, light and flexible Store for Vue using the Composition API\n- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) - Auto import Vuetify 2 components\n- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use Vue Composition API and others without importing\n- [PortalVue](https://github.com/linusborg/portal-vue) - Use [`<Teleport>`](https://vuejs.org/guide/built-ins/teleport.html) of Vue 3 in Vue 2\n- [Vue I18n](https://github.com/intlify/vue-i18n-next) - Internationalization\n  - [`vue-i18n-bridge`](https://github.com/intlify/vue-i18n-next/tree/master/packages/vue-i18n-bridge#readme) - Backport Composition API and message format syntax to Vue 2\n  - [`unplugin-vue-i18n`](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n) - Prebundle Vue I18n messages and support SFC i18n custom block\n- [VueUse](https://github.com/antfu/vueuse) - Collection of useful composition APIs\n- [Mock Service Worker](https://github.com/mswjs/msw) - Seamless REST/GraphQL API mocking library for browser and Node.js\n- [`vite-plugin-vue2-svg`](https://github.com/pakholeung37/vite-plugin-vue2-svg) - Load SVG files as Vue components, and auto register as Vuetify `v-icon`s\n\n### Compatibility\n\n- [`@vitejs/plugin-legacy`](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) - Generate polyfills with `@babel/preset-env` in production bundle\n- [`postcss-preset-env`](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env) - Convert modern CSS into what most browsers understand, determining polyfills based on `browserslist`\n\n### Coding Style\n\n- [Prettier](https://prettier.io/), single quotes, no semi\n- [ESLint](https://eslint.org/) with Flat Config\n\n### Dev tools\n\n- [TypeScript](https://www.typescriptlang.org/)\n- [Vitest](https://github.com/vitest-dev/vitest) - Unit testing powered by Vite\n- [Cypress](https://cypress.io/) - E2E testing\n- [pnpm](https://pnpm.js.org/) - Fast, disk space efficient package manager\n- [Netlify](https://www.netlify.com/) - zero-config deployment\n- [VS Code Extensions](./.vscode/extensions.json)\n  - [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - TypeScript support inside Vue SFCs\n  - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - All in one i18n support\n  - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Find and fix problems in your code\n  - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatter\n  - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)\n  - [Material Design Icons Intellisense](https://marketplace.visualstudio.com/items?itemName=lukas-tr.materialdesignicons-intellisense)\n\n## Try it now!\n\n> Vitify Admin requires Node >=16.6.0\n\n### GitHub Template\n\n[Create a repo from this template on GitHub](https://github.com/kingyue737/vitify-admin/generate).\n\n### Clone to local\n\nIf you prefer to do it manually with the cleaner git history\n\n```bash\nnpx degit kingyue737/vitify-admin my-vitify-app\ncd my-vitify-app\npnpm i\n```\n\n> Vitify Admin requires [`pnpm patch`](https://pnpm.io/cli/patch) for bug fixing in dependencies before maintainers release them. If you are using `yarn`, you can use [`yarn patch`](https://yarnpkg.com/cli/patch). For `npm` users, [`patch-package`](https://github.com/ds300/patch-package) is required as `npm` has no built-in patching functionality.\n\n## Checklist\n\nWhen you use this template, try follow the checklist to update your info properly\n\n- [ ] Change the author name in `LICENSE`\n- [ ] Change the title in `index.html`, navigation drawer and login page\n- [ ] Change the hostname in `vite.config.ts`\n- [ ] Change the favicon in `public`\n- [ ] Clean up the `README` and remove routes\n- [ ] Change the copyright in navigation drawer and login page\n- [ ] Change default locale of `vue-i18n`\n- [ ] Change or remove [Cypress Cloud](https://cloud.cypress.io/) related ID in [ci.yml](https://github.com/kingyue737/vitify-admin/blob/main/.github/workflows/ci.yml)\n\nAnd, enjoy :)\n\n## Usage\n\n### Development\n\nJust run and visit http://localhost:9527\n\n```bash\npnpm dev\n```\n\n### Build\n\nTo build the App, run\n\n```bash\npnpm build\n```\n\nAnd you will see the generated file in `dist` that ready to be served.\n\n### Type Check\n\n```\npnpm typecheck\n```\n\n### Testing\n\n```\npnpm test:unit\n```\n\nFor E2E test, you need to build the project first\n\n```\npnpm build\npnpm test:e2e\n```\n\n### Record on Cypress Cloud\n\nGo to [Cypress Cloud](https://cloud.cypress.io/), create a new project and add its `projectId` as `${CYPRESS_PROJECT_ID}`, its `record key` as `$CYPRESS_RECORD_KEY` in your repositry secrets (https://github.com/your-name/project-name/settings/secrets/actions).\n\nIf you don't want to use Cypress Cloud, remove `record: true` and the entire `env` block from [`.github/workflows/ci.yml`](https://github.com/kingyue737/vitify-admin/blob/main/.github/workflows/ci.yml):\n\n```yml\n- name: Cypress\n  uses: cypress-io/github-action@v4\n  with:\n    install-command: echo\n    build: pnpm run build\n    start: pnpm run preview\n    record: true\n    command-prefix: '--'\n  env:\n    # pass the Dashboard record key as an environment variable\n    CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n    # pass GitHub token to allow accurately detecting a build vs a re-run build\n    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    # pass the project ID from the secrets through environment variable\n    CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}\n```\n\n### Deploy on Netlify\n\nGo to [Netlify](https://app.netlify.com/start) and select your clone, `OK` along the way, and your App will be live in a minute.\n\n### Documentation\n\nThe [documentation](https://kingyue737.github.io/vitify-docs/) of this template is powered by [VitePress](https://vitepress.vuejs.org/) and [DocSearch](https://docsearch.algolia.com/)\n\nRepo: https://github.com/kingyue737/vitify-docs\n\n### Acknowledgement\n\nInspired by [vitesse](https://github.com/antfu/vitesse) and [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 💖. Thanks for every developer for making frontend community better.\n\nI made this starter template for promptly scaffolding admin projects of my company, along with some good practices I've learned during making these apps.\n\nCurrently, plenty of awesome Vue 2 librarys have not migrated to Vue 3 ecosystem, maybe never 😭. There is still a [long way to go](https://vuetifyjs.com/en/introduction/roadmap/#in-development) before Vuetify 3 includes all the features of Vuetify 2. So I struggle with bridging perfect DX of Vue 3 to my Vuetify 2 projects.\nIt's strongly opinionated, but hope it can help you to avoid detours.\n\nDon't hesitate to open an issue or a discussion if you meet any problem.\n"
  },
  {
    "path": "cypress/e2e/example.spec.ts",
    "content": "describe('Example Test', () => {\n  it('login', () => {\n    cy.visit('/')\n    cy.url().should('eq', 'http://localhost:5050/login')\n    cy.contains('Vitify').should('exist')\n\n    cy.contains('label', 'Username')\n      .invoke('attr', 'for')\n      .then((id) => {\n        cy.get('#' + id)\n      })\n      .type('your-username')\n\n    cy.contains('label', 'Password')\n      .invoke('attr', 'for')\n      .then((id) => {\n        cy.get('#' + id)\n      })\n      .type('your-password{Enter}')\n\n    cy.url().should('eq', 'http://localhost:5050/homepage')\n  })\n})\n"
  },
  {
    "path": "cypress.config.ts",
    "content": "import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    baseUrl: 'http://localhost:5050',\n    viewportWidth: 1280,\n    viewportHeight: 900,\n    chromeWebSecurity: false,\n    specPattern: 'cypress/e2e/**/*.spec.*',\n    supportFile: false,\n  },\n})\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import { includeIgnoreFile } from '@eslint/compat'\nimport js from '@eslint/js'\nimport eslintPluginVue from 'eslint-plugin-vue'\nimport ts from 'typescript-eslint'\nimport eslintConfigPrettier from 'eslint-config-prettier'\nimport pluginCypress from 'eslint-plugin-cypress/flat'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\nconst gitignorePath = path.resolve(__dirname, '.gitignore')\n\nexport default ts.config(\n  includeIgnoreFile(gitignorePath),\n  js.configs.recommended,\n  ...ts.configs.recommended,\n  ...eslintPluginVue.configs['flat/vue2-recommended'],\n  {\n    files: ['*.vue', '**/*.vue'],\n    languageOptions: {\n      parserOptions: {\n        parser: '@typescript-eslint/parser',\n      },\n    },\n  },\n  pluginCypress.configs.recommended,\n  eslintConfigPrettier,\n  {\n    rules: {\n      'no-undef': 'off',\n      'vue/multi-word-component-names': 'off',\n      'vue/valid-v-slot': ['error', { allowModifiers: true }], // allow vuetify slot modifier\n      'vue/html-self-closing': ['error', { html: { void: 'any' } }], // not conflict with prettier\n      '@typescript-eslint/no-unused-vars': 'off',\n      '@typescript-eslint/no-explicit-any': 'off',\n    },\n  },\n  { ignores: ['public/mockServiceWorker.js'] },\n)\n"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Vitify Admin</title>\n    <link rel=\"icon\" type=\"image/svg+xml\" as=\"image\" href=\"/favicon.svg\" />\n    <link rel=\"short icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "netlify.toml",
    "content": "[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200\n\n[[headers]]\n  for = \"/manifest.webmanifest\"\n  [headers.values]\n    Content-Type = \"application/manifest+json\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"pnpm@9.11.0\",\n  \"scripts\": {\n    \"dev\": \"vite --open --host\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview --port 5050 --host --config vite.config.preview.ts\",\n    \"test:e2e\": \"start-server-and-test preview http://127.0.0.1:5050/ 'cypress open'\",\n    \"test:e2e:ci\": \"start-server-and-test preview http://127.0.0.1:5050/ 'cypress run'\",\n    \"test:unit\": \"vitest\",\n    \"coverage\": \"vitest run --coverage\",\n    \"typecheck\": \"vue-tsc --build --force\",\n    \"lint\": \"eslint . --fix\",\n    \"format\": \"prettier . --write\"\n  },\n  \"dependencies\": {\n    \"@mdi/js\": \"^7.4.47\",\n    \"@vueuse/core\": \"^11.1.0\",\n    \"axios\": \"^1.7.7\",\n    \"echarts\": \"^5.5.1\",\n    \"pinia\": \"^2.2.2\",\n    \"portal-vue\": \"^2.1.7\",\n    \"vue\": \"^2.7.16\",\n    \"vue-echarts\": \"^7.0.3\",\n    \"vue-i18n\": \"^8.28.2\",\n    \"vue-i18n-bridge\": \"^9.14.1\",\n    \"vue-router\": \"^3.6.5\",\n    \"vuetify\": \"^2.7.2\"\n  },\n  \"devDependencies\": {\n    \"@eslint/compat\": \"^1.1.1\",\n    \"@intlify/core-base\": \"^9.14.1\",\n    \"@intlify/unplugin-vue-i18n\": \"^2.0.0\",\n    \"@kingyue/vite-plugin-vue2-svg\": \"^0.6.0\",\n    \"@pinia/testing\": \"^0.1.5\",\n    \"@testing-library/vue\": \"^5.9.0\",\n    \"@types/jsdom\": \"^21.1.7\",\n    \"@types/node\": \"^20.16.10\",\n    \"@vitejs/plugin-legacy\": \"^5.4.2\",\n    \"@vitejs/plugin-vue2\": \"^2.3.1\",\n    \"@vue/test-utils\": \"^1.3.6\",\n    \"browserslist\": \"^4.24.0\",\n    \"browserslist-to-esbuild\": \"^2.1.1\",\n    \"cypress\": \"^13.15.0\",\n    \"eslint\": \"^9.11.1\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-cypress\": \"^3.5.0\",\n    \"eslint-plugin-vue\": \"^9.28.0\",\n    \"flush-promises\": \"^1.0.2\",\n    \"jsdom\": \"^25.0.1\",\n    \"msw\": \"^2.4.9\",\n    \"postcss-preset-env\": \"^10.0.5\",\n    \"prettier\": \"^3.3.3\",\n    \"rollup-plugin-regexp\": \"^5.0.1\",\n    \"sass\": \"~1.32.13\",\n    \"start-server-and-test\": \"^2.0.8\",\n    \"terser\": \"^5.34.1\",\n    \"typescript\": \"^5.6.2\",\n    \"typescript-eslint\": \"^8.7.0\",\n    \"unplugin-auto-import\": \"^0.18.3\",\n    \"unplugin-vue-components\": \"^0.27.4\",\n    \"vite\": \"^5.4.8\",\n    \"vite-plugin-inspect\": \"^0.8.7\",\n    \"vite-plugin-pages\": \"^0.32.3\",\n    \"vite-plugin-vue-layouts\": \"^0.8.0\",\n    \"vitest\": \"^2.1.1\",\n    \"vue-template-compiler\": \"^2.7.16\",\n    \"vue-tsc\": \"^2.1.6\",\n    \"vuetify2-component-types\": \"^2.7.2\"\n  },\n  \"browserslist\": [\n    \"> 1.3%\",\n    \"last 2 versions\",\n    \"not dead\",\n    \"not op_mini all\",\n    \"not ie>0\"\n  ],\n  \"msw\": {\n    \"workerDirectory\": \"public\"\n  },\n  \"pnpm\": {\n    \"peerDependencyRules\": {\n      \"allowedVersions\": {\n        \"vite-plugin-vue-layouts>vite\": \"5\"\n      }\n    },\n    \"allowedDeprecatedVersions\": {\n      \"vue\": \"2\"\n    },\n    \"patchedDependencies\": {\n      \"vite-plugin-vue-layouts@0.8.0\": \"patches/vite-plugin-vue-layouts@0.8.0.patch\"\n    }\n  }\n}\n"
  },
  {
    "path": "patches/vite-plugin-vue-layouts@0.8.0.patch",
    "content": "diff --git a/package.json b/package.json\nindex de999023f87c55dee2e57e2583e1e96b6f95095f..3df13b998cd92bc3a2cfd1767147c4b042edc6a9 100644\n--- a/package.json\n+++ b/package.json\n@@ -18,11 +18,15 @@\n     \"client.d.ts\"\n   ],\n   \"exports\": {\n-    \".\": {\n-      \"require\": \"./dist/index.js\",\n-      \"import\": \"./dist/index.mjs\"\n+    \"./client\": {\n+      \"types\": \"./client.d.ts\"\n     },\n-    \"./*\": \"./*\"\n+    \"./*\": \"./*\",\n+    \".\": {\n+      \"types\": \"./dist/index.d.ts\",\n+      \"import\": \"./dist/index.mjs\",\n+      \"require\": \"./dist/index.js\"\n+    }\n   },\n   \"scripts\": {\n     \"dev\": \"npm run build -- --watch\",\n@@ -59,3 +63,4 @@\n     \"vue-router\": \"^4.0.13\"\n   }\n }\n+\n"
  },
  {
    "path": "prettier.config.js",
    "content": "/** @type {import(\"prettier\").Config} */\nexport default {\n  semi: false,\n  singleQuote: true,\n}\n"
  },
  {
    "path": "public/mockServiceWorker.js",
    "content": "/* eslint-disable */\n/* tslint:disable */\n\n/**\n * Mock Service Worker.\n * @see https://github.com/mswjs/msw\n * - Please do NOT modify this file.\n * - Please do NOT serve this file on production.\n */\n\nconst PACKAGE_VERSION = '2.4.7'\nconst INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'\nconst IS_MOCKED_RESPONSE = Symbol('isMockedResponse')\nconst activeClientIds = new Set()\n\nself.addEventListener('install', function () {\n  self.skipWaiting()\n})\n\nself.addEventListener('activate', function (event) {\n  event.waitUntil(self.clients.claim())\n})\n\nself.addEventListener('message', async function (event) {\n  const clientId = event.source.id\n\n  if (!clientId || !self.clients) {\n    return\n  }\n\n  const client = await self.clients.get(clientId)\n\n  if (!client) {\n    return\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  switch (event.data) {\n    case 'KEEPALIVE_REQUEST': {\n      sendToClient(client, {\n        type: 'KEEPALIVE_RESPONSE',\n      })\n      break\n    }\n\n    case 'INTEGRITY_CHECK_REQUEST': {\n      sendToClient(client, {\n        type: 'INTEGRITY_CHECK_RESPONSE',\n        payload: {\n          packageVersion: PACKAGE_VERSION,\n          checksum: INTEGRITY_CHECKSUM,\n        },\n      })\n      break\n    }\n\n    case 'MOCK_ACTIVATE': {\n      activeClientIds.add(clientId)\n\n      sendToClient(client, {\n        type: 'MOCKING_ENABLED',\n        payload: true,\n      })\n      break\n    }\n\n    case 'MOCK_DEACTIVATE': {\n      activeClientIds.delete(clientId)\n      break\n    }\n\n    case 'CLIENT_CLOSED': {\n      activeClientIds.delete(clientId)\n\n      const remainingClients = allClients.filter((client) => {\n        return client.id !== clientId\n      })\n\n      // Unregister itself when there are no more clients\n      if (remainingClients.length === 0) {\n        self.registration.unregister()\n      }\n\n      break\n    }\n  }\n})\n\nself.addEventListener('fetch', function (event) {\n  const { request } = event\n\n  // Bypass navigation requests.\n  if (request.mode === 'navigate') {\n    return\n  }\n\n  // Opening the DevTools triggers the \"only-if-cached\" request\n  // that cannot be handled by the worker. Bypass such requests.\n  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {\n    return\n  }\n\n  // Bypass all requests when there are no active clients.\n  // Prevents the self-unregistered worked from handling requests\n  // after it's been deleted (still remains active until the next reload).\n  if (activeClientIds.size === 0) {\n    return\n  }\n\n  // Generate unique request ID.\n  const requestId = crypto.randomUUID()\n  event.respondWith(handleRequest(event, requestId))\n})\n\nasync function handleRequest(event, requestId) {\n  const client = await resolveMainClient(event)\n  const response = await getResponse(event, client, requestId)\n\n  // Send back the response clone for the \"response:*\" life-cycle events.\n  // Ensure MSW is active and ready to handle the message, otherwise\n  // this message will pend indefinitely.\n  if (client && activeClientIds.has(client.id)) {\n    ;(async function () {\n      const responseClone = response.clone()\n\n      sendToClient(\n        client,\n        {\n          type: 'RESPONSE',\n          payload: {\n            requestId,\n            isMockedResponse: IS_MOCKED_RESPONSE in response,\n            type: responseClone.type,\n            status: responseClone.status,\n            statusText: responseClone.statusText,\n            body: responseClone.body,\n            headers: Object.fromEntries(responseClone.headers.entries()),\n          },\n        },\n        [responseClone.body],\n      )\n    })()\n  }\n\n  return response\n}\n\n// Resolve the main client for the given event.\n// Client that issues a request doesn't necessarily equal the client\n// that registered the worker. It's with the latter the worker should\n// communicate with during the response resolving phase.\nasync function resolveMainClient(event) {\n  const client = await self.clients.get(event.clientId)\n\n  if (client?.frameType === 'top-level') {\n    return client\n  }\n\n  const allClients = await self.clients.matchAll({\n    type: 'window',\n  })\n\n  return allClients\n    .filter((client) => {\n      // Get only those clients that are currently visible.\n      return client.visibilityState === 'visible'\n    })\n    .find((client) => {\n      // Find the client ID that's recorded in the\n      // set of clients that have registered the worker.\n      return activeClientIds.has(client.id)\n    })\n}\n\nasync function getResponse(event, client, requestId) {\n  const { request } = event\n\n  // Clone the request because it might've been already used\n  // (i.e. its body has been read and sent to the client).\n  const requestClone = request.clone()\n\n  function passthrough() {\n    const headers = Object.fromEntries(requestClone.headers.entries())\n\n    // Remove internal MSW request header so the passthrough request\n    // complies with any potential CORS preflight checks on the server.\n    // Some servers forbid unknown request headers.\n    delete headers['x-msw-intention']\n\n    return fetch(requestClone, { headers })\n  }\n\n  // Bypass mocking when the client is not active.\n  if (!client) {\n    return passthrough()\n  }\n\n  // Bypass initial page load requests (i.e. static assets).\n  // The absence of the immediate/parent client in the map of the active clients\n  // means that MSW hasn't dispatched the \"MOCK_ACTIVATE\" event yet\n  // and is not ready to handle requests.\n  if (!activeClientIds.has(client.id)) {\n    return passthrough()\n  }\n\n  // Notify the client that a request has been intercepted.\n  const requestBuffer = await request.arrayBuffer()\n  const clientMessage = await sendToClient(\n    client,\n    {\n      type: 'REQUEST',\n      payload: {\n        id: requestId,\n        url: request.url,\n        mode: request.mode,\n        method: request.method,\n        headers: Object.fromEntries(request.headers.entries()),\n        cache: request.cache,\n        credentials: request.credentials,\n        destination: request.destination,\n        integrity: request.integrity,\n        redirect: request.redirect,\n        referrer: request.referrer,\n        referrerPolicy: request.referrerPolicy,\n        body: requestBuffer,\n        keepalive: request.keepalive,\n      },\n    },\n    [requestBuffer],\n  )\n\n  switch (clientMessage.type) {\n    case 'MOCK_RESPONSE': {\n      return respondWithMock(clientMessage.data)\n    }\n\n    case 'PASSTHROUGH': {\n      return passthrough()\n    }\n  }\n\n  return passthrough()\n}\n\nfunction sendToClient(client, message, transferrables = []) {\n  return new Promise((resolve, reject) => {\n    const channel = new MessageChannel()\n\n    channel.port1.onmessage = (event) => {\n      if (event.data && event.data.error) {\n        return reject(event.data.error)\n      }\n\n      resolve(event.data)\n    }\n\n    client.postMessage(\n      message,\n      [channel.port2].concat(transferrables.filter(Boolean)),\n    )\n  })\n}\n\nasync function respondWithMock(response) {\n  // Setting response status code to 0 is a no-op.\n  // However, when responding with a \"Response.error()\", the produced Response\n  // instance will have status code set to 0. Since it's not possible to create\n  // a Response instance with status code 0, handle that use-case separately.\n  if (response.status === 0) {\n    return Response.error()\n  }\n\n  const mockedResponse = new Response(response.body, response)\n\n  Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {\n    value: true,\n    enumerable: true,\n  })\n\n  return mockedResponse\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { THEME_KEY, INIT_OPTIONS_KEY, UPDATE_OPTIONS_KEY } from 'vue-echarts'\nimport { useVuetify } from './composables/useVuetify'\nconst vuetify = useVuetify()\nconst { locale } = useI18n()\nprovide(\n  THEME_KEY,\n  computed(() => (vuetify?.theme.dark ? 'dark' : undefined)),\n)\nprovide(\n  INIT_OPTIONS_KEY,\n  computed(() => ({ locale: locale.value.toUpperCase() })),\n)\nprovide(UPDATE_OPTIONS_KEY, { notMerge: false })\n</script>\n\n<template>\n  <v-app>\n    <PortalTarget name=\"app\" class=\"d-contents\" />\n    <v-fade-transition mode=\"out-in\">\n      <router-view />\n    </v-fade-transition>\n  </v-app>\n</template>\n"
  },
  {
    "path": "src/api/users.ts",
    "content": "import service from '@/utils/request'\n\nexport type Role = 'superuser' | 'admin' | 'staff'\nexport type Group = {\n  id?: number\n  name: Role\n  permissions: number[]\n}\n\nexport interface IUserData {\n  id: number\n  username: string\n  name?: string\n  email?: string\n  groups: number[]\n  joinDate: string\n}\n\nexport type Token = {\n  accessToken: string\n  refreshToken: string\n  // tokenType: string\n  // expiresAt: number\n  // issuedAt: number\n  // refreshTokenExpiresAt: number\n  // refreshTokenIssuedAt: number\n}\n\nexport const getUsers = () => service.get<IUserData[]>('/users')\n\nexport const getUser = (userId: number) =>\n  service.get<IUserData>(`/users/${userId}`)\n\nexport const createUser = (user: IUserData) => service.post('/users', user)\n\nexport const updateUser = (user: Partial<IUserData>) =>\n  service.patch(`/users/${user.id}`, user)\n\nexport const deleteUser = (userId: number) => service.delete(`/users/${userId}`)\n\nexport const getToken = (username: string, password: string) =>\n  service.post<Token>(\n    '/auth/access-token',\n    new URLSearchParams({ username, password }),\n  )\n\nexport const refreshToken = (refreshToken: string) =>\n  service.post<Token>('/auth/refresh-token', { refreshToken })\n\nexport const resetPassword = (newPassword: string, oldPassword: string) =>\n  service.post(`/users/reset-password`, {\n    newPassword,\n    oldPassword,\n  })\n\nexport const getGroup = (id: number) => service.get<Group>(`/groups/${id}`)\n\nexport const getGroups = () => service.get<Group[]>(`/groups`)\n"
  },
  {
    "path": "src/assets/styles/_overrides.scss",
    "content": "@import './variables.scss';\n\n.v-main__wrap {\n  > .container--fluid {\n    padding-left: 20px;\n    padding-right: 20px;\n  }\n  > .d-contents,\n  > .d-fake > .d-fake,\n  > .d-fake {\n    > .container--fluid {\n      padding-left: 20px;\n      padding-right: 20px;\n    }\n  }\n}\n.v-dialog {\n  > .v-card {\n    > .v-card__title {\n      font-size: 18px;\n      text-align: center;\n      width: 100%;\n      padding: 24px 24px 0;\n    }\n    > .v-card__text {\n      padding-top: 24px;\n    }\n    > .v-card__actions {\n      padding-left: 24px;\n      padding-right: 24px;\n    }\n  }\n}\n.theme--light.v-text-field > .v-input__control > .v-input__slot:before {\n  border-color: #d2d2d2;\n}\n.theme--light .v-main {\n  background-color: #f2f5f8;\n}\n.v-data-table > .v-data-table__wrapper tbody {\n  tr:first-child:hover td:first-child {\n    border-top-left-radius: 0px;\n  }\n  tr:first-child:hover td:last-child {\n    border-top-right-radius: 0px;\n  }\n}\n.v-text-field.v-text-field--solo:not(.v-text-field--solo-flat)\n  > .v-input__control\n  > .v-input__slot {\n  box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14);\n}\n.v-select:not(.v-select--is-multiple) {\n  .v-chip {\n    cursor: pointer;\n    &:hover::before {\n      opacity: 0;\n    }\n  }\n}\n.v-card {\n  &.heading-margin {\n    margin-top: #{$card-heading-margin};\n    &.fill-height {\n      height: calc(100% - #{$card-heading-margin});\n    }\n  }\n  &.v-sheet:not(.v-sheet--outlined, .v-card--flat, [class*=' elevation-']) {\n    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14) !important;\n  }\n}\n.v-application .d-contents {\n  display: contents !important;\n}\nhtml {\n  overflow-y: overlay;\n}\n.v-head-card__content {\n  > .echarts {\n    margin-top: -30px;\n    height: calc(100% + 30px);\n    min-height: 50px;\n  }\n  .v-data-table:not(.v-data-table--dense) {\n    > .v-data-table__wrapper {\n      max-height: calc(\n        100vh - 170px - (#{$app-bar-height} + #{$footer-height})\n      );\n    }\n  }\n}\n.v-data-table__wrapper {\n  overflow: overlay;\n}\n.v-data-table--fixed-header {\n  > .v-data-table__wrapper {\n    overflow-y: overlay;\n  }\n}\n.v-menu__content {\n  overflow-y: overlay;\n}\n.v-data-table--dense {\n  .v-skeleton-loader__table-cell {\n    height: 32px;\n    width: 54px;\n  }\n}\n.v-data-footer {\n  padding-left: 0;\n}\n\n.v-application--is-ltr .v-data-table--fixed-header .v-data-footer {\n  margin-right: 0;\n}\n\n.v-app-bar {\n  .v-breadcrumbs {\n    flex-wrap: nowrap;\n    padding-left: 2px;\n    padding-right: 2px;\n    li {\n      white-space: nowrap;\n      transition-duration: 0.6s !important;\n      &:nth-child(even) {\n        padding: 0 0px;\n      }\n      &::before {\n        float: left;\n        padding: 0 12px;\n        color: rgba(122, 122, 122, 0.5);\n        content: '/';\n      }\n    }\n  }\n}\n\n.v-icon__component {\n  fill: currentColor;\n}\n\n.v-form {\n  display: contents;\n}\n\n.v-icon svg {\n  height: 1em;\n  width: auto;\n}\n\nhtml.dark {\n  color-scheme: dark;\n}\n"
  },
  {
    "path": "src/assets/styles/_scrollbar.scss",
    "content": "::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n::-webkit-scrollbar-thumb {\n  background-color: rgba(149, 149, 149, 0.4);\n  background-clip: content-box;\n  min-height: 28px;\n  border: 2px solid transparent;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background-color: rgba(149, 149, 149, 0.4);\n  border: 1px solid transparent;\n  border-radius: 4px;\n}\n"
  },
  {
    "path": "src/assets/styles/_utils.scss",
    "content": ".svg-up {\n  transform: rotate(0deg);\n}\n\n.svg-right {\n  transform: rotate(90deg);\n}\n\n.svg-down {\n  transform: rotate(180deg);\n}\n\n.svg-left {\n  transform: rotate(-90deg);\n}\n"
  },
  {
    "path": "src/assets/styles/index.scss",
    "content": "@import 'scrollbar';\n@import 'utils';\n@import 'overrides';\n"
  },
  {
    "path": "src/assets/styles/variables.scss",
    "content": "$footer-height: 30px;\n$app-bar-height: 60px;\n$card-heading-margin: 10px;\n"
  },
  {
    "path": "src/assets/styles/vuetify-variables.scss",
    "content": "$grid-breakpoints: (\n  'xs': 0,\n  'sm': 600px,\n  'md': 960px,\n  'lg': 1280px - 0px,\n  'xl': 1920px - 0px,\n);\n"
  },
  {
    "path": "src/auto-imports.d.ts",
    "content": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin-auto-import\n// biome-ignore lint: disable\nexport {}\ndeclare global {\n  const EffectScope: typeof import('vue')['EffectScope']\n  const Message: typeof import('./stores/message')['Message']\n  const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']\n  const computed: typeof import('vue')['computed']\n  const createApp: typeof import('vue')['createApp']\n  const createPinia: typeof import('pinia')['createPinia']\n  const customRef: typeof import('vue')['customRef']\n  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']\n  const defineComponent: typeof import('vue')['defineComponent']\n  const defineStore: typeof import('pinia')['defineStore']\n  const effectScope: typeof import('vue')['effectScope']\n  const getActivePinia: typeof import('pinia')['getActivePinia']\n  const getCurrentInstance: typeof import('vue')['getCurrentInstance']\n  const getCurrentScope: typeof import('vue')['getCurrentScope']\n  const h: typeof import('vue')['h']\n  const inject: typeof import('vue')['inject']\n  const isProxy: typeof import('vue')['isProxy']\n  const isReactive: typeof import('vue')['isReactive']\n  const isReadonly: typeof import('vue')['isReadonly']\n  const isRef: typeof import('vue')['isRef']\n  const mapActions: typeof import('pinia')['mapActions']\n  const mapGetters: typeof import('pinia')['mapGetters']\n  const mapState: typeof import('pinia')['mapState']\n  const mapStores: typeof import('pinia')['mapStores']\n  const mapWritableState: typeof import('pinia')['mapWritableState']\n  const markRaw: typeof import('vue')['markRaw']\n  const nextTick: typeof import('vue')['nextTick']\n  const onActivated: typeof import('vue')['onActivated']\n  const onBeforeMount: typeof import('vue')['onBeforeMount']\n  const onBeforeRouteLeave: typeof import('vue-router/composables')['onBeforeRouteLeave']\n  const onBeforeRouteUpdate: typeof import('vue-router/composables')['onBeforeRouteUpdate']\n  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']\n  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']\n  const onDeactivated: typeof import('vue')['onDeactivated']\n  const onErrorCaptured: typeof import('vue')['onErrorCaptured']\n  const onMounted: typeof import('vue')['onMounted']\n  const onRenderTracked: typeof import('vue')['onRenderTracked']\n  const onRenderTriggered: typeof import('vue')['onRenderTriggered']\n  const onScopeDispose: typeof import('vue')['onScopeDispose']\n  const onServerPrefetch: typeof import('vue')['onServerPrefetch']\n  const onUnmounted: typeof import('vue')['onUnmounted']\n  const onUpdated: typeof import('vue')['onUpdated']\n  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']\n  const provide: typeof import('vue')['provide']\n  const reactive: typeof import('vue')['reactive']\n  const readonly: typeof import('vue')['readonly']\n  const ref: typeof import('vue')['ref']\n  const resolveComponent: typeof import('vue')['resolveComponent']\n  const setActivePinia: typeof import('pinia')['setActivePinia']\n  const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']\n  const shallowReactive: typeof import('vue')['shallowReactive']\n  const shallowReadonly: typeof import('vue')['shallowReadonly']\n  const shallowRef: typeof import('vue')['shallowRef']\n  const storeToRefs: typeof import('pinia')['storeToRefs']\n  const toRaw: typeof import('vue')['toRaw']\n  const toRef: typeof import('vue')['toRef']\n  const toRefs: typeof import('vue')['toRefs']\n  const toValue: typeof import('vue')['toValue']\n  const triggerRef: typeof import('vue')['triggerRef']\n  const unref: typeof import('vue')['unref']\n  const useAppStore: typeof import('./stores/app')['useAppStore']\n  const useAttrs: typeof import('vue')['useAttrs']\n  const useCssModule: typeof import('vue')['useCssModule']\n  const useCssVars: typeof import('vue')['useCssVars']\n  const useI18n: typeof import('vue-i18n-bridge')['useI18n']\n  const useId: typeof import('vue')['useId']\n  const useLink: typeof import('vue-router/composables')['useLink']\n  const useMessageStore: typeof import('./stores/message')['useMessageStore']\n  const useModel: typeof import('vue')['useModel']\n  const useRoute: typeof import('vue-router/composables')['useRoute']\n  const useRouter: typeof import('vue-router/composables')['useRouter']\n  const useSlots: typeof import('vue')['useSlots']\n  const useTemplateRef: typeof import('vue')['useTemplateRef']\n  const useUserStore: typeof import('./stores/user')['useUserStore']\n  const watch: typeof import('vue')['watch']\n  const watchEffect: typeof import('vue')['watchEffect']\n  const watchPostEffect: typeof import('vue')['watchPostEffect']\n  const watchSyncEffect: typeof import('vue')['watchSyncEffect']\n}\n// for type re-export\ndeclare global {\n  // @ts-ignore\n  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'\n  import('vue')\n}\n"
  },
  {
    "path": "src/components/DialogConfirm.vue",
    "content": "<script lang=\"ts\">\nexport default defineComponent({\n  setup() {\n    const { t } = useI18n()\n    return { t }\n  },\n  data: () => ({\n    dialog: false,\n    confirmed: false,\n    resolve: (confirmed: boolean) => {},\n    reject: (val: unknown) => {},\n    message: '',\n  }),\n  watch: {\n    dialog(value) {\n      if (value === false) {\n        this.resolve(this.confirmed)\n      }\n    },\n  },\n  methods: {\n    open(message: string) {\n      this.confirmed = false\n      this.dialog = true\n      this.message = message\n      return new Promise<boolean>((resolve, reject) => {\n        this.resolve = resolve\n        this.reject = reject\n      })\n    },\n    confirm() {\n      this.confirmed = true\n      this.dialog = false\n    },\n    cancel() {\n      this.confirmed = false\n      this.dialog = false\n    },\n  },\n})\n</script>\n\n<template>\n  <v-dialog v-model=\"dialog\" max-width=\"400px\">\n    <v-card style=\"z-index: -1\">\n      <v-card-title class=\"font-weight-bold d-flex justify-center\">\n        <v-icon class=\"mr-2\" color=\"warning\">$warning</v-icon>\n        <span style=\"line-height: 24px\">{{ message }}</span>\n      </v-card-title>\n      <v-card-actions>\n        <v-spacer />\n        <v-btn color=\"primary darken-1\" text @click=\"cancel\">{{\n          t('cancel')\n        }}</v-btn>\n        <v-btn color=\"primary darken-1\" text @click=\"confirm\">{{\n          t('confirm')\n        }}</v-btn>\n        <v-spacer />\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n"
  },
  {
    "path": "src/components/StatsCard.vue",
    "content": "<script setup lang=\"ts\">\nwithDefaults(\n  defineProps<{\n    icon: string\n    iconClass?: string\n    color: string\n    title: string\n    value: number | null\n    unit?: string\n    formatter?: (v: number) => string\n  }>(),\n  {\n    iconClass: '',\n    value: null,\n    unit: '',\n    formatter: (v: number) => v.toString(),\n  },\n)\n</script>\n\n<template>\n  <v-card class=\"stats-card\" v-bind=\"$attrs\" v-on=\"$listeners\">\n    <v-icon class=\"stats-icon\" :color=\"color\" :class=\"iconClass\">{{\n      icon\n    }}</v-icon>\n    <div class=\"card-title ml-auto text-right\">\n      <span\n        class=\"card-title--name font-weight-bold text--darken-2\"\n        :class=\"`${color}--text`\"\n        v-text=\"title\"\n      />\n      <h3\n        class=\"font-weight-regular text--primary d-inline-block ml-2\"\n        style=\"font-size: 18px\"\n      >\n        {{ value != null ? formatter(value) : '' }}\n        <small v-if=\"unit\">{{ unit }}</small>\n      </h3>\n      <v-divider />\n    </div>\n    <div\n      class=\"v-alert__border v-alert__border--top v-alert__border--has-color\"\n      :class=\"color\"\n    />\n    <div\n      v-if=\"$slots.footer\"\n      class=\"grey--text text-right stats-footer text-caption\"\n    >\n      <slot name=\"footer\" />\n    </div>\n  </v-card>\n</template>\n\n<style lang=\"scss\" scoped>\n.stats-card {\n  padding: 5px;\n  padding-top: 10px;\n  .card-title {\n    width: fit-content;\n    .card-title--name {\n      display: inline-block;\n      backdrop-filter: blur(3px);\n    }\n  }\n  .caption {\n    font-size: 12px;\n    letter-spacing: 0;\n  }\n  .stats-icon {\n    position: absolute;\n    opacity: 0.3;\n    :deep(svg) {\n      height: 35px;\n    }\n  }\n  .stats-footer {\n    :deep(span) {\n      display: inline-block;\n      font-size: 12px !important;\n      letter-spacing: 0 !important;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/VHeadCard.vue",
    "content": "<script setup lang=\"ts\">\ndefineProps({\n  color: {\n    type: String,\n    default: 'primary',\n  },\n  icon: {\n    type: String,\n    default: undefined,\n  },\n  text: {\n    type: String,\n    default: '',\n  },\n  title: {\n    type: String,\n    default: '',\n  },\n  iconClass: {\n    type: String,\n    default: '',\n  },\n})\n</script>\n\n<template>\n  <v-card\n    v-bind=\"$attrs\"\n    class=\"v-head-card pa-3 d-flex flex-column justify-space-between\"\n  >\n    <div class=\"d-flex justify-start\">\n      <v-sheet\n        :color=\"color\"\n        :height=\"icon ? 72 : undefined\"\n        elevation=\"3\"\n        class=\"text-center v-head-card__heading mb-n5 pa-5\"\n        dark\n      >\n        <slot v-if=\"$slots.heading\" name=\"heading\" />\n\n        <div\n          v-else-if=\"title && !icon\"\n          class=\"text-h5 font-weight-bold\"\n          v-text=\"title\"\n        />\n\n        <v-icon v-else-if=\"icon\" size=\"32\" :class=\"iconClass\">\n          {{ icon }}\n        </v-icon>\n\n        <div v-if=\"text\" class=\"text-h5 font-weight-thin\" v-text=\"text\" />\n      </v-sheet>\n      <div\n        v-if=\"icon && title\"\n        class=\"ml-2 text-h5 font-weight-light v-head-card__title\"\n        v-text=\"title\"\n      />\n      <div\n        v-if=\"$slots['after-heading']\"\n        id=\"after-heading\"\n        class=\"ml-auto d-flex align-center justify-space-between\"\n        style=\"max-width: 80%\"\n      >\n        <slot name=\"after-heading\" />\n      </div>\n    </div>\n\n    <div\n      class=\"flex-grow-1 v-head-card__content fill-height\"\n      style=\"position: relative; min-height: 0\"\n    >\n      <slot />\n    </div>\n    <template v-if=\"$slots.actions\">\n      <v-card-actions class=\"pb-0 px-0 flex-wrap\">\n        <slot name=\"actions\" />\n      </v-card-actions>\n    </template>\n  </v-card>\n</template>\n\n<style lang=\"scss\" scoped>\n@import '@/assets/styles/variables.scss';\n.v-head-card {\n  margin-top: $card-heading-margin;\n  max-height: calc(100% - #{$card-heading-margin});\n  &__title {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  &.fill-height {\n    height: calc(100% - #{$card-heading-margin});\n  }\n  &__heading {\n    position: relative;\n    top: -22px;\n    transition: 0.3s ease;\n    z-index: 3;\n    border-radius: 4px;\n  }\n  :deep(#after-heading) > * {\n    margin-left: 1em;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/demo-charts/ChartBar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ECOption } from '@/plugins/echarts'\nconst option: ECOption = {\n  backgroundColor: 'transparent',\n  tooltip: {\n    trigger: 'axis',\n    axisPointer: {\n      type: 'shadow',\n    },\n  },\n  grid: {\n    top: 10,\n    left: '2%',\n    right: '2%',\n    bottom: '3%',\n    containLabel: true,\n  },\n  xAxis: [\n    {\n      type: 'category',\n      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],\n      axisTick: {\n        alignWithLabel: true,\n      },\n    },\n  ],\n  yAxis: [\n    {\n      type: 'value',\n      axisTick: {\n        show: false,\n      },\n    },\n  ],\n  series: [\n    {\n      name: 'pageA',\n      type: 'bar',\n      stack: 'vistors',\n      barWidth: '60%',\n      data: [79, 52, 200, 334, 390, 330, 220],\n    },\n    {\n      name: 'pageB',\n      type: 'bar',\n      stack: 'vistors',\n      barWidth: '60%',\n      data: [80, 52, 200, 334, 390, 330, 220],\n    },\n    {\n      name: 'pageC',\n      type: 'bar',\n      stack: 'vistors',\n      barWidth: '60%',\n      data: [30, 52, 200, 334, 390, 330, 220],\n    },\n  ],\n}\n</script>\n\n<template>\n  <v-chart :option=\"option\" autoresize />\n</template>\n"
  },
  {
    "path": "src/components/demo-charts/ChartLine.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ECOption } from '@/plugins/echarts'\n\nconst data = [\n  ['2022-06-05', 116],\n  ['2022-06-06', 129],\n  ['2022-06-07', 135],\n  ['2022-06-08', 86],\n  ['2022-06-09', 73],\n  ['2022-06-10', 85],\n  ['2022-06-11', 73],\n  ['2022-06-12', 68],\n  ['2022-06-13', 92],\n  ['2022-06-14', 130],\n  ['2022-06-15', 245],\n  ['2022-06-16', 139],\n  ['2022-06-17', 115],\n  ['2022-06-18', 111],\n  ['2022-06-19', 309],\n  ['2022-06-20', 206],\n  ['2022-06-21', 137],\n  ['2022-06-22', 128],\n  ['2022-06-23', 85],\n  ['2022-06-24', 94],\n  ['2022-06-25', 71],\n  ['2022-06-26', 106],\n  ['2022-06-27', 84],\n  ['2022-06-28', 93],\n  ['2022-06-29', 85],\n  ['2022-06-30', 73],\n  ['2022-07-01', 83],\n  ['2022-07-02', 125],\n  ['2022-07-03', 107],\n  ['2022-07-04', 82],\n  ['2022-07-05', 44],\n  ['2022-07-06', 72],\n  ['2022-07-07', 106],\n  ['2022-07-08', 107],\n  ['2022-07-09', 66],\n  ['2022-07-10', 91],\n  ['2022-07-11', 92],\n  ['2022-07-12', 113],\n  ['2022-07-13', 107],\n  ['2022-07-14', 131],\n  ['2022-07-15', 111],\n  ['2022-07-16', 64],\n  ['2022-07-17', 69],\n  ['2022-07-18', 88],\n  ['2022-07-19', 77],\n  ['2022-07-20', 83],\n  ['2022-07-21', 111],\n  ['2022-07-22', 57],\n  ['2022-07-23', 55],\n  ['2022-07-24', 60],\n]\n\nconst option: ECOption = {\n  backgroundColor: 'transparent',\n  dataset: { source: data },\n  visualMap: {\n    show: false,\n    type: 'continuous',\n    min: 0,\n    max: 400,\n  },\n  tooltip: {\n    trigger: 'axis',\n  },\n  grid: {\n    top: 10,\n    left: '2%',\n    right: '2%',\n    bottom: '3%',\n    containLabel: true,\n  },\n  xAxis: {\n    type: 'time',\n  },\n  yAxis: {\n    type: 'value',\n  },\n  series: [\n    {\n      name: 'value',\n      type: 'line',\n      showSymbol: false,\n      lineStyle: {\n        width: 4,\n      },\n    },\n  ],\n}\n</script>\n\n<template>\n  <v-chart :option=\"option\" autoresize />\n</template>\n"
  },
  {
    "path": "src/components/demo-charts/ChartPie.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ECOption } from '@/plugins/echarts'\nconst option: ECOption = {\n  backgroundColor: 'transparent',\n  tooltip: {\n    trigger: 'item',\n  },\n  legend: {\n    left: 'center',\n    bottom: '10',\n    data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts'],\n  },\n  series: [\n    {\n      name: 'WEEKLY WRITE ARTICLES',\n      type: 'pie',\n      roseType: 'radius',\n      radius: [15, 95],\n      center: ['50%', '38%'],\n      data: [\n        { value: 320, name: 'Industries' },\n        { value: 240, name: 'Technology' },\n        { value: 149, name: 'Forex' },\n        { value: 100, name: 'Gold' },\n        { value: 59, name: 'Forecasts' },\n      ],\n      animationEasing: 'cubicInOut',\n    },\n  ],\n}\n</script>\n\n<template>\n  <v-chart :option=\"option\" autoresize />\n</template>\n"
  },
  {
    "path": "src/components/demo-charts/ChartRadar.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ECOption } from '@/plugins/echarts'\nconst option: ECOption = {\n  backgroundColor: 'transparent',\n  radar: {\n    radius: '66%',\n    center: ['50%', '42%'],\n    splitNumber: 8,\n    splitArea: {\n      areaStyle: {\n        color: 'rgba(127,95,132,.3)',\n        opacity: 1,\n        shadowBlur: 45,\n        shadowColor: 'rgba(0,0,0,.5)',\n        shadowOffsetX: 0,\n        shadowOffsetY: 15,\n      },\n    },\n    indicator: [\n      { name: 'Sales' },\n      { name: 'Administration' },\n      { name: 'Technology' },\n      { name: 'Customer Support' },\n      { name: 'Development' },\n      { name: 'Marketing' },\n    ],\n  },\n  legend: {\n    left: 'center',\n    bottom: '10',\n    data: ['Allocated Budget', 'Expected Spending', 'Actual Spending'],\n  },\n  series: [\n    {\n      type: 'radar',\n      symbolSize: 0,\n      areaStyle: {\n        shadowBlur: 13,\n        shadowColor: 'rgba(0,0,0,.2)',\n        shadowOffsetX: 0,\n        shadowOffsetY: 10,\n        opacity: 1,\n      },\n      data: [\n        {\n          value: [5000, 7000, 12000, 11000, 15000, 14000],\n          name: 'Allocated Budget',\n        },\n        {\n          value: [4000, 9000, 15000, 15000, 13000, 11000],\n          name: 'Expected Spending',\n        },\n        {\n          value: [5500, 5000, 12000, 15000, 8000, 6000],\n          name: 'Actual Spending',\n        },\n      ],\n    },\n  ],\n}\n</script>\n\n<template>\n  <v-chart :option=\"option\" autoresize />\n</template>\n"
  },
  {
    "path": "src/components/layout/AppBar.vue",
    "content": "<script setup lang=\"ts\">\nimport AppBreadcrumbs from './AppBreadcrumbs.vue'\nimport ButtonFullScreen from './ButtonFullScreen.vue'\nimport ButtonLocale from './ButtonLocale.vue'\nimport ButtonUser from './ButtonUser.vue'\n\nconst { drawer } = storeToRefs(useAppStore())\n</script>\n\n<template>\n  <v-app-bar app color=\"transparent\" elevate-on-scroll height=\"60px\">\n    <v-btn elevation=\"1\" fab small style=\"z-index: 1\" @click=\"drawer = !drawer\">\n      <v-icon>\n        {{ drawer ? 'mdi-backburger' : 'mdi-menu-open' }}\n      </v-icon>\n    </v-btn>\n    <AppBreadcrumbs />\n    <v-spacer />\n    <v-btn\n      text\n      min-width=\"0\"\n      href=\"https://github.com/kingyue737/vitify-admin\"\n      target=\"_blank\"\n    >\n      <v-icon>mdi-github</v-icon>\n    </v-btn>\n    <PortalTarget name=\"app-bar\" class=\"d-contents\" />\n    <ButtonFullScreen />\n    <ButtonLocale />\n    <ButtonUser />\n  </v-app-bar>\n</template>\n\n<style lang=\"scss\" scoped>\n.v-app-bar {\n  backdrop-filter: blur(10px);\n  .v-btn:not(.v-btn--text):not(.v-btn--outlined):focus:before {\n    opacity: 0;\n  }\n}\n@supports not (backdrop-filter: blur(10px)) {\n  .theme--light .v-app-bar {\n    background-color: rgba(242, 245, 248, 0.8) !important;\n  }\n  .theme--dark .v-app-bar {\n    background-color: rgba(18, 18, 18, 0.8) !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/layout/AppBreadcrumbs.vue",
    "content": "<script setup lang=\"ts\">\nconst route = useRoute()\nconst { t } = useI18n()\nconst items = computed(() => {\n  return route!.matched\n    .slice(1)\n    .filter(\n      (item) =>\n        item.meta && item.meta.title && !(item.meta?.breadcrumb === 'hidden'),\n    )\n    .map((route) => ({\n      text: t(route.meta.title!),\n      disabled: route.meta?.breadcrumb === 'disabled' || false,\n      to: route.path,\n    }))\n})\n</script>\n\n<template>\n  <v-breadcrumbs class=\"ml-n6 d-none d-sm-block\">\n    <v-slide-x-reverse-transition class=\"v-breadcrumbs\" leave-absolute group>\n      <v-breadcrumbs-item\n        v-for=\"item in items\"\n        :key=\"item.text.toString()\"\n        :to=\"item.to\"\n        exact\n        :disabled=\"item.disabled\"\n      >\n        {{ item.text }}\n      </v-breadcrumbs-item>\n    </v-slide-x-reverse-transition>\n  </v-breadcrumbs>\n</template>\n"
  },
  {
    "path": "src/components/layout/AppDrawer.vue",
    "content": "<script setup lang=\"ts\">\nimport { useVuetify } from '@/composables/useVuetify'\nimport AppDrawerItem from './AppDrawerItem.vue'\nimport generatedRoutes from '~pages'\nimport { isPermitted } from '@/utils/permission'\n\nconst appStore = useAppStore()\nconst {\n  drawer: drawerStored,\n  drawerImage,\n  drawerImageShow,\n} = storeToRefs(appStore)\nconst vuetify = useVuetify()\nconst drawer = computed({\n  get() {\n    return drawerStored.value || !vuetify.breakpoint.mobile\n  },\n  set(val: boolean) {\n    drawerStored.value = val\n  },\n})\nconst mini = computed(() => !drawerStored.value && !vuetify.breakpoint.mobile)\nconst gradient = computed(() =>\n  vuetify.theme.dark\n    ? 'to bottom, rgba(0, 0, 0, .7), rgba(0, 0, 0, .7)'\n    : 'to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, .7)',\n)\n\nconst groupedRoutes = computed(() =>\n  Object.values(\n    generatedRoutes.reduce<Record<string, typeof generatedRoutes>>(\n      (r, v, i, a, k = v.meta?.drawerGroup || 'PUC') => (\n        (r[k] || (r[k] = [])).push(v), r\n      ),\n      {},\n    ),\n  )\n    .map((rs) =>\n      rs\n        .filter(\n          (r) => r.meta?.icon && (!r.meta?.roles || isPermitted(r.meta.roles)),\n        )\n        .sort(\n          (a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98),\n        ),\n    )\n    .reverse(),\n)\nnextTick(() => {\n  drawerStored.value =\n    vuetify.breakpoint.lgAndUp && vuetify.breakpoint.width !== 1280\n})\n</script>\n\n<template>\n  <v-navigation-drawer\n    id=\"app-navigation-drawer\"\n    v-model=\"drawer\"\n    :expand-on-hover=\"mini\"\n    :src=\"drawerImageShow ? drawerImage : ''\"\n    :mini-variant=\"mini\"\n    app\n  >\n    <template #img=\"props\">\n      <v-img v-show=\"drawerImageShow\" :gradient=\"gradient\" v-bind=\"props\" />\n    </template>\n    <template #prepend>\n      <v-list nav>\n        <v-list-item class=\"pa-1\">\n          <v-list-item-icon class=\"mr-5 my-auto\">\n            <v-icon x-large class=\"logo-icon\" color=\"primary\">$vitify</v-icon>\n          </v-list-item-icon>\n\n          <v-list-item-content class=\"title-content pa-0\">\n            <v-list-item-title>\n              Vitify <span class=\"primary--text\">Admin</span>\n            </v-list-item-title>\n          </v-list-item-content>\n        </v-list-item>\n      </v-list>\n      <v-divider />\n    </template>\n\n    <v-list expand dense nav>\n      <template v-for=\"(routesInGroup, i) in groupedRoutes\">\n        <v-divider\n          v-if=\"routesInGroup.length && i !== 0\"\n          :key=\"`item-divider-${i}`\"\n          class=\"mb-1\"\n        />\n        <AppDrawerItem\n          v-for=\"(route, j) in routesInGroup\"\n          :key=\"`item-${i}-${j}`\"\n          :data-cy=\"`drawer-${route.meta ? route.meta.dataCy : ''}`\"\n          :item=\"route\"\n        />\n      </template>\n    </v-list>\n    <v-spacer />\n    <template #append>\n      <v-list-item\n        id=\"drawer-footer\"\n        class=\"px-0 d-flex flex-column justify-center\"\n      >\n        <div />\n        <div class=\"caption pt-6 pt-md-0 text-center\">\n          &copy; Copyright 2022\n          <a\n            href=\"https://github.com/kingyue737\"\n            class=\"font-weight-bold\"\n            target=\"_blank\"\n            >Yue JIN</a\n          >\n          <span> & </span>\n          <a\n            href=\"https://www.nustarnuclear.com/\"\n            class=\"font-weight-bold\"\n            target=\"_blank\"\n            >NuStar</a\n          >\n        </div>\n      </v-list-item>\n    </template>\n  </v-navigation-drawer>\n</template>\n\n<style lang=\"scss\">\n#app-navigation-drawer {\n  &.v-navigation-drawer--open-on-hover:not(.v-navigation-drawer--mini-variant) {\n    box-shadow: 0px 0px 6px 2px rgba(100, 100, 100, 0.6);\n  }\n  .v-navigation-drawer__content {\n    overflow-y: hidden;\n    &:hover {\n      overflow-y: overlay;\n    }\n  }\n  .v-list-group__header.v-list-item--active:before {\n    opacity: 0.24;\n  }\n  &.v-navigation-drawer--mini-variant {\n    .sub-bar-item {\n      padding-left: 0px !important;\n    }\n    .logo-icon {\n      height: 32px !important;\n      width: 32px !important;\n    }\n  }\n  .title-content {\n    .v-list-item__title {\n      line-height: 1.3;\n      font-size: 24px;\n      font-weight: bold;\n    }\n  }\n  #drawer-footer {\n    min-height: 30px;\n    div {\n      white-space: nowrap;\n    }\n    &::after {\n      min-height: 0;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/layout/AppDrawerItem.vue",
    "content": "<script lang=\"ts\">\nimport type { RouteConfig } from 'vue-router'\nimport type { PropType } from 'vue'\nimport type {} from '@intlify/core-base'\n\nexport default defineComponent({\n  name: 'AppDrawerItem',\n  props: {\n    level: {\n      type: Number,\n      default: 0,\n    },\n    item: {\n      type: Object as PropType<RouteConfig>,\n      required: true,\n    },\n  },\n  setup() {\n    return { t: useI18n().t }\n  },\n  computed: {\n    isItem() {\n      return !this.item.children || this.visibleChildrenNum <= 1\n    },\n    isItemInChild() {\n      return this.isItem && this.visibleChildrenNum === 1\n    },\n    indexItem() {\n      if (this.item.children) {\n        return this.item.children[0]\n      } else {\n        return this.item\n      }\n    },\n    icon() {\n      return this.item.meta?.icon || ''\n    },\n    title() {\n      return this.t(this.item.meta?.title || '')\n    },\n    subtitle() {\n      return this.item.meta?.subtitle || ''\n    },\n    visibleChildren() {\n      return this.item.children\n        ?.filter((child) => child.meta?.icon)\n        .sort(\n          (a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98),\n        )\n    },\n    visibleChildrenNum() {\n      return this.visibleChildren?.length || 0\n    },\n    group() {\n      return (\n        this.item.path ||\n        this.item.name ||\n        this.item.children?.find((v) => !v.path)?.name\n      )\n    },\n  },\n})\n</script>\n\n<template>\n  <div v-if=\"!item.meta || item.meta.icon\" :class=\"level && 'sub-bar-item'\">\n    <v-list-item\n      v-if=\"isItem\"\n      :to=\"{ name: item.name || visibleChildren?.[0].name }\"\n      active-class=\"primary white--text\"\n      class=\"mb-1\"\n    >\n      <v-list-item-icon>\n        <v-icon>{{ icon }}</v-icon>\n      </v-list-item-icon>\n      <v-list-item-content>\n        <v-list-item-title>{{ title }}</v-list-item-title>\n        <v-list-item-subtitle>{{ subtitle }}</v-list-item-subtitle>\n      </v-list-item-content>\n    </v-list-item>\n    <v-list-group v-else :prepend-icon=\"icon\" :group=\"group\">\n      <template #activator>\n        <v-list-item-title>{{ title }}</v-list-item-title>\n      </template>\n      <AppDrawerItem\n        v-for=\"child in visibleChildren\"\n        :key=\"child.name\"\n        :item=\"child\"\n        :level=\"level + 1\"\n      />\n    </v-list-group>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.sub-bar-item {\n  padding-left: 12px;\n  transition: padding 0.2s;\n}\n</style>\n"
  },
  {
    "path": "src/components/layout/AppFooter.vue",
    "content": "<script setup lang=\"ts\">\nimport ButtonSettings from './ButtonSettings.vue'\nimport AppMessage from './AppMessage.vue'\nimport { useNow } from '@vueuse/core'\n\nconst { t } = useI18n()\nconst now = useNow()\n</script>\n\n<template>\n  <v-footer\n    id=\"app-footer\"\n    app\n    inset\n    padless\n    height=\"30\"\n    class=\"font-weight-light\"\n  >\n    <v-icon small class=\"ml-3 mr-1\"> mdi-clock-outline </v-icon>\n    <span>\n      {{ now.toLocaleString() }}\n    </span>\n\n    <v-spacer />\n    <v-tooltip top>\n      <template #activator=\"{ on }\">\n        <v-btn\n          text\n          tile\n          small\n          width=\"40\"\n          href=\"https://kingyue737.github.io/vitify-docs/\"\n          target=\"_blank\"\n          v-on=\"on\"\n        >\n          <v-icon>mdi-book-outline</v-icon>\n          <v-icon size=\"12\">mdi-open-in-new</v-icon>\n        </v-btn>\n      </template>\n      <span>{{ t('documentation') }}</span>\n    </v-tooltip>\n    <AppMessage />\n    <ButtonSettings />\n    <div class=\"ml-2\" />\n  </v-footer>\n</template>\n\n<style lang=\"scss\" scoped>\n#app-footer {\n  user-select: none;\n  span {\n    font-size: 14px;\n  }\n  > :deep(.v-btn),\n  > :deep(.d-contents) > .v-btn {\n    height: 100% !important;\n    min-width: 0 !important;\n    width: 30px;\n    font-size: 14px;\n    font-weight: 300;\n    .v-icon svg {\n      height: 18px;\n    }\n  }\n}\n.theme--light.v-footer {\n  background-color: #d4dee8;\n}\n</style>\n"
  },
  {
    "path": "src/components/layout/AppMessage.vue",
    "content": "<script setup lang=\"ts\">\nimport AppMessageItem from './AppMessageItem.vue'\nimport { formatTime } from '@/utils/date'\n\nconst { t } = useI18n()\nconst messageStore = useMessageStore()\nconst { messages } = storeToRefs(messageStore)\nconst messagesShown = computed(() =>\n  messages.value.filter((message) => message.show).reverse(),\n)\nconst showAll = ref(false)\nconst timeout = ref(5000)\nfunction deleteMessage(id: number) {\n  messageStore.delMessage(id)\n}\nfunction emptyMessages() {\n  messageStore.$reset()\n}\nfunction toggleAll() {\n  showAll.value = !showAll.value\n  messages.value.forEach((m) => {\n    m.show = showAll.value\n  })\n  if (showAll.value) {\n    timeout.value = -1\n  } else {\n    timeout.value = 5000\n  }\n}\n</script>\n\n<template>\n  <div class=\"d-contents\">\n    <v-tooltip top>\n      <template #activator=\"{ on }\">\n        <v-btn text tile small @click=\"toggleAll\" v-on=\"on\">\n          <v-icon>{{\n            messages.length ? 'mdi-bell-badge-outline' : 'mdi-bell-outline'\n          }}</v-icon>\n        </v-btn>\n      </template>\n      <span>{{ t('notification') }}</span>\n    </v-tooltip>\n    <Portal to=\"app\">\n      <v-card\n        elevation=\"6\"\n        width=\"400\"\n        class=\"d-flex flex-column message-card\"\n        :class=\"{ 'message-card--open': showAll }\"\n      >\n        <v-toolbar flat dense>\n          <v-toolbar-title class=\"font-weight-light text-body-1\">{{\n            t(messages.length ? 'notification' : 'noNew')\n          }}</v-toolbar-title>\n          <v-spacer />\n          <v-btn small icon :title=\"t('clearAll')\" @click=\"emptyMessages\">\n            <v-icon>mdi-bell-remove</v-icon>\n          </v-btn>\n          <v-btn small icon :title=\"t('hide')\" @click=\"toggleAll\">\n            <v-icon>$expand</v-icon>\n          </v-btn>\n        </v-toolbar>\n        <v-slide-y-reverse-transition\n          tag=\"div\"\n          class=\"d-flex flex-column message-box\"\n          group\n          hide-on-leave\n        >\n          <AppMessageItem\n            v-for=\"message in messagesShown\"\n            :key=\"message.id\"\n            v-model=\"message.show\"\n            class=\"message-item\"\n            :colored-border=\"showAll\"\n            border=\"left\"\n            :type=\"message.type\"\n            :timeout=\"timeout\"\n            dismissible\n            :elevation=\"showAll ? 0 : 10\"\n            @close=\"deleteMessage(message.id)\"\n          >\n            <small>{{ formatTime(message.time) }}</small>\n            <div>\n              {{ message.text }}\n            </div>\n          </AppMessageItem>\n        </v-slide-y-reverse-transition>\n      </v-card>\n    </Portal>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n@import '@/assets/styles/variables.scss';\n.message-item {\n  width: 100%;\n}\n.message-card {\n  position: fixed;\n  z-index: 210;\n  right: 15px;\n  bottom: calc(#{$footer-height} + 5px);\n  max-height: 100vh;\n  visibility: hidden;\n  &.message-card--open {\n    visibility: visible;\n    overflow: hidden;\n    max-height: calc(100vh - #{$footer-height} - #{$app-bar-height} - 10px);\n    .message-box {\n      justify-content: initial;\n      height: auto;\n      overflow-y: overlay;\n      pointer-events: auto;\n      .message-item {\n        transition: none !important;\n        margin: 0;\n        border-radius: 0;\n        border-top: 1px solid #5656563d !important;\n        padding-top: 5px;\n        padding-bottom: 5px;\n      }\n    }\n  }\n}\n.message-box {\n  overflow-y: visible;\n  visibility: visible;\n  height: calc(100vh - #{$footer-height} - 5px);\n  justify-content: end;\n  pointer-events: none;\n  .message-item {\n    pointer-events: initial;\n    user-select: initial;\n  }\n}\n:deep(.v-alert__content) {\n  max-width: 300px;\n}\n</style>\n\n<i18n lang=\"yaml\">\nzh:\n  noNew: 没有新的通知\n  clearAll: 清除所有通知\nen:\n  noNew: No New Notifications\n  clearAll: Clear All Notifications\n</i18n>\n"
  },
  {
    "path": "src/components/layout/AppMessageItem.vue",
    "content": "<script lang=\"ts\">\nimport { useVModel } from '@vueuse/core'\nexport default defineComponent({\n  props: {\n    value: {\n      type: Boolean,\n      default: false,\n    },\n    timeout: {\n      type: Number,\n      default: 5000,\n    },\n  },\n  emits: ['close'],\n  setup(props, { emit }) {\n    const isActive = useVModel(props, undefined, emit)\n    const timeout = toRef(props, 'timeout')\n    let activeTimeout: number\n    const startTimeout = () => {\n      clearTimeout(activeTimeout)\n\n      if (!isActive.value || timeout.value === -1) {\n        return\n      }\n\n      activeTimeout = window.setTimeout(() => {\n        isActive.value = false\n      }, timeout.value)\n    }\n    watch([isActive, timeout], startTimeout)\n    if (isActive.value) {\n      startTimeout()\n    }\n  },\n})\n</script>\n\n<template>\n  <v-alert v-bind=\"$attrs\" v-on=\"$listeners\">\n    <slot />\n    <template #close>\n      <v-btn\n        icon\n        small\n        class=\"v-alert__dismissible align-self-start mt-0\"\n        @click=\"$emit('close')\"\n      >\n        <v-icon small>$close</v-icon>\n      </v-btn>\n    </template>\n  </v-alert>\n</template>\n\n<style scoped lang=\"scss\"></style>\n"
  },
  {
    "path": "src/components/layout/AppView.vue",
    "content": "<script setup lang=\"ts\">\nimport { useVuetify } from '@/composables/useVuetify'\nconst showGoTop = ref(false)\nconst vuetify = useVuetify()\nconst toTop = () => {\n  vuetify.goTo(0)\n}\nconst onScroll = () => {\n  showGoTop.value = scrollY > screen.height / 2\n}\n</script>\n\n<template>\n  <v-main>\n    <v-slide-x-transition mode=\"out-in\">\n      <router-view />\n    </v-slide-x-transition>\n    <v-fab-transition>\n      <v-btn\n        v-show=\"showGoTop\"\n        v-scroll=\"onScroll\"\n        fixed\n        dark\n        fab\n        right\n        bottom\n        elevation=\"12\"\n        color=\"primary\"\n        style=\"z-index: 3; bottom: 40px\"\n        @click=\"toTop\"\n      >\n        <v-icon>mdi-chevron-up</v-icon>\n      </v-btn>\n    </v-fab-transition>\n  </v-main>\n</template>\n"
  },
  {
    "path": "src/components/layout/ButtonFullScreen.vue",
    "content": "<script setup lang=\"ts\">\nimport { useFullscreen } from '@vueuse/core'\nconst { isFullscreen, toggle } = useFullscreen()\nconst { t } = useI18n()\n</script>\n\n<template>\n  <v-tooltip bottom>\n    <template #activator=\"{ on }\">\n      <v-btn text min-width=\"0\" v-on=\"on\" @click=\"toggle\">\n        <v-icon>\n          {{ isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}\n        </v-icon>\n      </v-btn>\n    </template>\n    <span>{{ t('fullscreen') }}</span>\n  </v-tooltip>\n</template>\n"
  },
  {
    "path": "src/components/layout/ButtonLocale.vue",
    "content": "<script setup lang=\"ts\">\nimport { useVuetify } from '@/composables/useVuetify'\n\nconst { locale, t } = useI18n()\nconst vuetify = useVuetify()\nconst setLang = (lang: string) => {\n  locale.value = lang\n  vuetify.lang.current = lang\n}\n</script>\n\n<template>\n  <v-menu bottom left offset-y origin=\"top right\" transition=\"scale-transition\">\n    <template #activator=\"{ attrs, on: menu }\">\n      <v-tooltip bottom>\n        <template #activator=\"{ on: toolTip }\">\n          <v-btn\n            min-width=\"0\"\n            text\n            v-bind=\"attrs\"\n            v-on=\"{ ...toolTip, ...menu }\"\n          >\n            <v-icon>mdi-translate</v-icon>\n          </v-btn>\n        </template>\n        <span>{{ t('language') }}</span>\n      </v-tooltip>\n    </template>\n    <v-list>\n      <v-list-item\n        :class=\"{ 'v-list-item--active': locale === 'zh' }\"\n        @click=\"setLang('zh')\"\n      >\n        <v-list-item-icon class=\"mr-2\">\n          <v-icon> mdi-ideogram-cjk-variant </v-icon>\n        </v-list-item-icon>\n        <v-list-item-title link> 简体中文 </v-list-item-title>\n      </v-list-item>\n      <v-list-item\n        :class=\"{ 'v-list-item--active': locale === 'en' }\"\n        @click=\"setLang('en')\"\n      >\n        <v-list-item-icon class=\"mr-2\">\n          <v-icon> mdi-alphabetical-variant </v-icon>\n        </v-list-item-icon>\n        <v-list-item-title link> English </v-list-item-title>\n      </v-list-item>\n    </v-list>\n  </v-menu>\n</template>\n"
  },
  {
    "path": "src/components/layout/ButtonSettings.vue",
    "content": "<script setup lang=\"ts\">\nimport { useAppStore } from '@/stores/app'\nimport { useVuetify } from '@/composables/useVuetify'\nimport { useDark, useToggle } from '@vueuse/core'\nimport drawer1 from '@/assets/images/drawer/1.jpg'\nimport drawer2 from '@/assets/images/drawer/2.jpg'\nimport drawer3 from '@/assets/images/drawer/3.jpg'\n\nconst appStore = useAppStore()\nconst { t } = useI18n()\nconst { drawerImage, drawerImageShow } = storeToRefs(appStore)\nif (drawerImage.value) {\n  drawerImage.value = drawer1\n}\nconst vuetify = useVuetify()\nconst color = computed({\n  get() {\n    return vuetify!.theme.themes.light.primary as string\n  },\n  set(val: string) {\n    localStorage.setItem('theme-primary', val)\n    vuetify!.theme.themes.light.primary = val\n    vuetify!.theme.themes.dark.primary = val\n  },\n})\nconst colors = [\n  ['#0096C7', '#ff9800'],\n  ['#4CAF50', '#FF5252'],\n  ['#9C27b0', '#E91E63'],\n  ['#304156', '#3f51b5'],\n  ['#002FA7', '#492d22'],\n]\nconst images = [drawer1, drawer2, drawer3]\nconst menuShow = ref(false)\nconst isDark: WritableComputedRef<boolean> = useDark({\n  onChanged(dark: boolean) {\n    vuetify.theme.dark = dark\n  },\n})\nconst toggleDark = useToggle(isDark)\n</script>\n\n<template>\n  <v-menu\n    v-model=\"menuShow\"\n    :close-on-content-click=\"false\"\n    content-class=\"v-settings\"\n    top\n    left\n    origin=\"right\"\n    nudge-left=\"5\"\n    nudge-top=\"5\"\n    offset-y\n    transition=\"slide-x-reverse-transition\"\n  >\n    <template #activator=\"{ attrs, on: menu }\">\n      <v-tooltip top>\n        <template #activator=\"{ on: toolTip }\">\n          <v-btn text tile small v-bind=\"attrs\" v-on=\"{ ...toolTip, ...menu }\">\n            <v-icon>mdi-palette-outline</v-icon>\n          </v-btn>\n        </template>\n        <span>{{ t('interfaceSettings') }}</span>\n      </v-tooltip>\n    </template>\n    <v-card class=\"text-center mb-0\" width=\"320\">\n      <v-card-text>\n        <strong class=\"mb-3 d-inline-block\">{{ t('themeColor') }}</strong>\n        <v-color-picker v-model=\"color\" show-swatches :swatches=\"colors\" />\n        <v-divider class=\"my-3\" />\n        <v-row align=\"center\" no-gutters>\n          <v-col cols=\"auto\">{{ t('darkMode') }}</v-col>\n          <v-spacer />\n          <v-col cols=\"auto\">\n            <v-switch\n              :input-value=\"isDark\"\n              class=\"ma-0 pa-0\"\n              color=\"primary\"\n              hide-details\n              @change=\"toggleDark\"\n            />\n          </v-col>\n        </v-row>\n        <v-divider class=\"my-3\" />\n        <v-row align=\"center\" no-gutters>\n          <v-col cols=\"auto\">{{ t('drawerBackground') }}</v-col>\n          <v-spacer />\n          <v-col cols=\"auto\">\n            <v-switch\n              v-model=\"drawerImageShow\"\n              class=\"ma-0 pa-0\"\n              color=\"primary\"\n              hide-details\n            />\n          </v-col>\n        </v-row>\n        <v-card :disabled=\"!drawerImageShow\" flat>\n          <v-item-group\n            v-model=\"drawerImage\"\n            class=\"d-flex justify-space-between my-3 mx-2\"\n            mandatory\n          >\n            <v-item v-for=\"img in images\" :key=\"img\" :value=\"img\">\n              <template #default=\"{ active, toggle }\">\n                <v-sheet\n                  class=\"d-inline-block v-settings__item\"\n                  :class=\"active && 'v-settings__item--active'\"\n                  @click=\"toggle\"\n                >\n                  <v-img :src=\"img\" height=\"100\" width=\"50\" />\n                </v-sheet>\n              </template>\n            </v-item>\n          </v-item-group>\n        </v-card>\n      </v-card-text>\n    </v-card>\n  </v-menu>\n</template>\n\n<style lang=\"scss\" scoped>\n.v-settings {\n  border-radius: 10px;\n  .theme--dark.v-sheet {\n    background-color: #272727;\n  }\n  &__item {\n    cursor: pointer;\n    border-width: 3px;\n    border-style: solid;\n    border-color: transparent;\n    border-radius: 10px;\n    &--active {\n      border-color: var(--v-primary-base) !important;\n    }\n    &:hover {\n      border-color: var(--v-primary-lighten2);\n    }\n    .v-image {\n      border-radius: 7px !important;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/layout/ButtonUser.vue",
    "content": "<script setup lang=\"ts\">\nconst router = useRouter()\nconst { t } = useI18n()\nconst userStore = useUserStore()\nconst { name } = storeToRefs(userStore)\nconst logOut = () => {\n  userStore.logOut()\n  router.push('/login')\n}\n</script>\n<template>\n  <v-menu bottom left offset-y origin=\"top right\" transition=\"scale-transition\">\n    <template #activator=\"{ attrs, on: menu }\">\n      <v-tooltip bottom>\n        <template #activator=\"{ on: toolTip }\">\n          <v-btn\n            min-width=\"0\"\n            text\n            v-bind=\"attrs\"\n            v-on=\"{ ...toolTip, ...menu }\"\n          >\n            <v-icon>mdi-account</v-icon>\n          </v-btn>\n        </template>\n        <span>{{ name }}</span>\n      </v-tooltip>\n    </template>\n    <v-list>\n      <v-list-item @click=\"router.push({ name: 'reset-password' })\">\n        <v-list-item-icon class=\"mr-2\">\n          <v-icon> mdi-key-variant </v-icon>\n        </v-list-item-icon>\n        <v-list-item-title link> {{ t('resetPassword') }} </v-list-item-title>\n      </v-list-item>\n      <v-list-item @click=\"logOut\">\n        <v-list-item-icon class=\"mr-2\">\n          <v-icon> mdi-logout </v-icon>\n        </v-list-item-icon>\n        <v-list-item-title link> {{ t('logout') }} </v-list-item-title>\n      </v-list-item>\n    </v-list>\n  </v-menu>\n</template>\n"
  },
  {
    "path": "src/components/layout/RouterWrapper.vue",
    "content": "<script setup lang=\"ts\"></script>\n\n<template>\n  <div class=\"d-fake\">\n    <v-slide-x-transition mode=\"out-in\">\n      <router-view />\n    </v-slide-x-transition>\n  </div>\n</template>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components.d.ts",
    "content": "import type { DefineComponent } from 'vue'\n\ndeclare module 'vue' {\n  export interface GlobalComponents {\n    VChart: (typeof import('vue-echarts'))['default']\n    VHeadCard: (typeof import('@/components/VHeadCard.vue'))['default']\n    RouterView: (typeof import('vue-router'))['RouterView']\n    RouterLink: (typeof import('vue-router'))['RouterLink']\n    Portal: DefineComponent<{\n      disabled?: boolean\n      name?: string\n      order?: number\n      slim?: boolean\n      slotProps?: any\n      tag?: string\n      to: string\n    }>\n    PortalTarget: DefineComponent<{\n      multiple?: boolean\n      name: string\n      slim?: boolean\n      slotProps?: any\n      tag?: string\n      transition?: boolean | string | object\n    }>\n  }\n}\n\nexport {}\n"
  },
  {
    "path": "src/composables/useVuetify.ts",
    "content": "import type { VuetifyParsedTheme } from 'vuetify/types/services/theme'\n\nexport function useVuetify() {\n  const instance = getCurrentInstance()\n  if (!instance) {\n    throw new Error(`useVuetify should be called in setup().`)\n  }\n  return instance.proxy.$vuetify\n}\n\nexport function useParsedTheme() {\n  // parsedTheme is only for internal usage and not typed in vuetify\n  return (useVuetify().theme as any).parsedTheme as VuetifyParsedTheme\n}\n"
  },
  {
    "path": "src/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vite-plugin-pages/client\" />\n/// <reference types=\"vite-plugin-vue-layouts/client\" />\n/// <reference types=\"vuetify2-component-types\" />\n\ninterface ImportMetaEnv {\n  readonly VITE_API_URL: string\n  readonly VITE_MOCK: string\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv\n}\n"
  },
  {
    "path": "src/layouts/default.vue",
    "content": "<script setup lang=\"ts\">\nimport AppBar from '@/components/layout/AppBar.vue'\nimport AppDrawer from '@/components/layout/AppDrawer.vue'\nimport AppView from '@/components/layout/AppView.vue'\nimport AppFooter from '@/components/layout/AppFooter.vue'\nimport { useTitle } from '@vueuse/core'\n\nconst route = useRoute()\nconst { t } = useI18n()\nconst title = computed(() => {\n  const title = t(route.meta?.title || route.matched[0].meta?.title || '')\n  return title ? `${title} | Vitify Admin` : 'Vitify Admin'\n})\nuseTitle(title)\n</script>\n\n<template>\n  <div style=\"display: contents\">\n    <AppBar />\n    <AppDrawer />\n    <AppView />\n    <AppFooter />\n  </div>\n</template>\n"
  },
  {
    "path": "src/layouts/empty.vue",
    "content": "<template>\n  <div style=\"display: contents\">\n    <router-view />\n  </div>\n</template>\n"
  },
  {
    "path": "src/locales/en.json",
    "content": "{\n  \"homepage\": \"Homepage\",\n  \"fullscreen\": \"Fullscreen\",\n  \"userManagement\": \"User Management\",\n  \"userDetail\": \"User Detail\",\n  \"dashboard\": \"Dashboard\",\n  \"username\": \"Username\",\n  \"name\": \"Name\",\n  \"user\": \"User\",\n  \"password\": \"Password\",\n  \"resetPassword\": \"Reset Password\",\n  \"language\": \"Language\",\n  \"login\": \"Login\",\n  \"logout\": \"Logout\",\n  \"email\": \"Email\",\n  \"group\": \"Group\",\n  \"joinDate\": \"Joining Date\",\n  \"actions\": \"Actions\",\n  \"edit\": \"Edit\",\n  \"delete\": \"Delete\",\n  \"deleted\": \"Deleted\",\n  \"submit\": \"Submit\",\n  \"listOf\": \"{0} List\",\n  \"userLogin\": \"@:user @:login\",\n  \"pleaseEnter\": \"Please enter {0}\",\n  \"lengthOf\": \"{0} length\",\n  \"form\": {\n    \"LTE\": \"{input} must be less than or equal to {limit}\",\n    \"LT\": \"{input} must be less than {limit}\",\n    \"GTE\": \"{input} must be greater than or equal to {limit}\",\n    \"GT\": \"{input} must be greater than {limit}\"\n  },\n  \"darkMode\": \"Dark Mode\",\n  \"drawerBackground\": \"Drawer Background\",\n  \"image\": \"Image\",\n  \"interfaceSettings\": \"Interface Settings\",\n  \"notification\": \"Notifications\",\n  \"confirm\": \"Confirm\",\n  \"cancel\": \"Cancel\",\n  \"themeColor\": \"Theme Color\",\n  \"documentation\": \"Documentation\",\n  \"nestedRoutes\": \"Nested Routes\"\n}\n"
  },
  {
    "path": "src/locales/zh.json",
    "content": "{\n  \"homepage\": \"主页\",\n  \"fullscreen\": \"全屏\",\n  \"userManagement\": \"用户管理\",\n  \"userDetail\": \"用户详情\",\n  \"dashboard\": \"仪表板\",\n  \"username\": \"用户名\",\n  \"user\": \"用户\",\n  \"group\": \"用户组\",\n  \"actions\": \"动作\",\n  \"joinDate\": \"注册时间\",\n  \"submit\": \"提交\",\n  \"edit\": \"编辑\",\n  \"delete\": \"删除\",\n  \"deleted\": \"已删除\",\n  \"name\": \"姓名\",\n  \"password\": \"密码\",\n  \"resetPassword\": \"重置密码\",\n  \"language\": \"语言\",\n  \"login\": \"登录\",\n  \"logout\": \"注销\",\n  \"email\": \"邮箱\",\n  \"listOf\": \"{0}列表\",\n  \"userLogin\": \"@:user@:login\",\n  \"pleaseEnter\": \"请填写{0}\",\n  \"lengthOf\": \"{0}长度\",\n  \"form\": {\n    \"LTE\": \"{input}必须小于等于{limit}\",\n    \"LT\": \"{input}必须小于{limit}\",\n    \"GTE\": \"{input}必须大于等于{limit}\",\n    \"GT\": \"{input}必须大于{limit}\"\n  },\n  \"image\": \"图片\",\n  \"drawerBackground\": \"侧边栏背景\",\n  \"notification\": \"通知\",\n  \"darkMode\": \"暗模式\",\n  \"interfaceSettings\": \"界面设置\",\n  \"themeColor\": \"主题色\",\n  \"confirm\": \"确认\",\n  \"cancel\": \"取消\",\n  \"hide\": \"隐藏\",\n  \"documentation\": \"文档\",\n  \"nestedRoutes\": \"嵌套路由\"\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "import Vue from 'vue'\nimport App from './App.vue'\nimport '@/assets/styles/index.scss'\nimport { filename } from './utils/string'\nimport type { InstallPlugin } from './utils/types'\n\nVue.config.productionTip = false\nif (import.meta.env.VITE_MOCK) {\n  ;(await import('./mocks')).worker.start({\n    onUnhandledRequest: 'bypass',\n  })\n}\n\nconst app = new Vue({\n  ...Object.fromEntries(\n    Object.entries(\n      import.meta.glob<{ install: InstallPlugin }>('./plugins/*.ts', {\n        eager: true,\n      }),\n    )\n      .map(([k, v]) => [filename(k), v.install?.(Vue)] as [string, any])\n      .filter((entry) => entry[1]),\n  ),\n  render: (h) => h(App),\n})\napp.$mount('#app')\n"
  },
  {
    "path": "src/mocks/index.ts",
    "content": "import { setupWorker } from 'msw/browser'\nimport { http } from 'msw'\n\nconst baseURL =\n  import.meta.env.VITE_API_URL ||\n  `${window.location.protocol}//${window.location.hostname}:9529/api/v1`\nconst url = (path: string) => {\n  return baseURL + path\n}\n\nconst groups = [\n  { id: 1, name: 'admin' },\n  { id: 2, name: 'staff' },\n]\n\nconst users = [\n  {\n    id: 1,\n    groups: [1],\n    username: 'kingyue737',\n    name: 'Yue JIN',\n    email: 'yuejin13@fudan.edu.cn',\n    joinDate: '2021-11-08T07:35:09.709Z',\n  },\n  {\n    id: 2,\n    groups: [1],\n    username: 'lodgepole',\n    name: 'Ada Zhang',\n    email: '',\n    joinDate: '2021-04-08T23:45:09.709Z',\n  },\n  {\n    id: 3,\n    groups: [2],\n    username: 'fuant',\n    name: 'Antony Fu',\n    joinDate: '2022-07-08T21:32:00.709Z',\n  },\n  {\n    id: 4,\n    groups: [2],\n    name: 'Ivan You',\n    username: 'xiaoyouyou',\n    joinDate: '2022-07-08T12:35:09.709Z',\n  },\n  {\n    id: 5,\n    groups: [2],\n    name: 'Johnny Leider',\n    username: 'johnnyleider',\n    joinDate: '2022-01-21T12:35:09.709Z',\n  },\n]\n\nexport const worker = setupWorker(\n  http.get(url('/users/:id'), async ({ request }) => {\n    return Response.json({ id: 99, groups: [1] })\n  }),\n  http.get(url('/users'), async ({ request }) => {\n    return Response.json(users)\n  }),\n  http.delete(url('/users/:id'), ({ params }) => {\n    users.splice(users.map((x) => x.id).indexOf(Number(params.id)), 1)\n    return new Response(null, { status: 204 })\n  }),\n  http.post(url('/auth/access-token'), async () => {\n    return Response.json({\n      accessToken: 'admin',\n      refreshToken: 'admin',\n    })\n  }),\n  http.get(url('/groups'), async () => {\n    return Response.json(groups)\n  }),\n  http.get(url('/groups/:id'), async ({ params }) => {\n    return Response.json(groups.find((g) => g.id === Number(params.id)))\n  }),\n)\n"
  },
  {
    "path": "src/pages/[...all].vue",
    "content": "<script lang=\"ts\">\nexport default defineComponent({\n  name: 'ErrorPage',\n})\n</script>\n\n<template>\n  <div>\n    <div class=\"wrapper\">\n      <v-icon class=\"logo mb-4\">$nustar</v-icon>\n      <p class=\"text-h5\">404 Not Found</p>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.wrapper {\n  position: relative;\n  top: calc(50vh - 250px);\n  text-align: center;\n  opacity: 0.6;\n}\n.logo {\n  font-size: 12em;\n  opacity: 0.2;\n}\n</style>\n"
  },
  {
    "path": "src/pages/__tests__/login.spec.ts",
    "content": "import LoginPage from '../login.vue'\nimport { fireEvent } from '@testing-library/vue'\nimport { renderWithVuetify } from '@/../test/helpers'\n\ndescribe('login page', () => {\n  it('login correctly', async () => {\n    const { getByText, getByLabelText } = renderWithVuetify(LoginPage)\n    getByText('User Login')\n    const userInput = getByLabelText('Username')\n    await fireEvent.update(userInput, 'admin')\n\n    const passwordInput = getByLabelText('Password')\n    await fireEvent.update(passwordInput, 'admin123')\n\n    const button = getByText('Login')\n    await fireEvent.click(button)\n\n    const store = useUserStore()\n    expect(store.login).toBeCalledWith('admin', 'admin123')\n  })\n})\n"
  },
  {
    "path": "src/pages/dashboard.vue",
    "content": "<script setup lang=\"ts\">\nimport ChartRadar from '@/components/demo-charts/ChartRadar.vue'\nimport ChartLine from '../components/demo-charts/ChartLine.vue'\nimport ChartBar from '../components/demo-charts/ChartBar.vue'\nimport ChartPie from '../components/demo-charts/ChartPie.vue'\nimport StatsCard from '../components/StatsCard.vue'\n\nconst stats = ref([\n  {\n    icon: 'mdi-web',\n    title: 'Bandwidth',\n    value: 230,\n    unit: 'GB',\n    color: 'primary',\n    caption: 'Up: 100, Down: 130',\n  },\n  {\n    icon: 'mdi-rss',\n    title: 'Submissions',\n    value: 108,\n    color: 'primary',\n    caption: 'Too young, too naive',\n  },\n  {\n    icon: 'mdi-send',\n    title: 'Requests',\n    value: 1238,\n    color: 'warning',\n    caption: 'Limit: 1320',\n  },\n  {\n    icon: 'mdi-bell',\n    title: 'Messages',\n    value: 9042,\n    color: 'primary',\n    caption: 'Warnings: 300, erros: 47',\n  },\n  {\n    icon: 'mdi-github',\n    title: 'Github Stars',\n    value: NaN,\n    color: 'grey',\n    caption: 'API has no response',\n  },\n  {\n    icon: 'mdi-currency-cny',\n    title: 'Total Fee',\n    value: 2300,\n    unit: '￥',\n    color: 'error',\n    caption: 'Upper Limit: 2000 ￥',\n  },\n])\n</script>\n\n<template>\n  <v-container fluid>\n    <v-row>\n      <v-col\n        v-for=\"stat in stats\"\n        :key=\"stat.title\"\n        cols=\"12\"\n        sm=\"6\"\n        md=\"4\"\n        lg=\"2\"\n      >\n        <StatsCard\n          :title=\"stat.title\"\n          :unit=\"stat.unit\"\n          :color=\"stat.color\"\n          :icon=\"stat.icon\"\n          :value=\"stat.value\"\n        >\n          <template #footer>\n            {{ stat.caption }}\n          </template>\n        </StatsCard>\n      </v-col>\n    </v-row>\n    <v-row>\n      <v-col cols=\"12\" md=\"6\" lg=\"12\">\n        <v-card class=\"pa-2\">\n          <ChartLine />\n        </v-card>\n      </v-col>\n      <v-col cols=\"12\" md=\"6\" lg=\"4\">\n        <v-card class=\"pa-2\">\n          <ChartRadar />\n        </v-card>\n      </v-col>\n      <v-col cols=\"12\" md=\"6\" lg=\"4\">\n        <v-card class=\"pa-2\">\n          <ChartPie />\n        </v-card>\n      </v-col>\n      <v-col cols=\"12\" md=\"6\" lg=\"4\">\n        <v-card class=\"pa-2\">\n          <ChartBar />\n        </v-card>\n      </v-col>\n    </v-row>\n  </v-container>\n</template>\n\n<style scoped lang=\"scss\">\n.v-card:not(.stats-card) {\n  height: 350px;\n}\n</style>\n\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"dashboard\",\n    \"icon\": \"mdi-monitor-dashboard\",\n    \"drawerIndex\": 1\n  }\n}\n</route>\n"
  },
  {
    "path": "src/pages/homepage.vue",
    "content": "<script setup lang=\"ts\">\nconst { t } = useI18n()\nconst name = ref('')\nfunction sayHi() {\n  Message.success(`Hi, ${name.value}!`)\n}\nfunction warning() {\n  Message.warning(t('warnMessage', [name.value]))\n}\n</script>\n\n<template>\n  <div>\n    <div class=\"wrapper\">\n      <v-icon class=\"logo mb-4\">$vitify</v-icon>\n      <p class=\"mb-10\">{{ t('description') }}</p>\n      <v-text-field\n        v-model=\"name\"\n        placeholder=\"Hello World\"\n        outlined\n        :label=\"t('inputLabel')\"\n        class=\"input mx-auto\"\n      />\n      <v-btn-toggle />\n      <v-btn :disabled=\"!name\" class=\"mr-2\" color=\"primary\" @click=\"sayHi\">\n        {{ t('confirm') }}\n      </v-btn>\n      <v-btn :disabled=\"!name\" @click=\"warning\">{{ t('cancel') }}</v-btn>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.wrapper {\n  position: relative;\n  top: max(50vh - 300px, 0px);\n  text-align: center;\n  opacity: 0.9;\n}\n.logo {\n  font-size: 10em;\n  opacity: 0.2;\n}\n.input {\n  width: 300px;\n}\n</style>\n\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"homepage\",\n    \"icon\": \"mdi-home\",\n    \"drawerIndex\": 0\n  }\n}\n</route>\n\n<i18n lang=\"yaml\">\nen:\n  description: Opinionated Admin Starter Template\n  inputLabel: What's your name?\n  warnMessage: How dare you refuse me, {0}!\nzh:\n  description: 固执己见的后台项目模板\n  inputLabel: 你的名字?\n  warnMessage: 你胆敢拒绝我, {0}!\n</i18n>\n"
  },
  {
    "path": "src/pages/index.vue",
    "content": "<template>\n  <div />\n</template>\n<route lang=\"json\">\n{\n  \"redirect\": \"homepage\"\n}\n</route>\n"
  },
  {
    "path": "src/pages/login.vue",
    "content": "<script setup lang=\"ts\">\nimport type { VForm } from '@/utils/types'\nimport background from '@/assets/images/drawer/1.jpg'\nimport ButtonLocale from '@/components/layout/ButtonLocale.vue'\nconst { t } = useI18n()\nconst router = useRouter()\nconst userStore = useUserStore()\nconst form = ref<VForm | null>(null)\nconst loginShowed = ref(false)\nconst username = ref('')\nconst password = ref('')\nconst waiting = ref(false)\nconst showPassword = ref(false)\nconst valid = ref(true)\nconst snackbar = ref(false)\nconst snackMessage = ref('')\nconst timeout = ref(3000)\nconst nameRules = [\n  (v: string) => !!v || t('pleaseEnter', [t('username')]),\n  (v: string) =>\n    v.length <= 15 ||\n    t('form.LTE', { input: t('lengthOf', [t('username')]), limit: 15 }),\n]\nconst passwordRules = [(v: string) => !!v || t('pleaseEnter', [t('password')])]\nonMounted(() => {\n  loginShowed.value = true\n})\nasync function onSubmit() {\n  if (form.value?.validate()) {\n    try {\n      waiting.value = true\n      await userStore.login(username.value, password.value)\n      await router.push({ path: '/' }).catch(() => {})\n    } catch (e) {\n      snackMessage.value = JSON.stringify(e)\n      snackbar.value = true\n    } finally {\n      waiting.value = false\n    }\n  }\n}\n</script>\n\n<template>\n  <v-main>\n    <v-img\n      :src=\"background\"\n      gradient=\"to top, rgba(0,0,0,.5), rgba(0,0,0,.5)\"\n      style=\"min-height: 100vh; height: 100vh\"\n    >\n      <v-app-bar color=\"transparent\" flat absolute dark>\n        <v-img\n          class=\"mx-2\"\n          src=\"/favicon.svg\"\n          max-height=\"40\"\n          max-width=\"40\"\n          contain\n        />\n        <v-app-bar-title class=\"font-weight-bold\">\n          Vitify <span class=\"primary--text text--lighten-1\">Admin</span>\n        </v-app-bar-title>\n        <v-spacer />\n        <v-tooltip bottom>\n          <template #activator=\"{ on }\">\n            <v-btn\n              dark\n              text\n              min-width=\"0\"\n              href=\"https://kingyue737.github.io/vitify-docs/\"\n              target=\"_blank\"\n              v-on=\"on\"\n            >\n              <v-icon>mdi-book-outline</v-icon>\n            </v-btn>\n          </template>\n          <span>{{ t('documentation') }}</span>\n        </v-tooltip>\n        <ButtonLocale />\n      </v-app-bar>\n      <v-container fill-height>\n        <v-row align=\"center\" justify=\"center\">\n          <v-col cols=\"12\">\n            <v-head-card light class=\"px-5 py-3 mx-auto login-card mt-0\">\n              <template #heading>\n                <v-icon>mdi-login</v-icon>\n                {{ t('userLogin') }}\n              </template>\n              <v-expand-transition>\n                <v-row v-show=\"loginShowed\" align=\"center\" justify=\"center\">\n                  <v-col>\n                    <v-form ref=\"form\" v-model=\"valid\" lazy-validation>\n                      <v-text-field\n                        v-model.trim=\"username\"\n                        :counter=\"15\"\n                        :rules=\"nameRules\"\n                        :label=\"t('username')\"\n                        prepend-icon=\"mdi-account-outline\"\n                        required\n                        @keydown.enter.prevent=\"onSubmit\"\n                      />\n                      <v-text-field\n                        v-model=\"password\"\n                        prepend-icon=\"mdi-lock-outline\"\n                        :append-icon=\"showPassword ? 'mdi-eye' : 'mdi-eye-off'\"\n                        :type=\"showPassword ? 'text' : 'password'\"\n                        :rules=\"passwordRules\"\n                        :label=\"t('password')\"\n                        required\n                        autocomplete=\"on\"\n                        @click:append=\"showPassword = !showPassword\"\n                        @keydown.enter.prevent=\"onSubmit\"\n                      />\n                    </v-form>\n                  </v-col>\n                </v-row>\n              </v-expand-transition>\n              <template #actions>\n                <v-btn\n                  id=\"login-btn\"\n                  color=\"primary mx-auto\"\n                  block\n                  :disabled=\"waiting || !valid\"\n                  :loading=\"waiting\"\n                  @click=\"onSubmit\"\n                >\n                  {{ t('login') }}\n                </v-btn>\n              </template>\n            </v-head-card>\n          </v-col>\n        </v-row>\n      </v-container>\n      <v-footer absolute color=\"transparent\" dark>\n        <v-col class=\"text-center\" cols=\"12\">\n          &copy; Copyright 2022\n          <a href=\"https://github.com/kingyue737\" target=\"blank\">Yue JIN</a>\n          <span> & </span>\n          <a href=\"http://www.nustarnuclear.com/\" target=\"blank\"\n            ><v-icon class=\"mt-n1\">$nustar</v-icon>NuStar Nuclear</a\n          >\n        </v-col>\n      </v-footer>\n    </v-img>\n    <v-snackbar v-model=\"snackbar\" color=\"error\" :timeout=\"timeout\">\n      {{ snackMessage }}\n      <template #action=\"{ attrs }\">\n        <v-btn icon small v-bind=\"attrs\" class=\"ml-4\" @click=\"snackbar = false\">\n          <v-icon>$cancel</v-icon>\n        </v-btn>\n      </template>\n    </v-snackbar>\n  </v-main>\n</template>\n\n<style lang=\"scss\" scoped>\n.v-app-bar-title {\n  :deep(.v-app-bar-title__content) {\n    text-overflow: clip !important;\n  }\n}\n.login-card {\n  max-width: 300px;\n  background-color: rgba(255, 255, 255, 0.85) !important;\n  :deep(.v-head-card__heading) {\n    width: 100%;\n    margin-bottom: -12px !important;\n    padding: 24px !important;\n  }\n}\na {\n  color: inherit !important;\n}\n</style>\n\n<route lang=\"yaml\">\nmeta:\n  layout: empty\n</route>\n"
  },
  {
    "path": "src/pages/nested/menu1.vue",
    "content": "<script setup lang=\"ts\"></script>\n<template>\n  <v-container fluid>empty page</v-container>\n</template>\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"Menu 1\",\n    \"icon\": \"mdi-animation\"\n  }\n}\n</route>\n"
  },
  {
    "path": "src/pages/nested/menu2/menu2-1.vue",
    "content": "<script setup lang=\"ts\"></script>\n<template>\n  <v-container fluid>empty page</v-container>\n</template>\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"Menu 2-1\",\n    \"icon\": \"mdi-animation\"\n  }\n}\n</route>\n"
  },
  {
    "path": "src/pages/nested/menu2/menu2-2.vue",
    "content": "<script setup lang=\"ts\"></script>\n<template>\n  <v-container fluid>empty page</v-container>\n</template>\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"Menu 2-2\",\n    \"icon\": \"mdi-animation\"\n  }\n}\n</route>\n"
  },
  {
    "path": "src/pages/nested/menu2.vue",
    "content": "<script setup lang=\"ts\">\nimport RouterWrapper from '../../components/layout/RouterWrapper.vue'\n</script>\n<template>\n  <RouterWrapper />\n</template>\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"Menu 2\",\n    \"icon\": \"mdi-view-list\"\n  }\n}\n</route>\n"
  },
  {
    "path": "src/pages/nested.vue",
    "content": "<script setup lang=\"ts\">\nimport RouterWrapper from '../components/layout/RouterWrapper.vue'\n</script>\n<template>\n  <RouterWrapper />\n</template>\n\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"nestedRoutes\",\n    \"icon\": \"mdi-view-list\",\n    \"breadcrumb\": \"disabled\"\n  }\n}\n</route>\n"
  },
  {
    "path": "src/pages/reset-password.vue",
    "content": "<script setup lang=\"ts\">\nimport { resetPassword } from '@/api/users'\nimport type { VForm } from '@/utils/types'\n\nconst { t } = useI18n()\nconst router = useRouter()\nconst userStore = useUserStore()\nconst showCurrent = ref(false)\nconst showNew = ref(false)\nconst showConfirm = ref(false)\nconst confirm = ref('')\nconst password = ref('')\nconst current = ref('')\nconst valid = ref(true)\nconst rules = [\n  (v: string) => !!v || t('pleaseEnter', [t('password')]),\n  (v: string) =>\n    v.length <= 20 ||\n    t('form.LTE', { input: t('lengthOf', [t('password')]), limit: 20 }),\n  (v: string) =>\n    v.length >= 5 ||\n    t('form.GTE', { input: t('lengthOf', [t('password')]), limit: 5 }),\n]\nconst confirmedRules = computed(() => [\n  ...rules,\n  password.value === confirm.value || t('notEqualErr'),\n])\nconst form = ref<VForm | null>(null)\nasync function submit() {\n  if (form.value!.validate()) {\n    try {\n      await resetPassword(password.value, current.value)\n      Message.success(t('passwordUpdated'))\n      userStore.logOut()\n      router.push({ name: 'login' }).catch()\n    } catch (e) {\n      Message.error(e)\n    }\n  }\n}\n</script>\n\n<template>\n  <v-container fluid fill-height>\n    <v-row>\n      <v-col cols=\"12\">\n        <v-head-card\n          class=\"mx-auto\"\n          style=\"max-width: 350px; position: relative; top: -100px\"\n        >\n          <template #heading>\n            <v-icon>mdi-key-variant</v-icon>\n            {{ t('resetPassword') }}\n          </template>\n          <v-container>\n            <v-form ref=\"form\" v-model=\"valid\" lazy-validation>\n              <v-text-field\n                v-model=\"current\"\n                :label=\"t('currentPassword')\"\n                :counter=\"20\"\n                :append-icon=\"showCurrent ? 'mdi-eye' : 'mdi-eye-off'\"\n                :type=\"showCurrent ? 'text' : 'password'\"\n                required\n                :rules=\"rules\"\n                autocomplete=\"current-password\"\n                @click:append=\"showCurrent = !showCurrent\"\n              />\n              <v-text-field\n                v-model=\"password\"\n                :label=\"t('newPassword')\"\n                :counter=\"20\"\n                :append-icon=\"showNew ? 'mdi-eye' : 'mdi-eye-off'\"\n                :type=\"showNew ? 'text' : 'password'\"\n                :rules=\"rules\"\n                autocomplete=\"off\"\n                required\n                @click:append=\"showNew = !showNew\"\n              />\n              <v-text-field\n                v-model=\"confirm\"\n                :label=\"t('confirmPassword')\"\n                :counter=\"20\"\n                :append-icon=\"showConfirm ? 'mdi-eye' : 'mdi-eye-off'\"\n                :type=\"showConfirm ? 'text' : 'password'\"\n                :rules=\"confirmedRules\"\n                autocomplete=\"off\"\n                required\n                @click:append=\"showConfirm = !showConfirm\"\n              />\n            </v-form>\n          </v-container>\n          <template #actions>\n            <v-spacer />\n            <v-btn color=\"primary\" :disabled=\"!valid\" @click=\"submit\">\n              {{ t('submit') }}\n            </v-btn>\n          </template>\n        </v-head-card>\n      </v-col>\n    </v-row>\n  </v-container>\n</template>\n\n<i18n lang=\"json\">\n{\n  \"en\": {\n    \"passwordUpdated\": \"Password Updated\",\n    \"currentPassword\": \"Current Password\",\n    \"confirmPassword\": \"Confirm Password\",\n    \"newPassword\": \"New Password\",\n    \"notEqualErr\": \"Must be the same as New Password\"\n  },\n  \"zh\": {\n    \"passwordUpdated\": \"密码已更新\",\n    \"currentPassword\": \"当前密码\",\n    \"confirmPassword\": \"确认密码\",\n    \"newPassword\": \"新密码\",\n    \"notEqualErr\": \"与新密码不匹配，请重新输入\"\n  }\n}\n</i18n>\n"
  },
  {
    "path": "src/pages/user-manage/[id].vue",
    "content": "<template>\n  <v-container fluid>Username: {{ props.id }}</v-container>\n</template>\n\n<script setup lang=\"ts\">\nconst props = defineProps<{ id: string }>()\n</script>\n\n<script lang=\"ts\">\nexport default defineComponent({ name: 'UserDetail' })\n</script>\n\n<route lang=\"json\">\n{ \"meta\": { \"title\": \"userDetail\", \"breadcrumb\": \"disabled\" } }\n</route>\n"
  },
  {
    "path": "src/pages/user-manage/index.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  getUsers,\n  getGroups,\n  updateUser,\n  deleteUser,\n  type Group,\n  type IUserData,\n} from '@/api/users'\nimport type { DataTableHeader } from 'vuetify'\nimport { formatTime } from '@/utils/date'\nimport DialogConfirm from '@/components/DialogConfirm.vue'\n\nconst { t } = useI18n()\nconst headers: DataTableHeader[] = [\n  { text: t('username'), value: 'username' },\n  { text: t('group'), value: 'groups' },\n  { text: t('name'), value: 'name' },\n  { text: t('email'), value: 'email' },\n  { text: t('joinDate'), value: 'joinDate' },\n  { text: t('actions'), value: 'actions', sortable: false, align: 'end' },\n]\nconst loading = ref(true)\n\nconst users = ref<IUserData[]>([])\nconst groups = ref<Group[]>([])\ngetGroups().then((promise) => (groups.value = promise.data))\ngetUsers()\n  .then((promise) => {\n    users.value = promise.data\n  })\n  .finally(() => {\n    loading.value = false\n  })\n\nfunction groupColor(id: number) {\n  const name = groupName(id)\n  switch (name) {\n    case 'superuser':\n      return 'accent'\n    case 'admin':\n      return 'primary darken-3'\n    case 'staff':\n      return 'primary lighten-1'\n    default:\n      return ''\n  }\n}\nfunction groupName(id: number) {\n  return groups.value.find((group) => group.id === id)?.name || ''\n}\n\nconst dialogDelete = ref<InstanceType<typeof DialogConfirm> | null>(null)\nfunction showDialogDelete(id: number) {\n  dialogDelete.value?.open(t('confirmMsg')).then(async (confirmed: boolean) => {\n    if (confirmed) {\n      try {\n        await deleteUser(id)\n        Message.success(t('deleted'))\n        users.value = (await getUsers()).data\n      } catch (e) {\n        Message.error(e)\n      }\n    }\n  })\n}\n</script>\n\n<template>\n  <v-container fluid>\n    <v-row>\n      <v-col cols=\"12\">\n        <v-head-card :title=\"t('listOf', [t('user')])\" icon=\"mdi-view-list\">\n          <v-data-table\n            :headers=\"headers\"\n            :items=\"users\"\n            fixed-header\n            :loading=\"loading\"\n          >\n            <template #item.username=\"{ item }\">\n              <router-link :to=\"item.username\">{{ item.username }}</router-link>\n            </template>\n            <template #item.groups=\"{ item }\">\n              <v-chip\n                v-for=\"(groupId, i) in item.groups\"\n                :key=\"i\"\n                :color=\"groupColor(groupId)\"\n              >\n                {{ groupName(groupId) }}\n              </v-chip>\n            </template>\n            <template #item.joinDate=\"{ item }\">{{\n              formatTime(item.joinDate)\n            }}</template>\n            <template #item.actions=\"{ item }\">\n              <v-icon\n                class=\"mr-1\"\n                size=\"20\"\n                :title=\"t('edit')\"\n                @click=\"() => {}\"\n              >\n                mdi-pencil\n              </v-icon>\n              <v-icon\n                size=\"20\"\n                :title=\"t('delete')\"\n                @click.stop=\"showDialogDelete(item.id)\"\n              >\n                mdi-delete\n              </v-icon>\n            </template>\n          </v-data-table>\n          <DialogConfirm ref=\"dialogDelete\" />\n        </v-head-card>\n      </v-col>\n    </v-row>\n  </v-container>\n</template>\n\n<route lang=\"json\">\n{ \"meta\": { \"icon\": \"XXX\" } }\n</route>\n\n<i18n lang=\"json\">\n{\n  \"en\": {\n    \"confirmMsg\": \"Are you sure to delete this user?\"\n  },\n  \"zh\": {\n    \"confirmMsg\": \"你确定要删除此用户吗？\"\n  }\n}\n</i18n>\n"
  },
  {
    "path": "src/pages/user-manage.vue",
    "content": "<script setup lang=\"ts\">\nimport RouterWrapper from '@/components/layout/RouterWrapper.vue'\n</script>\n\n<template>\n  <RouterWrapper />\n</template>\n\n<route lang=\"json\">\n{\n  \"meta\": {\n    \"title\": \"userManagement\",\n    \"icon\": \"mdi-account-group\",\n    \"roles\": [\"admin\"],\n    \"drawerGroup\": \"admin\",\n    \"dataCy\": \"userManage\"\n  }\n}\n</route>\n"
  },
  {
    "path": "src/plugins/README.md",
    "content": "## Plugins\n\nA custom user plugin system. Place a `.ts` file with the following template, it will be installed automatically.\n\n```ts\nimport type { InstallPlugin } from '@/utils/types'\n\nexport const install: InstallPlugin = (vue) => {\n  // do something\n}\n```\n"
  },
  {
    "path": "src/plugins/components.ts",
    "content": "import VChart from 'vue-echarts'\nimport VHeadCard from '@/components/VHeadCard.vue'\nimport type { InstallPlugin } from '@/utils/types'\n\nexport const install: InstallPlugin = (vue) => {\n  vue.component('VHeadCard', VHeadCard)\n  vue.component('VChart', VChart)\n}\n"
  },
  {
    "path": "src/plugins/echarts.ts",
    "content": "import * as echarts from 'echarts/core'\n\nimport {\n  LineChart,\n  type LineSeriesOption,\n  BarChart,\n  type BarSeriesOption,\n  EffectScatterChart,\n  type EffectScatterSeriesOption,\n  ScatterChart,\n  type ScatterSeriesOption,\n  PieChart,\n  type PieSeriesOption,\n  RadarChart,\n  type RadarSeriesOption,\n} from 'echarts/charts'\nimport { CanvasRenderer } from 'echarts/renderers'\nimport {\n  DataZoomComponent,\n  type DataZoomComponentOption,\n  LegendComponent,\n  type LegendComponentOption,\n  TooltipComponent,\n  type TooltipComponentOption,\n  ToolboxComponent,\n  type ToolboxComponentOption,\n  GridComponent,\n  type GridComponentOption,\n  TitleComponent,\n  type TitleComponentOption,\n  MarkPointComponent,\n  type MarkPointComponentOption,\n  DatasetComponent,\n  type DatasetComponentOption,\n  VisualMapComponent,\n  type VisualMapComponentOption,\n} from 'echarts/components'\n\necharts.use([\n  LineChart,\n  BarChart,\n  PieChart,\n  RadarChart,\n  EffectScatterChart,\n  ScatterChart,\n  CanvasRenderer,\n  DataZoomComponent,\n  LegendComponent,\n  TooltipComponent,\n  ToolboxComponent,\n  GridComponent,\n  TitleComponent,\n  MarkPointComponent,\n  DatasetComponent,\n  VisualMapComponent,\n])\n\nexport type ECOption = echarts.ComposeOption<\n  | LineSeriesOption\n  | BarSeriesOption\n  | PieSeriesOption\n  | RadarSeriesOption\n  | EffectScatterSeriesOption\n  | ScatterSeriesOption\n  | DataZoomComponentOption\n  | LegendComponentOption\n  | TooltipComponentOption\n  | ToolboxComponentOption\n  | GridComponentOption\n  | TitleComponentOption\n  | MarkPointComponentOption\n  | DatasetComponentOption\n  | VisualMapComponentOption\n>\n\nexport default echarts\n"
  },
  {
    "path": "src/plugins/i18n.ts",
    "content": "import VueI18n from 'vue-i18n'\nimport { castToVueI18n, createI18n } from 'vue-i18n-bridge'\n\nimport en from '@/locales/en.json'\nimport zh from '@/locales/zh.json'\nimport type { InstallPlugin } from '@/utils/types'\n\nexport const install: InstallPlugin = (vue) => {\n  vue.use(VueI18n, { bridge: true })\n\n  const i18n = castToVueI18n(\n    createI18n(\n      {\n        legacy: false,\n        locale: 'en',\n        messages: { zh, en },\n        missingWarn: false,\n        fallbackWarn: false,\n      },\n      VueI18n,\n    ),\n  )\n  vue.use(i18n)\n  return i18n\n}\n"
  },
  {
    "path": "src/plugins/pinia.ts",
    "content": "import { createPinia, PiniaVuePlugin } from 'pinia'\nimport type { InstallPlugin } from '@/utils/types'\n\nexport const install: InstallPlugin = (vue) => {\n  vue.use(PiniaVuePlugin)\n  return createPinia()\n}\n"
  },
  {
    "path": "src/plugins/portal-vue.ts",
    "content": "/* Replace this component by Vue 3 built-in Teleport in the future */\nimport PortalVue from 'portal-vue'\nimport type { InstallPlugin } from '@/utils/types'\n\nexport const install: InstallPlugin = (vue) => {\n  vue.use(PortalVue)\n}\n"
  },
  {
    "path": "src/plugins/router.ts",
    "content": "import Router from 'vue-router'\nimport { setupLayouts } from 'virtual:generated-layouts'\nimport generatedRoutes from '~pages'\nimport { isPermitted } from '@/utils/permission'\nimport type { InstallPlugin } from '@/utils/types'\n\nexport const routes = setupLayouts(generatedRoutes)\n\nexport const install: InstallPlugin = (vue) => {\n  vue.use(Router)\n\n  const router = new Router({\n    mode: 'history',\n    routes,\n  })\n\n  router.beforeEach(async (to, from, next) => {\n    const userStore = useUserStore()\n    // Determine whether the user has logged in\n    if (userStore.token) {\n      if (to.path === '/login') {\n        // If is logged in, redirect to the home page\n        next({ path: '/' })\n      } else {\n        // Check whether the user has obtained his permission roles\n        if (userStore.roles.length === 0) {\n          try {\n            await userStore.getUserInfo()\n          } catch (e) {\n            // Remove token and redirect to login page\n            userStore.logOut()\n            Message.error(e)\n            next('/login')\n            return\n          }\n        }\n        if (!to.meta?.roles || isPermitted(to.meta.roles)) {\n          next()\n          return\n        }\n        // Redirect to 404 error page if not permitted\n        next({ name: 'all' })\n      }\n    } else {\n      if (to.path === '/login') {\n        next()\n      } else {\n        next('/login')\n      }\n    }\n  })\n\n  return router\n}\n"
  },
  {
    "path": "src/plugins/vuetify.ts",
    "content": "import Vuetify from 'vuetify/lib'\nimport type { VuetifyParsedTheme } from 'vuetify/types/services/theme'\nimport { Ripple, Resize, Scroll } from 'vuetify/lib/directives'\nimport { useDark } from '@vueuse/core'\nimport en from 'vuetify/lib/locale/en'\nimport zh from 'vuetify/lib/locale/zh-Hans'\nimport type { InstallPlugin } from '@/utils/types'\nimport { filename } from '@/utils/string'\nimport type { Component } from 'vue'\n\nconst svgIcons = Object.fromEntries(\n  Object.entries(\n    import.meta.glob<Component>('@/assets/icons/*.svg', {\n      eager: true,\n      import: 'default',\n    }),\n  ).map(([k, v]) => [filename(k), { component: v }]),\n)\nconst theme = {\n  primary: localStorage.getItem('theme-primary') || '#3f51b5',\n  secondary: '#03A9F4',\n  accent: '#9C27b0',\n  info: '#00CAE3',\n}\n\nexport const install: InstallPlugin = (vue) => {\n  vue.use(Vuetify, {\n    directives: {\n      Ripple,\n      Resize,\n      Scroll,\n    },\n  })\n  return new Vuetify({\n    lang: {\n      locales: { zh, en },\n      current: 'en',\n    },\n    theme: {\n      dark: useDark().value,\n      themes: {\n        dark: theme,\n        light: theme,\n      },\n      options: {\n        themeCache: {\n          // https://vuetifyjs.com/features/theme/#section-30ad30e330c330b730e5\n          get: (key: VuetifyParsedTheme) => {\n            return localStorage.getItem(`parsed-theme-${key.primary.base}`)\n          },\n          set: (key: VuetifyParsedTheme, value: string) => {\n            localStorage.setItem(`parsed-theme-${key.primary.base}`, value)\n          },\n        },\n        customProperties: true,\n      },\n    },\n    icons: {\n      iconfont: 'mdiSvg',\n      values: {\n        ...svgIcons,\n      },\n    },\n    breakpoint: {\n      thresholds: {\n        xs: 600,\n        sm: 960,\n        md: 1280,\n        lg: 1920,\n      },\n      mobileBreakpoint: 'sm',\n      scrollBarWidth: 0,\n    },\n  })\n}\n"
  },
  {
    "path": "src/route-meta.d.ts",
    "content": "export {}\n\nimport 'vue-router'\nimport type { RouteConfig } from 'vue-router'\nimport type { Role } from '@/api/users'\n\ndeclare module 'vue-router' {\n  interface RouteMeta {\n    /** Drawer item icon */\n    icon?: string\n    /** Groups will be separated by divider line in drawer */\n    drawerGroup?: 'admin' | 'PUC'\n    /** Determine the order of item in drawer */\n    drawerIndex?: number\n    /** Drawer item and breadcrumb text */\n    title?: string\n    /** Subtitle in drawer item */\n    subtitle?: string\n    /** Authorized user groups */\n    roles?: Role[]\n    /** For cypress location */\n    dataCy?: string\n    /** Hide this route in drawer if truthy */\n    hidden?: boolean\n    /** Default is enabled */\n    breadcrumb?: 'hidden' | 'disabled'\n  }\n  type RouteRecordRaw = RouteConfig // shim plugins for vue-router v4\n}\n"
  },
  {
    "path": "src/shims.d.ts",
    "content": "declare module 'vuetify/lib/locale/*' {\n  import type { VuetifyLocale } from 'vuetify/types/services/lang'\n  const locale: VuetifyLocale\n  export default locale\n}\n"
  },
  {
    "path": "src/stores/__tests__/message.spec.ts",
    "content": "describe('Message Store', () => {\n  beforeEach(() => {\n    // creates a fresh pinia and make it active so it's automatically picked\n    // up by any useStore() call without having to pass it to it:\n    // `useStore(pinia)`\n    setActivePinia(createPinia())\n  })\n\n  it('Add and delete message', () => {\n    const store = useMessageStore()\n    store.addMessage('A message!')\n    expect(store.messageCount).toBe(1)\n    store.delMessage(0)\n    expect(store.messages.length).toBe(0)\n  })\n\n  it('Message utils', () => {\n    const store = useMessageStore()\n    Message.error('Error message')\n    const message = store.messages.at(-1)\n    expect(message!.text).toBe('Error message')\n    expect(message!.type).toBe('error')\n  })\n})\n\nexport {}\n"
  },
  {
    "path": "src/stores/app.ts",
    "content": "export const useAppStore = defineStore('app', {\n  state: () => {\n    return {\n      drawer: true,\n      drawerImage: '占位',\n      drawerImageShow: true,\n    }\n  },\n})\n"
  },
  {
    "path": "src/stores/message.ts",
    "content": "interface Message {\n  show: boolean\n  type: 'info' | 'error' | 'success' | 'warning'\n  text: string\n  time: Date\n  id: number\n}\n\nexport const useMessageStore = defineStore('message', {\n  state: () => {\n    const messages: Message[] = []\n    return {\n      messages,\n      messageCount: 0,\n    }\n  },\n  actions: {\n    addMessage(text: string, type: Message['type'] = 'info') {\n      this.messages.push({\n        id: this.messageCount++,\n        text: text,\n        type: type,\n        time: new Date(),\n        show: true,\n      })\n    },\n    delMessage(id: number) {\n      const index = this.messages.findIndex((m) => m.id === id)\n      if (index !== -1) {\n        this.messages.splice(index, 1)\n      }\n    },\n  },\n})\n\nexport const Message = {\n  info: (text: string) => useMessageStore().addMessage(text, 'info'),\n  success: (text: string) => useMessageStore().addMessage(text, 'success'),\n  warning: (text: string) => useMessageStore().addMessage(text, 'warning'),\n  error: (val: any) => {\n    let text = ''\n    if (typeof val === 'string') {\n      text = val\n    } else if (val instanceof Error) {\n      text = val.message\n    } else {\n      text = JSON.stringify(val)\n    }\n    useMessageStore().addMessage(text, 'error')\n  },\n}\n"
  },
  {
    "path": "src/stores/user.ts",
    "content": "import {\n  getToken,\n  getUser,\n  refreshToken,\n  getGroup,\n  type Role,\n} from '@/api/users'\n\nexport const useUserStore = defineStore('user', {\n  state: () => {\n    const roles: Role[] = []\n    return {\n      name: localStorage.getItem('username') || '',\n      id: parseInt(localStorage.getItem('id') || '-1'),\n      token: localStorage.getItem('access') || '',\n      roles,\n    }\n  },\n  actions: {\n    async login(username: string, password: string) {\n      const { data } = await getToken(username, password)\n      Object.entries({\n        access: data.accessToken,\n        refresh: data.refreshToken,\n        username,\n      }).map(([k, v]: [string, string]) => localStorage.setItem(k, v))\n      this.token = data.accessToken\n      this.name = username\n      await this.getUserInfo()\n      useMessageStore().$reset()\n    },\n    async getUserInfo() {\n      const { data: user } = await getUser(this.id)\n      const { groups } = user\n      // roles must be a non-empty array\n      if (!groups || groups.length <= 0) {\n        throw Error('getUserInfo: roles must be a non-null array!')\n      }\n      const roles: Role[] = []\n      for (const id of groups) {\n        roles.push((await getGroup(id)).data.name)\n      }\n      this.roles = roles\n      localStorage.setItem('id', user.id.toString())\n      this.id = user.id\n    },\n    logOut() {\n      ;['access', 'refresh', 'username'].forEach((k) =>\n        localStorage.removeItem(k),\n      )\n      this.$reset()\n    },\n    async refreshToken() {\n      const response = await refreshToken(localStorage.getItem('refresh')!)\n      localStorage.setItem('access', response.data.accessToken)\n      localStorage.setItem('fresh', response.data.refreshToken)\n      this.token = response.data.accessToken\n      return response\n    },\n  },\n})\n"
  },
  {
    "path": "src/utils/date.ts",
    "content": "const utcOffset = new Date().getTimezoneOffset() * 60000\n\nfunction localeISOString(d?: Date | string | number): string {\n  return new Date((d ? new Date(d) : new Date()).getTime() - utcOffset)\n    .toISOString()\n    .slice(0, -1)\n}\n\nexport function localeISODateString(d?: Date | string | number): string {\n  return localeISOString(d).slice(0, 10)\n}\n\n/**\n * @param t1 - minuend\n * @param t2 - subtrahend\n * @return t1 - t2\n */\nexport function deltaTime(t1: string | Date, t2: string | Date) {\n  return new Date(t1).getTime() - new Date(t2).getTime()\n}\n\nexport function formatTime(time: string | Date) {\n  return new Date(time).toLocaleString('zh-CN', { hour12: false })\n}\n"
  },
  {
    "path": "src/utils/permission.ts",
    "content": "import type { Role } from '@/api/users'\n\nexport function isPermitted(\n  allowedRoles: Role[],\n  roles: Role[] = useUserStore().roles,\n): boolean {\n  if (roles.some((role) => allowedRoles.includes(role))) {\n    return true\n  } else {\n    for (const role of roles) {\n      for (const allowRole of allowedRoles) {\n        if (isSubGroup(allowRole, role)) {\n          return true\n        }\n      }\n    }\n  }\n  return false\n}\n\n/**\n * So far, we don't have two roles having intersection but no one is subset\n * of the other. All these roles form a chain like: A ∈ B ∈ C ∈ D. For example,\n * admin role has all authority of staff, staff have all authority of guest.\n *\n * If one day there are two roles A and B in a chain in this situation: A, B both have\n * authority of action 1, A has authority of action 2 but B, B is accessible to\n * action 3 but A, they would never form a chain, so the chain should be corrected.\n *\n * TODO: correct the chain if there is the situation above.\n * */\nconst rolesChain: {\n  [k in Role]: number\n} = {\n  superuser: 100,\n  admin: 90,\n  staff: 60,\n}\n\nexport function isSubGroup(role1: Role, role2: Role): boolean {\n  return rolesChain[role2] >= rolesChain[role1]\n}\n"
  },
  {
    "path": "src/utils/request.ts",
    "content": "import axios, { type AxiosError } from 'axios'\n\nconst service = axios.create({\n  baseURL:\n    import.meta.env.VITE_API_URL ||\n    `${window.location.protocol}//${window.location.hostname}:9529/api/v1`,\n  timeout: 5000,\n})\n\nconst errHandler = async (error: AxiosError) => {\n  const response = error.response\n  const userStore = useUserStore()\n  if (response) {\n    switch (response.status) {\n      case 401:\n        // TODO: refresh token according to your backend\n        // if (userStore.token) {\n        //   return userStore.refreshToken().then((resp) => {\n        //     return service(error.response!.config)\n        //   })\n        // }\n        break\n    }\n    if (!response.headers['content-type']?.includes('text/html')) {\n      throw response.data\n    }\n  }\n  throw error\n}\n\n// Request interceptors\nservice.interceptors.request.use(\n  async (config) => {\n    // Add X-Access-Token header to every request, you can add other custom headers here\n    const token = useUserStore().token\n    if (token) {\n      config.headers!.Authorization = `Bearer ${token}`\n    }\n    return config\n  },\n  // Add Error Handler Below\n)\n\n// Response interceptors\nservice.interceptors.response.use((response) => {\n  return response\n}, errHandler)\n\nexport default service\n"
  },
  {
    "path": "src/utils/string.ts",
    "content": "export function filename(path: string) {\n  return path\n    .split(/(\\\\|\\/)/g)\n    .pop()!\n    .replace(/\\.[^/.]+$/, '')\n}\n"
  },
  {
    "path": "src/utils/types.ts",
    "content": "import type Vue from 'vue'\n\nexport type VForm = typeof Vue & {\n  validate: () => boolean\n  resetValidation: () => boolean\n  reset: () => void\n}\n\nexport type InstallPlugin = (vue: typeof Vue) => any\n"
  },
  {
    "path": "test/helpers.ts",
    "content": "import { createLocalVue, mount, shallowMount } from '@vue/test-utils'\nimport Vuetify from 'vuetify/lib'\nimport { PiniaVuePlugin } from 'pinia'\nimport { createTestingPinia } from '@pinia/testing'\nimport { render } from '@testing-library/vue'\nimport Router from 'vue-router'\nimport Vue from 'vue'\nimport { install as installI18n } from '@/plugins/i18n'\nimport { install as installComponents } from '@/plugins/components'\n\nexport function mountComposable<T>(composable: () => T) {\n  let result: T | undefined\n  const app = new Vue({\n    pinia: createTestingPinia(),\n    setup() {\n      result = composable()\n      return () => {}\n    },\n  })\n  Vue.use(PiniaVuePlugin)\n  app.$mount(document.createElement('div'))\n\n  return {\n    composable: result!,\n    vm: app,\n  }\n}\n\nexport function createWrapper(\n  component: Parameters<typeof mount>[0],\n  options: Parameters<typeof mount>[1] = {},\n  shallow = false,\n) {\n  const localVue = createLocalVue()\n  installComponents(localVue)\n  const i18n = installI18n(localVue)\n  const vuetify = new Vuetify()\n  const mountOptions = { vuetify, localVue, i18n, ...options }\n  if (!shallow) {\n    return mount(component, mountOptions)\n  } else {\n    return shallowMount(component, mountOptions)\n  }\n}\n\nexport function renderWithVuetify(\n  component: Parameters<typeof render>[0],\n  options: Parameters<typeof render>[1] = {},\n) {\n  const root = document.createElement('div')\n  root.setAttribute('data-app', 'true')\n  return render(\n    component,\n    {\n      container: document.body.appendChild(root),\n      vuetify: new Vuetify(),\n      pinia: createTestingPinia(),\n      router: new Router(),\n      stubs: ['Portal'],\n      ...options,\n    },\n    (vue) => {\n      installComponents(vue)\n      const i18n = installI18n(vue)\n      vue.use(PiniaVuePlugin)\n      return { i18n }\n    },\n  )\n}\n"
  },
  {
    "path": "test/vitest.setup.ts",
    "content": "import Vue from 'vue'\nVue.config.devtools = false\nVue.config.productionTip = false\n\n// import vuetify after suppressing devtools warning\nconst Vuetify = (await import('vuetify/lib')).default\n\nVue.use(Vuetify)\n\n// mock window.matchMedia which not implemented by jsdom\nObject.defineProperty(window, 'matchMedia', {\n  writable: true,\n  value: vi.fn().mockImplementation((query) => ({\n    matches: false,\n    media: query,\n    onchange: null,\n    addListener: vi.fn(), // deprecated\n    removeListener: vi.fn(), // deprecated\n    addEventListener: vi.fn(),\n    removeEventListener: vi.fn(),\n    dispatchEvent: vi.fn(),\n  })),\n})\n"
  },
  {
    "path": "tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"target\": \"esnext\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"Bundler\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"sourceMap\": true,\n    \"resolveJsonModule\": true,\n    \"esModuleInterop\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    // No `ScriptHost` as dropping support for IE\n    \"lib\": [\"esnext\", \"DOM\", \"DOM.iterable\"],\n    \"types\": [],\n    \"skipLibCheck\": true\n  },\n  \"vueCompilerOptions\": {\n    \"target\": 2.7\n  },\n  \"include\": [\"src/**/*\", \"src/**/*.vue\"],\n  \"exclude\": [\"src/**/__tests__/*\"]\n}\n"
  },
  {
    "path": "tsconfig.cypress.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"compilerOptions\": {\n    \"isolatedModules\": false,\n    \"types\": [\"cypress\"]\n  },\n  \"include\": [\"./cypress/**/*.ts\"],\n  \"exclude\": []\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.node.json\"\n    },\n    {\n      \"path\": \"./tsconfig.vitest.json\"\n    },\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.cypress.json\"\n    }\n  ],\n  \"ts-node\": {\n    \"transpileOnly\": true,\n    \"compilerOptions\": {\n      \"module\": \"ESNext\",\n      \"lib\": [\"es2023\"],\n      \"target\": \"es2022\"\n    }\n  }\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"include\": [\"vite.config.*\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n    \"types\": [\"node\", \"vitest\"],\n    \"lib\": []\n  }\n}\n"
  },
  {
    "path": "tsconfig.vitest.json",
    "content": "{\n  \"extends\": \"./tsconfig.app.json\",\n  \"include\": [\"./src/**/*\", \"env.d.ts\", \"./test/*\"],\n  \"exclude\": [],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.vitest.tsbuildinfo\",\n    \"types\": [\"node\", \"jsdom\", \"vitest/globals\"]\n  }\n}\n"
  },
  {
    "path": "vite.config.preview.ts",
    "content": "import { defineConfig } from 'vite'\n// The more plugins, the slower the startup of `vite preview`\n// this file is for instant preview with an empty config\nexport default defineConfig({\n  preview: {},\n})\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import path from 'path'\nimport { defineConfig } from 'vite'\nimport vue2 from '@vitejs/plugin-vue2'\nimport legacy from '@vitejs/plugin-legacy'\nimport AutoImport from 'unplugin-auto-import/vite'\nimport Components from 'unplugin-vue-components/vite'\nimport { createSvgPlugin } from '@kingyue/vite-plugin-vue2-svg'\nimport Pages from 'vite-plugin-pages'\nimport Layouts from 'vite-plugin-vue-layouts'\nimport Inspect from 'vite-plugin-inspect'\nimport VueI18n from '@intlify/unplugin-vue-i18n/vite'\nimport browserslistToEsbuild from 'browserslist-to-esbuild'\nimport postcssPresetEnv from 'postcss-preset-env'\nimport regexpPlugin from 'rollup-plugin-regexp'\nimport * as mdicons from '@mdi/js'\nimport browserslist from 'browserslist'\n\nconst mdi: Record<string, string> = {}\nObject.keys(mdicons).forEach((key) => {\n  const value = (mdicons as Record<string, string>)[key]\n  mdi[\n    key\n      .replace(/([A-Z])/g, '-$1')\n      .toLowerCase()\n      .replace(/([0-9]+)/g, '-$1')\n  ] = value\n})\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  build: { target: browserslistToEsbuild() },\n  server: {\n    port: 9527,\n  },\n  plugins: [\n    regexpPlugin({\n      exclude: ['node_modules/**'],\n      find: /\\b(?<![/\\w])(mdi-[\\w-]+)\\b(?!\\.)/,\n      replace: (match: string) => {\n        if (mdi[match]) {\n          return mdi[match]\n        } else {\n          console.warn('[plugin-regexp] No matched svg icon for ' + match)\n          return match\n        }\n      },\n      sourcemap: false,\n    }),\n    vue2(),\n    Pages(),\n    Layouts(),\n    legacy({\n      modernPolyfills: true,\n      renderLegacyChunks: false,\n      modernTargets: browserslist.loadConfig({\n        path: path.resolve(__dirname),\n      }),\n    }),\n    Components({\n      resolvers: [\n        {\n          type: 'component',\n          resolve: (name) => {\n            const blackList = ['VChart', 'VHeadCard']\n            if (name.match(/^V[A-Z]/) && !blackList.includes(name))\n              return { name, from: 'vuetify/lib' }\n          },\n        },\n      ],\n      dirs: [],\n      dts: false,\n      types: [],\n    }),\n    AutoImport({\n      imports: [\n        'vue',\n        'pinia',\n        'vue-router/composables',\n        { 'vue-i18n-bridge': ['useI18n'] },\n      ],\n      dts: 'src/auto-imports.d.ts',\n      dirs: ['src/stores'],\n      vueTemplate: false,\n    }),\n    createSvgPlugin({\n      svgoConfig: {\n        plugins: [\n          'cleanupEnableBackground',\n          'removeDoctype',\n          'removeMetadata',\n          'removeComments',\n          'removeXMLNS',\n          'removeXMLProcInst',\n          'sortDefsChildren',\n          'convertTransform',\n        ],\n      },\n    }),\n    VueI18n({\n      runtimeOnly: false,\n      compositionOnly: true,\n      fullInstall: false,\n      include: [path.resolve(__dirname, 'src/locales/**')],\n    }),\n    Inspect(),\n  ],\n  css: {\n    devSourcemap: true,\n    preprocessorMaxWorkers: true,\n    // https://vitejs.dev/config/#css-preprocessoroptions\n    preprocessorOptions: {\n      sass: {\n        additionalData: [\n          // vuetify variable overrides\n          '@import \"@/assets/styles/vuetify-variables.scss\"',\n          '',\n        ].join('\\n'),\n      },\n    },\n    postcss: {\n      plugins: [postcssPresetEnv({ stage: 3 })],\n    },\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, 'src'),\n      'vue-i18n-bridge':\n        'vue-i18n-bridge/dist/vue-i18n-bridge.runtime.esm-bundler.js',\n    },\n  },\n  test: {\n    globals: true,\n    include: ['test/**/*.test.ts', 'src/**/__tests__/*'],\n    environment: 'jsdom',\n    setupFiles: ['./test/vitest.setup.ts'],\n    onConsoleLog(log) {\n      /* Suppress EOL warning from vue-i18n */\n      if (log.startsWith('vue-i18n-bridge v10')) return false\n    },\n  },\n})\n"
  }
]