Full Code of hooray/fantastic-startkit for AI

main a0b3692dd9ea cached
132 files
144.8 KB
49.0k tokens
68 symbols
1 requests
Download .txt
Repository: hooray/fantastic-startkit
Branch: main
Commit: a0b3692dd9ea
Files: 132
Total size: 144.8 KB

Directory structure:
gitextract_vtf2u88_/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── docs.yml
│       └── sync.yml
├── .gitignore
├── .lintstagedrc
├── .node-version
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── LICENSE
├── README.md
├── apps/
│   ├── core/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── postcss.config.js
│   │   ├── src/
│   │   │   ├── App.vue
│   │   │   ├── api/
│   │   │   │   ├── index.ts
│   │   │   │   └── modules/
│   │   │   │       ├── user.fake.ts
│   │   │   │       └── user.ts
│   │   │   ├── assets/
│   │   │   │   ├── icons/
│   │   │   │   │   └── .gitkeep
│   │   │   │   ├── images/
│   │   │   │   │   └── .gitkeep
│   │   │   │   └── styles/
│   │   │   │       ├── globals.css
│   │   │   │       └── resources/
│   │   │   │           ├── utils.scss
│   │   │   │           └── variables.scss
│   │   │   ├── components/
│   │   │   │   └── .gitkeep
│   │   │   ├── composables/
│   │   │   │   └── useGlobalProperties.ts
│   │   │   ├── layouts/
│   │   │   │   └── index.vue
│   │   │   ├── main.ts
│   │   │   ├── router/
│   │   │   │   ├── index.ts
│   │   │   │   └── modules/
│   │   │   │       └── root.ts
│   │   │   ├── store/
│   │   │   │   ├── index.ts
│   │   │   │   └── modules/
│   │   │   │       ├── settings.ts
│   │   │   │       └── user.ts
│   │   │   ├── types/
│   │   │   │   ├── auto-imports.d.ts
│   │   │   │   ├── components.d.ts
│   │   │   │   ├── env.d.ts
│   │   │   │   ├── shims.d.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── utils/
│   │   │   │   ├── dayjs.ts
│   │   │   │   └── eventBus.ts
│   │   │   └── views/
│   │   │       ├── [...all].vue
│   │   │       ├── index.vue
│   │   │       └── login.vue
│   │   ├── tsconfig.app.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   ├── vite/
│   │   │   └── plugins.ts
│   │   └── vite.config.ts
│   └── example/
│       ├── index.html
│       ├── package.json
│       ├── postcss.config.js
│       ├── src/
│       │   ├── App.vue
│       │   ├── api/
│       │   │   ├── index.ts
│       │   │   └── modules/
│       │   │       ├── news.fake.ts
│       │   │       ├── news.ts
│       │   │       ├── user.fake.ts
│       │   │       └── user.ts
│       │   ├── assets/
│       │   │   └── styles/
│       │   │       ├── globals.css
│       │   │       └── resources/
│       │   │           ├── utils.scss
│       │   │           └── variables.scss
│       │   ├── components/
│       │   │   └── DemoButton/
│       │   │       └── index.vue
│       │   ├── composables/
│       │   │   └── useGlobalProperties.ts
│       │   ├── layouts/
│       │   │   ├── example.vue
│       │   │   └── index.vue
│       │   ├── main.ts
│       │   ├── router/
│       │   │   ├── index.ts
│       │   │   └── modules/
│       │   │       ├── example.ts
│       │   │       └── root.ts
│       │   ├── store/
│       │   │   ├── index.ts
│       │   │   └── modules/
│       │   │       ├── example.ts
│       │   │       ├── settings.ts
│       │   │       └── user.ts
│       │   ├── types/
│       │   │   ├── auto-imports.d.ts
│       │   │   ├── components.d.ts
│       │   │   ├── env.d.ts
│       │   │   ├── shims.d.ts
│       │   │   └── vite-env.d.ts
│       │   ├── utils/
│       │   │   ├── dayjs.ts
│       │   │   └── eventBus.ts
│       │   └── views/
│       │       ├── [...all].vue
│       │       ├── example/
│       │       │   ├── axios.vue
│       │       │   ├── component.vue
│       │       │   ├── components/
│       │       │   │   └── ExampleList/
│       │       │   │       └── index.vue
│       │       │   ├── icon.vue
│       │       │   ├── params/
│       │       │   │   └── [test].vue
│       │       │   ├── permission-js.vue
│       │       │   ├── permission-router.vue
│       │       │   ├── pinia.vue
│       │       │   ├── query.vue
│       │       │   └── reload.vue
│       │       ├── index.vue
│       │       └── login.vue
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       ├── vite/
│       │   └── plugins.ts
│       └── vite.config.ts
├── docs/
│   ├── .gitignore
│   ├── .vitepress/
│   │   ├── config.ts
│   │   └── theme/
│   │       ├── components/
│   │       │   └── SponsorsAside.vue
│   │       ├── fonts/
│   │       │   └── fira_code/
│   │       │       └── fira_code.css
│   │       ├── index.ts
│   │       └── styles/
│   │           └── var.css
│   ├── guide/
│   │   ├── api.md
│   │   ├── axios.md
│   │   ├── build.md
│   │   ├── coding-standard.md
│   │   ├── env.md
│   │   ├── icon.md
│   │   ├── ready.md
│   │   ├── resources.md
│   │   ├── router.md
│   │   ├── start.md
│   │   └── store.md
│   ├── index.md
│   ├── package.json
│   ├── public/
│   │   └── .nojekyll
│   ├── support.md
│   └── tsconfig.json
├── eslint.config.js
├── package.json
├── packages/
│   ├── components/
│   │   ├── package.json
│   │   ├── resolver.ts
│   │   ├── src/
│   │   │   ├── icon/
│   │   │   │   └── index.vue
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── copyright/
│       ├── index.ts
│       ├── package.json
│       └── tsconfig.json
├── pnpm-workspace.yaml
├── scripts/
│   └── cli.ts
├── stylelint.config.js
├── tsconfig.json
└── uno.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .github/workflows/docs.yml
================================================
name: Deploy Docs

on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
      - pnpm-lock.yaml
      - pnpm-workspace.yaml
  workflow_dispatch:

concurrency:
  group: pages-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24

      - uses: pnpm/action-setup@v4
        name: Install pnpm
        with:
          run_install: false

      - name: Get pnpm store directory
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

      - uses: actions/cache@v5
        name: Setup pnpm cache
        with:
          path: ${{ env.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build site
        run: pnpm --filter docs build

      - uses: actions/upload-pages-artifact@v4
        with:
          path: docs/.vitepress/dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


================================================
FILE: .github/workflows/sync.yml
================================================
name: branches-sync

on:
  # 每天 00:00 自动同步
  schedule:
    - cron: '0 0 * * *'
  # 手动触发部署
  workflow_dispatch:

jobs:
  sync-to-gitee:
    runs-on: ubuntu-latest
    steps:
      - name: Sync to Gitee
        uses: wearerequired/git-mirror-action@master
        env:
          # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY
          SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
        with:
          # 注意替换为你的 GitHub 源仓库地址
          source-repo: git@github.com:hooray/fantastic-startkit.git
          # 注意替换为你的 Gitee 目标仓库地址
          destination-repo: git@gitee.com:hooray/fantastic-startkit.git


================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
dist*
dist-ssr
*.local
.eslintcache
.stylelintcache
.turbo


================================================
FILE: .lintstagedrc
================================================
{
  "*.{ts,tsx,vue}": "eslint --cache --fix",
  "*.{css,scss,vue}": "stylelint --cache --fix"
}


================================================
FILE: .node-version
================================================
24


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "EditorConfig.EditorConfig",
    "mikestead.dotenv",
    "dbaeumer.vscode-eslint",
    "stylelint.vscode-stylelint",
    "Vue.volar",
    "antfu.unocss",
    "antfu.pnpm-catalog-lens"
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "npm.packageManager": "pnpm",
  "js/ts.tsdk.path": "node_modules/typescript/lib",
  "search.exclude": {
    "**/dist*": true
  },
  "prettier.enable": false,
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.fixAll.stylelint": "explicit",
    "source.organizeImports": "never"
  },
  "eslint.useFlatConfig": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "jsonc",
    "yaml"
  ],
  "stylelint.validate": [
    "css",
    "postcss",
    "scss",
    "vue"
  ]
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 fantastic-startkit

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Fantastic-startkit

<p><b>简单好用</b>的 Vue3 项目启动套件</p>

<p>
  <a href="https://hurui.me/fantastic-startkit/" target="_blank">官网</a>
<p>

<p>
  <a href="###"><img src="https://img.shields.io/github/license/hooray/fantastic-startkit?label=%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE&style=flat-square" /></a>
</p>

## 特点

- 采用 Monorepo 架构,基于 pnpm workspace
- 支持 TypeScript
- 默认集成 vue-router 和 pinia
- 支持基于文件系统的路由
- 全局组件自动引入
- 全局 SCSS 资源引入
- 支持 Unocss
- 支持 SVG 文件图标、Iconify 图标、UnoCSS 图标
- 支持 mock 数据,可摆脱后端束缚独立开发
- 支持 gzip / brotli 优化项目体积,提高加载速度
- 结合 IDE 插件、ESlint 、stylelint 、Git 钩子,轻松实现团队代码规范

## 支持

如果觉得模版不错,或者已经在使用了,希望你可以去 **Github** 或者 **Gitee(码云)** 帮我点个 ⭐ ,这将对本产品的推广有极大帮助。

[![star](https://img.shields.io/github/stars/hooray/fantastic-startkit?style=social)](https://github.com/hooray/fantastic-startkit)

[![star](https://gitee.com/hooray/fantastic-startkit/badge/star.svg?theme=dark)](https://gitee.com/hooray/fantastic-startkit)

## 生态

- [`Fantastic-admin`](https://fantastic-admin.hurui.me/) - 面向 AI 的管理系统框架
- [`Fantastic-mobile`](https://fantastic-mobile.hurui.me/) - 让你的 H5 项目拥有稳固的工程底座
- [`One-step-admin`](https://one-step-admin.hurui.me) - 干啥都快人一步的 Vue 中后台管理系统框架


================================================
FILE: apps/core/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: apps/core/package.json
================================================
{
  "name": "@fantastic-startkit/core",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build:test": "vue-tsc -b && vite build --mode test",
    "build": "vue-tsc -b && vite build",
    "serve:test": "http-server ./dist-test -o",
    "serve": "http-server ./dist -o",
    "svgo": "svgo -f src/assets/icons",
    "lint": "npm-run-all -s lint:tsc lint:eslint lint:stylelint",
    "lint:tsc": "vue-tsc -b",
    "lint:eslint": "eslint . --cache --fix",
    "lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix"
  },
  "dependencies": {
    "@fantastic-startkit/components": "workspace:*",
    "axios": "catalog:",
    "dayjs": "catalog:",
    "eruda": "catalog:",
    "mitt": "catalog:",
    "nprogress": "catalog:",
    "pinia": "catalog:",
    "pinia-plugin-persistedstate": "catalog:",
    "qs": "catalog:",
    "vconsole": "catalog:",
    "vue": "catalog:",
    "vue-router": "catalog:"
  },
  "devDependencies": {
    "@faker-js/faker": "catalog:",
    "@fantastic-startkit/copyright": "workspace:*",
    "@iconify/json": "catalog:",
    "@iconify/vue": "catalog:",
    "@spiriit/vite-plugin-svg-spritemap": "catalog:",
    "@types/nprogress": "catalog:",
    "@types/qs": "catalog:",
    "@vitejs/plugin-vue": "catalog:",
    "@vitejs/plugin-vue-jsx": "catalog:",
    "@vue/tsconfig": "catalog:",
    "autoprefixer": "catalog:",
    "http-server": "catalog:",
    "npm-run-all2": "catalog:",
    "postcss": "catalog:",
    "postcss-nested": "catalog:",
    "sass-embedded": "catalog:",
    "svgo": "catalog:",
    "unplugin-auto-import": "catalog:",
    "unplugin-turbo-console": "catalog:",
    "unplugin-vue-components": "catalog:",
    "vite": "catalog:",
    "vite-plugin-archiver": "catalog:",
    "vite-plugin-compression2": "catalog:",
    "vite-plugin-env-parse": "catalog:",
    "vite-plugin-fake-server": "catalog:",
    "vite-plugin-pages": "catalog:",
    "vite-plugin-vue-devtools": "catalog:",
    "vite-plugin-vue-layouts-next": "catalog:",
    "vue-tsc": "catalog:"
  }
}


================================================
FILE: apps/core/postcss.config.js
================================================
export default {
  plugins: {
    'autoprefixer': {},
    'postcss-nested': {},
  },
}


================================================
FILE: apps/core/src/App.vue
================================================
<script setup lang="ts">
const isRouterAlive = ref(true)
const settingsStore = useSettingsStore()

provide('reload', reload)
function reload() {
  isRouterAlive.value = false
  nextTick(() => (isRouterAlive.value = true))
}

watch(() => settingsStore.title, () => {
  const title = settingsStore.title
  document.title = title ? `${title} - ${import.meta.env.VITE_APP_TITLE}` : import.meta.env.VITE_APP_TITLE
}, {
  immediate: true,
})
</script>

<template>
  <RouterView v-if="isRouterAlive" />
</template>


================================================
FILE: apps/core/src/api/index.ts
================================================
import axios from 'axios'
// import qs from 'qs'

// 请求重试配置
const MAX_RETRY_COUNT = 3 // 最大重试次数
const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)

// 扩展 AxiosRequestConfig 类型
declare module 'axios' {
  export interface AxiosRequestConfig {
    retry?: boolean
    retryCount?: number
    fake?: boolean
  }
}

const api = axios.create({
  baseURL: (import.meta.env.DEV && import.meta.env.VITE_ENABLE_PROXY) ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
  timeout: 1000 * 60,
  responseType: 'json',
})

api.interceptors.request.use(
  (request) => {
    // 如果设置了 fake 属性,强制使用 fake 的 baseURL
    if (request.fake) {
      request.baseURL = '/fake/'
    }
    /**
     * 全局拦截请求发送前提交的参数
     * 以下代码为示例,在请求头里带上 token 信息
     */
    const userStore = useUserStore()
    if (userStore.isLogin && request.headers) {
      request.headers.Token = userStore.token
    }
    // 是否将 POST 请求参数进行字符串化处理
    if (request.method === 'post') {
      // request.data = qs.stringify(request.data, {
      //   arrayFormat: 'brackets',
      // })
    }
    return request
  },
)

// 处理错误信息的函数
function handleError(error: any) {
  if (error.status === 401) {
    useUserStore().logout()
  }
  else {
    // 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
    // Message.error(error.message)
  }
  return Promise.reject(error)
}

api.interceptors.response.use(
  (response) => {
    /**
     * 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
     * 约定的数据格式:{ status: 1 | 0, error: string, data: object }
     * status 只有两种状态,1 表示请求成功,0 表示接口需要登录或者登录状态失效,需要重新登录
     * error 只有在请求出错时才会有值,表示错误信息
     * data 只有在请求成功时才会有值,表示请求返回的数据
     */
    if (typeof response.data === 'object') {
      if (response.data.status === 1) {
        if (response.data.error) {
          // 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
          // Message.error(response.data.error)
          return Promise.reject(response.data)
        }
      }
      else {
        useUserStore().logout()
      }
      return Promise.resolve(response.data)
    }
    else {
      return Promise.reject(response.data)
    }
  },
  async (error) => {
    // 获取请求配置
    const config = error.config
    // 如果配置不存在或未启用重试,则直接处理错误
    if (!config || !config.retry) {
      return handleError(error)
    }
    // 设置重试次数
    config.retryCount = config.retryCount || 0
    // 判断是否超过重试次数
    if (config.retryCount >= MAX_RETRY_COUNT) {
      return handleError(error)
    }
    // 重试次数自增
    config.retryCount += 1
    // 延迟重试
    await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
    // 重新发起请求
    return api(config)
  },
)

export default api


================================================
FILE: apps/core/src/api/modules/user.fake.ts
================================================
import { faker } from '@faker-js/faker'
import { defineFakeRoute } from 'vite-plugin-fake-server/client'

export default defineFakeRoute([
  {
    url: '/fake/user/login',
    method: 'post',
    response: ({ body }) => {
      return {
        error: '',
        status: 1,
        data: {
          account: body.account,
          token: `${body.account}_${faker.internet.jwt()}`,
        },
      }
    },
  },
])


================================================
FILE: apps/core/src/api/modules/user.ts
================================================
import api from '../index'

export default {
  // 登录
  login: (data: {
    account: string
    password: string
  }) => api.post('user/login', data, {
    fake: true,
  }),
}


================================================
FILE: apps/core/src/assets/icons/.gitkeep
================================================


================================================
FILE: apps/core/src/assets/images/.gitkeep
================================================


================================================
FILE: apps/core/src/assets/styles/globals.css
================================================
/* 全局样式 */


================================================
FILE: apps/core/src/assets/styles/resources/utils.scss
================================================
// @mixin 通过 @include 调用使用
// % 通过 @extend 调用使用

// 文字超出隐藏,默认为单行超出隐藏,可设置多行
@mixin text-overflow($line: 1, $fixed-width: true) {
  @if $line == 1 and $fixed-width == true {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    display: box;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: $line;
  }
}

// 定位居中,默认水平居中,可选择垂直居中,或者水平垂直都居中
@mixin position-center($type: x) {
  position: absolute;

  @if $type == x {
    left: 50%;
    transform: translateX(-50%);
  }

  @if $type == y {
    top: 50%;
    transform: translateY(-50%);
  }

  @if $type == xy {
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
  }
}

// 文字两端对齐
%justify-align {
  text-align: justify;
  text-align-last: justify;
}

// 清除浮动
%clearfix {
  zoom: 1;

  &::before,
  &::after {
    clear: both;
    display: block;
    content: "";
  }
}


================================================
FILE: apps/core/src/assets/styles/resources/variables.scss
================================================
// 全局变量


================================================
FILE: apps/core/src/components/.gitkeep
================================================


================================================
FILE: apps/core/src/composables/useGlobalProperties.ts
================================================
import type { ComponentInternalInstance } from 'vue'

export default function useGlobalProperties() {
  const { appContext } = getCurrentInstance() as ComponentInternalInstance
  return appContext.config.globalProperties
}


================================================
FILE: apps/core/src/layouts/index.vue
================================================
<template>
  <RouterView />
</template>


================================================
FILE: apps/core/src/main.ts
================================================
import App from './App.vue'
import router from './router'
import pinia from './store'

import 'virtual:uno.css'

// 全局样式
import '@/assets/styles/globals.css'

const app = createApp(App)

app.use(pinia)
app.use(router)

app.mount('#app')


================================================
FILE: apps/core/src/router/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router'
import NProgress from 'nprogress'
// import { setupLayouts } from 'virtual:generated-layouts'
// import generatedRoutes from 'virtual:generated-pages'
import { createRouter, createWebHashHistory } from 'vue-router'
import 'nprogress/nprogress.css'

let routes: RouteRecordRaw[] = []

const routesContext: any = import.meta.glob('./modules/*.ts', { eager: true })
Object.keys(routesContext).forEach((v) => {
  routes.push(routesContext[v].default)
})
routes.push({
  path: '/:pathMatch(.*)*',
  component: () => import('@/views/[...all].vue'),
  meta: {
    title: '找不到页面',
  },
})
routes = routes.flat()

// generatedRoutes.forEach((v) => {
//   routes.push(v?.meta?.layout !== false ? setupLayouts([v])[0] : v)
// })

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

router.beforeEach((to, _from) => {
  const userStore = useUserStore()
  NProgress.start()
  if (to.meta.requireLogin) {
    if (!userStore.isLogin) {
      return {
        name: 'login',
        query: {
          redirect: to.fullPath,
        },
      }
    }
  }
})

router.afterEach((to) => {
  NProgress.done()
  useSettingsStore().setTitle(to.meta.title ?? '')
})

export default router


================================================
FILE: apps/core/src/router/modules/root.ts
================================================
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: () => import('@/views/index.vue'),
  },
  {
    path: '/login',
    component: () => import('@/views/login.vue'),
    meta: {
      title: '登录',
    },
  },
]

export default routes


================================================
FILE: apps/core/src/store/index.ts
================================================
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia


================================================
FILE: apps/core/src/store/modules/settings.ts
================================================
export const useSettingsStore = defineStore(
  // 唯一ID
  'settings',
  () => {
    const title = ref('')

    // 设置网页标题
    function setTitle(val: string) {
      title.value = val
    }

    return {
      title,
      setTitle,
    }
  },
)


================================================
FILE: apps/core/src/store/modules/user.ts
================================================
import apiUser from '@/api/modules/user'
import router from '@/router'

export const useUserStore = defineStore(
  // 唯一ID
  'user',
  () => {
    const token = ref(localStorage.token ?? '')

    const isLogin = computed(() => {
      if (token.value) {
        return true
      }
      return false
    })

    function login(data: {
      account: string
      password: string
    }) {
      return new Promise((resolve, reject) => {
        apiUser.login(data).then((res) => {
          localStorage.setItem('token', res.data.token)
          token.value = res.data.token
          resolve(res)
        }).catch((error) => {
          reject(error)
        })
      })
    }
    function logout(redirect = router.currentRoute.value.fullPath) {
      // 模拟退出登录,清除 token 信息
      localStorage.removeItem('token')
      token.value = ''
      router.push({
        name: 'login',
        query: {
          ...(router.currentRoute.value.name !== 'login' && { redirect }),
        },
      })
    }

    return {
      token,
      isLogin,
      login,
      logout,
    }
  },
)


================================================
FILE: apps/core/src/types/auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
  const EffectScope: typeof import('vue').EffectScope
  const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
  const computed: typeof import('vue').computed
  const createApp: typeof import('vue').createApp
  const createPinia: typeof import('pinia').createPinia
  const customRef: typeof import('vue').customRef
  const defineAsyncComponent: typeof import('vue').defineAsyncComponent
  const defineComponent: typeof import('vue').defineComponent
  const defineStore: typeof import('pinia').defineStore
  const effectScope: typeof import('vue').effectScope
  const getActivePinia: typeof import('pinia').getActivePinia
  const getCurrentInstance: typeof import('vue').getCurrentInstance
  const getCurrentScope: typeof import('vue').getCurrentScope
  const getCurrentWatcher: typeof import('vue').getCurrentWatcher
  const h: typeof import('vue').h
  const inject: typeof import('vue').inject
  const isProxy: typeof import('vue').isProxy
  const isReactive: typeof import('vue').isReactive
  const isReadonly: typeof import('vue').isReadonly
  const isRef: typeof import('vue').isRef
  const isShallow: typeof import('vue').isShallow
  const mapActions: typeof import('pinia').mapActions
  const mapGetters: typeof import('pinia').mapGetters
  const mapState: typeof import('pinia').mapState
  const mapStores: typeof import('pinia').mapStores
  const mapWritableState: typeof import('pinia').mapWritableState
  const markRaw: typeof import('vue').markRaw
  const nextTick: typeof import('vue').nextTick
  const onActivated: typeof import('vue').onActivated
  const onBeforeMount: typeof import('vue').onBeforeMount
  const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
  const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
  const onBeforeUnmount: typeof import('vue').onBeforeUnmount
  const onBeforeUpdate: typeof import('vue').onBeforeUpdate
  const onDeactivated: typeof import('vue').onDeactivated
  const onErrorCaptured: typeof import('vue').onErrorCaptured
  const onMounted: typeof import('vue').onMounted
  const onRenderTracked: typeof import('vue').onRenderTracked
  const onRenderTriggered: typeof import('vue').onRenderTriggered
  const onScopeDispose: typeof import('vue').onScopeDispose
  const onServerPrefetch: typeof import('vue').onServerPrefetch
  const onUnmounted: typeof import('vue').onUnmounted
  const onUpdated: typeof import('vue').onUpdated
  const onWatcherCleanup: typeof import('vue').onWatcherCleanup
  const provide: typeof import('vue').provide
  const reactive: typeof import('vue').reactive
  const readonly: typeof import('vue').readonly
  const ref: typeof import('vue').ref
  const resolveComponent: typeof import('vue').resolveComponent
  const setActivePinia: typeof import('pinia').setActivePinia
  const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
  const shallowReactive: typeof import('vue').shallowReactive
  const shallowReadonly: typeof import('vue').shallowReadonly
  const shallowRef: typeof import('vue').shallowRef
  const storeToRefs: typeof import('pinia').storeToRefs
  const toRaw: typeof import('vue').toRaw
  const toRef: typeof import('vue').toRef
  const toRefs: typeof import('vue').toRefs
  const toValue: typeof import('vue').toValue
  const triggerRef: typeof import('vue').triggerRef
  const unref: typeof import('vue').unref
  const useAttrs: typeof import('vue').useAttrs
  const useCssModule: typeof import('vue').useCssModule
  const useCssVars: typeof import('vue').useCssVars
  const useGlobalProperties: typeof import('../composables/useGlobalProperties').default
  const useId: typeof import('vue').useId
  const useLink: typeof import('vue-router').useLink
  const useModel: typeof import('vue').useModel
  const useRoute: typeof import('vue-router').useRoute
  const useRouter: typeof import('vue-router').useRouter
  const useSettingsStore: typeof import('../store/modules/settings').useSettingsStore
  const useSlots: typeof import('vue').useSlots
  const useTemplateRef: typeof import('vue').useTemplateRef
  const useUserStore: typeof import('../store/modules/user').useUserStore
  const watch: typeof import('vue').watch
  const watchEffect: typeof import('vue').watchEffect
  const watchPostEffect: typeof import('vue').watchPostEffect
  const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
  // @ts-ignore
  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
  import('vue')
}


================================================
FILE: apps/core/src/types/components.d.ts
================================================
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import { GlobalComponents } from 'vue'

export {}

/* prettier-ignore */
declare module 'vue' {
  export interface GlobalComponents {
    FsIcon: typeof import('@fantastic-startkit/components')['FsIcon']
  }
}

// For TSX support
declare global {
  const FsIcon: typeof import('@fantastic-startkit/components')['FsIcon']
}

================================================
FILE: apps/core/src/types/env.d.ts
================================================
/// <reference types="vite/client" />
interface ImportMetaEnv {
  // Auto generate by env-parse
  /**
   * 接口请求地址,会设置到 axios 的 baseURL 参数上
   */
  readonly VITE_APP_API_BASEURL: string
  /**
   * 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
   */
  readonly VITE_APP_DEBUG_TOOL: string
  /**
   * 页面标题
   */
  readonly VITE_APP_TITLE: string
  /**
   * 是否开启代理
   */
  readonly VITE_ENABLE_PROXY: boolean
  /**
   * 是否启用 console 工具
   */
  readonly VITE_ENABLE_TURBO_CONSOLE: boolean
  /**
   * 是否启用 Vue 开发工具
   */
  readonly VITE_ENABLE_VUE_DEVTOOLS: boolean
  /**
   * 启动编辑器,该配置用于 vite-plugin-vue-devtools 和 unplugin-turbo-console
   * 支持IDE列表 https://github.com/yyx990803/launch-editor#supported-editors
   */
  readonly VITE_LAUNCH_EDITOR: string
}


================================================
FILE: apps/core/src/types/shims.d.ts
================================================
import 'vue-router'

declare interface Window {
  // extend the window
}

declare module 'vue-router' {
  interface RouteMeta {
    title?: string
  }
}


================================================
FILE: apps/core/src/types/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: apps/core/src/utils/dayjs.ts
================================================
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'

dayjs.locale('zh-cn')

export default dayjs


================================================
FILE: apps/core/src/utils/eventBus.ts
================================================
import mitt from 'mitt'

interface MittTypes {
  [key: string | symbol]: any
}

export default mitt<MittTypes>()


================================================
FILE: apps/core/src/views/[...all].vue
================================================
<route lang="yaml">
meta:
  layout: false
  title: 找不到页面
</route>

<template>
  <div>
    我真的尽力了,但还是找不到页面
  </div>
</template>


================================================
FILE: apps/core/src/views/index.vue
================================================
<route lang="yaml">
meta:
  layout: false
</route>

<template>
  <div class="bg-slate-50 flex flex-col min-h-screen items-center justify-center relative overflow-hidden">
    <div class="dots-bg" />

    <div class="text-center relative z-10">
      <div class="text-xs text-slate-500 font-medium mb-6 px-4 py-1.5 border border-slate-200 rounded-full bg-white inline-flex gap-2 shadow-sm items-center">
        <span class="rounded-full bg-emerald-500 size-1.5" />
        Vue 3 · Vite · TypeScript
      </div>

      <h1 class="text-[56px] text-slate-900 leading-none tracking-tight font-extrabold mb-4">
        Fantastic
        <span class="text-blue-600">Startkit</span>
      </h1>

      <p class="text-[17px] text-slate-500 mb-10">
        开箱即用的企业级前端开发脚手架
      </p>

      <div class="flex gap-3 items-center justify-center">
        <a
          rel="noreferrer"
          href="https://hurui.me/fantastic-startkit/"
          target="_blank"
          class="text-[14px] text-slate-600 font-semibold px-6 py-2.5 border border-slate-200 rounded-xl bg-white no-underline inline-flex gap-1.5 cursor-pointer shadow-sm transition-all items-center hover:bg-slate-50 hover:shadow-md hover:-translate-y-0.5"
        >
          文档
        </a>
      </div>
    </div>

    <div class="text-xs text-slate-400 flex gap-6 bottom-8 left-0 right-0 justify-center absolute">
      <span>Vue 3</span>
      <span class="text-slate-300">·</span>
      <span>Vite</span>
      <span class="text-slate-300">·</span>
      <span>UnoCSS</span>
      <span class="text-slate-300">·</span>
      <span>Pinia</span>
      <span class="text-slate-300">·</span>
      <span>Vue Router</span>
    </div>
  </div>
</template>

<style scoped>
.dots-bg {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, #cbd5e1 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.5;
}
</style>


================================================
FILE: apps/core/src/views/login.vue
================================================
<route lang="yaml">
meta:
  layout: false
  title: 登录
</route>

<script setup lang="ts">
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()

function login() {
  userStore.login({
    account: 'admin',
    password: '123456',
  }).then(() => {
    if (route.query.redirect) {
      router.replace({
        path: route.query.redirect as string,
      })
    }
    else {
      if (window.history.length <= 1) {
        router.push({ path: '/' })
      }
      else {
        router.go(-1)
      }
    }
  })
}
</script>

<template>
  <div class="bg-slate-50 flex min-h-screen items-center justify-center relative overflow-hidden">
    <div class="dots-bg" />

    <div class="px-4 max-w-[360px] w-full relative z-10">
      <div class="p-8 border border-slate-200 rounded-2xl bg-white shadow-sm">
        <div class="mb-8 text-center">
          <div class="text-lg text-white font-bold mx-auto mb-4 rounded-xl bg-blue-600 flex size-12 shadow-md items-center justify-center">
            F
          </div>
          <h1 class="text-[20px] text-slate-900 font-bold">
            欢迎回来
          </h1>
          <p class="text-sm text-slate-400 mt-1">
            Fantastic Startkit 演示登录
          </p>
        </div>

        <div class="mb-5 space-y-3">
          <div class="px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
            <div class="text-[11px] text-slate-400 font-medium mb-0.5">
              账号
            </div>
            <div class="text-sm text-slate-700">
              admin
            </div>
          </div>
          <div class="px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
            <div class="text-[11px] text-slate-400 font-medium mb-0.5">
              密码
            </div>
            <div class="text-sm text-slate-700 tracking-widest">
              ••••••
            </div>
          </div>
        </div>

        <button
          class="text-sm text-white font-semibold py-2.5 border-0 rounded-xl bg-blue-600 w-full cursor-pointer shadow-sm transition-all hover:bg-blue-700 hover:shadow-md active:scale-[0.98]"
          @click="login"
        >
          模拟登录
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.dots-bg {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, #cbd5e1 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.5;
}
</style>


================================================
FILE: apps/core/tsconfig.app.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "paths": {
      "@/*": ["./src/*"],
      "#/*": ["./src/types/*"]
    },
    "resolveJsonModule": true,
    "types": [
      "vite/client",
      "vite-plugin-pages/client",
      "vite-plugin-vue-layouts-next/client",
      "@spiriit/vite-plugin-svg-spritemap/client"
    ],
    "allowJs": false,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "sourceMap": true,
    "esModuleInterop": true
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ]
}


================================================
FILE: apps/core/tsconfig.json
================================================
{
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "files": []
}


================================================
FILE: apps/core/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "moduleDetection": "force",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": [
    "package.json",
    "vite.config.ts",
    "vite/**/*.ts"
  ]
}


================================================
FILE: apps/core/vite/plugins.ts
================================================
import type { PluginOption } from 'vite'
import process from 'node:process'
import { ComponentsResolver as FantasticStartkitComponentsResolver, ComponentsType as FantasticStartkitComponentsType } from '@fantastic-startkit/components/resolver'
import { createCopyrightPlugins as createFantasticStartkitCopyrightPlugins } from '@fantastic-startkit/copyright'
import VitePluginSvgSpritemap from '@spiriit/vite-plugin-svg-spritemap'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Unocss from 'unocss/vite'
import autoImport from 'unplugin-auto-import/vite'
import TurboConsole from 'unplugin-turbo-console/vite'
import components from 'unplugin-vue-components/vite'
import { loadEnv } from 'vite'
import Archiver from 'vite-plugin-archiver'
import { compression } from 'vite-plugin-compression2'
import { envParse, parseLoadedEnv } from 'vite-plugin-env-parse'
import { vitePluginFakeServer } from 'vite-plugin-fake-server'
import Pages from 'vite-plugin-pages'
import VueDevTools from 'vite-plugin-vue-devtools'
import Layouts from 'vite-plugin-vue-layouts-next'

export default function createVitePlugins(mode: string, isBuild = false) {
  const viteEnv = parseLoadedEnv(loadEnv(mode, process.cwd()))
  const vitePlugins: (PluginOption | PluginOption[])[] = [
    vue(),
    vueJsx(),

    // https://github.com/vuejs/devtools
    viteEnv.VITE_ENABLE_VUE_DEVTOOLS && VueDevTools({
      launchEditor: viteEnv.VITE_LAUNCH_EDITOR,
    }),

    envParse({
      dtsPath: 'src/types/env.d.ts',
    }),

    // https://github.com/unplugin/unplugin-auto-import
    autoImport({
      imports: [
        'vue',
        'vue-router',
        'pinia',
      ],
      dts: './src/types/auto-imports.d.ts',
      dirs: [
        './src/store/modules',
        './src/composables',
      ],
    }),

    // https://github.com/unplugin/unplugin-vue-components
    components({
      globs: [
        'src/components/*/index.vue',
      ],
      dts: './src/types/components.d.ts',
      resolvers: [
        FantasticStartkitComponentsResolver(),
      ],
      types: [
        FantasticStartkitComponentsType,
      ],
    }),

    Unocss(),

    // https://github.com/SpiriitLabs/vite-plugin-svg-spritemap
    VitePluginSvgSpritemap('./src/assets/icons/*.svg'),

    // https://github.com/condorheroblog/vite-plugin-fake-server
    vitePluginFakeServer({
      logger: !isBuild,
      include: 'src/api/modules',
      exclude: 'src/api/modules/**/!(*.fake).{ts,js,mjs,cjs,cts,mts}',
      enableProd: isBuild && viteEnv.VITE_BUILD_FAKE,
    }),

    // https://github.com/loicduong/vite-plugin-vue-layouts-next
    Layouts(),

    // https://github.com/hannoeru/vite-plugin-pages
    Pages({
      dirs: 'src/views',
      exclude: [
        '**/components/**/*.vue',
      ],
    }),

    // https://github.com/nonzzz/vite-plugin-compression
    viteEnv.VITE_BUILD_COMPRESS && compression({
      exclude: [/\.(br)$/, /\.(gz)$/],
      algorithms: viteEnv.VITE_BUILD_COMPRESS.split(',').map((item: string) => ({
        gzip: 'gzip',
        brotli: 'brotliCompress',
      }[item])),
    }),

    viteEnv.VITE_BUILD_ARCHIVE && Archiver({
      archiveType: viteEnv.VITE_BUILD_ARCHIVE,
    }),

    // https://github.com/unplugin/unplugin-turbo-console
    viteEnv.VITE_ENABLE_TURBO_CONSOLE && TurboConsole({
      launchEditor: {
        specifiedEditor: viteEnv.VITE_LAUNCH_EDITOR,
      },
    }),

    createFantasticStartkitCopyrightPlugins(),

    {
      name: 'vite-plugin-debug-plugin',
      transform: (code, id) => {
        if (/src\/main.ts$/.test(id)) {
          if (viteEnv.VITE_APP_DEBUG_TOOL === 'eruda') {
            code = `
${code}
import eruda from 'eruda'
eruda.init()
            `
          }
          else if (viteEnv.VITE_APP_DEBUG_TOOL === 'vconsole') {
            code = `
${code}
import VConsole from 'vconsole'
new VConsole()
            `
          }
          return {
            code,
            map: null,
          }
        }
      },
    },
  ]
  return vitePlugins
}


================================================
FILE: apps/core/vite.config.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { defineConfig, loadEnv } from 'vite'
import { parseLoadedEnv } from 'vite-plugin-env-parse'
import createVitePlugins from './vite/plugins'

// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
  const env = parseLoadedEnv(loadEnv(mode, process.cwd()))
  // 全局 scss 资源
  const scssResources: string[] = []
  fs.readdirSync('src/assets/styles/resources').forEach((dirname) => {
    if (fs.statSync(`src/assets/styles/resources/${dirname}`).isFile()) {
      scssResources.push(`@use "/src/assets/styles/resources/${dirname}" as *;`)
    }
  })
  return {
    // 开发服务器选项 https://cn.vitejs.dev/config/server-options
    server: {
      open: true,
      host: true,
      proxy: {
        '/proxy': {
          target: env.VITE_APP_API_BASEURL,
          changeOrigin: command === 'serve' && env.VITE_ENABLE_PROXY,
          rewrite: path => path.replace(/\/proxy/, ''),
        },
      },
    },
    // 构建选项 https://cn.vitejs.dev/config/build-options
    build: {
      outDir: mode === 'production' ? 'dist' : `dist-${mode}`,
      sourcemap: env.VITE_BUILD_SOURCEMAP,
    },
    plugins: createVitePlugins(mode, command === 'build'),
    optimizeDeps: {
      exclude: [
        '@fantastic-startkit/components',
      ],
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '#': path.resolve(__dirname, 'src/types'),
      },
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: scssResources.join(''),
        },
      },
    },
  }
})


================================================
FILE: apps/example/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: apps/example/package.json
================================================
{
  "name": "@fantastic-startkit/example",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build:test": "vue-tsc -b && vite build --mode test",
    "build": "vue-tsc -b && vite build",
    "serve:test": "http-server ./dist-test -o",
    "serve": "http-server ./dist -o",
    "svgo": "svgo -f src/assets/icons",
    "lint": "vue-tsc -b"
  },
  "dependencies": {
    "@fantastic-startkit/components": "workspace:*",
    "axios": "catalog:",
    "dayjs": "catalog:",
    "eruda": "catalog:",
    "mitt": "catalog:",
    "nprogress": "catalog:",
    "pinia": "catalog:",
    "pinia-plugin-persistedstate": "catalog:",
    "qs": "catalog:",
    "vconsole": "catalog:",
    "vue": "catalog:",
    "vue-router": "catalog:"
  },
  "devDependencies": {
    "@faker-js/faker": "catalog:",
    "@fantastic-startkit/copyright": "workspace:*",
    "@iconify/json": "catalog:",
    "@iconify/vue": "catalog:",
    "@spiriit/vite-plugin-svg-spritemap": "catalog:",
    "@types/nprogress": "catalog:",
    "@types/qs": "catalog:",
    "@vitejs/plugin-vue": "catalog:",
    "@vitejs/plugin-vue-jsx": "catalog:",
    "@vue/tsconfig": "catalog:",
    "autoprefixer": "catalog:",
    "http-server": "catalog:",
    "postcss": "catalog:",
    "postcss-nested": "catalog:",
    "sass-embedded": "catalog:",
    "svgo": "catalog:",
    "unplugin-auto-import": "catalog:",
    "unplugin-turbo-console": "catalog:",
    "unplugin-vue-components": "catalog:",
    "vite": "catalog:",
    "vite-plugin-archiver": "catalog:",
    "vite-plugin-compression2": "catalog:",
    "vite-plugin-env-parse": "catalog:",
    "vite-plugin-fake-server": "catalog:",
    "vite-plugin-pages": "catalog:",
    "vite-plugin-vue-devtools": "catalog:",
    "vite-plugin-vue-layouts-next": "catalog:",
    "vue-tsc": "catalog:"
  }
}


================================================
FILE: apps/example/postcss.config.js
================================================
export default {
  plugins: {
    'autoprefixer': {},
    'postcss-nested': {},
  },
}


================================================
FILE: apps/example/src/App.vue
================================================
<script setup lang="ts">
const isRouterAlive = ref(true)
const settingsStore = useSettingsStore()

provide('reload', reload)
function reload() {
  isRouterAlive.value = false
  nextTick(() => (isRouterAlive.value = true))
}

watch(() => settingsStore.title, () => {
  const title = settingsStore.title
  document.title = title ? `${title} - ${import.meta.env.VITE_APP_TITLE}` : import.meta.env.VITE_APP_TITLE
}, {
  immediate: true,
})
</script>

<template>
  <RouterView v-if="isRouterAlive" />
</template>


================================================
FILE: apps/example/src/api/index.ts
================================================
import axios from 'axios'
// import qs from 'qs'

// 请求重试配置
const MAX_RETRY_COUNT = 3 // 最大重试次数
const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)

// 扩展 AxiosRequestConfig 类型
declare module 'axios' {
  export interface AxiosRequestConfig {
    retry?: boolean
    retryCount?: number
    fake?: boolean
  }
}

const api = axios.create({
  baseURL: (import.meta.env.DEV && import.meta.env.VITE_ENABLE_PROXY) ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
  timeout: 1000 * 60,
  responseType: 'json',
})

api.interceptors.request.use(
  (request) => {
    // 如果设置了 fake 属性,强制使用 fake 的 baseURL
    if (request.fake) {
      request.baseURL = '/fake/'
    }
    /**
     * 全局拦截请求发送前提交的参数
     * 以下代码为示例,在请求头里带上 token 信息
     */
    const userStore = useUserStore()
    if (userStore.isLogin && request.headers) {
      request.headers.Token = userStore.token
    }
    // 是否将 POST 请求参数进行字符串化处理
    if (request.method === 'post') {
      // request.data = qs.stringify(request.data, {
      //   arrayFormat: 'brackets',
      // })
    }
    return request
  },
)

// 处理错误信息的函数
function handleError(error: any) {
  if (error.status === 401) {
    useUserStore().logout()
  }
  else {
    // 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
    // Message.error(error.message)
  }
  return Promise.reject(error)
}

api.interceptors.response.use(
  (response) => {
    /**
     * 全局拦截请求发送后返回的数据,如果数据有报错则在这做全局的错误提示
     * 约定的数据格式:{ status: 1 | 0, error: string, data: object }
     * status 只有两种状态,1 表示请求成功,0 表示接口需要登录或者登录状态失效,需要重新登录
     * error 只有在请求出错时才会有值,表示错误信息
     * data 只有在请求成功时才会有值,表示请求返回的数据
     */
    if (typeof response.data === 'object') {
      if (response.data.status === 1) {
        if (response.data.error) {
          // 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
          // Message.error(response.data.error)
          return Promise.reject(response.data)
        }
      }
      else {
        useUserStore().logout()
      }
      return Promise.resolve(response.data)
    }
    else {
      return Promise.reject(response.data)
    }
  },
  async (error) => {
    // 获取请求配置
    const config = error.config
    // 如果配置不存在或未启用重试,则直接处理错误
    if (!config || !config.retry) {
      return handleError(error)
    }
    // 设置重试次数
    config.retryCount = config.retryCount || 0
    // 判断是否超过重试次数
    if (config.retryCount >= MAX_RETRY_COUNT) {
      return handleError(error)
    }
    // 重试次数自增
    config.retryCount += 1
    // 延迟重试
    await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
    // 重新发起请求
    return api(config)
  },
)

export default api


================================================
FILE: apps/example/src/api/modules/news.fake.ts
================================================
import { faker } from '@faker-js/faker'
import { defineFakeRoute } from 'vite-plugin-fake-server/client'

const list: any[] = []
for (let i = 0; i < 10; i++) {
  list.push({
    id: i + 1,
    title: faker.lorem.sentence(),
  })
}

export default defineFakeRoute([
  {
    url: '/fake/news/list',
    method: 'get',
    response: () => {
      return {
        error: '',
        status: 1,
        data: {
          list,
        },
      }
    },
  },
])


================================================
FILE: apps/example/src/api/modules/news.ts
================================================
import api from '../index'

export default {
  list: () => api.get('news/list', {
    fake: true,
  }),
}


================================================
FILE: apps/example/src/api/modules/user.fake.ts
================================================
import { faker } from '@faker-js/faker'
import { defineFakeRoute } from 'vite-plugin-fake-server/client'

export default defineFakeRoute([
  {
    url: '/fake/user/login',
    method: 'post',
    response: ({ body }) => {
      return {
        error: '',
        status: 1,
        data: {
          account: body.account,
          token: `${body.account}_${faker.internet.jwt()}`,
        },
      }
    },
  },
])


================================================
FILE: apps/example/src/api/modules/user.ts
================================================
import api from '../index'

export default {
  // 登录
  login: (data: {
    account: string
    password: string
  }) => api.post('user/login', data, {
    fake: true,
  }),
}


================================================
FILE: apps/example/src/assets/styles/globals.css
================================================
/* 全局样式 */
html,
body {
  padding: 0;
  margin: 0;
  font-family: Manrope, system-ui, sans-serif;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(10px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}


================================================
FILE: apps/example/src/assets/styles/resources/utils.scss
================================================
// @mixin 通过 @include 调用使用
// % 通过 @extend 调用使用

// 文字超出隐藏,默认为单行超出隐藏,可设置多行
@mixin text-overflow($line: 1, $fixed-width: true) {
  @if $line == 1 and $fixed-width == true {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    display: box;
    overflow: hidden;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: $line;
  }
}

// 定位居中,默认水平居中,可选择垂直居中,或者水平垂直都居中
@mixin position-center($type: x) {
  position: absolute;

  @if $type == x {
    left: 50%;
    transform: translateX(-50%);
  }

  @if $type == y {
    top: 50%;
    transform: translateY(-50%);
  }

  @if $type == xy {
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
  }
}

// 文字两端对齐
%justify-align {
  text-align: justify;
  text-align-last: justify;
}

// 清除浮动
%clearfix {
  zoom: 1;

  &::before,
  &::after {
    clear: both;
    display: block;
    content: "";
  }
}


================================================
FILE: apps/example/src/assets/styles/resources/variables.scss
================================================
// 全局变量


================================================
FILE: apps/example/src/components/DemoButton/index.vue
================================================
<script setup lang="ts">
defineOptions({
  name: 'DemoButton',
})

defineProps<{
  label?: string
}>()

const emit = defineEmits<{
  click: []
}>()
</script>

<template>
  <button
    class="text-[13px] text-slate-900 tracking-wider font-bold px-5 py-2.5 border-2 border-slate-900 rounded-none bg-amber-400 inline-flex gap-2 cursor-pointer select-none uppercase shadow-[4px_4px_0_#0f172a] transition-all duration-150 items-center active:shadow-none hover:shadow-none active:translate-x-[4px] active:translate-y-[4px] hover:translate-x-[4px] hover:translate-y-[4px]"
    @click="emit('click')"
  >
    {{ label ?? '按钮' }}
  </button>
</template>


================================================
FILE: apps/example/src/composables/useGlobalProperties.ts
================================================
import type { ComponentInternalInstance } from 'vue'

export default function useGlobalProperties() {
  const { appContext } = getCurrentInstance() as ComponentInternalInstance
  return appContext.config.globalProperties
}


================================================
FILE: apps/example/src/layouts/example.vue
================================================
<template>
  <div class="bg-slate-50 min-h-screen">
    <header class="border-b border-slate-200 bg-white/90 top-0 sticky z-50 backdrop-blur-md">
      <div class="mx-auto px-6 py-3 border-b border-slate-100 flex gap-4 max-w-[800px] items-center">
        <RouterLink to="/" class="text-[15px] text-slate-900 tracking-tight font-bold no-underline" style="view-transition-name: site-title;">
          Fantastic <span class="text-blue-600">Startkit</span>
        </RouterLink>
        <span class="bg-slate-200 h-4 w-px" />
        <span class="text-xs text-slate-400">演示示例</span>
      </div>
      <nav class="mx-auto px-6 py-2.5 flex flex-wrap gap-1 max-w-[800px] items-center">
        <RouterLink to="/example/icon">
          Icon
        </RouterLink>
        <RouterLink to="/example/component">
          组件
        </RouterLink>
        <RouterLink to="/example/axios">
          Axios
        </RouterLink>
        <RouterLink to="/example/pinia">
          Pinia
        </RouterLink>
        <RouterLink :to="{ name: 'exampleParams', params: { test: '123' } }">
          路由 Params
        </RouterLink>
        <RouterLink :to="{ path: '/example/query', query: { test: '123' } }">
          路由 Query
        </RouterLink>
        <RouterLink to="/example/reload">
          刷新页面
        </RouterLink>
        <RouterLink to="/example/permission-router">
          Router 鉴权
        </RouterLink>
        <RouterLink to="/example/permission-js">
          JS 鉴权
        </RouterLink>
      </nav>
    </header>
    <main class="mx-auto px-6 py-10 pb-20 max-w-[800px]">
      <RouterView />
    </main>
  </div>
</template>

<style scoped>
nav :deep(a) {
  padding: 4px 12px;
  font-size: 12.5px;
  font-weight: 500;
  color: #64748b;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  background: transparent;
  border-radius: 6px;
  transition: all 0.15s ease;
}

nav :deep(a:hover) {
  color: #1e40af;
  background: #eff6ff;
}

nav :deep(a.router-link-active) {
  font-weight: 600;
  color: #1d4ed8;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: 6px;
}
</style>


================================================
FILE: apps/example/src/layouts/index.vue
================================================
<template>
  <RouterView />
</template>


================================================
FILE: apps/example/src/main.ts
================================================
import App from './App.vue'
import router from './router'
import pinia from './store'

import 'virtual:uno.css'

// 全局样式
import '@/assets/styles/globals.css'

const app = createApp(App)

app.use(pinia)
app.use(router)

app.mount('#app')


================================================
FILE: apps/example/src/router/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router'
import NProgress from 'nprogress'
// import { setupLayouts } from 'virtual:generated-layouts'
// import generatedRoutes from 'virtual:generated-pages'
import { nextTick } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import 'nprogress/nprogress.css'

let routes: RouteRecordRaw[] = []

const routesContext: any = import.meta.glob('./modules/*.ts', { eager: true })
Object.keys(routesContext).forEach((v) => {
  routes.push(routesContext[v].default)
})
routes.push({
  path: '/:pathMatch(.*)*',
  component: () => import('@/views/[...all].vue'),
  meta: {
    title: '找不到页面',
  },
})
routes = routes.flat()

// generatedRoutes.forEach((v) => {
//   routes.push(v?.meta?.layout !== false ? setupLayouts([v])[0] : v)
// })

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

router.beforeEach((to) => {
  const userStore = useUserStore()
  NProgress.start()
  if (to.meta.requireLogin) {
    if (!userStore.isLogin) {
      return {
        name: 'login',
        query: {
          redirect: to.fullPath,
        },
      }
    }
  }
  if (!document.startViewTransition) {
    return
  }
  return new Promise<void>((resolve) => {
    document.startViewTransition(() => {
      resolve()
      return nextTick()
    })
  })
})

router.afterEach((to) => {
  NProgress.done()
  useSettingsStore().setTitle(to.meta.title ?? '')
})

export default router


================================================
FILE: apps/example/src/router/modules/example.ts
================================================
import type { RouteRecordRaw } from 'vue-router'

function ExampleLayout() {
  return import('@/layouts/example.vue')
}

const routes: RouteRecordRaw = {
  path: '/example',
  redirect: '/example/icon',
  component: ExampleLayout,
  children: [
    {
      path: 'icon',
      component: () => import('@/views/example/icon.vue'),
      meta: {
        title: 'Icon',
      },
    },
    {
      path: 'component',
      component: () => import('@/views/example/component.vue'),
    },
    {
      path: 'axios',
      component: () => import('@/views/example/axios.vue'),
    },
    {
      path: 'pinia',
      component: () => import('@/views/example/pinia.vue'),
    },
    {
      path: 'params/:test',
      name: 'exampleParams', // 设置路由的name时,建议加上模块名,避免name和其他模块重名
      component: () => import('@/views/example/params/[test].vue'),
    },
    {
      path: 'query',
      component: () => import('@/views/example/query.vue'),
    },
    {
      path: 'reload',
      component: () => import('@/views/example/reload.vue'),
    },
    {
      path: 'permission-router',
      component: () => import('@/views/example/permission-router.vue'),
      meta: {
        requireLogin: true, // 鉴权
      },
    },
    {
      path: 'permission-js',
      component: () => import('@/views/example/permission-js.vue'),
    },
  ],
}

export default routes


================================================
FILE: apps/example/src/router/modules/root.ts
================================================
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: () => import('@/views/index.vue'),
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login.vue'),
    meta: {
      title: '登录',
    },
  },
]

export default routes


================================================
FILE: apps/example/src/store/index.ts
================================================
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia


================================================
FILE: apps/example/src/store/modules/example.ts
================================================
import apiNews from '@/api/modules/news'

export const useExampleStore = defineStore(
  // 唯一ID
  'example',
  () => {
    const news = ref<{
      title: string
    }[]>([])

    const newsCount = computed(() => {
      return news.value.length
    })

    function getNews() {
      apiNews.list().then((res) => {
        news.value = res.data.list
      })
    }
    function removeLast() {
      news.value.splice(news.value.length - 1, 1)
    }

    return {
      news,
      newsCount,
      getNews,
      removeLast,
    }
  },
)


================================================
FILE: apps/example/src/store/modules/settings.ts
================================================
export const useSettingsStore = defineStore(
  // 唯一ID
  'settings',
  () => {
    const title = ref('')

    // 设置网页标题
    function setTitle(val: string) {
      title.value = val
    }

    return {
      title,
      setTitle,
    }
  },
)


================================================
FILE: apps/example/src/store/modules/user.ts
================================================
import apiUser from '@/api/modules/user'
import router from '@/router'

export const useUserStore = defineStore(
  // 唯一ID
  'user',
  () => {
    const token = ref(localStorage.token ?? '')

    const isLogin = computed(() => {
      if (token.value) {
        return true
      }
      return false
    })

    function login(data: {
      account: string
      password: string
    }) {
      return new Promise((resolve, reject) => {
        apiUser.login(data).then((res) => {
          localStorage.setItem('token', res.data.token)
          token.value = res.data.token
          resolve(res)
        }).catch((error) => {
          reject(error)
        })
      })
    }
    function logout(redirect = router.currentRoute.value.fullPath) {
      // 模拟退出登录,清除 token 信息
      localStorage.removeItem('token')
      token.value = ''
      router.push({
        name: 'login',
        query: {
          ...(router.currentRoute.value.name !== 'login' && { redirect }),
        },
      })
    }

    return {
      token,
      isLogin,
      login,
      logout,
    }
  },
)


================================================
FILE: apps/example/src/types/auto-imports.d.ts
================================================
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
  const EffectScope: typeof import('vue').EffectScope
  const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
  const computed: typeof import('vue').computed
  const createApp: typeof import('vue').createApp
  const createPinia: typeof import('pinia').createPinia
  const customRef: typeof import('vue').customRef
  const defineAsyncComponent: typeof import('vue').defineAsyncComponent
  const defineComponent: typeof import('vue').defineComponent
  const defineStore: typeof import('pinia').defineStore
  const effectScope: typeof import('vue').effectScope
  const getActivePinia: typeof import('pinia').getActivePinia
  const getCurrentInstance: typeof import('vue').getCurrentInstance
  const getCurrentScope: typeof import('vue').getCurrentScope
  const getCurrentWatcher: typeof import('vue').getCurrentWatcher
  const h: typeof import('vue').h
  const inject: typeof import('vue').inject
  const isProxy: typeof import('vue').isProxy
  const isReactive: typeof import('vue').isReactive
  const isReadonly: typeof import('vue').isReadonly
  const isRef: typeof import('vue').isRef
  const isShallow: typeof import('vue').isShallow
  const mapActions: typeof import('pinia').mapActions
  const mapGetters: typeof import('pinia').mapGetters
  const mapState: typeof import('pinia').mapState
  const mapStores: typeof import('pinia').mapStores
  const mapWritableState: typeof import('pinia').mapWritableState
  const markRaw: typeof import('vue').markRaw
  const nextTick: typeof import('vue').nextTick
  const onActivated: typeof import('vue').onActivated
  const onBeforeMount: typeof import('vue').onBeforeMount
  const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
  const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
  const onBeforeUnmount: typeof import('vue').onBeforeUnmount
  const onBeforeUpdate: typeof import('vue').onBeforeUpdate
  const onDeactivated: typeof import('vue').onDeactivated
  const onErrorCaptured: typeof import('vue').onErrorCaptured
  const onMounted: typeof import('vue').onMounted
  const onRenderTracked: typeof import('vue').onRenderTracked
  const onRenderTriggered: typeof import('vue').onRenderTriggered
  const onScopeDispose: typeof import('vue').onScopeDispose
  const onServerPrefetch: typeof import('vue').onServerPrefetch
  const onUnmounted: typeof import('vue').onUnmounted
  const onUpdated: typeof import('vue').onUpdated
  const onWatcherCleanup: typeof import('vue').onWatcherCleanup
  const provide: typeof import('vue').provide
  const reactive: typeof import('vue').reactive
  const readonly: typeof import('vue').readonly
  const ref: typeof import('vue').ref
  const resolveComponent: typeof import('vue').resolveComponent
  const setActivePinia: typeof import('pinia').setActivePinia
  const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
  const shallowReactive: typeof import('vue').shallowReactive
  const shallowReadonly: typeof import('vue').shallowReadonly
  const shallowRef: typeof import('vue').shallowRef
  const storeToRefs: typeof import('pinia').storeToRefs
  const toRaw: typeof import('vue').toRaw
  const toRef: typeof import('vue').toRef
  const toRefs: typeof import('vue').toRefs
  const toValue: typeof import('vue').toValue
  const triggerRef: typeof import('vue').triggerRef
  const unref: typeof import('vue').unref
  const useAttrs: typeof import('vue').useAttrs
  const useCssModule: typeof import('vue').useCssModule
  const useCssVars: typeof import('vue').useCssVars
  const useExampleStore: typeof import('../store/modules/example').useExampleStore
  const useGlobalProperties: typeof import('../composables/useGlobalProperties').default
  const useId: typeof import('vue').useId
  const useLink: typeof import('vue-router').useLink
  const useModel: typeof import('vue').useModel
  const useRoute: typeof import('vue-router').useRoute
  const useRouter: typeof import('vue-router').useRouter
  const useSettingsStore: typeof import('../store/modules/settings').useSettingsStore
  const useSlots: typeof import('vue').useSlots
  const useTemplateRef: typeof import('vue').useTemplateRef
  const useUserStore: typeof import('../store/modules/user').useUserStore
  const watch: typeof import('vue').watch
  const watchEffect: typeof import('vue').watchEffect
  const watchPostEffect: typeof import('vue').watchPostEffect
  const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
  // @ts-ignore
  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
  import('vue')
}


================================================
FILE: apps/example/src/types/components.d.ts
================================================
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import { GlobalComponents } from 'vue'

export {}

/* prettier-ignore */
declare module 'vue' {
  export interface GlobalComponents {
    DemoButton: typeof import('./../components/DemoButton/index.vue')['default']
    FsIcon: typeof import('@fantastic-startkit/components')['FsIcon']
  }
}

// For TSX support
declare global {
  const DemoButton: typeof import('./../components/DemoButton/index.vue')['default']
  const FsIcon: typeof import('@fantastic-startkit/components')['FsIcon']
}

================================================
FILE: apps/example/src/types/env.d.ts
================================================
/// <reference types="vite/client" />
interface ImportMetaEnv {
  // Auto generate by env-parse
  /**
   * 接口请求地址,会设置到 axios 的 baseURL 参数上
   */
  readonly VITE_APP_API_BASEURL: string
  /**
   * 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
   */
  readonly VITE_APP_DEBUG_TOOL: string
  /**
   * 页面标题
   */
  readonly VITE_APP_TITLE: string
  /**
   * 是否开启代理
   */
  readonly VITE_ENABLE_PROXY: boolean
  /**
   * 是否启用 console 工具
   */
  readonly VITE_ENABLE_TURBO_CONSOLE: boolean
  /**
   * 是否启用 Vue 开发工具
   */
  readonly VITE_ENABLE_VUE_DEVTOOLS: boolean
  /**
   * 启动编辑器,该配置用于 vite-plugin-vue-devtools 和 unplugin-turbo-console
   * 支持IDE列表 https://github.com/yyx990803/launch-editor#supported-editors
   */
  readonly VITE_LAUNCH_EDITOR: string
}


================================================
FILE: apps/example/src/types/shims.d.ts
================================================
import 'vue-router'

declare interface Window {
  // extend the window
}

declare module 'vue-router' {
  interface RouteMeta {
    title?: string
  }
}


================================================
FILE: apps/example/src/types/vite-env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: apps/example/src/utils/dayjs.ts
================================================
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'

dayjs.locale('zh-cn')

export default dayjs


================================================
FILE: apps/example/src/utils/eventBus.ts
================================================
import mitt from 'mitt'

interface MittTypes {
  [key: string | symbol]: any
}

export default mitt<MittTypes>()


================================================
FILE: apps/example/src/views/[...all].vue
================================================
<route lang="yaml">
meta:
  layout: false
  title: 找不到页面
</route>

<template>
  <div>
    我真的尽力了,但还是找不到页面
  </div>
</template>


================================================
FILE: apps/example/src/views/example/axios.vue
================================================
<route lang="yaml">
meta:
  layout: example
</route>

<script setup lang="ts">
import apiNews from '@/api/modules/news'

const news = ref<any[]>([])
const loading = ref(false)

async function getInfo() {
  loading.value = true
  try {
    const [res1, res2] = await Promise.all([apiNews.list(), apiNews.list()])
    news.value = [...res1.data.list, ...res2.data.list]
  }
  finally {
    loading.value = false
  }
}
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        Axios 请求
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        使用 <code class="text-xs text-blue-600 px-1.5 py-0.5 border border-blue-100 rounded bg-blue-50">Promise.all</code>
        并发请求两个接口,合并返回的 mock 数据。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <div class="mb-5">
        <button
          class="text-[13px] text-white font-semibold px-4 py-2 border-0 rounded-lg bg-blue-600 inline-flex gap-2 cursor-pointer transition-colors items-center hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
          :disabled="loading"
          @click="getInfo"
        >
          <span v-if="loading" class="spinner" />
          {{ loading ? '请求中...' : '获取 Mock 数据' }}
        </button>
      </div>

      <transition-group
        v-if="news.length"
        name="news"
        tag="ul"
        class="m-0 p-0 list-none"
      >
        <li
          v-for="(item, index) in news"
          :key="index"
          class="py-2.5 border-b border-slate-100 flex gap-3 items-baseline last:border-b-0"
        >
          <span class="text-[11px] text-slate-400 shrink-0">{{ String(index + 1).padStart(2, '0') }}</span>
          <span class="text-[13.5px] text-slate-600">{{ item.title }}</span>
        </li>
      </transition-group>

      <div v-else-if="!loading" class="text-sm text-slate-400 py-8 text-center">
        点击按钮获取数据
      </div>
    </div>
  </div>
</template>

<style scoped>
.spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid rgb(255 255 255 / 40%);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

.news-enter-active {
  transition: all 0.2s ease;
}

.news-enter-from {
  opacity: 0;
  transform: translateY(6px);
}
</style>


================================================
FILE: apps/example/src/views/example/component.vue
================================================
<route lang="yaml">
meta:
  layout: example
  title: 组件
</route>

<script setup lang="ts">
import ExampleList from './components/ExampleList/index.vue'

const index = ref(1)
const list = ref(['张三', '李四', '王五'])

function add() {
  list.value.push(`这是新添加的${index.value++}`)
}
</script>

<template>
  <div class="max-w-full space-y-6">
    <!-- 页头 -->
    <div>
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-1.5">
        组件
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        项目中的组件分为两类:需要手动引入的<strong class="text-slate-700">局部组件</strong>,以及无需引入可直接使用的<strong class="text-slate-700">全局组件</strong>。
      </p>
    </div>

    <!-- 局部组件 -->
    <div class="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
      <div class="px-5 py-3.5 border-b border-slate-100 bg-slate-50 flex gap-3 items-center">
        <span class="rounded-full bg-slate-400 size-2" />
        <span class="text-xs text-slate-500 tracking-widest font-semibold uppercase">局部组件</span>
      </div>
      <div class="p-5">
        <p class="text-sm text-slate-500 mb-4">
          存放于页面的
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">components/</code>
          子目录下,使用前需在
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">&lt;script setup&gt;</code>
          中手动
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">import</code>
          引入。
        </p>

        <!-- 交互 demo -->
        <div class="p-4 border border-slate-200 rounded-lg bg-slate-50">
          <p class="text-[11px] text-slate-400 tracking-widest font-semibold mb-3 uppercase">
            ExampleList 演示
          </p>
          <ExampleList :list="list" />
          <button
            class="text-[13px] text-slate-600 font-semibold px-4 py-2 border border-slate-200 rounded-lg bg-white inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:text-slate-900 hover:border-slate-300 hover:bg-slate-50"
            @click="add"
          >
            + 添加一项
          </button>
        </div>

        <div class="text-xs text-slate-500 mt-4 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
          <div><span class="text-slate-400">// 手动引入</span></div>
          <div>
            <span class="text-violet-500">import</span>
            <span class="text-sky-600"> ExampleList </span>
            <span class="text-violet-500">from</span>
            <span class="text-amber-600"> './components/ExampleList/index.vue'</span>
          </div>
        </div>
      </div>
    </div>

    <!-- 全局组件 -->
    <div class="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
      <div class="px-5 py-3.5 border-b border-slate-100 bg-slate-50 flex gap-3 items-center">
        <span class="rounded-full bg-blue-400 size-2" />
        <span class="text-xs text-slate-500 tracking-widest font-semibold uppercase">全局组件</span>
      </div>
      <div class="p-5 space-y-4">
        <p class="text-sm text-slate-500">
          通过
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">unplugin-vue-components</code>
          实现自动导入,无需手动
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">import</code>,
          可直接在模板中使用。全局组件有两个来源:
        </p>

        <!-- 来源一:src/components -->
        <div class="p-4 border border-slate-200 rounded-lg">
          <div class="mb-2 flex gap-2 items-center">
            <span class="text-[11px] text-slate-500 font-semibold px-2 py-0.5 rounded bg-slate-100">来源一</span>
            <code class="text-xs text-slate-600">src/components/</code>
          </div>
          <p class="text-sm text-slate-500 mb-3">
            存放于
            <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">src/components/</code>
            目录下,每个组件独占一个文件夹,<strong class="text-slate-700">文件夹名即组件名</strong>,目录内包含
            <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">index.vue</code>
            入口文件即可。以
            <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">DemoButton</code>
            为例:
          </p>
          <div class="mb-3 p-4 border border-slate-200 rounded-lg bg-slate-50">
            <p class="text-[11px] text-slate-400 tracking-widest font-semibold mb-3 uppercase">
              DemoButton 演示
            </p>
            <div class="flex flex-wrap gap-2">
              <DemoButton label="默认按钮" />
              <DemoButton label="确认" />
              <DemoButton label="取消" />
            </div>
          </div>
          <div class="text-xs text-slate-500 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
            <div class="text-slate-400">
              // 无需 import,直接使用
            </div>
            <div class="mt-1">
              <span class="text-violet-500">&lt;DemoButton</span>
              <span class="text-sky-600"> label</span>=<span class="text-amber-600">"默认按钮"</span>
              <span class="text-violet-500"> /&gt;</span>
            </div>
          </div>
        </div>

        <!-- 来源二:子包 -->
        <div class="p-4 border border-slate-200 rounded-lg">
          <div class="mb-2 flex gap-2 items-center">
            <span class="text-[11px] text-slate-500 font-semibold px-2 py-0.5 rounded bg-slate-100">来源二</span>
            <code class="text-xs text-slate-600">@fantastic-startkit/components</code>
          </div>
          <p class="text-sm text-slate-500 mb-3">
            monorepo 子包提供的公共组件,通过
            <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">FantasticComponentsResolver</code>
            自动识别并导入,组件名以
            <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">Fs</code>
            开头。以
            <RouterLink to="/example/icon" class="text-blue-500 font-medium hover:text-blue-600 hover:underline">
              FsIcon
            </RouterLink>
            为例:
          </p>
          <div class="text-xs text-slate-500 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
            <span class="text-slate-400">// 无需 import,直接使用</span>
            <br>
            <span class="text-violet-500">&lt;FsIcon</span>
            <span class="text-sky-600"> name</span>=<span class="text-amber-600">"logos:vue"</span>
            <span class="text-violet-500"> /&gt;</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/example/components/ExampleList/index.vue
================================================
<script setup lang="ts">
defineOptions({
  name: 'ExampleList',
})

defineProps<{
  list: string[]
}>()
</script>

<template>
  <div class="mb-4">
    <transition-group name="list" tag="ul" class="m-0 p-0 list-none">
      <li
        v-for="(item, index) in list"
        :key="index"
        class="text-[13.5px] text-slate-600 py-2.5 border-b border-slate-100 flex gap-2.5 items-center last:border-b-0"
      >
        <span class="rounded-full bg-blue-500 shrink-0 size-1.5" />
        {{ item }}
      </li>
    </transition-group>
  </div>
</template>

<style scoped>
.list-enter-active {
  transition: all 0.2s ease;
}

.list-enter-from {
  opacity: 0;
  transform: translateX(-8px);
}
</style>


================================================
FILE: apps/example/src/views/example/icon.vue
================================================
<route lang="yaml">
meta:
  layout: example
  title: 图标
</route>

<template>
  <div class="max-w-full space-y-6">
    <!-- 页头 -->
    <div>
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-1.5">
        图标
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        通过
        <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">FsIcon</code>
        组件统一渲染,支持三种图标来源,通过
        <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">name</code>
        属性自动识别类型。
      </p>
    </div>

    <!-- 本地 SVG -->
    <div class="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
      <div class="px-5 py-3.5 border-b border-slate-100 bg-slate-50 flex gap-3 items-center">
        <span class="rounded-full bg-slate-300 size-2" />
        <span class="text-xs text-slate-500 tracking-widest font-semibold uppercase">本地 SVG</span>
      </div>
      <div class="p-5">
        <p class="text-sm text-slate-500 mb-4">
          将 SVG 文件放入
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">src/assets/icons/</code>
          目录,文件名即为
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">name</code>
          的值。支持单色和彩色 SVG。
        </p>
        <div class="flex flex-wrap gap-3">
          <div
            v-for="name in ['example', 'example.color']"
            :key="name"
            class="group px-6 py-4 border border-slate-200 rounded-lg bg-slate-50 flex flex-col gap-2.5 transition-all items-center hover:border-slate-300 hover:bg-white hover:shadow-sm"
          >
            <FsIcon :name="name" class="text-[40px]" />
            <span class="text-[11px] text-slate-400 group-hover:text-slate-600">{{ name }}</span>
          </div>
        </div>
        <div class="text-xs text-slate-500 mt-4 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
          <span class="text-violet-500">&lt;FsIcon</span>
          <span class="text-sky-600"> name</span>=<span class="text-amber-600">"example"</span>
          <span class="text-violet-500"> /&gt;</span>
        </div>
      </div>
    </div>

    <!-- Iconify -->
    <div class="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
      <div class="px-5 py-3.5 border-b border-slate-100 bg-slate-50 flex gap-3 items-center">
        <span class="rounded-full bg-blue-400 size-2" />
        <span class="text-xs text-slate-500 tracking-widest font-semibold uppercase">Iconify</span>
        <a
          href="https://icon-sets.iconify.design/"
          target="_blank"
          class="text-xs text-blue-500 ml-auto hover:text-blue-600 hover:underline"
        >浏览图标库 →</a>
      </div>
      <div class="p-5">
        <p class="text-sm text-slate-500 mb-4">
          格式为
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">{图标集}:{图标名}</code>,
          渲染为内联 SVG,支持彩色。
        </p>
        <div class="flex flex-wrap gap-3">
          <div
            v-for="icon in ['logos:vue', 'logos:vitejs', 'logos:typescript-icon', 'logos:pnpm']"
            :key="icon"
            class="group px-5 py-4 border border-slate-200 rounded-lg bg-slate-50 flex flex-col gap-2.5 transition-all items-center hover:border-slate-300 hover:bg-white hover:shadow-sm"
          >
            <FsIcon :name="icon" class="size-10" />
            <span class="text-[11px] text-slate-400 group-hover:text-slate-600">{{ icon }}</span>
          </div>
        </div>
        <div class="text-xs text-slate-500 mt-4 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
          <span class="text-violet-500">&lt;FsIcon</span>
          <span class="text-sky-600"> name</span>=<span class="text-amber-600">"logos:vue"</span>
          <span class="text-violet-500"> /&gt;</span>
        </div>
      </div>
    </div>

    <!-- UnoCSS -->
    <div class="border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
      <div class="px-5 py-3.5 border-b border-slate-100 bg-slate-50 flex gap-3 items-center">
        <span class="rounded-full bg-emerald-400 size-2" />
        <span class="text-xs text-slate-500 tracking-widest font-semibold uppercase">UnoCSS</span>
      </div>
      <div class="p-5">
        <p class="text-sm text-slate-500 mb-4">
          格式为
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">i-{图标集}:{图标名}</code>,
          渲染为 CSS mask,颜色继承自
          <code class="text-xs text-slate-700 px-1.5 py-0.5 border border-slate-200 rounded bg-slate-100">currentColor</code>,适合单色场景。
        </p>
        <div class="flex flex-wrap gap-3">
          <div
            v-for="icon in ['i-logos:vue', 'i-logos:vitejs', 'i-logos:typescript-icon', 'i-logos:pnpm']"
            :key="icon"
            class="group px-5 py-4 border border-slate-200 rounded-lg bg-slate-50 flex flex-col gap-2.5 transition-all items-center hover:border-slate-300 hover:bg-white hover:shadow-sm"
          >
            <FsIcon :name="icon" class="size-10" />
            <span class="text-[11px] text-slate-400 group-hover:text-slate-600">{{ icon }}</span>
          </div>
        </div>
        <div class="text-xs text-slate-500 mt-4 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
          <span class="text-violet-500">&lt;FsIcon</span>
          <span class="text-sky-600"> name</span>=<span class="text-amber-600">"i-logos:vue"</span>
          <span class="text-violet-500"> /&gt;</span>
        </div>
      </div>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/example/params/[test].vue
================================================
<route lang="yaml">
name: exampleParams
meta:
  layout: example
</route>

<script setup lang="ts">
const route = useRoute()
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        路由 Params
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        通过
        <code class="text-xs text-blue-600 px-1.5 py-0.5 border border-blue-100 rounded bg-blue-50">route.params</code>
        读取动态路由段的参数值。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <p class="text-[11px] text-slate-400 tracking-widest font-semibold mb-2.5 uppercase">
        params.test
      </p>
      <div class="text-sm px-4 py-3 border border-slate-200 rounded-lg bg-slate-50 flex gap-2 items-center">
        <span class="text-blue-600">test</span>
        <span class="text-slate-400">=</span>
        <span class="text-emerald-600">{{ route.params.test ?? '—' }}</span>
      </div>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/example/permission-js.vue
================================================
<route lang="yaml">
meta:
  layout: example
</route>

<script setup lang="ts">
const router = useRouter()
const userStore = useUserStore()

function user() {
  if (userStore.isLogin) {
    // eslint-disable-next-line no-alert
    alert(`token信息:${userStore.token}`)
  }
  else {
    router.push({
      path: '/login',
      query: {
        redirect: router.currentRoute.value.fullPath,
      },
    })
  }
}

function remove() {
  userStore.logout()
}
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        JS 鉴权
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        在 JS 逻辑中手动检查登录状态,未登录时跳转至登录页。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <div class="mb-5 px-4 py-3 border border-slate-200 rounded-lg bg-slate-50 flex gap-2.5 items-center">
        <span
          class="rounded-full shrink-0 size-2"
          :class="userStore.isLogin ? 'bg-emerald-500 shadow-[0_0_6px_#10b981]' : 'bg-slate-300'"
        />
        <span class="text-[13.5px] text-slate-600">
          {{ userStore.isLogin ? '当前已登录' : '当前未登录' }}
        </span>
      </div>

      <div class="flex flex-wrap gap-2.5">
        <button
          class="text-[13px] text-white font-semibold px-4 py-2 border-0 rounded-lg bg-blue-600 inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:bg-blue-700"
          @click="user"
        >
          {{ userStore.isLogin ? '查看 Token' : '点击登录' }}
        </button>
        <button
          v-if="userStore.isLogin"
          class="text-[13px] text-red-600 font-semibold px-4 py-2 border border-red-100 rounded-lg bg-red-50 inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:bg-red-100"
          @click="remove"
        >
          清除登录状态
        </button>
      </div>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/example/permission-router.vue
================================================
<route lang="yaml">
meta:
  layout: example
  requireLogin: true
</route>

<script setup lang="ts">
const userStore = useUserStore()
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        Router 鉴权
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        通过路由 meta 配置
        <code class="text-xs text-blue-600 px-1.5 py-0.5 border border-blue-100 rounded bg-blue-50">requireLogin: true</code>,
        由路由守卫统一拦截未登录访问。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <p class="text-[11px] text-slate-400 tracking-widest font-semibold mb-2.5 uppercase">
        当前 Token
      </p>
      <div class="text-sm px-4 py-3 border border-slate-200 rounded-lg bg-slate-50 break-all">
        <span v-if="userStore.token" class="text-blue-600">{{ userStore.token }}</span>
        <span v-else class="text-slate-400">— 暂无 Token —</span>
      </div>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/example/pinia.vue
================================================
<route lang="yaml">
meta:
  layout: example
</route>

<script setup lang="ts">
const exampleStore = useExampleStore()

const news = computed(() => exampleStore.news)
const newsCount = computed(() => exampleStore.newsCount)

function getInfo() {
  return exampleStore.getNews()
}

function removeLast() {
  return exampleStore.removeLast()
}

function getLength() {
  // eslint-disable-next-line no-alert
  alert(newsCount.value)
}
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        Pinia 状态管理
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        跨路由共享状态,切换页面后返回数据依然保留。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <div class="mb-5 flex flex-wrap gap-2.5">
        <button
          class="text-[13px] text-white font-semibold px-4 py-2 border-0 rounded-lg bg-blue-600 inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:bg-blue-700"
          type="button"
          @click="getInfo"
        >
          获取数据
        </button>
        <button
          class="text-[13px] text-slate-600 font-semibold px-4 py-2 border border-slate-200 rounded-lg bg-transparent inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:text-slate-900 hover:bg-slate-100"
          type="button"
          @click="removeLast"
        >
          删除最后一条
        </button>
        <button
          class="text-[13px] text-slate-600 font-semibold px-4 py-2 border border-slate-200 rounded-lg bg-transparent inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:text-slate-900 hover:bg-slate-100"
          type="button"
          @click="getLength"
        >
          查看数据长度
        </button>
      </div>

      <div v-if="newsCount" class="text-[12.5px] text-emerald-700 font-medium mb-4 px-4 py-2.5 border border-emerald-100 rounded-lg bg-emerald-50">
        ✦ 切换路由后返回,数据依然存在
      </div>

      <transition-group
        v-if="news.length"
        name="news"
        tag="ul"
        class="m-0 p-0 list-none"
      >
        <li
          v-for="(item, index) in news"
          :key="index"
          class="py-2.5 border-b border-slate-100 flex gap-3 items-baseline last:border-b-0"
        >
          <span class="text-[11px] text-slate-400 shrink-0">{{ String(index + 1).padStart(2, '0') }}</span>
          <span class="text-[13.5px] text-slate-600">{{ item.title }}</span>
        </li>
      </transition-group>

      <div v-else class="text-sm text-slate-400 py-8 text-center">
        点击「获取数据」加载数据
      </div>
    </div>
  </div>
</template>

<style scoped>
.news-enter-active {
  transition: all 0.2s ease;
}

.news-enter-from {
  opacity: 0;
  transform: translateY(6px);
}
</style>


================================================
FILE: apps/example/src/views/example/query.vue
================================================
<route lang="yaml">
meta:
  layout: example
</route>

<script setup lang="ts">
const route = useRoute()
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        路由 Query
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        通过
        <code class="text-xs text-blue-600 px-1.5 py-0.5 border border-blue-100 rounded bg-blue-50">route.query</code>
        读取 URL 中的查询参数。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <p class="text-[11px] text-slate-400 tracking-widest font-semibold mb-2.5 uppercase">
        query.test
      </p>
      <div class="text-sm px-4 py-3 border border-slate-200 rounded-lg bg-slate-50 flex gap-2 items-center">
        <span class="text-blue-600">test</span>
        <span class="text-slate-400">=</span>
        <span class="text-emerald-600">{{ route.query.test ?? '—' }}</span>
      </div>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/example/reload.vue
================================================
<route lang="yaml">
meta:
  layout: example
</route>

<script setup lang="ts">
const reload = inject('reload') as any

const value = ref(0)

function plus() {
  value.value += 1
}
</script>

<template>
  <div class="max-w-full">
    <div class="mb-7">
      <h2 class="text-[22px] text-slate-900 tracking-tight font-bold mb-2">
        刷新当前页面
      </h2>
      <p class="text-sm text-slate-500 leading-relaxed">
        修改计数器后点击「刷新」,页面会重置回初始状态(组件重新挂载)。
      </p>
    </div>

    <div class="p-6 border border-slate-200 rounded-xl bg-white shadow-sm">
      <div class="mb-5">
        <p class="text-[11px] text-slate-400 tracking-widest font-semibold mb-2.5 uppercase">
          当前计数
        </p>
        <div class="flex gap-3 items-center">
          <div class="text-[20px] text-blue-600 font-semibold py-2 text-center border border-slate-200 rounded-lg bg-slate-50 w-20">
            {{ value }}
          </div>
          <button
            class="text-[13px] text-slate-600 font-semibold px-4 py-2 border border-slate-200 rounded-lg bg-transparent inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:text-slate-900 hover:bg-slate-100"
            type="button"
            @click="plus"
          >
            +1
          </button>
        </div>
      </div>

      <button
        class="text-[13px] text-white font-semibold px-4 py-2 border-0 rounded-lg bg-blue-600 inline-flex gap-1.5 cursor-pointer transition-colors items-center hover:bg-blue-700"
        type="button"
        @click="reload"
      >
        刷新页面
      </button>
    </div>
  </div>
</template>


================================================
FILE: apps/example/src/views/index.vue
================================================
<route lang="yaml">
meta:
  layout: false
</route>

<template>
  <div class="bg-slate-50 flex flex-col min-h-screen items-center justify-center relative overflow-hidden">
    <div class="dots-bg" />

    <div class="text-center relative z-10">
      <div class="text-xs text-slate-500 font-medium mb-6 px-4 py-1.5 border border-slate-200 rounded-full bg-white inline-flex gap-2 shadow-sm items-center">
        <span class="rounded-full bg-emerald-500 size-1.5" />
        Vue 3 · Vite · TypeScript
      </div>

      <h1 class="text-[56px] text-slate-900 leading-none tracking-tight font-extrabold mb-4" style="view-transition-name: site-title;">
        Fantastic
        <span class="text-blue-600">Startkit</span>
      </h1>

      <p class="text-[17px] text-slate-500 mb-10">
        开箱即用的企业级前端开发脚手架
      </p>

      <div class="flex gap-3 items-center justify-center">
        <RouterLink
          to="/example/icon"
          class="text-[14px] text-white font-semibold px-6 py-2.5 border-0 rounded-xl bg-blue-600 no-underline inline-flex gap-2 cursor-pointer shadow-md transition-all items-center hover:bg-blue-700 hover:shadow-lg hover:-translate-y-0.5"
        >
          查看演示
          <span class="text-blue-200">→</span>
        </RouterLink>
        <a
          rel="noreferrer"
          href="https://hurui.me/fantastic-startkit/"
          target="_blank"
          class="text-[14px] text-slate-600 font-semibold px-6 py-2.5 border border-slate-200 rounded-xl bg-white no-underline inline-flex gap-1.5 cursor-pointer shadow-sm transition-all items-center hover:bg-slate-50 hover:shadow-md hover:-translate-y-0.5"
        >
          文档
        </a>
      </div>
    </div>

    <div class="text-xs text-slate-400 flex gap-6 bottom-8 left-0 right-0 justify-center absolute">
      <span>Vue 3</span>
      <span class="text-slate-300">·</span>
      <span>Vite</span>
      <span class="text-slate-300">·</span>
      <span>UnoCSS</span>
      <span class="text-slate-300">·</span>
      <span>Pinia</span>
      <span class="text-slate-300">·</span>
      <span>Vue Router</span>
    </div>
  </div>
</template>

<style scoped>
.dots-bg {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, #cbd5e1 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.5;
}
</style>


================================================
FILE: apps/example/src/views/login.vue
================================================
<route lang="yaml">
meta:
  layout: false
  title: 登录
</route>

<script setup lang="ts">
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()

function login() {
  userStore.login({
    account: 'admin',
    password: '123456',
  }).then(() => {
    if (route.query.redirect) {
      router.replace({
        path: route.query.redirect as string,
      })
    }
    else {
      if (window.history.length <= 1) {
        router.push({ path: '/' })
      }
      else {
        router.go(-1)
      }
    }
  })
}
</script>

<template>
  <div class="bg-slate-50 flex min-h-screen items-center justify-center relative overflow-hidden">
    <div class="dots-bg" />

    <div class="px-4 max-w-[360px] w-full relative z-10">
      <div class="p-8 border border-slate-200 rounded-2xl bg-white shadow-sm">
        <div class="mb-8 text-center">
          <div class="text-lg text-white font-bold mx-auto mb-4 rounded-xl bg-blue-600 flex size-12 shadow-md items-center justify-center">
            F
          </div>
          <h1 class="text-[20px] text-slate-900 font-bold">
            欢迎回来
          </h1>
          <p class="text-sm text-slate-400 mt-1">
            Fantastic Startkit 演示登录
          </p>
        </div>

        <div class="mb-5 space-y-3">
          <div class="px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
            <div class="text-[11px] text-slate-400 font-medium mb-0.5">
              账号
            </div>
            <div class="text-sm text-slate-700">
              admin
            </div>
          </div>
          <div class="px-4 py-3 border border-slate-200 rounded-lg bg-slate-50">
            <div class="text-[11px] text-slate-400 font-medium mb-0.5">
              密码
            </div>
            <div class="text-sm text-slate-700 tracking-widest">
              ••••••
            </div>
          </div>
        </div>

        <button
          class="text-sm text-white font-semibold py-2.5 border-0 rounded-xl bg-blue-600 w-full cursor-pointer shadow-sm transition-all hover:bg-blue-700 hover:shadow-md active:scale-[0.98]"
          @click="login"
        >
          模拟登录
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.dots-bg {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, #cbd5e1 1px, transparent 1px);
  background-size: 28px 28px;
  opacity: 0.5;
}
</style>


================================================
FILE: apps/example/tsconfig.app.json
================================================
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "paths": {
      "@/*": ["./src/*"],
      "#/*": ["./src/types/*"]
    },
    "resolveJsonModule": true,
    "types": [
      "vite/client",
      "vite-plugin-pages/client",
      "vite-plugin-vue-layouts-next/client",
      "@spiriit/vite-plugin-svg-spritemap/client"
    ],
    "allowJs": false,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "sourceMap": true,
    "esModuleInterop": true
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ]
}


================================================
FILE: apps/example/tsconfig.json
================================================
{
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "files": []
}


================================================
FILE: apps/example/tsconfig.node.json
================================================
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "moduleDetection": "force",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": [
    "package.json",
    "vite.config.ts",
    "vite/**/*.ts"
  ]
}


================================================
FILE: apps/example/vite/plugins.ts
================================================
import type { PluginOption } from 'vite'
import process from 'node:process'
import { ComponentsResolver as FantasticStartkitComponentsResolver, ComponentsType as FantasticStartkitComponentsType } from '@fantastic-startkit/components/resolver'
import { createCopyrightPlugins as createFantasticStartkitCopyrightPlugins } from '@fantastic-startkit/copyright'
import VitePluginSvgSpritemap from '@spiriit/vite-plugin-svg-spritemap'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import Unocss from 'unocss/vite'
import autoImport from 'unplugin-auto-import/vite'
import TurboConsole from 'unplugin-turbo-console/vite'
import components from 'unplugin-vue-components/vite'
import { loadEnv } from 'vite'
import Archiver from 'vite-plugin-archiver'
import { compression } from 'vite-plugin-compression2'
import { envParse, parseLoadedEnv } from 'vite-plugin-env-parse'
import { vitePluginFakeServer } from 'vite-plugin-fake-server'
import Pages from 'vite-plugin-pages'
import VueDevTools from 'vite-plugin-vue-devtools'
import Layouts from 'vite-plugin-vue-layouts-next'

export default function createVitePlugins(mode: string, isBuild = false) {
  const viteEnv = parseLoadedEnv(loadEnv(mode, process.cwd()))
  const vitePlugins: (PluginOption | PluginOption[])[] = [
    vue(),
    vueJsx(),

    // https://github.com/vuejs/devtools
    viteEnv.VITE_ENABLE_VUE_DEVTOOLS && VueDevTools({
      launchEditor: viteEnv.VITE_LAUNCH_EDITOR,
    }),

    envParse({
      dtsPath: 'src/types/env.d.ts',
    }),

    // https://github.com/unplugin/unplugin-auto-import
    autoImport({
      imports: [
        'vue',
        'vue-router',
        'pinia',
      ],
      dts: './src/types/auto-imports.d.ts',
      dirs: [
        './src/store/modules',
        './src/composables',
      ],
    }),

    // https://github.com/unplugin/unplugin-vue-components
    components({
      globs: [
        'src/components/*/index.vue',
      ],
      dts: './src/types/components.d.ts',
      resolvers: [
        FantasticStartkitComponentsResolver(),
      ],
      types: [
        FantasticStartkitComponentsType,
      ],
    }),

    Unocss(),

    // https://github.com/SpiriitLabs/vite-plugin-svg-spritemap
    VitePluginSvgSpritemap('./src/assets/icons/*.svg'),

    // https://github.com/condorheroblog/vite-plugin-fake-server
    vitePluginFakeServer({
      logger: !isBuild,
      include: 'src/api/modules',
      exclude: 'src/api/modules/**/!(*.fake).{ts,js,mjs,cjs,cts,mts}',
      enableProd: isBuild && viteEnv.VITE_BUILD_FAKE,
    }),

    // https://github.com/loicduong/vite-plugin-vue-layouts-next
    Layouts(),

    // https://github.com/hannoeru/vite-plugin-pages
    Pages({
      dirs: 'src/views',
      exclude: [
        '**/components/**/*.vue',
      ],
    }),

    // https://github.com/nonzzz/vite-plugin-compression
    viteEnv.VITE_BUILD_COMPRESS && compression({
      exclude: [/\.(br)$/, /\.(gz)$/],
      algorithms: viteEnv.VITE_BUILD_COMPRESS.split(',').map((item: string) => ({
        gzip: 'gzip',
        brotli: 'brotliCompress',
      }[item])),
    }),

    viteEnv.VITE_BUILD_ARCHIVE && Archiver({
      archiveType: viteEnv.VITE_BUILD_ARCHIVE,
    }),

    // https://github.com/unplugin/unplugin-turbo-console
    viteEnv.VITE_ENABLE_TURBO_CONSOLE && TurboConsole({
      launchEditor: {
        specifiedEditor: viteEnv.VITE_LAUNCH_EDITOR,
      },
    }),

    createFantasticStartkitCopyrightPlugins(),

    {
      name: 'vite-plugin-debug-plugin',
      transform: (code, id) => {
        if (/src\/main.ts$/.test(id)) {
          if (viteEnv.VITE_APP_DEBUG_TOOL === 'eruda') {
            code = `
${code}
import eruda from 'eruda'
eruda.init()
            `
          }
          else if (viteEnv.VITE_APP_DEBUG_TOOL === 'vconsole') {
            code = `
${code}
import VConsole from 'vconsole'
new VConsole()
            `
          }
          return {
            code,
            map: null,
          }
        }
      },
    },
  ]
  return vitePlugins
}


================================================
FILE: apps/example/vite.config.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { defineConfig, loadEnv } from 'vite'
import { parseLoadedEnv } from 'vite-plugin-env-parse'
import createVitePlugins from './vite/plugins'

// https://vitejs.dev/config/
export default defineConfig(({ mode, command }) => {
  const env = parseLoadedEnv(loadEnv(mode, process.cwd()))
  // 全局 scss 资源
  const scssResources: string[] = []
  fs.readdirSync('src/assets/styles/resources').forEach((dirname) => {
    if (fs.statSync(`src/assets/styles/resources/${dirname}`).isFile()) {
      scssResources.push(`@use "/src/assets/styles/resources/${dirname}" as *;`)
    }
  })
  return {
    // 开发服务器选项 https://cn.vitejs.dev/config/server-options
    server: {
      open: true,
      host: true,
      proxy: {
        '/proxy': {
          target: env.VITE_APP_API_BASEURL,
          changeOrigin: command === 'serve' && env.VITE_ENABLE_PROXY,
          rewrite: path => path.replace(/\/proxy/, ''),
        },
      },
    },
    // 构建选项 https://cn.vitejs.dev/config/build-options
    build: {
      outDir: mode === 'production' ? 'dist' : `dist-${mode}`,
      sourcemap: env.VITE_BUILD_SOURCEMAP,
    },
    plugins: createVitePlugins(mode, command === 'build'),
    optimizeDeps: {
      exclude: [
        '@fantastic-startkit/components',
      ],
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '#': path.resolve(__dirname, 'src/types'),
      },
    },
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: scssResources.join(''),
        },
      },
    },
  }
})


================================================
FILE: docs/.gitignore
================================================
node_modules
.vitepress/cache
.vitepress/dist
.eslintcache

================================================
FILE: docs/.vitepress/config.ts
================================================
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'Fantastic-startkit 官方文档',
  description: '一款简单好用的 Vue3 项目启动套件',
  lang: 'zh-CN',
  base: '/fantastic-startkit/',
  head: [
    ['meta', { name: 'keywords', content: 'vue,vite,router,vuex,pinia,typescript,template,startkit,starter,启动套件,模板' }],
    ['keywords', { content: 'vue,vite,router,vuex,pinia,typescript,template,startkit,starter,启动套件,模板' }],
    ['description', { content: '一款简单好用的 Vue3 项目启动套件' }],
  ],
  themeConfig: {
    footer: {
      copyright: 'Copyright © 2020-present Fantastic-startkit',
    },
    nav: [
      {
        text: '指南',
        link: '/guide/start',
      },
      {
        text: '技术支持',
        link: '/support',
      },
    ],
    socialLinks: [
      {
        icon: 'github',
        link: 'https://github.com/hooray/fantastic-startkit',
      },
      {
        icon: {
          svg: '<svg t="1663266323098" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2880" width="200" height="200"><path d="M512 1024C230.4 1024 0 793.6 0 512S230.4 0 512 0s512 230.4 512 512-230.4 512-512 512z m259.2-569.6H480c-12.8 0-25.6 12.8-25.6 25.6v64c0 12.8 12.8 25.6 25.6 25.6h176c12.8 0 25.6 12.8 25.6 25.6v12.8c0 41.6-35.2 76.8-76.8 76.8h-240c-12.8 0-25.6-12.8-25.6-25.6V416c0-41.6 35.2-76.8 76.8-76.8h355.2c12.8 0 25.6-12.8 25.6-25.6v-64c0-12.8-12.8-25.6-25.6-25.6H416c-105.6 0-188.8 86.4-188.8 188.8V768c0 12.8 12.8 25.6 25.6 25.6h374.4c92.8 0 169.6-76.8 169.6-169.6v-144c0-12.8-12.8-25.6-25.6-25.6z" p-id="2881"></path></svg>',
        },
        link: 'https://gitee.com/hooray/fantastic-startkit',
      },
    ],
    sidebar: {
      '/guide/': [
        {
          text: '指南',
          items: [
            { text: '准备工作', link: '/guide/ready' },
            { text: '开始', link: '/guide/start' },
            { text: '代码规范', link: '/guide/coding-standard' },
            { text: '环境配置', link: '/guide/env' },
            { text: '与服务端交互', link: '/guide/axios' },
            { text: '全局状态管理', link: '/guide/store' },
            { text: '资源', link: '/guide/resources' },
            { text: '图标', link: '/guide/icon' },
            { text: '路由', link: '/guide/router' },
            { text: '常用 API', link: '/guide/api' },
            { text: '构建与预览', link: '/guide/build' },
          ],
        },
      ],
      '/': [
        {
          text: '',
          items: [
            { text: '技术支持', link: '/support' },
          ],
        },
      ],
    },
    outline: 'deep',
    search: {
      provider: 'local',
      options: {
        translations: {
          button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
          modal: {
            noResultsText: '无法找到相关结果',
            resetButtonTitle: '清除查询条件',
            footer: { selectText: '选择', navigateText: '切换', closeText: '关闭' },
          },
        },
      },
    },
  },
})


================================================
FILE: docs/.vitepress/theme/components/SponsorsAside.vue
================================================
<template>
  <div>
    <div class="sponsors-aside-text">
      作者其他作品
    </div>
    <div class="sponsor-container special">
      <a href="https://fantastic-admin.hurui.me/" target="_blank" class="sponsor-item">
        <img src="https://fantastic-admin.hurui.me/logo.svg">
        <div class="info">
          <div class="main">Fantastic-admin</div>
          <div class="sub">杰出的管理系统框架</div>
        </div>
      </a>
      <a href="https://one-step-admin.hurui.me/" target="_blank" class="sponsor-item">
        <img src="https://one-step-admin.hurui.me/logo.png">
        <div class="info">
          <div class="main">One-step-admin</div>
          <div class="sub">巧妙的管理系统框架</div>
        </div>
      </a>
      <a href="https://fantastic-mobile.hurui.me/" target="_blank" class="sponsor-item">
        <img src="https://fantastic-mobile.hurui.me/logo.png">
        <div class="info">
          <div class="main">Fantastic-mobile</div>
          <div class="sub">自成一派的 H5 框架</div>
        </div>
      </a>
    </div>
  </div>
</template>

<style scoped>
.sponsors-aside-text {
  display: block;
  margin: 3em 0 1em;
  font-size: 12px;
  font-weight: 700;
  color: var(--vp-c-text-3);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}

.sponsor-container {
  --max-width: 100%;

  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--max-width), 1fr));
  column-gap: 4px;
}

.sponsor-item {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: center;
  height: calc(var(--max-width) / 2 - 6px);
  padding-inline: 20px;
  margin: 2px 0;
  font-size: 13px;
  background-color: var(--vp-c-bg-soft);
  border-radius: 2px;
  transition: background-color 0.2s ease;
}

.sponsor-item.action {
  font-size: 11px;
  color: var(--vt-c-text-3);
}

.sponsor-item img {
  max-width: calc(var(--max-width) - 30px);
  max-height: calc(var(--max-width) / 2 - 20px);
}

.sponsor-item .info {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
  justify-content: center;
}

.sponsor-item .info .main {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.5;
  color: var(--vp-c-text-1);
}

.sponsor-item .info .sub {
  font-size: 12px;
  line-height: 1.2;
  color: var(--vp-c-text-3);
}

.special .sponsor-item {
  height: 160px;
}

.special .sponsor-item img {
  max-width: 300px;
  max-height: 150px;
}

/* dark mode */
.dark .aside .sponsor-item,
.dark .landing .sponsor-item {
  background-color: var(--vp-c-gray-soft);
}

.aside .sponsor-item img,
.landing .sponsor-item img {
  transition: filter 0.2s ease;
}

.dark .aside .sponsor-item img,
.dark .landing .sponsor-item img {
  filter: grayscale(1) invert(1);
}

.dark .sponsor-item:hover img {
  filter: none;
}

/* aside mode (on content pages) */
.sponsor-container.platinum.aside {
  --max-width: 110px;

  column-gap: 1px;
}

.aside .sponsor-item {
  margin: 1px 0;
}

.aside .special .sponsor-item {
  width: 100%;
  height: 70px;
}

.aside .special .sponsor-item img {
  max-width: 120px;
  max-height: 36px;
}
</style>


================================================
FILE: docs/.vitepress/theme/fonts/fira_code/fira_code.css
================================================
@font-face {
  font-family: "Fira Code";
  font-style: normal;
  font-weight: 300;
  src:
    url("woff2/FiraCode-Light.woff2") format("woff2"),
    url("woff/FiraCode-Light.woff") format("woff");
}

@font-face {
  font-family: "Fira Code";
  font-style: normal;
  font-weight: 400;
  src:
    url("woff2/FiraCode-Regular.woff2") format("woff2"),
    url("woff/FiraCode-Regular.woff") format("woff");
}

@font-face {
  font-family: "Fira Code";
  font-style: normal;
  font-weight: 500;
  src:
    url("woff2/FiraCode-Medium.woff2") format("woff2"),
    url("woff/FiraCode-Medium.woff") format("woff");
}

@font-face {
  font-family: "Fira Code";
  font-style: normal;
  font-weight: 600;
  src:
    url("woff2/FiraCode-SemiBold.woff2") format("woff2"),
    url("woff/FiraCode-SemiBold.woff") format("woff");
}

@font-face {
  font-family: "Fira Code";
  font-style: normal;
  font-weight: 700;
  src:
    url("woff2/FiraCode-Bold.woff2") format("woff2"),
    url("woff/FiraCode-Bold.woff") format("woff");
}

@font-face {
  font-family: "Fira Code VF";
  font-style: normal;

  /* font-weight requires a range: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide#Using_a_variable_font_font-face_changes */
  font-weight: 300 700;
  src:
    url("woff2/FiraCode-VF.woff2") format("woff2-variations"),
    url("woff/FiraCode-VF.woff") format("woff-variations");
}


================================================
FILE: docs/.vitepress/theme/index.ts
================================================
import mediumZoom from 'medium-zoom'
import { useData, useRoute } from 'vitepress'
import giscusTalk from 'vitepress-plugin-comment-with-giscus'
import Theme from 'vitepress/theme'
import { h, nextTick, onMounted, toRefs, watch } from 'vue'
import SponsorsAside from './components/SponsorsAside.vue'
import './fonts/fira_code/fira_code.css'
import './styles/var.css'

export default {
  ...Theme,
  Layout() {
    return h(Theme.Layout, null, {
      'aside-bottom': () => h(SponsorsAside),
    })
  },
  setup() {
    const route = useRoute()

    const initZoom = () => {
      mediumZoom('[data-zoomable]', { background: 'var(--vp-c-bg)' })
    }
    onMounted(() => {
      initZoom()
    })
    watch(
      () => route.path,
      () => nextTick(() => {
        initZoom()
      }),
    )

    // 获取前言
    const { frontmatter } = toRefs(useData())

    // 评论组件 - https://giscus.app/
    giscusTalk({
      repo: 'hooray/fantastic-startkit',
      repoId: 'MDEwOlJlcG9zaXRvcnkzNzMzNjIxOTI=',
      category: 'Announcements', // 默认: `General`
      categoryId: 'DIC_kwDOFkEOEM4CeRHv',
      mapping: 'pathname', // 默认: `pathname`
      inputPosition: 'top', // 默认: `top`
      lang: 'zh-CN', // 默认: `zh-CN`
      lightTheme: 'light', // 默认: `light`
      darkTheme: 'transparent_dark', // 默认: `transparent_dark`
      loading: true,
    }, {
      frontmatter,
      route,
    },
    // 是否全部页面启动评论区。
    // 默认为 true,表示启用,此参数可忽略;
    // 如果为 false,表示不启用。
    // 可以在页面使用 `comment: true` 前言单独启用
    true)
  },
}


================================================
FILE: docs/.vitepress/theme/styles/var.css
================================================
/* medium-zoom */
.medium-zoom-overlay {
  z-index: 30;
}

.medium-zoom-image {
  z-index: 30;
}


================================================
FILE: docs/guide/api.md
================================================
# 常用 API

## 接口请求

详细可阅读《[与服务端交互 - 接口请求](axios#接口请求)》。

```ts
import api from '@/api'

api.get()
api.post()
```

## 事件总线

基于 [mitt](https://github.com/developit/mitt) 简单封装,使用方法请查阅官方文档。

```ts
import eventBus from '@/utils/eventBus'

eventBus.on()
eventBus.emit()
eventBus.off()
```

## 日期

基于 [dayjs](https://day.js.org/zh-CN/) 简单封装,使用方法请查阅官方文档。

```ts
import dayjs from '@/utils/dayjs'

dayjs()
```

================================================
FILE: docs/guide/axios.md
================================================
# 与服务端交互

框架使用 [Axios](https://axios-http.com/zh/) 做为异步请求工具,并进行了简单的封装。

## 接口请求

### 设置 baseURL

在 `apps/<app>/.env.*` 文件里的 `VITE_APP_API_BASEURL` 这个参数就是配置 axios 的 `baseURL` 。

例如项目的真实接口请求地址为:

- `http://api.test.com/news/list`
- `http://api.test.com/news/create`
- `http://api.test.com/shop/info`

则可设置为 `VITE_APP_API_BASEURL = http://api.test.com/` 。

### 请求调用

常用的 GET 和 POST 请求可使用以下的方法:

```ts
import api from '@/api'

// GET 请求
api.get('news/list', {
  params: {
    page: 1,
    size: 10,
  },
}).then((res) => {
  // 后续业务代码
})

// POST 请求
api.post('news/create', {
  title: '新闻标题',
  content: '新闻内容',
}).then((res) => {
  // 后续业务代码
})
```

### 拦截器

在 `apps/<app>/src/api/index.ts` 文件里实例化了 axios 对象,并对 `request` 和 `response` 设置了拦截器,拦截器的用处就是拦截每一次的请求和响应,然后做一些全局的处理。例如接口响应报错,可以在拦截器里用统一的报错提示来展示,方便业务开发。但因为每个公司提供的接口标准不同,所以该文件拦截器部分的代码,需要开发者根据实际情况去修改调整。

代码很简单,首先初始化 axios 对象,然后 `axios.interceptors.request.use()` 和 `axios.interceptors.response.use()` 就分别是请求和响应的拦截代码了。

参考代码里只做了简单的拦截处理,例如请求的时候会自动带上 token ,响应的时候会根据错误信息判断是登录失效还是接口报错,并做相应动作。

### 请求重试

框架扩展了请求配置参数,只需在请求时增加 `retry` 配置项,即可开启请求重试。

```ts
api.get('news/list', {
  retry: true,
})

api.post('news/create', {
  title: '新闻标题',
  content: '新闻内容',
}, {
  retry: true,
})
```

默认请求重试次数为 3 次,请求间隔为 1000 毫秒,可在 `apps/<app>/src/api/index.ts` 文件中修改 `MAX_RETRY_COUNT` 和 `RETRY_DELAY` 的默认配置。

## 模块管理

如果项目里的接口很多,推荐根据模块来统一管理接口,目录为 `apps/<app>/src/api/modules/` 。

## 跨域处理

生产环境的跨域需要服务端去解决,开发环境的跨域问题可在本地设置代理解决。如果本地开发环境请求接口提示跨域,可以设置 `apps/<app>/.env.development` 文件里 `VITE_ENABLE_PROXY = true` 开启代理。

```ts
import api from '@/api'

api.get('news/list') // http://localhost:9000/proxy/news/list
api.post('news/add') // http://localhost:9000/proxy/news/add
```

开启代理后,原有请求都会被指向到本地 `http://localhost:9000/proxy` ,因为 `/proxy` 匹配到了 vite.config.ts 里代理部分的设置,所以实际是请求依旧是 `VITE_APP_API_BASEURL` 所设置的地址。

```ts {2-9}
server: {
  // vite.config.ts 中 proxy 配置,该配置即用于代理 API 请求
  proxy: {
    '/proxy': {
      target: loadEnv(mode, process.cwd()).VITE_APP_API_BASEURL,
      changeOrigin: command === 'serve' && loadEnv(mode, process.cwd()).VITE_ENABLE_PROXY == 'true',
      rewrite: path => path.replace(/\/proxy/, ''),
    },
  },
},
```

## 多数据源

如果项目里需要从多个不同地址的数据源请求数据,你有两种方式可以实现。

如果只是几个接口需求从其它数据源请求,你可以使用覆盖 `baseURL` 的方式:

```ts
import api from '@/api'

api.get('/new/list', {
  baseURL: 'http://baidu.com/', // 直接覆盖 baseURL
})
```

这种方式的前提是,两个数据源的 `request` 和 `response` 规则要保持一致,因为只是覆盖 `baseURL` ,拦截器还是用的同一套规则。

所以如果两个数据源的请求和响应是完全不同的标准,你需要给第二个数据源单独实例化一个 axios 对象。首先在 `apps/<app>/.env.*` 文件里配置第二个数据源的 `baseURL` :

```
# 命名可随意,以 VITE_APP_ 开头即可
VITE_APP_API_BASEURL_2 = 此处填写接口地址
```

然后把 `apps/<app>/src/api/index.ts` 文件复制一份,例如就叫 `apps/<app>/src/api/index2.ts` ,并且将代码中使用到 `VITE_APP_API_BASEURL` 也替换为 `VITE_APP_API_BASEURL_2` ,这样你就可以在页面中通过引入不同的文件分别请求两个数据源了:

```ts
import api from '@/api'
import api2 from '@/api/index2'

// 请求默认数据源
api.get('/new/list')
// 请求第 2 个数据源
api2.get('/new/list')
```

需注意,如果第二个数据源也需要开启跨域处理的话,需要在 `apps/<app>/src/api/index2.ts` 里定一个新的 proxy 路径,例如 `/proxy2/` :

```ts {2}
const api = axios.create({
  baseURL: import.meta.env.DEV && import.meta.env.VITE_ENABLE_PROXY === 'true' ? '/proxy2/' : import.meta.env.VITE_APP_API_BASEURL_2,
  timeout: 10000,
  responseType: 'json',
})
```

同时在 `apps/<app>/vite.config.ts` 里增加一段新的 proxy 配置:

```ts {9-13}
server: {
  // vite.config.ts 中 proxy 配置,该配置即用于代理 API 请求
  proxy: {
    '/proxy': {
      target: loadEnv(mode, process.cwd()).VITE_APP_API_BASEURL,
      changeOrigin: command === 'serve' && loadEnv(mode, process.cwd()).VITE_ENABLE_PROXY == 'true',
      rewrite: path => path.replace(/\/proxy/, ''),
    },
    '/proxy2': {
      target: loadEnv(mode, process.cwd()).VITE_APP_API_BASEURL_2,
      changeOrigin: command === 'serve' && loadEnv(mode, process.cwd()).VITE_ENABLE_PROXY == 'true',
      rewrite: path => path.replace(/\/proxy2/, ''),
    },
  },
},
```

## 假数据

假数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发所阻塞。

:::tip
框架使用 [vite-plugin-fake-server](https://github.com/condorheroblog/vite-plugin-fake-server) 提供开发和生产模拟服务。
:::

### 开发环境

文件存放在 `apps/<app>/src/api/modules/` 目录下,并以 `*.fake.ts` 命名。文件新增或修改后会自动更新,不需要手动重启,可以在代码控制台查看日志信息。

以下为示例代码:

```ts
import { defineFakeRoute } from 'vite-plugin-fake-server/client'

// 管理员
const allList: any[] = []
for (let i = 0; i < 50; i++) {
  allList.push(i + 1)
}

export default defineFakeRoute([
  {
    url: '/fake/page/loadmore',
    method: 'get',
    response: ({ query }) => {
      const { from, limit } = query
      const pageList = allList.filter((_item, index) => {
        return index >= ~~from && index < (~~from + ~~limit)
      })
      return {
        error: '',
        status: 1,
        data: {
          list: pageList,
          total: allList.length,
        },
      }
    },
  },
])
```

参数获取:

- GET:`({ query }) => { }`
- POST:`({ body }) => { }`

为了让假数据接口与真实接口共存,即项目开发中,部分请求假数据接口,部分请求真实接口。需要在配置假数据接口的时候,给 `url` 参数统一设置 `/fake/` 前缀,并在调用接口的时候,设置 `fake: true` 。

如下所示,其中 `news/list` 会请求本地的假数据接口,而 `news/create` 依旧请求真实接口,即使开启跨域代理也不影响。

```ts {4}
import api from '@/api'

api.get('news/list', {
  fake: true, // <- 区别在这
  params: {
    page: 1,
    size: 10,
  },
}).then((res) => {
  // 后续业务代码
})

api.post('news/create', {
  title: '新闻标题',
  content: '新闻内容',
}).then((res) => {
  // 后续业务代码
})
```

### 生产环境

:::warning 注意
生产环境一般都是调用真实接口,如果需要使用假数据也只适用于一些简单的示例网站及预览网站。
:::

框架默认已经配置好生产环境,如果不想让生产环境里的请求走假数据,可在接口调用处删除 `fake: true` 设置。

需要注意一点,如果项目中有涉及到上传功能,请彻底关闭线上环境假数据,在环境配置里设置 `VITE_BUILD_FAKE = false` ,不然线上环境将会报错。

开发环境与生产环境使用假数据差异不大,比较大的区别是生产环境里调用假数据接口,在控制台内看不到接口请求日志。


================================================
FILE: docs/guide/build.md
================================================
# 构建与预览

## 构建

项目开发完成之后,可以执行 `pnpm run build` 命令进行构建,构建成功后会生成 `apps/<app>/dist/` 文件夹,里面就是构建好的文件。

如果构建的是测试环境,则生成的文件夹为 `apps/<app>/dist-test/` 。

:::tip
如果最终访问地址为域名非根节点,如 `https://www.example.com/app`,则需要在 `apps/<app>/vite.config.ts` 中设置 `base` 选项为 `/app/`,否则会出现资源引用错误。
:::

## 预览

生成好的 dist 文件夹一般需要部署至服务器才算部署发布成功,但为了保证构建出来的文件能正常运行,开发者通常希望能在本地先预览一下,可执行 `pnpm run serve` 命令预览不同环境的构建产物。


================================================
FILE: docs/guide/coding-standard.md
================================================
# 代码规范

:::tip
请确保已阅读《[准备工作 - 开发环境](start#开发环境)》,并且按照文档说明安装好相关软件及扩展。
:::

为保证代码风格统一,请使用 [Visual Studio Code](https://code.visualstudio.com/) 做为开发 IDE ,框架源码里已提供相关配置文件,可直接测试效果:在保存代码时,会自动对当前文件进行代码格式化操作。

## IDE 配置

配置文件为 `.editorconfig` ,通常情况下无需做任何修改。

## ESLint

配置文件为 `.eslintrc` ,框架使用 [antfu/eslint-config](https://github.com/antfu/eslint-config) 做为基础规范,如果你对默认的规则有异议,可以查阅 [ESLint](https://eslint.org/) 官网规则并在 `.eslintrc` 文件中进行覆盖。

当你对规则进行修改后,原有的代码可能会因为规则的变动导致编辑器大量提示错误,你可以通过运行 `pnpm run lint:eslint` 进行一次格式校验,如果规则支持自动修复,则会将不符合规则的代码自动进行格式化。

::: tip
通过修改 `.eslintignore` 可忽略无需做代码规范校验的文件,例如在项目中导入了一些第三方的插件代码或组件代码,我们就可以将其进行忽略。
:::

## StyleLint

配置文件为 `.stylelintrc` ,如果你对默认的规则有异议,可以查阅 [Stylelint](https://stylelint.io/) 官网规则并在 `.stylelintrc` 文件中进行修改。

当你对规则进行修改后,原有的代码可能会因为规则的变动导致编辑器大量提示错误,你可以通过运行 `pnpm run lint:stylelint` 进行一次格式校验,如果规则支持自动修复,则会将不符合规则的代码自动进行格式化。

::: tip
通过修改 `.stylelintignore` 可忽略无需做代码规范校验的文件,例如在项目中导入了一些第三方的插件代码或组件代码,我们就可以将其进行忽略。
:::

## simple-git-hooks & lint-staged

由于 IDE 能做的事比较有限,只能对代码的书写规范进行格式化,对于一些无法自动修复的错误代码,如果没有改正到就被推送到 git 仓库,在多人协作开发时,可能会影响到别人的开发体验。所以框架集成了 [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) 和 [lint-staged](https://github.com/okonet/lint-staged) 这两个依赖来解决这一问题。

在提交代码时, simple-git-hooks 会通过 lint-staged 对本次提交变更的文件进行分别进行 eslint 和 stylelint 检测,如果有报错,则会阻止本次代码提交,直到开发者修改完所有错误代码后,才允许提交到 git 仓库,这样可以确保 git 仓库里的代码不会有不规范的代码。

::: tip 注意
请确保在安装依赖前,已经使用 `git init` 对项目进行过 git 环境初始化,如果你在安装依赖后再初始化了 git 环境,请在 git 环境初始化之后再执行一遍 `pnpm install` 安装命令。

此外,如果 git 仓库目录和框架目录并非同一个,则需要在 `package.json` 中修改 `postinstall` 脚本,切换到 git 所在目录。例如 git 目录是 `project/` ,而框架目录是 `project/fantastic-startkit/` ,则在 `package.json` 里找到 `simple-git-hooks` 配置并修改:

```json {2}
"simple-git-hooks": {
  "pre-commit": "cd ./fantastic-startkit/ && pnpm lint-staged",
  "preserveUnused": true
}
```

修改后重新执行一下 `pnpm install` 即可。
:::

### 移除

如果不想在 git 提交时强制进行代码规范校验,可以在 `package.json` 中移除 `simple-git-hooks` 配置:

```json
{
  "scripts": {
    "postinstall": "simple-git-hooks", // [!code --]
  },
  "simple-git-hooks": { // [!code --]
    "pre-commit": "pnpm lint-staged", // [!code --]
    "preserveUnused": true // [!code --]
  }, // [!code --]
}
```

然后手动删除 `.git/hooks/pre-commit` 文件即可。

================================================
FILE: docs/guide/env.md
================================================
# 环境配置

环境变量配置文件在 `apps/<app>/` 根目录,默认提供三套配置,分别为:

::: code-group

<<< @/../apps/core/.env.development{env}[.env.development 开发环境]

<<< @/../apps/core/.env.test{env}[.env.test 测试环境]

<<< @/../apps/core/.env.production{env}[.env.production 生产环境]

:::

开发者可根据实际业务需求进行扩展,详细可阅读 [Vite - 环境变量和模式](https://cn.vitejs.dev/guide/env-and-mode.html) 章节。

## 通用配置项

即不管是在开发、测试,还是生产环境都会使用到。

### VITE_APP_TITLE

网站标题,会在浏览器标题处显示。

### VITE_APP_API_BASEURL

[扩展阅读](axios)

### VITE_APP_DEBUG_TOOL

方便在不支持启用浏览器开发者工具的环境,启用一个轻量级的调试工具。

```env
# 调试工具 eruda
VITE_APP_DEBUG_TOOL = eruda

# 调试工具 vconsole
VITE_APP_DEBUG_TOOL = vconsole
```

## 开发环境配置项

### VITE_ENABLE_PROXY

[扩展阅读](axios#跨域处理)

### VITE_ENABLE_VUE_DEVTOOLS

Vue 开发工具

### VITE_ENABLE_TURBO_CONSOLE

Console 工具

### VITE_LAUNCH_EDITOR

默认启动 IDE

**Vue 开发工具** 和 **Console 工具** 都支持在浏览器中打开 IDE 并定位到源文件。

默认打开 VSCode ,如果你使用其他 IDE ,建议创建 `apps/<app>/.env.development.local` 文件并写入:

```env
# 启动编辑器,用于 vite-plugin-vue-devtools 和 unplugin-turbo-console
# 支持的编辑器 https://github.com/yyx990803/launch-editor#supported-editors
VITE_LAUNCH_EDITOR = code
```

点击查看[支持的 IDE 列表](https://github.com/yyx990803/launch-editor#supported-editors)。

## 测试/生产环境

### VITE_BUILD_FAKE

[扩展阅读](axios#生产环境)

### VITE_BUILD_SOURCEMAP

开启后生成的构建产物里包含 sourcemap 文件

### VITE_BUILD_COMPRESS

可在构建时生成 `.gz` 和 `.br` 文件。

```env
# 单独开启 gzip
VITE_BUILD_COMPRESS = gzip

# 单独开启 brotli ,brotli 是比 gzip 压缩率更高的算法
VITE_BUILD_COMPRESS = brotli

# 也可以都开启,会同时生成 .gz 和 .br 文件
VITE_BUILD_COMPRESS = gzip,brotli
```

两者均需要 nginx 安装指定模块并开启后才会生效。

### VITE_BUILD_ARCHIVE

在构建完后成生成 `.zip` 或 `.tar.gz` 文件。

```env
# 生成 zip
VITE_BUILD_ARCHIVE = zip

# 生成 tar.gz
VITE_BUILD_ARCHIVE = tar
```


================================================
FILE: docs/guide/icon.md
================================================
# 图标

框架提供了三种使用图标的方式,你可以根据自己的使用需求自行选择。

## 自定义图标

推荐去[阿里巴巴矢量图标库](https://www.iconfont.cn/)下载图标,将准备好的 SVG 图标文件放到 `apps/<app>/src/assets/icons/` 目录下,然后在页面中就可以通过 `FsIcon` 组件使用了,name 就是 svg 的文件名。

```vue
<!-- apps/<app>/src/assets/icons/example.svg -->
<FsIcon name="example" />
```

## Iconify 图标

::: tip 介绍
[Iconify](https://github.com/iconify/iconify) 提供 100+ 套图标集,有 100,000+ 个图标可以免费使用。
:::

除了可以在 Iconify 官网上查找搜需要的图标,你还可以在 [Icônes 网站](https://icones.js.org/) 上查找,这是一个基于 Iconify 的在线图标搜索网站,它比 Iconify 官网的操作更直观。

![](/icones1.png){data-zoomable}

![](/icones2.png){data-zoomable}

### Unocss 方案

::: tip 说明
Unocss 方案采用了 CSS 去处理图标的展示,框架大部分核心模块里采用的是这种方式,如果你对其中的技术细节感兴趣,可以阅读这篇 Unocss 作者的《[聊聊纯 CSS 图标](https://antfu.me/posts/icons-in-pure-css-zh)》这篇文章。
:::

框架已经做好了所有配置,使用方式也极为简单,你只需进入 [Iconify 官网](https://icon-sets.iconify.design/) 上查找 Iconify 提供的所有图标,然后点击需要使用的图标,复制图标名称,在任意原生 HTML 标签上通过设置 class ,格式为 `i-{集合名}:{图标名}`,例如:

```vue
<div class="i-ep:arrow-right" />
<i class="i-ep:search" />
```

当然你同样也可以通过 `FsIcon` 组件使用它。

```vue
<FsIcon name="i-ep:arrow-right" />
```

在使用 Unocss 图标时,需要注意以下两点:

- 图标字符串不支持拼接

    ```vue
    <!-- 这样不会生效 -->
    <FsIcon :name="'i-ep' + ':search'" />
    ```

- 图标字符串不支持异步返回

    ```vue
    <!-- 这样不会生效 -->
    <!-- 假设 name 是异步请求返回的数据,name 为 i-ep:search -->
    <FsIcon :name="name" />
    ```

如果确实有以上需求,你可以使用 Iconify 原生提供的方案。

### Iconify 原生方案

::: tip 说明
框架保留了 Iconify 官方提供的使用方式,格式为 `{集合名}:{图标名}` 。
:::

```vue
<script setup>
import { Icon } from '@iconify/vue'
</script>

<template>
  <Icon icon="ep:arrow-right" />
</template>
```

当然这么使用并没有很方便,依旧还是需要手动导入一个 Icon 组件。如果你也觉得麻烦的话,那么你可以使用 `FsIcon` 组件来展示,框架已经帮你做好的所有处理。

```vue
<FsIcon name="ep:arrow-right" />
```


================================================
FILE: docs/guide/ready.md
================================================
# 准备工作

## 源码

可以直接下载源码,或者通过 git 拉取源码

```bash
# 从 Github 拉取
git clone https://github.com/hooray/fantastic-startkit.git
# 从 Gitee 拉取
git clone https://gitee.com/hooray/fantastic-startkit.git
```

## 开发环境

使用本套件前,需要在本地依次安装好 [Node.js](https://nodejs.org/), [pnpm](https://pnpm.io/zh/), [Git](https://git-scm.com/) 和 [Visual Studio Code](https://code.visualstudio.com/)。

:::warning 注意
在 [package.json](https://github.com/hooray/fantastic-startkit/blob/main/package.json#L3-L5) 文件中有限制 node 要求版本,建议使用最新 LTS 版本。
:::

然后在 Visual Studio Code 里安装好以下扩展:

- [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)
- [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv)
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
- [stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint)
- [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
- [UnoCSS](https://marketplace.visualstudio.com/items?itemName=antfu.unocss)

在 Visual Studio Code 里打开源码的文件夹,右下角会自动提示需要安装的依赖,直接点击安装即可。

::: tip 额外推荐
以上为开发时必备扩展,以下则是作者推荐安装的扩展,安装它们将在一定程度上提升开发效率。

- [Chinese (Simplified) Language Pack for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=MS-CEINTL.vscode-language-pack-zh-hans) 中文语言包
- [Catalog Lens](https://marketplace.visualstudio.com/items?itemName=antfu.pnpm-catalog-lens) 显示PNPM/Yarn/Bun目录的嵌套版本
- [Goto definition alias](https://marketplace.visualstudio.com/items?itemName=antfu.goto-alias) 转到別名重定向后的定义
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) 在代码中预览 iconify 图标
- [Color Highlight](https://marketplace.visualstudio.com/items?itemName=naumovs.color-highlight) 在代码中高亮颜色
- [Highlight Matching Tag](https://marketplace.visualstudio.com/items?itemName=vincaslt.highlight-matching-tag) 高亮显示匹配的标签
- [Image preview](https://marketplace.visualstudio.com/items?itemName=kisstkondoros.vscode-gutter-preview) 图片预览
- [indent-rainbow](https://marketplace.visualstudio.com/items?itemName=oderwat.indent-rainbow) 彩虹缩进提示
:::

## 技术栈

了解并熟悉框架使用到的技术栈,能让你使用本框架更得心应手。

- [Vite](https://cn.vitejs.dev/)
- [Vue 3](https://cn.vuejs.org/)
- [Vue Router](https://router.vuejs.org/zh/)
- [Pinia](https://pinia.vuejs.org/zh/)
- [UnoCSS](https://unocss.dev/)


================================================
FILE: docs/guide/resources.md
================================================
# 资源

## 图片

### 全局公共

全局公共图片存放在 `apps/<app>/src/assets/images/` 目录下,可自行新建文件夹分类管理。

### 局部私有

局部私有图片建议采用就近原则,你可以在需要的模块文件夹下建立一个 `images` 文件夹,专门用于存放局部私有图片。

## 样式

### 全局公共

全局公共样式存放在 `apps/<app>/src/assets/styles/` 目录下,可自行新建文件,并在 `apps/<app>/src/main.ts` 中引入即可。

此目录下还有一个特殊目录,即 `apps/<app>/src/assets/styles/resources/` ,这是全局 SCSS 资源目录,你可以在该目录下编写变量、函数、混合等支持 SCSS 特性的代码。

### 局部私有

基于单文件组件规范,局部私有样式建议直接在 `.vue` 文件中编写,模板集成了 UnoCSS / PostCSS / SCSS 方案,可选择自己适合的方案。更多单文件组件 CSS 功能请参考[这里](https://cn.vuejs.org/api/sfc-css-features)。

#### UnoCSS

```vue
<template>
  <div class="flex flex-1 flex-col select-none text-center all:transition-400">
    <div class="ma">
      <div class="animate-bounce-alt animate-duration-1s animate-count-infinite text-5xl fw100">
        UnoCSS
      </div>
    </div>
  </div>
</template>
```

#### PostCSS

模板内置了 [postcss-nested](https://github.com/postcss/postcss-nested) 插件,可实现接近于 SCSS 的写法和特性。

```vue
<style scoped>
.phone {
  &_title {
    width: 500px;
    @media (max-width: 500px) {
      width: auto;
    }
    body.is_dark & {
      color: white;
    }
  }
  img {
    display: block;
  }
}
</style>
```

#### SCSS

```vue
<style lang="scss" scoped>
$width: 500px;

.phone {
  &_title {
    width: $width;
    @media (max-width: $width) {
      width: auto;
    }
    body.is_dark & {
      color: white;
    }
  }
  img {
    display: block;
  }
}
</style>
```

## 组件

### 全局公共

::: tip 说明
全局公共组件在使用时,无需手动引入,模板会在你使用时自动引入,该特性由 [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components) 提供支持。
:::

全局公共组件有两个来源:

#### 来源一:应用内组件

存放在 `components/` 目录下,每个组件独占一个文件夹,文件夹名称即为组件名。文件夹内至少包含一个名为 `index.vue` 的入口文件。

```
src/components/
└── DemoButton/
    └── index.vue   # 组件名为 DemoButton,直接在模板中使用 <DemoButton />
```

#### 来源二:monorepo 子包

`@fantastic-startkit/components` 子包提供跨应用的公共组件,通过 `FantasticComponentsResolver` 自动识别并导入。子包组件名统一以 `Fs` 为前缀,例如 `<FsIcon />`。

如需新增子包组件,参考 `packages/components` 目录下的结构说明。

### 局部私有

局部私有组件建议采用就近原则,你可以在需要的模块文件夹下建立一个 `components` 文件夹,专门用于存放局部私有组件。


================================================
FILE: docs/guide/router.md
================================================
# 路由

路由实现了自动注册,路由配置存放在 `router/modules/` 目录下,每一个 ts 文件会被视为一个路由模块,可参考 `router/modules/example.ts` 文件。

更多使用技巧请移步至 Vue-router [官方文档](https://router.vuejs.org/zh/)。

## 基于文件系统的路由

> 该特性由 [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) 和 [vite-plugin-vue-layouts-next](https://github.com/loicduong/vite-plugin-vue-layouts-next) 提供支持。

传统使用路由的方式需要手动编写路由,而基于文件系统的路由则会根据文件的目录结构自动生成路由结构,从而节省开发周期。

默认未开启该模式,如果需要启用,到 `router/index.ts` 文件里找到下面代码片段,通过开启/关闭注释修改成下面这样:

```ts {2-13,16-20}
// 注释以下代码
// const routesContext: any = import.meta.glob('./modules/*.ts', { eager: true })
// Object.keys(routesContext).forEach((v) => {
//   routes.push(routesContext[v].default)
// })
// routes.push({
//   path: '/:pathMatch(.*)*',
//   component: () => import('@/views/[...all].vue'),
//   meta: {
//     title: '找不到页面',
//   },
// })
// routes = routes.flat()

// 开启以下代码
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
generatedRoutes.forEach(v => {
    routes.push(v?.meta?.layout != false ? setupLayouts([v])[0] : v)
})
```

启用基于文件系统的路由后,`router/modules/` 目录将不再被需要,而 `views/` 目录下的文件会自动被注册成路由。

```
文件系统                           路由地址                          路由 name

views
├─ example
│    ├─ components
│    │    └─ List
│    │         └─ index.vue
│    ├─ params
│    │    └─ [id].vue              /example/params/:id              example-params
│    ├─ axios.vue                  /example/axios                   example-axios
│    ├─ cookie.vue                 /example/cookie                  example-cookie
│    └─ icon.vue                   /example/icon                    example-icon
├─ [...all].vue                    /:all(.*)*                       all
├─ index.vue                       /                                index
└─ login.vue                       /login                           login
```

通过上面的示例,可以看出几个特性:

- 使用路由参数需通过 `[ ]` 将参数名包裹,并设为文件名
- 文件夹不会生成路由,例如 example 文件夹并没有生成 `/example` 路由
- 路由 name 会根据文件的目录结构自动生成,并用 `-` 连接,可确保 name 的唯一性
- 所有 components 目录均不会生成路由

---

默认生成的所有路由均为嵌套路由,父级 component 指向 `layout/index.vue` 组件,即:

```ts
// 生成的路由
{
  path: '/login',
  component: () => import('layout/index.vue'),
  children: [
    {
      path: '',
      component: () => import('views/login.vue'),
      name: 'login',
      meta: {
        layout: 'index',
      },
    },
  ],
}
```

你可以在 SFC 单文件组件里设置 layout :

```vue {1-4}
<route lang="yaml">
meta:
  layout: example
</route>

<template>
  <div>login 页面</div>
</template>
```

```ts {4,11}
// 生成的路由
{
  path: '/login',
  component: () => import('layout/example.vue'),
  children: [
    {
      path: '',
      component: () => import('views/login.vue'),
      name: 'login',
      meta: {
        layout: 'example',
      },
    },
  ],
}
```

同样也可以设置成 `layout: false` ,这样该路由就不会生成嵌套路由。

```vue {1-4}
<route lang="yaml">
meta:
  layout: false
</route>

<template>
  <div>login 页面</div>
</template>
```

```ts
// 生成的路由
{
  path: '/login',
  component: () => import('views/login.vue'),
  name: 'login',
  meta: {
    layout: false,
  },
}
```


================================================
FILE: docs/guide/start.md
================================================
# 开始

本项目采用 Monorepo(单体仓库)架构,基于 pnpm workspace 管理多个应用和公共包。

Monorepo 的优势:

- **代码共享**:多个应用可共享公共代码和依赖
- **统一管理**:统一版本控制和依赖管理
- **开发效率**:一次修改,多处生效
- **原子提交**:跨应用的变更可在一个提交中完成

## 目录结构

```
fantastic-startkit/
├── apps/              # 应用目录
│   ├── core           # 应用源码
│   └── example        # 示例应用
├── packages/          # 公共包目录
├── docs/              # 文档站点
├── scripts/           # 脚本工具
└── package.json       # 根目录 package.json
```

## 应用说明

### apps/core

应用源码,不含示例代码,仅保留必要的项目结构,适合直接用于项目开发。

使用时建议从此处复制一份在 `apps/` 目录下,同时修改 `apps/<app>/package.json` 中 `name` 属性。

这样做的目的是确保项目内始终保留一份原始应用源码,方便后续扩展更多应用。

### apps/example

示例应用,包含丰富的示例代码和最佳实践,适合学习和参考。

## 常用命令

:::tip 建议
在安装依赖前,可以将不需要的应用先删除。假设你已经足够熟悉框架,则可以将 `apps/example` 应用文件夹直接删除,减少无用依赖的安装。
:::

根目录 `package.json` 提供了统一的命令入口:

```bash
# 安装所有依赖
pnpm install

# 启动开发服务器(交互式选择应用)
pnpm dev

# 构建项目(交互式选择应用)
pnpm build

# 预览构建产物(交互式选择应用)
pnpm serve

# 代码检查
pnpm lint
```

其中 `pnpm lint` 会按以下顺序执行:

1. 在各应用目录执行 `vue-tsc`
2. 在根目录执行 `eslint`
3. 在根目录执行 `stylelint`

::: warning 报错
如果无法正常安装依赖,可能是因为 npm 默认源无法访问,可以尝试执行 `pnpm config set registry https://registry.npmmirror.com/` 切换为国内 npmmirror 镜像源(也可以使用 [nrm](https://github.com/Pana/nrm) 一键切换源),然后删除 `node_modules/` 文件夹并重新安装依赖。
:::

## 应用级命令

如果明确知道要操作哪个应用,也可以直接使用 pnpm filter 命令:

```bash
# 运行指定应用
pnpm -F @fantastic-startkit/core dev

# 构建指定应用
pnpm -F @fantastic-startkit/core build

# 在指定应用下执行任意命令
pnpm -F @fantastic-startkit/core lint
```

应用目录中的 `lint` 命令仅执行 `vue-tsc` ,用于当前应用的类型检查。

## 依赖管理

### 根目录依赖

在根目录安装的依赖为所有应用和包共享,通常是:
- ESLint、Stylelint 等代码规范工具
- 脚本工具(如 tsx、taze)
- Git 钩子相关依赖

### 应用/包级依赖

每个应用或包可以拥有自己独立的依赖,安装在各自的 `node_modules/` 目录下。

### 添加新依赖

```bash
# 为指定应用添加依赖
pnpm add axios -F @fantastic-startkit/core

# 为根目录添加开发依赖
pnpm add -D typescript -w
```

你也可以全局安装 [`@rizumu/nai`](https://github.com/LittleSound/nai) ,并通过交互式的 CLI 将依赖包安装到指定应用,如下图:

![](https://github.com/user-attachments/assets/83d164f3-8a13-41f1-a453-23ffd81ed387)


================================================
FILE: docs/guide/store.md
================================================
# 全局状态管理

:::tip 说明
[Pinia](https://pinia.vuejs.org/) 为 Vue.js 官方状态库,如果你对 Pinia 还不熟悉,请先阅读官方文档。
:::

全局状态文件存放在 `apps/<app>/src/store/modules/` 目录下,请按模块进行区分。同时请保证文件名和文件内唯一ID保持一致,建议使用 `pnpm new` 指令进行创建。

例如新建一个 `example.ts` 的文件:

```ts
export const useExampleStore = defineStore(
  // 唯一ID
  'example',
  () => {
    const someThing = ref(0)

    return {
      someThing,
    }
  },
)
```

使用方法:

```ts
const exampleStore = useExampleStore()

exampleStore.someThing
```


================================================
FILE: docs/index.md
================================================
---
layout: home

title: Fantastic-startkit
titleTemplate: 一款简单好用的 Vue3 项目启动套件

hero:
  name: Fantastic-startkit
  text: Vue3 项目启动套件
  tagline: 简单好用,就足够了
  actions:
    - theme: brand
      text: 开始
      link: /guide/ready

features:
- title: 先进的技术栈
  details: Vite + Vue3 + Vue-router + Pinia + UnoCSS ,采用业内先进的技术栈,使框架始终保持新鲜
- title: Monorepo 架构
  details: 基于 pnpm workspace 的 Monorepo 架构,支持多应用和公共包管理
- title: 全局 scss 资源引入
  details: 支持设置全局 scss 资源,如:变量、函数、mixin
- title: SVG 图标 & Iconify 图标
  details: SVG 图标自动载入,Iconify 图标支持按需引入
- title: 文件压缩
  details: 支持 gzip / brotli 优化项目体积,提高加载速度
- title: 代码规范
  details: 结合 IDE 插件、ESlint 、stylelint 、Git 钩子,轻松实现团队代码规范
---


================================================
FILE: docs/package.json
================================================
{
  "name": "@fantastic-startkit/docs",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "vitepress dev . --open",
    "build": "vitepress build ."
  },
  "dependencies": {
    "medium-zoom": "catalog:",
    "vitepress-plugin-comment-with-giscus": "catalog:docs"
  },
  "devDependencies": {
    "vitepress": "catalog:docs",
    "vue": "catalog:"
  }
}


================================================
FILE: docs/public/.nojekyll
================================================


================================================
FILE: docs/support.md
================================================
# 技术支持

在使用的过程中遇到问题请在仓库提交 Issue ,你可以更详细描述问题产生的操作步骤,或者提供完整的最小复现,这样做可以让更多的人参与讨论,也方便后人查阅。

- [Github Issue](https://github.com/hooray/fantastic-startkit/issues)
- [Gitee Issue](https://gitee.com/hooray/fantastic-startkit/issues)

================================================
FILE: docs/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ESNext",
    "jsx": "preserve",
    "lib": [
      "ESNext",
      "DOM"
    ],
    "useDefineForClassFields": true,
    "baseUrl": "./",
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "strict": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "exclude": [
    "**/cache/**",
    "**/dist/**",
    "**/node_modules/**"
  ]
}


================================================
FILE: eslint.config.js
================================================
import antfu from '@antfu/eslint-config'

export default antfu(
  {
    vue: true,
    unocss: true,
    ignores: [
      '**/public',
      '**/dist*',
      '**/*.md',
      '.vitepress/cache',
      '.vitepress/dist',
    ],
  },
  {
    rules: {
      'e18e/prefer-static-regex': 'off',
      'eslint-comments/no-unlimited-disable': 'off',
      'curly': ['error', 'all'],
      'ts/no-unused-expressions': ['error', {
        allowShortCircuit: true,
        allowTernary: true,
      }],
    },
  },
  {
    files: [
      '**/*.vue',
    ],
    rules: {
      'vue/block-order': ['error', {
        order: ['route', 'i18n', 'script', 'template', 'style'],
      }],
    },
  },
  {
    files: [
      'pnpm-workspace.yaml',
    ],
    rules: {
      'pnpm/yaml-enforce-settings': 'off',
    },
  },
)


================================================
FILE: package.json
================================================
{
  "name": "fantastic-startkit",
  "type": "module",
  "private": true,
  "packageManager": "pnpm@10.33.4",
  "engines": {
    "node": "^20.19.0 || ^22.13.0 || >=24"
  },
  "scripts": {
    "dev": "tsx scripts/cli.ts --mode=dev",
    "build": "tsx scripts/cli.ts --mode=build",
    "serve": "tsx scripts/cli.ts --mode=serve",
    "lint": "run-s lint:tsc lint:eslint lint:stylelint",
    "lint:tsc": "pnpm --filter './apps/*' -r run lint",
    "lint:eslint": "eslint . --cache --fix",
    "lint:stylelint": "stylelint \"{apps,packages}/**/*.{css,scss,vue}\" \"docs/.vitepress/**/*.{css,scss,vue}\" --cache --fix",
    "preinstall": "npx only-allow pnpm",
    "postinstall": "simple-git-hooks",
    "taze": "taze minor -wIr"
  },
  "devDependencies": {
    "@antfu/eslint-config": "catalog:",
    "@clack/prompts": "catalog:",
    "@stylistic/stylelint-config": "catalog:",
    "@types/cross-spawn": "catalog:",
    "@types/node": "catalog:",
    "@unocss/eslint-plugin": "catalog:",
    "cross-spawn": "catalog:",
    "eslint": "catalog:",
    "lint-staged": "catalog:",
    "npm-run-all2": "catalog:",
    "simple-git-hooks": "catalog:",
    "stylelint": "catalog:",
    "stylelint-config-recess-order": "catalog:",
    "stylelint-config-standard-scss": "catalog:",
    "stylelint-config-standard-vue": "catalog:",
    "stylelint-scss": "catalog:",
    "taze": "catalog:",
    "tsx": "catalog:",
    "typescript": "catalog:",
    "unocss": "catalog:"
  },
  "simple-git-hooks": {
    "pre-commit": "pnpm lint-staged",
    "preserveUnused": true
  }
}


================================================
FILE: packages/components/package.json
================================================
{
  "name": "@fantastic-startkit/components",
  "type": "module",
  "version": "0.0.0",
  "exports": {
    ".": "./src/index.ts",
    "./resolver": "./resolver.ts"
  },
  "peerDependencies": {
    "unplugin-vue-components": "catalog:",
    "vue": "catalog:"
  },
  "peerDependenciesMeta": {
    "unplugin-vue-components": {
      "optional": true
    }
  },
  "dependencies": {
    "@iconify/vue": "catalog:"
  }
}


================================================
FILE: packages/components/resolver.ts
================================================
import type { ComponentResolver, TypeImport } from 'unplugin-vue-components'
import { createRequire } from 'node:module'

const COMPONENT_PREFIX = 'Fs'
const PACKAGE_NAME = createRequire(import.meta.url)('./package.json').name

const COMPONENT_NAMES = [
  'FsIcon',
] as const

export function ComponentsResolver(): ComponentResolver {
  const names = new Set<string>(COMPONENT_NAMES)
  return {
    type: 'component',
    resolve(name: string) {
      if (name.startsWith(COMPONENT_PREFIX) && names.has(name)) {
        return {
          name,
          from: PACKAGE_NAME,
        }
      }
    },
  }
}

export const ComponentsType: TypeImport = {
  from: PACKAGE_NAME,
  names: [...COMPONENT_NAMES],
}


================================================
FILE: packages/components/src/icon/index.vue
================================================
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { computed } from 'vue'

defineOptions({
  name: 'FsIcon',
})

const props = defineProps<{
  name: string
}>()

const outputType = computed(() => {
  if (/i-[^:]+:[^:]+/.test(props.name)) {
    return 'unocss'
  }
  else if (props.name.includes(':')) {
    return 'iconify'
  }
  else {
    return 'svg'
  }
})
</script>

<template>
  <i class="leading-[1em] flex-inline size-[1em] items-center justify-center relative fill-current">
    <i v-if="outputType === 'unocss'" class="shrink-0 size-inherit" :class="name" />
    <Icon v-else-if="outputType === 'iconify'" :icon="name" class="shrink-0 size-inherit!" />
    <svg v-else-if="outputType === 'svg'" class="shrink-0 size-inherit" aria-hidden="true">
      <use :xlink:href="`./__spritemap#sprite-${name}`" />
    </svg>
  </i>
</template>


================================================
FILE: packages/components/src/index.ts
================================================
export { default as FsIcon } from './icon/index.vue'


================================================
FILE: packages/components/tsconfig.json
================================================
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "types": ["vite/client", "node"]
  },
  "include": [
    "resolver.ts",
    "src/**/*.ts",
    "src/**/*.vue",
    "src/**/*.d.ts"
  ]
}


================================================
FILE: packages/copyright/index.ts
================================================
import type { PluginOption } from 'vite'
import boxen from 'boxen'
import picocolors from 'picocolors'
import banner from 'vite-plugin-banner'

export interface CopyrightOptions {
  edition?: string
  website?: string
}

function resolveOptions(options: CopyrightOptions = {}) {
  return {
    edition: options.edition ?? 'Startkit',
    website: options.website ?? 'https://hurui.me/fantastic-startkit/',
  }
}

export function createBannerPlugin(options: CopyrightOptions = {}): PluginOption {
  const { website } = resolveOptions(options)

  return banner(`
/**
 * 由 Fantastic-startkit 提供技术支持
 * Powered by Fantastic-startkit
 * ${website}
 */
  `)
}

export function createTerminalInfoPlugin(options: CopyrightOptions = {}): PluginOption {
  const { edition, website } = resolveOptions(options)

  return {
    name: 'vite-plugin-terminal-info',
    apply: 'serve',
    async buildStart() {
      const { bold, green, magenta, bgGreen, underline } = picocolors

      // eslint-disable-next-line no-console
      console.log(
        boxen(
          `${bold(green(`由 ${bgGreen('Fantastic-startkit')} 驱动`))}\n\n${underline(website)}\n\n当前使用:${magenta(edition)}`,
          {
            padding: 1,
            margin: 1,
            borderStyle: 'double',
            textAlignment: 'center',
          },
        ),
      )
    },
  }
}

export function createSystemCopyrightPlugin(options: CopyrightOptions = {}): PluginOption {
  const { website } = resolveOptions(options)
  const fontFamily = 'font-family: "JetBrains Mono", "SF Mono", "Cascadia Code", Menlo, Consolas, "Liberation Mono", monospace;'
  const mainStyle = `${fontFamily} font-size: 14px; font-weight: 700; padding: 6px 8px; color: #35495e; background: #42b883;`
  const subStyle = `${fontFamily} font-size: 14px; font-weight: 400; padding: 6px 8px; color: #fff; background: #35495e;`
  const iconStyle = `${fontFamily} font-size: 14px; font-weight: 500; padding: 6px 4px 6px 8px; color: #d7f0ff; background: #1d5b85;`
  const linkStyle = `${fontFamily} font-size: 14px; font-weight: 500; letter-spacing: 0.2px; padding: 6px 8px 6px 4px; color: #d7f0ff; background: #1d5b85; text-decoration: underline; text-underline-offset: 2px;`

  return {
    name: 'vite-plugin-system-copyright',
    apply: 'build',
    transform(code, id) {
      if (!/src\/main\.ts$/.test(id)) {
        return
      }

      return {
        code: `
/* eslint-disable no-console */
if ((navigator.language).toLowerCase() === 'zh-cn') {
  console.info('%c由%cFantastic-startkit%c驱动%c👉%c${website}', ${JSON.stringify(subStyle)}, ${JSON.stringify(mainStyle)}, ${JSON.stringify(subStyle)}, ${JSON.stringify(iconStyle)}, ${JSON.stringify(linkStyle)})
}
else {
  console.info('%cPowered by %cFantastic-startkit%c%c👉%c${website}', ${JSON.stringify(subStyle)}, ${JSON.stringify(mainStyle)}, ${JSON.stringify(subStyle)}, ${JSON.stringify(iconStyle)}, ${JSON.stringify(linkStyle)})
}

${code}
        `,
        map: null,
      }
    },
  }
}

export function createCopyrightPlugins(options: CopyrightOptions = {}): PluginOption[] {
  return [
    createBannerPlugin(options),
    createTerminalInfoPlugin(options),
    createSystemCopyrightPlugin(options),
  ]
}


================================================
FILE: packages/copyright/package.json
================================================
{
  "name": "@fantastic-startkit/copyright",
  "type": "module",
  "version": "0.0.0",
  "exports": {
    ".": "./index.ts"
  },
  "peerDependencies": {
    "vite": "catalog:"
  },
  "dependencies": {
    "boxen": "catalog:",
    "picocolors": "catalog:",
    "vite-plugin-banner": "catalog:"
  }
}


================================================
FILE: packages/copyright/tsconfig.json
================================================
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "types": ["node"]
  },
  "include": [
    "index.ts",
    "build.config.ts"
  ]
}


================================================
FILE: pnpm-workspace.yaml
================================================
autoInstallPeers: true
catalogMode: prefer
cleanupUnusedCatalogs: true
engineStrict: true
ignoreWorkspaceRootCheck: true
shamefullyHoist: true
shellEmulator: true
verifyDepsBeforeRun: prompt

packages:
  - packages/*
  - apps/*
  - docs
catalog:
  '@antfu/eslint-config': ^9.0.0
  '@clack/prompts': ^1.4.0
  '@faker-js/faker': ^10.4.0
  '@iconify/json': ^2.2.474
  '@iconify/vue': ^5.0.1
  '@spiriit/vite-plugin-svg-spritemap': ^7.0.0
  '@stylistic/stylelint-config': ^5.0.0
  '@types/cross-spawn': ^6.0.6
  '@types/node': ^25.8.0
  '@types/nprogress': ^0.2.3
  '@types/qs': ^6.15.1
  '@unocss/eslint-plugin': ^66.6.8
  '@vitejs/plugin-vue': ^6.0.7
  '@vitejs/plugin-vue-jsx': ^5.1.5
  '@vue/tsconfig': ^0.9.1
  autoprefixer: ^10.5.0
  axios: ^1.16.1
  boxen: ^8.0.1
  cross-spawn: ^7.0.6
  dayjs: ^1.11.20
  eruda: ^3.4.3
  eslint: ^10.4.0
  http-server: ^14.1.1
  lint-staged: ^17.0.5
  medium-zoom: ^1.1.0
  mitt: ^3.0.1
  npm-run-all2: ^8.0.4
  nprogress: ^0.2.0
  picocolors: ^1.1.1
  pinia: ^3.0.4
  pinia-plugin-persistedstate: ^4.7.1
  postcss: ^8.5.14
  postcss-nested: ^7.0.2
  qs: ^6.15.2
  sass-embedded: ^1.99.0
  simple-git-hooks: ^2.13.1
  stylelint: ^17.11.1
  stylelint-config-recess-order: ^7.7.0
  stylelint-config-standard-scss: ^17.0.0
  stylelint-config-standard-vue: ^1.0.0
  stylelint-scss: ^7.1.1
  svgo: ^4.0.1
  taze: ^19.12.0
  tsx: ^4.22.1
  typescript: ^6.0.3
  unocss: ^66.6.8
  unplugin-auto-import: ^21.0.0
  unplugin-turbo-console: ^2.3.2
  unplugin-vue-components: ^32.0.0
  vconsole: ^3.15.1
  vite: ^8.0.13
  vite-plugin-archiver: ^0.3.2
  vite-plugin-banner: ^0.8.1
  vite-plugin-compression2: ^2.5.3
  vite-plugin-env-parse: ^1.0.15
  vite-plugin-fake-server: ^2.2.4
  vite-plugin-pages: ^0.33.3
  vite-plugin-vue-devtools: ^8.1.2
  vite-plugin-vue-layouts-next: ^2.1.0
  vue: ^3.5.34
  vue-router: ^5.0.7
  vue-tsc: ^3.2.9
catalogs:
  docs:
    vitepress: ^1.6.4
    vitepress-plugin-comment-with-giscus: ^1.1.15


================================================
FILE: scripts/cli.ts
================================================
#!/usr/bin/env node

import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import * as p from '@clack/prompts'
import spawn from 'cross-spawn'

interface PackageJson {
  name: string
  description?: string
  scripts?: Record<string, string>
}

interface WorkspaceEntry {
  name: string
  packageJsonPath: string
}

interface AppInfo {
  name: string
  packageName: string
  description: string
  scripts: string[]
}

interface RunMessageContext {
  app: AppInfo
  script: string
}

interface ModeConfig {
  action: Mode
  intro: string
  noAppsMessage: string
  appSelectMessage: string
  scriptMatcher: (scriptName: string) => boolean
  selectionMode: 'single' | 'multiple'
  completionMessage?: string
  getRunMessage?: (context: RunMessageContext) => string
  showRunMessageWhenSingleApp?: boolean
}

type Mode = 'build' | 'dev' | 'serve'

const currentDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(currentDir, '..')
const validModes: Mode[] = ['build', 'dev', 'serve']
const modeConfigs: Record<Mode, ModeConfig> = {
  build: {
    action: 'build',
    intro: 'Select apps to build',
    noAppsMessage: 'No apps found with build script',
    appSelectMessage: 'Which apps do you want to build?',
    scriptMatcher: isBuildScript,
    selectionMode: 'multiple',
    completionMessage: 'Build complete!',
    getRunMessage: ({ app, script }) => `Building "${app.name}" with script "${script}"...`,
  },
  dev: {
    action: 'dev',
    intro: 'Select an app to run',
    noAppsMessage: 'No apps found with dev script',
    appSelectMessage: 'Which app do you want to run?',
    scriptMatcher: isDevScript,
    selectionMode: 'single',
    getRunMessage: ({ app }) => `Starting ${app.packageName}...`,
  },
  serve: {
    action: 'serve',
    intro: 'Select an app to serve',
    noAppsMessage: 'No apps found with serve script',
    appSelectMessage: 'Which app do you want to serve?',
    scriptMatcher: isServeScript,
    selectionMode: 'single',
    getRunMessage: ({ app, script }) => `Serving "${app.name}" with script "${script}"...`,
  },
}

let childProcess: ReturnType<typeof spawn> | null = null

process.on('SIGINT', () => {
  childProcess?.kill('SIGINT')
})

function isBuildScript(scriptName: string): boolean {
  return scriptName === 'build' || scriptName.startsWith('build:')
}

function isDevScript(scriptName: string): boolean {
  return scriptName === 'dev' || scriptName.startsWith('dev:')
}

function isServeScript(scriptName: string): boolean {
  return scriptName === 'serve' || scriptName.startsWith('serve:')
}

function getWorkspaceEntries(): WorkspaceEntry[] {
  const entries: WorkspaceEntry[] = []
  const appsDir = path.resolve(rootDir, 'apps')

  if (fs.existsSync(appsDir)) {
    const apps = fs.readdirSync(appsDir, { withFileTypes: true })

    for (const app of apps) {
      if (!app.isDirectory()) {
        continue
      }

      const packageJsonPath = path.join(appsDir, app.name, 'package.json')
      if (!fs.existsSync(packageJsonPath)) {
        continue
      }

      entries.push({
        name: app.name,
        packageJsonPath,
      })
    }
  }

  const docsPackageJsonPath = path.resolve(rootDir, 'docs', 'package.json')
  if (fs.existsSync(docsPackageJsonPath)) {
    entries.push({
      name: 'docs',
      packageJsonPath: docsPackageJsonPath,
    })
  }

  return entries
}

function readPackageJson(packageJsonPath: string): PackageJson {
  return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as PackageJson
}

function getApps(scriptMatcher: (scriptName: string) => boolean): AppInfo[] {
  const apps: AppInfo[] = []

  for (const entry of getWorkspaceEntries()) {
    const packageJson = readPackageJson(entry.packageJsonPath)
    const scripts = Object.keys(packageJson.scripts ?? {}).filter(scriptMatcher)

    if (scripts.length === 0) {
      continue
    }

    apps.push({
      name: entry.name,
      packageName: packageJson.name,
      description: packageJson.description || '',
      scripts,
    })
  }

  return apps
}

function getAppChoices(apps: AppInfo[]): Array<{ value: string, label: string, hint: string }> {
  return apps.map(app => ({
    value: app.packageName,
    label: app.name,
    hint: app.description,
  }))
}

function getAppByPackageName(apps: AppInfo[], packageName: string): AppInfo {
  const app = apps.find(item => item.packageName === packageName)

  if (!app) {
    throw new Error(`App not found: ${packageName}`)
  }

  return app
}

function resolvePromptValue<T>(value: T | symbol): T {
  if (p.isCancel(value)) {
    p.cancel('Operation cancelled')
    process.exit(0)
  }

  return value
}

async function selectScript(app: AppInfo, action: Mode): Promise<string> {
  if (app.scripts.length === 1) {
    return app.scripts[0]
  }

  const script = await p.select({
    message: `Select ${action} script for "${app.name}":`,
    options: app.scripts.map(name => ({
      value: name,
      label: name,
    })),
  })

  return resolvePromptValue(script)
}

async function runPackageScript(packageName: string, script: string): Promise<void> {
  return new Promise((resolve, reject) => {
    childProcess = spawn('pnpm', ['--filter', packageName, 'run', script], {
      stdio: 'inherit',
      cwd: rootDir,
    })

    childProcess.on('close', (code) => {
      childProcess = null

      if (code === 0 || code === null) {
        resolve()
        return
      }

      reject(new Error(`Process exited with code ${code}`))
    })

    childProcess.on('error', (error) => {
      childProcess = null
      reject(error)
    })
  })
}

async function selectApps(apps: AppInfo[], message: string, selectionMode: ModeConfig['selectionMode']): Promise<AppInfo[]> {
  if (selectionMode === 'multiple') {
    const selectedPackageNames = resolvePromptValue(await p.multiselect({
      message,
      options: getAppChoices(apps),
      required: true,
    }))

    return selectedPackageNames.map(packageName => getAppByPackageName(apps, packageName))
  }

  const selectedPackageName = resolvePromptValue(await p.select({
    message,
    options: getAppChoices(apps),
  }))

  return [getAppByPackageName(apps, selectedPackageName)]
}

async function runSelectedApps(apps: AppInfo[], config: ModeConfig, showRunMessage: boolean): Promise<void> {
  for (const app of apps) {
    const script = await selectScript(app, config.action)

    if (showRunMessage) {
      const runMessage = config.getRunMessage?.({
        app,
        script,
      })

      if (runMessage) {
        p.log.info(runMessage)
      }
    }

    await runPackageScript(app.packageName, script)
  }
}

async function runMode(config: ModeConfig): Promise<void> {
  p.intro(config.intro)

  const apps = getApps(config.scriptMatcher)

  if (apps.length === 0) {
    p.log.error(config.noAppsMessage)
    process.exit(1)
  }

  if (apps.length === 1) {
    p.log.info(`Only one app found: ${apps[0].name}`)
    await runSelectedApps([apps[0]], config, config.showRunMessageWhenSingleApp ?? false)

    if (config.completionMessage) {
      p.outro(config.completionMessage)
    }

    return
  }

  const selectedApps = await selectApps(apps, config.appSelectMessage, config.selectionMode)
  await runSelectedApps(selectedApps, config, true)

  if (config.completionMessage) {
    p.outro(config.completionMessage)
  }
}

function parseModeArg(argv: string[]): string | undefined {
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index]

    if (arg === '--mode' || arg === '-m') {
      return argv[index + 1]
    }

    if (arg.startsWith('--mode=')) {
      return arg.slice('--mode='.length)
    }

    if (arg.startsWith('-m=')) {
      return arg.slice('-m='.length)
    }
  }

  return undefined
}

function hasHelpArg(argv: string[]): boolean {
  return argv.includes('--help') || argv.includes('-h')
}

function isMode(value: string | undefined): value is Mode {
  return value !== undefined && validModes.includes(value as Mode)
}

function printHelp(): void {
  console.log(`Usage: tsx scripts/cli.ts --mode=<${validModes.join('|')}>`)
  console.log('Examples:')
  console.log('  pnpm dev')
  console.log('  pnpm build')
  console.log('  pnpm serve')
  console.log('  pnpm cli -- --mode=dev')
}

async function main(): Promise<void> {
  const argv = process.argv.slice(2)
  const modeArg = parseModeArg(argv)

  if (hasHelpArg(argv)) {
    printHelp()
    return
  }

  if (!isMode(modeArg)) {
    p.log.error(`Invalid or missing mode. Expected one of: ${validModes.join(', ')}`)
    printHelp()
    process.exit(1)
  }

  await runMode(modeConfigs[modeArg])
}

main().catch((error: Error) => {
  p.log.error(error.message)
  process.exit(1)
})


================================================
FILE: stylelint.config.js
================================================
export default {
  extends: [
    'stylelint-config-standard-scss',
    'stylelint-config-standard-vue/scss',
    'stylelint-config-recess-order',
    '@stylistic/stylelint-config',
  ],
  plugins: [
    'stylelint-scss',
  ],
  rules: {
    'at-rule-no-unknown': null,
    'no-descending-specificity': null,
    'property-no-unknown': null,
    'font-family-no-missing-generic-family-keyword': null,
    'selector-class-pattern': null,
    'scss/double-slash-comment-empty-line-before': null,
    'scss/no-global-function-names': null,
    '@stylistic/max-line-length': null,
    '@stylistic/block-closing-brace-newline-after': [
      'always',
      {
        ignoreAtRules: ['if', 'else'],
      },
    ],
  },
  allowEmptyInput: true,
  ignoreFiles: [
    'node_modules/**/*',
    'dist*/**/*',
    'docs/.vitepress/cache/**/*',
    'docs/.vitepress/dist/**/*',
  ],
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["scripts/**/*"]
}


================================================
FILE: uno.config.ts
================================================
import {
  defineConfig,
  presetAttributify,
  presetIcons,
  presetTypography,
  presetWind4,
  transformerCompileClass,
  transformerDirectives,
  transformerVariantGroup,
} from 'unocss'

export default defineConfig({
  shortcuts: [
    [/^flex-?(col)?-(start|end|center|baseline|stretch)-?(start|end|center|between|around|evenly|left|right)?$/, ([, col, items, justify]) => {
      const cls = ['flex']
      if (col === 'col') {
        cls.push('flex-col')
      }
      if (items === 'center' && !justify) {
        cls.push('items-center')
        cls.push('justify-center')
      }
      else {
        cls.push(`items-${items}`)
        if (justify) {
          cls.push(`justify-${justify}`)
        }
      }
      return cls.join(' ')
    }],
  ],
  presets: [
    presetWind4(),
    presetAttributify(),
    presetIcons({
      extraProperties: {
        'display': 'inline-block',
        'vertical-align': 'middle',
      },
    }),
    presetTypography(),
  ],
  transformers: [
    transformerDirectives(),
    transformerVariantGroup(),
    transformerCompileClass(),
  ],
})
Download .txt
gitextract_vtf2u88_/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── docs.yml
│       └── sync.yml
├── .gitignore
├── .lintstagedrc
├── .node-version
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── LICENSE
├── README.md
├── apps/
│   ├── core/
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── postcss.config.js
│   │   ├── src/
│   │   │   ├── App.vue
│   │   │   ├── api/
│   │   │   │   ├── index.ts
│   │   │   │   └── modules/
│   │   │   │       ├── user.fake.ts
│   │   │   │       └── user.ts
│   │   │   ├── assets/
│   │   │   │   ├── icons/
│   │   │   │   │   └── .gitkeep
│   │   │   │   ├── images/
│   │   │   │   │   └── .gitkeep
│   │   │   │   └── styles/
│   │   │   │       ├── globals.css
│   │   │   │       └── resources/
│   │   │   │           ├── utils.scss
│   │   │   │           └── variables.scss
│   │   │   ├── components/
│   │   │   │   └── .gitkeep
│   │   │   ├── composables/
│   │   │   │   └── useGlobalProperties.ts
│   │   │   ├── layouts/
│   │   │   │   └── index.vue
│   │   │   ├── main.ts
│   │   │   ├── router/
│   │   │   │   ├── index.ts
│   │   │   │   └── modules/
│   │   │   │       └── root.ts
│   │   │   ├── store/
│   │   │   │   ├── index.ts
│   │   │   │   └── modules/
│   │   │   │       ├── settings.ts
│   │   │   │       └── user.ts
│   │   │   ├── types/
│   │   │   │   ├── auto-imports.d.ts
│   │   │   │   ├── components.d.ts
│   │   │   │   ├── env.d.ts
│   │   │   │   ├── shims.d.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── utils/
│   │   │   │   ├── dayjs.ts
│   │   │   │   └── eventBus.ts
│   │   │   └── views/
│   │   │       ├── [...all].vue
│   │   │       ├── index.vue
│   │   │       └── login.vue
│   │   ├── tsconfig.app.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   ├── vite/
│   │   │   └── plugins.ts
│   │   └── vite.config.ts
│   └── example/
│       ├── index.html
│       ├── package.json
│       ├── postcss.config.js
│       ├── src/
│       │   ├── App.vue
│       │   ├── api/
│       │   │   ├── index.ts
│       │   │   └── modules/
│       │   │       ├── news.fake.ts
│       │   │       ├── news.ts
│       │   │       ├── user.fake.ts
│       │   │       └── user.ts
│       │   ├── assets/
│       │   │   └── styles/
│       │   │       ├── globals.css
│       │   │       └── resources/
│       │   │           ├── utils.scss
│       │   │           └── variables.scss
│       │   ├── components/
│       │   │   └── DemoButton/
│       │   │       └── index.vue
│       │   ├── composables/
│       │   │   └── useGlobalProperties.ts
│       │   ├── layouts/
│       │   │   ├── example.vue
│       │   │   └── index.vue
│       │   ├── main.ts
│       │   ├── router/
│       │   │   ├── index.ts
│       │   │   └── modules/
│       │   │       ├── example.ts
│       │   │       └── root.ts
│       │   ├── store/
│       │   │   ├── index.ts
│       │   │   └── modules/
│       │   │       ├── example.ts
│       │   │       ├── settings.ts
│       │   │       └── user.ts
│       │   ├── types/
│       │   │   ├── auto-imports.d.ts
│       │   │   ├── components.d.ts
│       │   │   ├── env.d.ts
│       │   │   ├── shims.d.ts
│       │   │   └── vite-env.d.ts
│       │   ├── utils/
│       │   │   ├── dayjs.ts
│       │   │   └── eventBus.ts
│       │   └── views/
│       │       ├── [...all].vue
│       │       ├── example/
│       │       │   ├── axios.vue
│       │       │   ├── component.vue
│       │       │   ├── components/
│       │       │   │   └── ExampleList/
│       │       │   │       └── index.vue
│       │       │   ├── icon.vue
│       │       │   ├── params/
│       │       │   │   └── [test].vue
│       │       │   ├── permission-js.vue
│       │       │   ├── permission-router.vue
│       │       │   ├── pinia.vue
│       │       │   ├── query.vue
│       │       │   └── reload.vue
│       │       ├── index.vue
│       │       └── login.vue
│       ├── tsconfig.app.json
│       ├── tsconfig.json
│       ├── tsconfig.node.json
│       ├── vite/
│       │   └── plugins.ts
│       └── vite.config.ts
├── docs/
│   ├── .gitignore
│   ├── .vitepress/
│   │   ├── config.ts
│   │   └── theme/
│   │       ├── components/
│   │       │   └── SponsorsAside.vue
│   │       ├── fonts/
│   │       │   └── fira_code/
│   │       │       └── fira_code.css
│   │       ├── index.ts
│   │       └── styles/
│   │           └── var.css
│   ├── guide/
│   │   ├── api.md
│   │   ├── axios.md
│   │   ├── build.md
│   │   ├── coding-standard.md
│   │   ├── env.md
│   │   ├── icon.md
│   │   ├── ready.md
│   │   ├── resources.md
│   │   ├── router.md
│   │   ├── start.md
│   │   └── store.md
│   ├── index.md
│   ├── package.json
│   ├── public/
│   │   └── .nojekyll
│   ├── support.md
│   └── tsconfig.json
├── eslint.config.js
├── package.json
├── packages/
│   ├── components/
│   │   ├── package.json
│   │   ├── resolver.ts
│   │   ├── src/
│   │   │   ├── icon/
│   │   │   │   └── index.vue
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── copyright/
│       ├── index.ts
│       ├── package.json
│       └── tsconfig.json
├── pnpm-workspace.yaml
├── scripts/
│   └── cli.ts
├── stylelint.config.js
├── tsconfig.json
└── uno.config.ts
Download .txt
SYMBOL INDEX (68 symbols across 24 files)

FILE: apps/core/src/api/index.ts
  constant MAX_RETRY_COUNT (line 5) | const MAX_RETRY_COUNT = 3 // 最大重试次数
  constant RETRY_DELAY (line 6) | const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)
  type AxiosRequestConfig (line 10) | interface AxiosRequestConfig {
  function handleError (line 48) | function handleError(error: any) {

FILE: apps/core/src/composables/useGlobalProperties.ts
  function useGlobalProperties (line 3) | function useGlobalProperties() {

FILE: apps/core/src/store/modules/settings.ts
  function setTitle (line 8) | function setTitle(val: string) {

FILE: apps/core/src/store/modules/user.ts
  function login (line 17) | function login(data: {
  function logout (line 31) | function logout(redirect = router.currentRoute.value.fullPath) {

FILE: apps/core/src/types/components.d.ts
  type GlobalComponents (line 14) | interface GlobalComponents {

FILE: apps/core/src/types/env.d.ts
  type ImportMetaEnv (line 2) | interface ImportMetaEnv {

FILE: apps/core/src/types/shims.d.ts
  type Window (line 3) | interface Window {
  type RouteMeta (line 8) | interface RouteMeta {

FILE: apps/core/src/utils/eventBus.ts
  type MittTypes (line 3) | interface MittTypes {

FILE: apps/core/vite/plugins.ts
  function createVitePlugins (line 21) | function createVitePlugins(mode: string, isBuild = false) {

FILE: apps/example/src/api/index.ts
  constant MAX_RETRY_COUNT (line 5) | const MAX_RETRY_COUNT = 3 // 最大重试次数
  constant RETRY_DELAY (line 6) | const RETRY_DELAY = 1000 // 重试延迟时间(毫秒)
  type AxiosRequestConfig (line 10) | interface AxiosRequestConfig {
  function handleError (line 48) | function handleError(error: any) {

FILE: apps/example/src/composables/useGlobalProperties.ts
  function useGlobalProperties (line 3) | function useGlobalProperties() {

FILE: apps/example/src/router/modules/example.ts
  function ExampleLayout (line 3) | function ExampleLayout() {

FILE: apps/example/src/store/modules/example.ts
  function getNews (line 15) | function getNews() {
  function removeLast (line 20) | function removeLast() {

FILE: apps/example/src/store/modules/settings.ts
  function setTitle (line 8) | function setTitle(val: string) {

FILE: apps/example/src/store/modules/user.ts
  function login (line 17) | function login(data: {
  function logout (line 31) | function logout(redirect = router.currentRoute.value.fullPath) {

FILE: apps/example/src/types/components.d.ts
  type GlobalComponents (line 14) | interface GlobalComponents {

FILE: apps/example/src/types/env.d.ts
  type ImportMetaEnv (line 2) | interface ImportMetaEnv {

FILE: apps/example/src/types/shims.d.ts
  type Window (line 3) | interface Window {
  type RouteMeta (line 8) | interface RouteMeta {

FILE: apps/example/src/utils/eventBus.ts
  type MittTypes (line 3) | interface MittTypes {

FILE: apps/example/vite/plugins.ts
  function createVitePlugins (line 21) | function createVitePlugins(mode: string, isBuild = false) {

FILE: docs/.vitepress/theme/index.ts
  method Layout (line 12) | Layout() {
  method setup (line 17) | setup() {

FILE: packages/components/resolver.ts
  constant COMPONENT_PREFIX (line 4) | const COMPONENT_PREFIX = 'Fs'
  constant PACKAGE_NAME (line 5) | const PACKAGE_NAME = createRequire(import.meta.url)('./package.json').name
  constant COMPONENT_NAMES (line 7) | const COMPONENT_NAMES = [
  function ComponentsResolver (line 11) | function ComponentsResolver(): ComponentResolver {

FILE: packages/copyright/index.ts
  type CopyrightOptions (line 6) | interface CopyrightOptions {
  function resolveOptions (line 11) | function resolveOptions(options: CopyrightOptions = {}) {
  function createBannerPlugin (line 18) | function createBannerPlugin(options: CopyrightOptions = {}): PluginOption {
  function createTerminalInfoPlugin (line 30) | function createTerminalInfoPlugin(options: CopyrightOptions = {}): Plugi...
  function createSystemCopyrightPlugin (line 55) | function createSystemCopyrightPlugin(options: CopyrightOptions = {}): Pl...
  function createCopyrightPlugins (line 89) | function createCopyrightPlugins(options: CopyrightOptions = {}): PluginO...

FILE: scripts/cli.ts
  type PackageJson (line 10) | interface PackageJson {
  type WorkspaceEntry (line 16) | interface WorkspaceEntry {
  type AppInfo (line 21) | interface AppInfo {
  type RunMessageContext (line 28) | interface RunMessageContext {
  type ModeConfig (line 33) | interface ModeConfig {
  type Mode (line 45) | type Mode = 'build' | 'dev' | 'serve'
  function isBuildScript (line 87) | function isBuildScript(scriptName: string): boolean {
  function isDevScript (line 91) | function isDevScript(scriptName: string): boolean {
  function isServeScript (line 95) | function isServeScript(scriptName: string): boolean {
  function getWorkspaceEntries (line 99) | function getWorkspaceEntries(): WorkspaceEntry[] {
  function readPackageJson (line 134) | function readPackageJson(packageJsonPath: string): PackageJson {
  function getApps (line 138) | function getApps(scriptMatcher: (scriptName: string) => boolean): AppInf...
  function getAppChoices (line 160) | function getAppChoices(apps: AppInfo[]): Array<{ value: string, label: s...
  function getAppByPackageName (line 168) | function getAppByPackageName(apps: AppInfo[], packageName: string): AppI...
  function resolvePromptValue (line 178) | function resolvePromptValue<T>(value: T | symbol): T {
  function selectScript (line 187) | async function selectScript(app: AppInfo, action: Mode): Promise<string> {
  function runPackageScript (line 203) | async function runPackageScript(packageName: string, script: string): Pr...
  function selectApps (line 228) | async function selectApps(apps: AppInfo[], message: string, selectionMod...
  function runSelectedApps (line 247) | async function runSelectedApps(apps: AppInfo[], config: ModeConfig, show...
  function runMode (line 266) | async function runMode(config: ModeConfig): Promise<void> {
  function parseModeArg (line 295) | function parseModeArg(argv: string[]): string | undefined {
  function hasHelpArg (line 315) | function hasHelpArg(argv: string[]): boolean {
  function isMode (line 319) | function isMode(value: string | undefined): value is Mode {
  function printHelp (line 323) | function printHelp(): void {
  function main (line 332) | async function main(): Promise<void> {
Condensed preview — 132 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (180K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 1564,
    "preview": "name: Deploy Docs\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'docs/**'\n      - pnpm-lock.yaml\n      - pnpm-wor"
  },
  {
    "path": ".github/workflows/sync.yml",
    "chars": 610,
    "preview": "name: branches-sync\n\non:\n  # 每天 00:00 自动同步\n  schedule:\n    - cron: '0 0 * * *'\n  # 手动触发部署\n  workflow_dispatch:\n\njobs:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 82,
    "preview": "node_modules\n.DS_Store\ndist*\ndist-ssr\n*.local\n.eslintcache\n.stylelintcache\n.turbo\n"
  },
  {
    "path": ".lintstagedrc",
    "chars": 96,
    "preview": "{\n  \"*.{ts,tsx,vue}\": \"eslint --cache --fix\",\n  \"*.{css,scss,vue}\": \"stylelint --cache --fix\"\n}\n"
  },
  {
    "path": ".node-version",
    "chars": 3,
    "preview": "24\n"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 219,
    "preview": "{\n  \"recommendations\": [\n    \"EditorConfig.EditorConfig\",\n    \"mikestead.dotenv\",\n    \"dbaeumer.vscode-eslint\",\n    \"sty"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 649,
    "preview": "{\n  \"npm.packageManager\": \"pnpm\",\n  \"js/ts.tsdk.path\": \"node_modules/typescript/lib\",\n  \"search.exclude\": {\n    \"**/dist"
  },
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "MIT License\n\nCopyright (c) 2020 fantastic-startkit\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 1168,
    "preview": "# Fantastic-startkit\n\n<p><b>简单好用</b>的 Vue3 项目启动套件</p>\n\n<p>\n  <a href=\"https://hurui.me/fantastic-startkit/\" target=\"_bla"
  },
  {
    "path": "apps/core/index.html",
    "chars": 320,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.sv"
  },
  {
    "path": "apps/core/package.json",
    "chars": 2038,
    "preview": "{\n  \"name\": \"@fantastic-startkit/core\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"bu"
  },
  {
    "path": "apps/core/postcss.config.js",
    "chars": 87,
    "preview": "export default {\n  plugins: {\n    'autoprefixer': {},\n    'postcss-nested': {},\n  },\n}\n"
  },
  {
    "path": "apps/core/src/App.vue",
    "chars": 508,
    "preview": "<script setup lang=\"ts\">\nconst isRouterAlive = ref(true)\nconst settingsStore = useSettingsStore()\n\nprovide('reload', rel"
  },
  {
    "path": "apps/core/src/api/index.ts",
    "chars": 2573,
    "preview": "import axios from 'axios'\n// import qs from 'qs'\n\n// 请求重试配置\nconst MAX_RETRY_COUNT = 3 // 最大重试次数\nconst RETRY_DELAY = 1000"
  },
  {
    "path": "apps/core/src/api/modules/user.fake.ts",
    "chars": 418,
    "preview": "import { faker } from '@faker-js/faker'\nimport { defineFakeRoute } from 'vite-plugin-fake-server/client'\n\nexport default"
  },
  {
    "path": "apps/core/src/api/modules/user.ts",
    "chars": 175,
    "preview": "import api from '../index'\n\nexport default {\n  // 登录\n  login: (data: {\n    account: string\n    password: string\n  }) => "
  },
  {
    "path": "apps/core/src/assets/icons/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "apps/core/src/assets/images/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "apps/core/src/assets/styles/globals.css",
    "chars": 11,
    "preview": "/* 全局样式 */\n"
  },
  {
    "path": "apps/core/src/assets/styles/resources/utils.scss",
    "chars": 911,
    "preview": "// @mixin 通过 @include 调用使用\n// % 通过 @extend 调用使用\n\n// 文字超出隐藏,默认为单行超出隐藏,可设置多行\n@mixin text-overflow($line: 1, $fixed-width: "
  },
  {
    "path": "apps/core/src/assets/styles/resources/variables.scss",
    "chars": 8,
    "preview": "// 全局变量\n"
  },
  {
    "path": "apps/core/src/components/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "apps/core/src/composables/useGlobalProperties.ts",
    "chars": 223,
    "preview": "import type { ComponentInternalInstance } from 'vue'\n\nexport default function useGlobalProperties() {\n  const { appConte"
  },
  {
    "path": "apps/core/src/layouts/index.vue",
    "chars": 40,
    "preview": "<template>\n  <RouterView />\n</template>\n"
  },
  {
    "path": "apps/core/src/main.ts",
    "chars": 237,
    "preview": "import App from './App.vue'\nimport router from './router'\nimport pinia from './store'\n\nimport 'virtual:uno.css'\n\n// 全局样式"
  },
  {
    "path": "apps/core/src/router/index.ts",
    "chars": 1241,
    "preview": "import type { RouteRecordRaw } from 'vue-router'\nimport NProgress from 'nprogress'\n// import { setupLayouts } from 'virt"
  },
  {
    "path": "apps/core/src/router/modules/root.ts",
    "chars": 301,
    "preview": "import type { RouteRecordRaw } from 'vue-router'\n\nconst routes: RouteRecordRaw[] = [\n  {\n    path: '/',\n    component: ("
  },
  {
    "path": "apps/core/src/store/index.ts",
    "chars": 156,
    "preview": "import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'\n\nconst pinia = createPinia()\npinia.use(piniaPluginPe"
  },
  {
    "path": "apps/core/src/store/modules/settings.ts",
    "chars": 243,
    "preview": "export const useSettingsStore = defineStore(\n  // 唯一ID\n  'settings',\n  () => {\n    const title = ref('')\n\n    // 设置网页标题\n"
  },
  {
    "path": "apps/core/src/store/modules/user.ts",
    "chars": 1082,
    "preview": "import apiUser from '@/api/modules/user'\nimport router from '@/router'\n\nexport const useUserStore = defineStore(\n  // 唯一"
  },
  {
    "path": "apps/core/src/types/auto-imports.d.ts",
    "chars": 4889,
    "preview": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin"
  },
  {
    "path": "apps/core/src/types/components.d.ts",
    "chars": 510,
    "preview": "/* eslint-disable */\n// @ts-nocheck\n// biome-ignore lint: disable\n// oxlint-disable\n// ------\n// Generated by unplugin-v"
  },
  {
    "path": "apps/core/src/types/env.d.ts",
    "chars": 744,
    "preview": "/// <reference types=\"vite/client\" />\ninterface ImportMetaEnv {\n  // Auto generate by env-parse\n  /**\n   * 接口请求地址,会设置到 a"
  },
  {
    "path": "apps/core/src/types/shims.d.ts",
    "chars": 153,
    "preview": "import 'vue-router'\n\ndeclare interface Window {\n  // extend the window\n}\n\ndeclare module 'vue-router' {\n  interface Rout"
  },
  {
    "path": "apps/core/src/types/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/core/src/utils/dayjs.ts",
    "chars": 99,
    "preview": "import dayjs from 'dayjs'\nimport 'dayjs/locale/zh-cn'\n\ndayjs.locale('zh-cn')\n\nexport default dayjs\n"
  },
  {
    "path": "apps/core/src/utils/eventBus.ts",
    "chars": 113,
    "preview": "import mitt from 'mitt'\n\ninterface MittTypes {\n  [key: string | symbol]: any\n}\n\nexport default mitt<MittTypes>()\n"
  },
  {
    "path": "apps/core/src/views/[...all].vue",
    "chars": 127,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: false\n  title: 找不到页面\n</route>\n\n<template>\n  <div>\n    我真的尽力了,但还是找不到页面\n  </div>\n</tem"
  },
  {
    "path": "apps/core/src/views/index.vue",
    "chars": 1904,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: false\n</route>\n\n<template>\n  <div class=\"bg-slate-50 flex flex-col min-h-screen item"
  },
  {
    "path": "apps/core/src/views/login.vue",
    "chars": 2422,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: false\n  title: 登录\n</route>\n\n<script setup lang=\"ts\">\nconst router = useRouter()\ncons"
  },
  {
    "path": "apps/core/tsconfig.app.json",
    "chars": 788,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsc"
  },
  {
    "path": "apps/core/tsconfig.json",
    "chars": 119,
    "preview": "{\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ],\n  \"files\": []\n}\n"
  },
  {
    "path": "apps/core/tsconfig.node.json",
    "chars": 597,
    "preview": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n"
  },
  {
    "path": "apps/core/vite/plugins.ts",
    "chars": 4043,
    "preview": "import type { PluginOption } from 'vite'\nimport process from 'node:process'\nimport { ComponentsResolver as FantasticStar"
  },
  {
    "path": "apps/core/vite.config.ts",
    "chars": 1638,
    "preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { defineConfig, loadEnv "
  },
  {
    "path": "apps/example/index.html",
    "chars": 320,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.sv"
  },
  {
    "path": "apps/example/package.json",
    "chars": 1817,
    "preview": "{\n  \"name\": \"@fantastic-startkit/example\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    "
  },
  {
    "path": "apps/example/postcss.config.js",
    "chars": 87,
    "preview": "export default {\n  plugins: {\n    'autoprefixer': {},\n    'postcss-nested': {},\n  },\n}\n"
  },
  {
    "path": "apps/example/src/App.vue",
    "chars": 508,
    "preview": "<script setup lang=\"ts\">\nconst isRouterAlive = ref(true)\nconst settingsStore = useSettingsStore()\n\nprovide('reload', rel"
  },
  {
    "path": "apps/example/src/api/index.ts",
    "chars": 2573,
    "preview": "import axios from 'axios'\n// import qs from 'qs'\n\n// 请求重试配置\nconst MAX_RETRY_COUNT = 3 // 最大重试次数\nconst RETRY_DELAY = 1000"
  },
  {
    "path": "apps/example/src/api/modules/news.fake.ts",
    "chars": 457,
    "preview": "import { faker } from '@faker-js/faker'\nimport { defineFakeRoute } from 'vite-plugin-fake-server/client'\n\nconst list: an"
  },
  {
    "path": "apps/example/src/api/modules/news.ts",
    "chars": 106,
    "preview": "import api from '../index'\n\nexport default {\n  list: () => api.get('news/list', {\n    fake: true,\n  }),\n}\n"
  },
  {
    "path": "apps/example/src/api/modules/user.fake.ts",
    "chars": 418,
    "preview": "import { faker } from '@faker-js/faker'\nimport { defineFakeRoute } from 'vite-plugin-fake-server/client'\n\nexport default"
  },
  {
    "path": "apps/example/src/api/modules/user.ts",
    "chars": 175,
    "preview": "import api from '../index'\n\nexport default {\n  // 登录\n  login: (data: {\n    account: string\n    password: string\n  }) => "
  },
  {
    "path": "apps/example/src/assets/styles/globals.css",
    "chars": 310,
    "preview": "/* 全局样式 */\nhtml,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family: Manrope, system-ui, sans-serif;\n}\n\n@keyframes spin {\n "
  },
  {
    "path": "apps/example/src/assets/styles/resources/utils.scss",
    "chars": 911,
    "preview": "// @mixin 通过 @include 调用使用\n// % 通过 @extend 调用使用\n\n// 文字超出隐藏,默认为单行超出隐藏,可设置多行\n@mixin text-overflow($line: 1, $fixed-width: "
  },
  {
    "path": "apps/example/src/assets/styles/resources/variables.scss",
    "chars": 8,
    "preview": "// 全局变量\n"
  },
  {
    "path": "apps/example/src/components/DemoButton/index.vue",
    "chars": 645,
    "preview": "<script setup lang=\"ts\">\ndefineOptions({\n  name: 'DemoButton',\n})\n\ndefineProps<{\n  label?: string\n}>()\n\nconst emit = def"
  },
  {
    "path": "apps/example/src/composables/useGlobalProperties.ts",
    "chars": 223,
    "preview": "import type { ComponentInternalInstance } from 'vue'\n\nexport default function useGlobalProperties() {\n  const { appConte"
  },
  {
    "path": "apps/example/src/layouts/example.vue",
    "chars": 2118,
    "preview": "<template>\n  <div class=\"bg-slate-50 min-h-screen\">\n    <header class=\"border-b border-slate-200 bg-white/90 top-0 stick"
  },
  {
    "path": "apps/example/src/layouts/index.vue",
    "chars": 40,
    "preview": "<template>\n  <RouterView />\n</template>\n"
  },
  {
    "path": "apps/example/src/main.ts",
    "chars": 237,
    "preview": "import App from './App.vue'\nimport router from './router'\nimport pinia from './store'\n\nimport 'virtual:uno.css'\n\n// 全局样式"
  },
  {
    "path": "apps/example/src/router/index.ts",
    "chars": 1454,
    "preview": "import type { RouteRecordRaw } from 'vue-router'\nimport NProgress from 'nprogress'\n// import { setupLayouts } from 'virt"
  },
  {
    "path": "apps/example/src/router/modules/example.ts",
    "chars": 1352,
    "preview": "import type { RouteRecordRaw } from 'vue-router'\n\nfunction ExampleLayout() {\n  return import('@/layouts/example.vue')\n}\n"
  },
  {
    "path": "apps/example/src/router/modules/root.ts",
    "chars": 320,
    "preview": "import type { RouteRecordRaw } from 'vue-router'\n\nconst routes: RouteRecordRaw[] = [\n  {\n    path: '/',\n    component: ("
  },
  {
    "path": "apps/example/src/store/index.ts",
    "chars": 156,
    "preview": "import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'\n\nconst pinia = createPinia()\npinia.use(piniaPluginPe"
  },
  {
    "path": "apps/example/src/store/modules/example.ts",
    "chars": 539,
    "preview": "import apiNews from '@/api/modules/news'\n\nexport const useExampleStore = defineStore(\n  // 唯一ID\n  'example',\n  () => {\n "
  },
  {
    "path": "apps/example/src/store/modules/settings.ts",
    "chars": 243,
    "preview": "export const useSettingsStore = defineStore(\n  // 唯一ID\n  'settings',\n  () => {\n    const title = ref('')\n\n    // 设置网页标题\n"
  },
  {
    "path": "apps/example/src/store/modules/user.ts",
    "chars": 1082,
    "preview": "import apiUser from '@/api/modules/user'\nimport router from '@/router'\n\nexport const useUserStore = defineStore(\n  // 唯一"
  },
  {
    "path": "apps/example/src/types/auto-imports.d.ts",
    "chars": 4972,
    "preview": "/* eslint-disable */\n/* prettier-ignore */\n// @ts-nocheck\n// noinspection JSUnusedGlobalSymbols\n// Generated by unplugin"
  },
  {
    "path": "apps/example/src/types/components.d.ts",
    "chars": 676,
    "preview": "/* eslint-disable */\n// @ts-nocheck\n// biome-ignore lint: disable\n// oxlint-disable\n// ------\n// Generated by unplugin-v"
  },
  {
    "path": "apps/example/src/types/env.d.ts",
    "chars": 744,
    "preview": "/// <reference types=\"vite/client\" />\ninterface ImportMetaEnv {\n  // Auto generate by env-parse\n  /**\n   * 接口请求地址,会设置到 a"
  },
  {
    "path": "apps/example/src/types/shims.d.ts",
    "chars": 153,
    "preview": "import 'vue-router'\n\ndeclare interface Window {\n  // extend the window\n}\n\ndeclare module 'vue-router' {\n  interface Rout"
  },
  {
    "path": "apps/example/src/types/vite-env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/example/src/utils/dayjs.ts",
    "chars": 99,
    "preview": "import dayjs from 'dayjs'\nimport 'dayjs/locale/zh-cn'\n\ndayjs.locale('zh-cn')\n\nexport default dayjs\n"
  },
  {
    "path": "apps/example/src/utils/eventBus.ts",
    "chars": 113,
    "preview": "import mitt from 'mitt'\n\ninterface MittTypes {\n  [key: string | symbol]: any\n}\n\nexport default mitt<MittTypes>()\n"
  },
  {
    "path": "apps/example/src/views/[...all].vue",
    "chars": 127,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: false\n  title: 找不到页面\n</route>\n\n<template>\n  <div>\n    我真的尽力了,但还是找不到页面\n  </div>\n</tem"
  },
  {
    "path": "apps/example/src/views/example/axios.vue",
    "chars": 2392,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n</route>\n\n<script setup lang=\"ts\">\nimport apiNews from '@/api/modules/news'\n"
  },
  {
    "path": "apps/example/src/views/example/component.vue",
    "chars": 6750,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n  title: 组件\n</route>\n\n<script setup lang=\"ts\">\nimport ExampleList from './co"
  },
  {
    "path": "apps/example/src/views/example/components/ExampleList/index.vue",
    "chars": 702,
    "preview": "<script setup lang=\"ts\">\ndefineOptions({\n  name: 'ExampleList',\n})\n\ndefineProps<{\n  list: string[]\n}>()\n</script>\n\n<temp"
  },
  {
    "path": "apps/example/src/views/example/icon.vue",
    "chars": 5707,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n  title: 图标\n</route>\n\n<template>\n  <div class=\"max-w-full space-y-6\">\n    <!"
  },
  {
    "path": "apps/example/src/views/example/params/[test].vue",
    "chars": 1058,
    "preview": "<route lang=\"yaml\">\nname: exampleParams\nmeta:\n  layout: example\n</route>\n\n<script setup lang=\"ts\">\nconst route = useRout"
  },
  {
    "path": "apps/example/src/views/example/permission-js.vue",
    "chars": 1943,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n</route>\n\n<script setup lang=\"ts\">\nconst router = useRouter()\nconst userStor"
  },
  {
    "path": "apps/example/src/views/example/permission-router.vue",
    "chars": 1051,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n  requireLogin: true\n</route>\n\n<script setup lang=\"ts\">\nconst userStore = us"
  },
  {
    "path": "apps/example/src/views/example/pinia.vue",
    "chars": 2826,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n</route>\n\n<script setup lang=\"ts\">\nconst exampleStore = useExampleStore()\n\nc"
  },
  {
    "path": "apps/example/src/views/example/query.vue",
    "chars": 1036,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n</route>\n\n<script setup lang=\"ts\">\nconst route = useRoute()\n</script>\n\n<temp"
  },
  {
    "path": "apps/example/src/views/example/reload.vue",
    "chars": 1600,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: example\n</route>\n\n<script setup lang=\"ts\">\nconst reload = inject('reload') as any\n\nc"
  },
  {
    "path": "apps/example/src/views/index.vue",
    "chars": 2329,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: false\n</route>\n\n<template>\n  <div class=\"bg-slate-50 flex flex-col min-h-screen item"
  },
  {
    "path": "apps/example/src/views/login.vue",
    "chars": 2422,
    "preview": "<route lang=\"yaml\">\nmeta:\n  layout: false\n  title: 登录\n</route>\n\n<script setup lang=\"ts\">\nconst router = useRouter()\ncons"
  },
  {
    "path": "apps/example/tsconfig.app.json",
    "chars": 788,
    "preview": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.dom.json\",\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsc"
  },
  {
    "path": "apps/example/tsconfig.json",
    "chars": 119,
    "preview": "{\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ],\n  \"files\": []\n}\n"
  },
  {
    "path": "apps/example/tsconfig.node.json",
    "chars": 597,
    "preview": "{\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n    \"target\": \"ES2022\",\n"
  },
  {
    "path": "apps/example/vite/plugins.ts",
    "chars": 4043,
    "preview": "import type { PluginOption } from 'vite'\nimport process from 'node:process'\nimport { ComponentsResolver as FantasticStar"
  },
  {
    "path": "apps/example/vite.config.ts",
    "chars": 1638,
    "preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { defineConfig, loadEnv "
  },
  {
    "path": "docs/.gitignore",
    "chars": 58,
    "preview": "node_modules\n.vitepress/cache\n.vitepress/dist\n.eslintcache"
  },
  {
    "path": "docs/.vitepress/config.ts",
    "chars": 2909,
    "preview": "import { defineConfig } from 'vitepress'\n\nexport default defineConfig({\n  title: 'Fantastic-startkit 官方文档',\n  descriptio"
  },
  {
    "path": "docs/.vitepress/theme/components/SponsorsAside.vue",
    "chars": 3053,
    "preview": "<template>\n  <div>\n    <div class=\"sponsors-aside-text\">\n      作者其他作品\n    </div>\n    <div class=\"sponsor-container speci"
  },
  {
    "path": "docs/.vitepress/theme/fonts/fira_code/fira_code.css",
    "chars": 1391,
    "preview": "@font-face {\n  font-family: \"Fira Code\";\n  font-style: normal;\n  font-weight: 300;\n  src:\n    url(\"woff2/FiraCode-Light."
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "chars": 1513,
    "preview": "import mediumZoom from 'medium-zoom'\nimport { useData, useRoute } from 'vitepress'\nimport giscusTalk from 'vitepress-plu"
  },
  {
    "path": "docs/.vitepress/theme/styles/var.css",
    "chars": 97,
    "preview": "/* medium-zoom */\n.medium-zoom-overlay {\n  z-index: 30;\n}\n\n.medium-zoom-image {\n  z-index: 30;\n}\n"
  },
  {
    "path": "docs/guide/api.md",
    "chars": 399,
    "preview": "# 常用 API\n\n## 接口请求\n\n详细可阅读《[与服务端交互 - 接口请求](axios#接口请求)》。\n\n```ts\nimport api from '@/api'\n\napi.get()\napi.post()\n```\n\n## 事件总线"
  },
  {
    "path": "docs/guide/axios.md",
    "chars": 5594,
    "preview": "# 与服务端交互\n\n框架使用 [Axios](https://axios-http.com/zh/) 做为异步请求工具,并进行了简单的封装。\n\n## 接口请求\n\n### 设置 baseURL\n\n在 `apps/<app>/.env.*` 文"
  },
  {
    "path": "docs/guide/build.md",
    "chars": 385,
    "preview": "# 构建与预览\n\n## 构建\n\n项目开发完成之后,可以执行 `pnpm run build` 命令进行构建,构建成功后会生成 `apps/<app>/dist/` 文件夹,里面就是构建好的文件。\n\n如果构建的是测试环境,则生成的文件夹为 `"
  },
  {
    "path": "docs/guide/coding-standard.md",
    "chars": 2208,
    "preview": "# 代码规范\n\n:::tip\n请确保已阅读《[准备工作 - 开发环境](start#开发环境)》,并且按照文档说明安装好相关软件及扩展。\n:::\n\n为保证代码风格统一,请使用 [Visual Studio Code](https://cod"
  },
  {
    "path": "docs/guide/env.md",
    "chars": 1681,
    "preview": "# 环境配置\n\n环境变量配置文件在 `apps/<app>/` 根目录,默认提供三套配置,分别为:\n\n::: code-group\n\n<<< @/../apps/core/.env.development{env}[.env.develop"
  },
  {
    "path": "docs/guide/icon.md",
    "chars": 1691,
    "preview": "# 图标\n\n框架提供了三种使用图标的方式,你可以根据自己的使用需求自行选择。\n\n## 自定义图标\n\n推荐去[阿里巴巴矢量图标库](https://www.iconfont.cn/)下载图标,将准备好的 SVG 图标文件放到 `apps/<a"
  },
  {
    "path": "docs/guide/ready.md",
    "chars": 2355,
    "preview": "# 准备工作\n\n## 源码\n\n可以直接下载源码,或者通过 git 拉取源码\n\n```bash\n# 从 Github 拉取\ngit clone https://github.com/hooray/fantastic-startkit.git\n"
  },
  {
    "path": "docs/guide/resources.md",
    "chars": 2028,
    "preview": "# 资源\n\n## 图片\n\n### 全局公共\n\n全局公共图片存放在 `apps/<app>/src/assets/images/` 目录下,可自行新建文件夹分类管理。\n\n### 局部私有\n\n局部私有图片建议采用就近原则,你可以在需要的模块文件"
  },
  {
    "path": "docs/guide/router.md",
    "chars": 3086,
    "preview": "# 路由\n\n路由实现了自动注册,路由配置存放在 `router/modules/` 目录下,每一个 ts 文件会被视为一个路由模块,可参考 `router/modules/example.ts` 文件。\n\n更多使用技巧请移步至 Vue-ro"
  },
  {
    "path": "docs/guide/start.md",
    "chars": 1942,
    "preview": "# 开始\n\n本项目采用 Monorepo(单体仓库)架构,基于 pnpm workspace 管理多个应用和公共包。\n\nMonorepo 的优势:\n\n- **代码共享**:多个应用可共享公共代码和依赖\n- **统一管理**:统一版本控制和依"
  },
  {
    "path": "docs/guide/store.md",
    "chars": 468,
    "preview": "# 全局状态管理\n\n:::tip 说明\n[Pinia](https://pinia.vuejs.org/) 为 Vue.js 官方状态库,如果你对 Pinia 还不熟悉,请先阅读官方文档。\n:::\n\n全局状态文件存放在 `apps/<app"
  },
  {
    "path": "docs/index.md",
    "chars": 664,
    "preview": "---\nlayout: home\n\ntitle: Fantastic-startkit\ntitleTemplate: 一款简单好用的 Vue3 项目启动套件\n\nhero:\n  name: Fantastic-startkit\n  text:"
  },
  {
    "path": "docs/package.json",
    "chars": 368,
    "preview": "{\n  \"name\": \"@fantastic-startkit/docs\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vitepress dev "
  },
  {
    "path": "docs/public/.nojekyll",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/support.md",
    "chars": 225,
    "preview": "# 技术支持\n\n在使用的过程中遇到问题请在仓库提交 Issue ,你可以更详细描述问题产生的操作步骤,或者提供完整的最小复现,这样做可以让更多的人参与讨论,也方便后人查阅。\n\n- [Github Issue](https://github."
  },
  {
    "path": "docs/tsconfig.json",
    "chars": 479,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"jsx\": \"preserve\",\n    \"lib\": [\n      \"ESNext\",\n      \"DOM\"\n    ],\n"
  },
  {
    "path": "eslint.config.js",
    "chars": 808,
    "preview": "import antfu from '@antfu/eslint-config'\n\nexport default antfu(\n  {\n    vue: true,\n    unocss: true,\n    ignores: [\n    "
  },
  {
    "path": "package.json",
    "chars": 1552,
    "preview": "{\n  \"name\": \"fantastic-startkit\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@10.33.4\",\n  \"engines\""
  },
  {
    "path": "packages/components/package.json",
    "chars": 415,
    "preview": "{\n  \"name\": \"@fantastic-startkit/components\",\n  \"type\": \"module\",\n  \"version\": \"0.0.0\",\n  \"exports\": {\n    \".\": \"./src/i"
  },
  {
    "path": "packages/components/resolver.ts",
    "chars": 707,
    "preview": "import type { ComponentResolver, TypeImport } from 'unplugin-vue-components'\nimport { createRequire } from 'node:module'"
  },
  {
    "path": "packages/components/src/icon/index.vue",
    "chars": 862,
    "preview": "<script setup lang=\"ts\">\nimport { Icon } from '@iconify/vue'\nimport { computed } from 'vue'\n\ndefineOptions({\n  name: 'Fs"
  },
  {
    "path": "packages/components/src/index.ts",
    "chars": 53,
    "preview": "export { default as FsIcon } from './icon/index.vue'\n"
  },
  {
    "path": "packages/components/tsconfig.json",
    "chars": 248,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": "
  },
  {
    "path": "packages/copyright/index.ts",
    "chars": 3205,
    "preview": "import type { PluginOption } from 'vite'\nimport boxen from 'boxen'\nimport picocolors from 'picocolors'\nimport banner fro"
  },
  {
    "path": "packages/copyright/package.json",
    "chars": 299,
    "preview": "{\n  \"name\": \"@fantastic-startkit/copyright\",\n  \"type\": \"module\",\n  \"version\": \"0.0.0\",\n  \"exports\": {\n    \".\": \"./index."
  },
  {
    "path": "packages/copyright/tsconfig.json",
    "chars": 147,
    "preview": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\"]\n  },\n  \"include\": [\n    \"index.ts\",\n "
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 1953,
    "preview": "autoInstallPeers: true\ncatalogMode: prefer\ncleanupUnusedCatalogs: true\nengineStrict: true\nignoreWorkspaceRootCheck: true"
  },
  {
    "path": "scripts/cli.ts",
    "chars": 8785,
    "preview": "#!/usr/bin/env node\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { f"
  },
  {
    "path": "stylelint.config.js",
    "chars": 874,
    "preview": "export default {\n  extends: [\n    'stylelint-config-standard-scss',\n    'stylelint-config-standard-vue/scss',\n    'style"
  },
  {
    "path": "tsconfig.json",
    "chars": 273,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolve"
  },
  {
    "path": "uno.config.ts",
    "chars": 1096,
    "preview": "import {\n  defineConfig,\n  presetAttributify,\n  presetIcons,\n  presetTypography,\n  presetWind4,\n  transformerCompileClas"
  }
]

About this extraction

This page contains the full source code of the hooray/fantastic-startkit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 132 files (144.8 KB), approximately 49.0k tokens, and a symbol index with 68 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!