├── src ├── api │ ├── modules │ │ ├── index.ts │ │ └── login.ts │ └── index.ts ├── worker-import.js ├── types │ ├── index.d.ts │ └── shims-axios.d.ts ├── assets │ ├── images │ │ └── logo.png │ └── styles │ │ ├── index.scss │ │ ├── element │ │ ├── dark.scss │ │ └── index.scss │ │ └── reset.scss ├── store │ ├── index.ts │ └── user.ts ├── utils │ ├── theme.ts │ ├── toast.ts │ ├── token.ts │ └── axios.ts ├── composables │ ├── dark.ts │ └── index.ts ├── tests │ ├── basic.test.ts │ └── component.test.ts ├── mocks │ ├── index.ts │ └── handlers.ts ├── worker.js ├── pages │ ├── Index.vue │ └── Detail.vue ├── components │ ├── ThemeSwitcher.vue │ ├── LangSwitcher.vue │ ├── HelloWorld.vue │ ├── shared │ │ ├── Footer.vue │ │ └── Header.vue │ ├── Menu.vue │ ├── ServiceWorker.vue │ ├── VueUse.vue │ └── LoginButton.vue ├── pwa │ ├── claims-sw.ts │ ├── prompt-sw.ts │ └── ReloadPrompt.vue ├── i18n │ └── index.ts ├── main.ts ├── router │ └── index.ts ├── App.vue └── env.d.ts ├── .npmrc ├── .eslintignore ├── public ├── favicon.ico ├── pwa-192x192.png └── pwa-512x512.png ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── postcss.config.js ├── .lintstagedrc ├── tsconfig.node.json ├── .editorconfig ├── .vscode ├── extensions.json └── settings.json ├── .gitignore ├── locales ├── zh-CN.yml └── en.yml ├── index.html ├── prettier.config.js ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── uno.config.ts ├── commitlint.config.ts ├── .cz-config.js ├── .github └── workflows │ └── deploy.yml ├── README.md ├── .stylelintrc.js ├── package.json └── vite.config.mts /src/api/modules/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist = true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | index.html 4 | 5 | public/mockServiceWorker.js 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yugasun/vue-ts-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/worker-import.js: -------------------------------------------------------------------------------- 1 | export const msg = 'pong'; 2 | export const mode = process.env.NODE_ENV; 3 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yugasun/vue-ts-starter/HEAD/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yugasun/vue-ts-starter/HEAD/public/pwa-512x512.png -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | export type UserModule = (app: App) => void; 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yugasun/vue-ts-starter/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscoode 2 | dist 3 | node_modules 4 | 5 | pnpm-lock.yaml 6 | *.json 7 | 8 | public/mockServiceWorker.js 9 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | 3 | const store = createPinia(); 4 | 5 | export { store }; 6 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | import { useDark } from '@vueuse/core'; 2 | 3 | export function updateTheme() { 4 | useDark(); 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | syntax: 'postcss-scss', 3 | 4 | plugins: { 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as login from './modules/login'; 2 | import * as index from './modules/index'; 3 | 4 | export default Object.assign({}, login, index); 5 | -------------------------------------------------------------------------------- /src/composables/dark.ts: -------------------------------------------------------------------------------- 1 | import { useDark, useToggle } from '@vueuse/core'; 2 | 3 | export const isDark = useDark(); 4 | export const toggleDark = useToggle(isDark); 5 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | import { useDark, useToggle } from '@vueuse/core'; 2 | 3 | export const isDark = useDark(); 4 | export const toggleDark = useToggle(isDark); 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*": "prettier --write --ignore-unknown", 3 | "src/*": "eslint --fix --ext .js,.jsx,.ts,.tsx,.vue", 4 | "**/*.{scss,less,css}": "stylelint --fix" 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | describe('basic', () => { 4 | it('should work', () => { 5 | expect(true).toBe(true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.mts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | /* @tailwind base; */ 2 | @import './reset'; 3 | 4 | a, 5 | a:active, 6 | a:visited { 7 | color: var(--el-primary-color); 8 | } 9 | 10 | :root { 11 | --link-color: #42b983; 12 | } 13 | -------------------------------------------------------------------------------- /src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw'; 2 | import { handlers } from './handlers'; 3 | 4 | // This configures a Service Worker with the given request handlers. 5 | export const worker = setupWorker(...handlers); 6 | -------------------------------------------------------------------------------- /src/utils/toast.ts: -------------------------------------------------------------------------------- 1 | import 'element-plus/es/components/message/style/css'; 2 | 3 | import { ElMessage, MessageParams } from 'element-plus'; 4 | 5 | export function toast(options: MessageParams) { 6 | ElMessage(options); 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/styles/element/dark.scss: -------------------------------------------------------------------------------- 1 | $base-colors: ( 2 | 'primary': ( 3 | 'base': #589ef8, 4 | ), 5 | ); 6 | 7 | @forward 'element-plus/theme-chalk/src/dark/var.scss' with ( 8 | $colors: $base-colors 9 | ); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | indent_style = space 4 | indent_size = 4 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | 8 | [*.{js,vue,scss}] 9 | insert_final_newline = true 10 | max_line_length = 150 11 | 12 | [*.{json,yml}] 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "antfu.unocss", 5 | "antfu.vite", 6 | "antfu.goto-alias", 7 | "csstools.postcss", 8 | "dbaeumer.vscode-eslint", 9 | "vue.volar", 10 | "lokalise.i18n-ally", 11 | "streetsidesoftware.code-spell-checker" 12 | ] 13 | } -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | import { msg, mode } from './worker-import'; 2 | 3 | let counter = 1; 4 | 5 | self.onmessage = (e) => { 6 | if (e.data === 'ping') { 7 | self.postMessage({ msg: `${msg} - ${counter++}`, mode }); 8 | } else if (e.data === 'clear') { 9 | counter = 1; 10 | self.postMessage({ msg: null, mode: null }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .pnpm-store 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | .turbo 26 | 27 | public/mockServiceWorker.js 28 | 29 | auto-imports.d.ts 30 | components.d.ts -------------------------------------------------------------------------------- /locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | detail: 2 | loginTip: 这是详情页,需要登录 3 | 4 | common: 5 | hello: '使用 Vue3 + TypeScript + Vite + Pinia 开发的项目模板🚀' 6 | welcome: '欢迎, {name}' 7 | template: 模版 8 | docs: 文档 9 | example: 示例 10 | isSupported: 是否支持 11 | menus: 菜单 12 | home: 主页 13 | Detail: 详情页 14 | 15 | home: 16 | recommendIde: '推荐使用的编辑器配置' 17 | remarks: '阅读 README.md 获得更多信息。' 18 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/utils/token.ts: -------------------------------------------------------------------------------- 1 | import { useCookies } from '@vueuse/integrations/useCookies'; 2 | 3 | const cookies = useCookies(); 4 | 5 | const TOKEN_KEY = 'v_token'; 6 | 7 | export function setToken(val: string) { 8 | cookies.set(TOKEN_KEY, val, { 9 | maxAge: 3600, 10 | }); 11 | } 12 | 13 | export function getToken() { 14 | return cookies.get(TOKEN_KEY); 15 | } 16 | 17 | export function removeToken() { 18 | cookies.remove(TOKEN_KEY); 19 | } 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-ts-starter 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | detail: 2 | loginTip: 'This is Detail Page, need login' 3 | 4 | common: 5 | hello: 'Vue template for starter using Vue3 + TypeScript + Vite + Pinia 🚀' 6 | welcome: 'Welcome, {name}' 7 | template: Template 8 | docs: Docs 9 | example: Example 10 | isSupported: isSupported 11 | menus: Menus 12 | home: Home 13 | Detail: Detail 14 | 15 | home: 16 | recommendIde: 'Recommended IDE setup' 17 | remarks: 'See README.md for more information.' 18 | -------------------------------------------------------------------------------- /src/components/LangSwitcher.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 4, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'all', 10 | bracketSpacing: true, 11 | bracketSameLine: false, 12 | arrowParens: 'always', 13 | rangeStart: 0, 14 | rangeEnd: Infinity, 15 | requirePragma: false, 16 | insertPragma: false, 17 | proseWrap: 'preserve', 18 | htmlWhitespaceSensitivity: 'css', 19 | endOfLine: 'auto', 20 | }; 21 | -------------------------------------------------------------------------------- /src/pwa/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanupOutdatedCaches, 3 | createHandlerBoundToURL, 4 | precacheAndRoute, 5 | } from 'workbox-precaching'; 6 | import { clientsClaim } from 'workbox-core'; 7 | import { NavigationRoute, registerRoute } from 'workbox-routing'; 8 | 9 | declare let self: ServiceWorkerGlobalScope; 10 | 11 | // self.__WB_MANIFEST is default injection point 12 | precacheAndRoute(self.__WB_MANIFEST); 13 | 14 | // clean old assets 15 | cleanupOutdatedCaches(); 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))); 19 | 20 | self.skipWaiting(); 21 | clientsClaim(); 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | jsx: true, 9 | }, 10 | }, 11 | extends: [ 12 | 'plugin:vue/vue3-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'prettier', 15 | 'plugin:prettier/recommended', 16 | ], 17 | 18 | rules: { 19 | // override/add rules settings here, such as: 20 | 'vue/multi-word-component-names': 'off', 21 | '@typescript-eslint/no-explicit-any': 'warn', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/pwa/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanupOutdatedCaches, 3 | createHandlerBoundToURL, 4 | precacheAndRoute, 5 | } from 'workbox-precaching'; 6 | import { NavigationRoute, registerRoute } from 'workbox-routing'; 7 | 8 | declare let self: ServiceWorkerGlobalScope; 9 | 10 | self.addEventListener('message', (event) => { 11 | if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting(); 12 | }); 13 | 14 | // self.__WB_MANIFEST is default injection point 15 | precacheAndRoute(self.__WB_MANIFEST); 16 | 17 | // clean old assets 18 | cleanupOutdatedCaches(); 19 | 20 | // to allow work offline 21 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))); 22 | -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 2 | 3 | const service = axios.create(); 4 | 5 | // Request interceptors 6 | service.interceptors.request.use( 7 | (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { 8 | // do something 9 | return config; 10 | }, 11 | (error: any) => { 12 | Promise.reject(error); 13 | }, 14 | ); 15 | 16 | // Response interceptors 17 | service.interceptors.response.use( 18 | async (response: AxiosResponse) => { 19 | // do something 20 | return response.data; 21 | }, 22 | (error: any) => { 23 | // do something 24 | return Promise.reject(error); 25 | }, 26 | ); 27 | 28 | export default service; 29 | -------------------------------------------------------------------------------- /src/pages/Detail.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 35 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | 3 | // Import i18n resources 4 | // https://vitejs.dev/guide/features.html#glob-import 5 | const messages = Object.fromEntries( 6 | Object.entries( 7 | // glob yaml/yml/ts files 8 | import.meta.glob<{ default: any }>( 9 | '../../locales/*.{yaml,yml,ts,json}', 10 | { 11 | eager: true, 12 | }, 13 | ), 14 | ).map(([key, value]) => { 15 | if (key.endsWith('.ts')) { 16 | return [key.slice(14, -3), value.default]; 17 | } 18 | const yaml = key.endsWith('.yaml'); 19 | return [key.slice(14, yaml ? -5 : -4), value.default]; 20 | }), 21 | ); 22 | 23 | const i18n = createI18n({ 24 | legacy: false, 25 | locale: 'en', 26 | messages, 27 | }); 28 | 29 | export { i18n }; 30 | -------------------------------------------------------------------------------- /src/api/modules/login.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/axios'; 2 | 3 | interface IResponseType

> { 4 | code?: number; 5 | status: number; 6 | msg: string; 7 | data: P; 8 | } 9 | interface ILogin { 10 | token: string; 11 | expires: number; 12 | } 13 | 14 | interface IUser { 15 | username: string; 16 | } 17 | 18 | /** 19 | * login 20 | * 21 | * @param {string} username 22 | * @param {string} password 23 | * @return {*} 24 | */ 25 | const login = (username: string, password: string) => { 26 | return request>({ 27 | url: '/api/login', 28 | method: 'post', 29 | data: { 30 | username, 31 | password, 32 | }, 33 | }); 34 | }; 35 | 36 | const getUser = () => { 37 | return request>({ 38 | url: '/api/user', 39 | method: 'get', 40 | }); 41 | }; 42 | 43 | export { login, getUser }; 44 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import ElementPlus from 'element-plus'; 3 | 4 | import App from './App.vue'; 5 | import { store } from './store'; 6 | import { router } from './router'; 7 | import { i18n } from './i18n'; 8 | import { updateTheme } from './utils/theme'; 9 | 10 | import 'virtual:uno.css'; 11 | import '@/assets/styles/index.scss'; 12 | // If you want to use ElMessage, import it. 13 | import 'element-plus/theme-chalk/src/message.scss'; 14 | 15 | async function main() { 16 | // Start mock server 17 | if (import.meta.env.DEV || import.meta.env.VITE_IS_VERCEL) { 18 | const { worker } = await import('./mocks/index'); 19 | worker.start({ onUnhandledRequest: 'bypass' }); 20 | } 21 | 22 | const app = createApp(App); 23 | 24 | // load plugins 25 | app.use(store); 26 | app.use(router); 27 | app.use(ElementPlus); 28 | app.use(i18n); 29 | 30 | app.mount('#app'); 31 | 32 | updateTheme(); 33 | } 34 | 35 | main(); 36 | -------------------------------------------------------------------------------- /src/assets/styles/element/index.scss: -------------------------------------------------------------------------------- 1 | $base-colors: ( 2 | 'primary': ( 3 | 'base': green, 4 | ), 5 | 'success': ( 6 | 'base': #21ba45, 7 | ), 8 | 'warning': ( 9 | 'base': #f2711c, 10 | ), 11 | 'danger': ( 12 | 'base': #db2828, 13 | ), 14 | 'error': ( 15 | 'base': #db2828, 16 | ), 17 | 'info': ( 18 | 'base': #42b8dd, 19 | ), 20 | ); 21 | 22 | // You should use them in scss, because we calculate it by sass. 23 | // comment next lines to use default color 24 | @forward 'element-plus/theme-chalk/src/common/var.scss' with ( 25 | $colors: $base-colors, 26 | $button-padding-horizontal: ( 27 | 'default': 50px, 28 | ) 29 | ); 30 | 31 | // if you want to import all 32 | // @use "element-plus/theme-chalk/src/index.scss" as *; 33 | 34 | // You can comment it to hide debug info. 35 | // @debug $--colors; 36 | 37 | // custom dark variables 38 | @use './dark.scss'; 39 | 40 | // import dark theme 41 | @use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *; 42 | -------------------------------------------------------------------------------- /src/types/shims-axios.d.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | /** 3 | * 自定义扩展axios模块 4 | * @author Maybe 5 | */ 6 | declare module 'axios' { 7 | export interface AxiosInstance { 8 | (config: AxiosRequestConfig): Promise; 9 | request(config: AxiosRequestConfig): Promise; 10 | get(url: string, config?: AxiosRequestConfig): Promise; 11 | delete(url: string, config?: AxiosRequestConfig): Promise; 12 | head(url: string, config?: AxiosRequestConfig): Promise; 13 | post( 14 | url: string, 15 | data?: any, 16 | config?: AxiosRequestConfig, 17 | ): Promise; 18 | put( 19 | url: string, 20 | data?: any, 21 | config?: AxiosRequestConfig, 22 | ): Promise; 23 | patch( 24 | url: string, 25 | data?: any, 26 | config?: AxiosRequestConfig, 27 | ): Promise; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from '@/utils/token'; 2 | import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; 3 | 4 | const routes: Array = [ 5 | { 6 | path: '/', 7 | name: 'Index', 8 | meta: { 9 | title: 'Home Page', 10 | keepAlive: true, 11 | requireAuth: false, 12 | }, 13 | component: () => import('@/pages/Index.vue'), 14 | }, 15 | { 16 | path: '/detail', 17 | name: 'Detail', 18 | meta: { 19 | title: 'Detail Page', 20 | keepAlive: true, 21 | requireAuth: true, 22 | }, 23 | component: () => import('@/pages/Detail.vue'), 24 | }, 25 | ]; 26 | 27 | const router = createRouter({ 28 | history: createWebHistory(), 29 | routes, 30 | }); 31 | 32 | router.beforeEach(async (to) => { 33 | const token = getToken(); 34 | if (!token && to.name !== 'Index') { 35 | return { name: 'Index' }; 36 | } 37 | }); 38 | 39 | export { router }; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": [ 4 | "node_modules/@types", 5 | "src/types" 6 | ], 7 | "target": "esnext", 8 | "useDefineForClassFields": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "jsx": "preserve", 13 | "sourceMap": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "esModuleInterop": true, 17 | "lib": [ 18 | "esnext", 19 | "webworker", 20 | "dom" 21 | ], 22 | "skipLibCheck": true, 23 | "baseUrl": "./", 24 | "paths": { 25 | "@": [ 26 | "src" 27 | ], 28 | "@/*": [ 29 | "src/*" 30 | ] 31 | }, 32 | "types": [ 33 | "vite/client", 34 | ] 35 | }, 36 | "include": [ 37 | "src/**/*.ts", 38 | "src/**/*.d.ts", 39 | "src/**/*.tsx", 40 | "src/**/*.vue", 41 | "components.d.ts", 42 | "auto-imports.d.ts", 43 | ], 44 | "references": [ 45 | { 46 | "path": "./tsconfig.node.json" 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import api from '@/api'; 3 | import { removeToken, getToken } from '@/utils/token'; 4 | 5 | interface UserInfo { 6 | username: string; 7 | [prop: string]: any; 8 | } 9 | 10 | interface UserState { 11 | userInfo: UserInfo | null; 12 | } 13 | 14 | export const useUserStore = defineStore({ 15 | id: 'user', 16 | state(): UserState { 17 | return { 18 | userInfo: null, 19 | }; 20 | }, 21 | getters: { 22 | isLogin: (state: UserState) => !!state.userInfo, 23 | }, 24 | actions: { 25 | async initUser() { 26 | const token = getToken(); 27 | if (token) { 28 | const res = await api.getUser(); 29 | if (res.code === 0) { 30 | this.updateUser(res.data); 31 | } 32 | } 33 | }, 34 | updateUser(userInfo: UserInfo | null) { 35 | if (!userInfo) { 36 | removeToken(); 37 | } 38 | this.userInfo = userInfo; 39 | }, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yuga Sun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | presetUno, 7 | presetWebFonts, 8 | transformerDirectives, 9 | transformerVariantGroup, 10 | } from 'unocss'; 11 | 12 | export default defineConfig({ 13 | shortcuts: [ 14 | [ 15 | 'btn', 16 | 'px-4 py-1 rounded inline-block bg-teal-700 text-white cursor-pointer hover:bg-teal-800 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50', 17 | ], 18 | [ 19 | 'icon-btn', 20 | 'inline-block cursor-pointer select-none opacity-75 transition duration-200 ease-in-out hover:opacity-100 hover:text-teal-600', 21 | ], 22 | ], 23 | presets: [ 24 | presetUno(), 25 | presetAttributify(), 26 | presetIcons({ 27 | scale: 1.2, 28 | warn: true, 29 | }), 30 | presetTypography(), 31 | presetWebFonts({ 32 | fonts: { 33 | sans: 'DM Sans', 34 | serif: 'DM Serif Display', 35 | mono: 'DM Mono', 36 | }, 37 | }), 38 | ], 39 | transformers: [transformerDirectives(), transformerVariantGroup()], 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | 48 | -------------------------------------------------------------------------------- /src/components/shared/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 36 | 46 | -------------------------------------------------------------------------------- /src/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 8 | 34 | 45 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 34 | 51 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional', 'cz'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'feat', 9 | 'bug', 10 | 'fix', 11 | 'ui', 12 | 'docs', 13 | 'style', 14 | 'perf', 15 | 'release', 16 | 'deploy', 17 | 'refactor', 18 | 'test', 19 | 'chore', 20 | 'revert', 21 | 'merge', 22 | 'build', 23 | ], 24 | ], 25 | // low case 26 | 'type-case': [2, 'always', 'lower-case'], 27 | // cannot empty 28 | 'type-empty': [2, 'never'], 29 | // cannot empty 30 | 'scope-empty': [0, 'never'], 31 | // scope 32 | 'scope-case': [0], 33 | // message commot empty 34 | 'subject-empty': [2, 'never'], 35 | // disable stop char 36 | 'subject-full-stop': [0, 'never'], 37 | // disable subject case 38 | 'subject-case': [0, 'never'], 39 | // start with blank 40 | 'body-leading-blank': [1, 'always'], 41 | 'header-max-length': [0, 'always', 72], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/ServiceWorker.vue: -------------------------------------------------------------------------------- 1 | 28 | 46 | 47 | -------------------------------------------------------------------------------- /src/tests/component.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { describe, expect, it, vi, beforeEach } from 'vitest'; 3 | import { createTestingPinia } from '@pinia/testing'; 4 | 5 | import LoginButton from '../components/LoginButton.vue'; 6 | import { createPinia, setActivePinia } from 'pinia'; 7 | 8 | describe('LoginButton', () => { 9 | beforeEach(() => { 10 | // creates a fresh pinia and make it active so it's automatically picked 11 | // up by any useStore() call without having to pass it to it: 12 | // `useStore(pinia)` 13 | setActivePinia(createPinia()); 14 | }); 15 | it('should render', () => { 16 | const wrapper = mount(LoginButton, { 17 | global: { 18 | plugins: [ 19 | createTestingPinia({ 20 | createSpy: vi.fn, 21 | }), 22 | ], 23 | }, 24 | }); 25 | expect(wrapper.text()).toContain('Login'); 26 | }); 27 | 28 | it('should be interactive', async () => { 29 | const wrapper = mount(LoginButton, { 30 | global: { 31 | plugins: [ 32 | createTestingPinia({ 33 | createSpy: vi.fn, 34 | }), 35 | ], 36 | }, 37 | }); 38 | await wrapper.get('#login-btn').trigger('click'); 39 | expect(wrapper.get('#login-dialog').isVisible()).toBe(true); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | 10 | declare module 'virtual:pwa-register' { 11 | export interface RegisterSWOptions { 12 | immediate?: boolean; 13 | onNeedRefresh?: () => void; 14 | onOfflineReady?: () => void; 15 | onRegistered?: ( 16 | registration: ServiceWorkerRegistration | undefined, 17 | ) => void; 18 | onRegisterError?: (error: any) => void; 19 | } 20 | 21 | export function registerSW( 22 | options?: RegisterSWOptions, 23 | ): (reloadPage?: boolean) => Promise; 24 | } 25 | 26 | declare module 'virtual:pwa-register/vue' { 27 | // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error 28 | import type { Ref } from 'vue'; 29 | 30 | export interface RegisterSWOptions { 31 | immediate?: boolean; 32 | onNeedRefresh?: () => void; 33 | onOfflineReady?: () => void; 34 | onRegistered?: ( 35 | registration: ServiceWorkerRegistration | undefined, 36 | ) => void; 37 | onRegisterError?: (error: any) => void; 38 | } 39 | 40 | export function useRegisterSW(options?: RegisterSWOptions): { 41 | needRefresh: Ref; 42 | offlineReady: Ref; 43 | updateServiceWorker: (reloadPage?: boolean) => Promise; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feat', name: 'feat: New Feature' }, 4 | { value: 'bug', name: 'bug: Should With Bug Number' }, 5 | { value: 'fix', name: 'fix: Fix bug' }, 6 | { value: 'ui', name: 'ui: Update UI' }, 7 | { value: 'docs', name: 'docs: Modify documents' }, 8 | { 9 | value: 'style', 10 | name: 'style: Update code style(do not influence business code)', 11 | }, 12 | { value: 'perf', name: 'perf: Performance' }, 13 | { 14 | value: 'refactor', 15 | name: 'refactor: Refact neither feature or bug fix', 16 | }, 17 | { value: 'release', name: 'release: Release' }, 18 | { value: 'deploy', name: 'deploy: Deployment' }, 19 | { value: 'test', name: 'test: Add test case' }, 20 | { 21 | value: 'chore', 22 | name: 'chore: Update build process or project tools(do not influence business code)', 23 | }, 24 | { value: 'revert', name: 'revert: Revert' }, 25 | { value: 'build', name: 'build: Build' }, 26 | ], 27 | // override the messages, defaults are as follows 28 | messages: { 29 | type: 'Please choose commit type:', 30 | customScope: 'Please input modify scope (optional):', 31 | subject: 'Please describe your commit message (required):', 32 | body: 'Please input decription in detail(optional):', 33 | footer: 'Please input issue to be closed(optional):', 34 | confirmCommit: 'Confirm to submit?(y/n/e/h)', 35 | }, 36 | allowCustomScopes: true, 37 | skipQuestions: ['body', 'footer'], 38 | subjectLimit: 72, 39 | }; 40 | -------------------------------------------------------------------------------- /src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import { setToken, getToken } from '@/utils/token'; 3 | 4 | export const handlers = [ 5 | rest.get('/api/status', (req, res, ctx) => { 6 | return res( 7 | ctx.status(200), 8 | ctx.json({ 9 | status: 'ok', 10 | }), 11 | ); 12 | }), 13 | rest.post('/api/login', (req, res, ctx) => { 14 | const token = Math.random().toString(16).slice(2); 15 | setToken(token); 16 | 17 | return res( 18 | // Respond with a 200 status code 19 | ctx.json({ 20 | code: 0, 21 | status: 200, 22 | msg: 'Login Success', 23 | data: { 24 | expire: -1, 25 | token: Math.random().toString(16).slice(2), 26 | }, 27 | }), 28 | ); 29 | }), 30 | rest.get('/api/user', (req, res, ctx) => { 31 | const isAuthenticated = getToken(); 32 | if (!isAuthenticated) { 33 | // If not authenticated, respond with a 403 error 34 | return res( 35 | ctx.status(403), 36 | ctx.json({ 37 | code: -1, 38 | status: 403, 39 | msg: 'Not authorized', 40 | }), 41 | ); 42 | } 43 | // If authenticated, return a mocked user details 44 | return res( 45 | ctx.status(200), 46 | ctx.json({ 47 | code: 0, 48 | status: 200, 49 | msg: 'Login Success', 50 | data: { 51 | username: 'admin', 52 | }, 53 | }), 54 | ); 55 | }), 56 | ]; 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"], 3 | "i18n-ally.sourceLanguage": "en", 4 | "i18n-ally.keystyle": "nested", 5 | "i18n-ally.localesPaths": [ 6 | "locales" 7 | ], 8 | "i18n-ally.sortKeys": true, 9 | "i18n-ally.enabledParsers": [ 10 | "yaml", 11 | "ts", 12 | "json", 13 | ], 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": "explicit" 16 | }, 17 | "editor.formatOnSave": false, 18 | "editor.guides.bracketPairs": "active", 19 | "editor.quickSuggestions": { 20 | "strings": true 21 | }, 22 | "editor.tabSize": 2, 23 | "eslint.validate": [ 24 | "javascript", 25 | "javascriptreact", 26 | "typescript", 27 | "typescriptreact", 28 | "vue", 29 | "json" 30 | ], 31 | "files.associations": { 32 | "*.env.*": "dotenv", 33 | "*.css": "postcss" 34 | }, 35 | "gutterpreview.paths": { 36 | "@": "/src", 37 | }, 38 | "path-intellisense.mappings": { 39 | "@": "${workspaceFolder}/src", 40 | }, 41 | "[html]": { 42 | "editor.defaultFormatter": "esbenp.prettier-vscode" 43 | }, 44 | "[json]": { 45 | "editor.defaultFormatter": "esbenp.prettier-vscode" 46 | }, 47 | "[jsonc]": { 48 | "editor.defaultFormatter": "esbenp.prettier-vscode" 49 | }, 50 | "[javascript]": { 51 | "editor.defaultFormatter": "esbenp.prettier-vscode" 52 | }, 53 | "[javascriptreact]": { 54 | "editor.defaultFormatter": "esbenp.prettier-vscode" 55 | }, 56 | "[markdown]": { 57 | "editor.defaultFormatter": "yzhang.markdown-all-in-one" 58 | }, 59 | "[typescript]": { 60 | "editor.defaultFormatter": "esbenp.prettier-vscode" 61 | }, 62 | "[typescriptreact]": { 63 | "editor.defaultFormatter": "esbenp.prettier-vscode" 64 | }, 65 | "[vue]": { 66 | "editor.defaultFormatter": "Vue.volar" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/shared/Header.vue: -------------------------------------------------------------------------------- 1 | 10 | 35 | 75 | -------------------------------------------------------------------------------- /src/assets/styles/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | box-sizing: border-box; 83 | margin: 0; 84 | padding: 0; 85 | border: 0; 86 | font-size: 100%; 87 | font: inherit; // stylelint-disable-line 88 | vertical-align: baseline; 89 | } 90 | 91 | /* HTML5 display-role reset for older browsers */ 92 | article, 93 | aside, 94 | details, 95 | figcaption, 96 | figure, 97 | footer, 98 | header, 99 | hgroup, 100 | menu, 101 | nav, 102 | section { 103 | display: block; 104 | } 105 | 106 | body { 107 | padding: 0 !important; 108 | line-height: 1; 109 | } 110 | 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | 116 | blockquote, 117 | q { 118 | quotes: none; 119 | } 120 | 121 | blockquote::before, 122 | blockquote::after, 123 | q::before, 124 | q::after { 125 | content: ''; 126 | content: none; 127 | } 128 | 129 | table { 130 | border-spacing: 0; 131 | border-collapse: collapse; 132 | } 133 | 134 | [hidden] { 135 | display: none !important; 136 | } 137 | 138 | a, 139 | a:hover { 140 | text-decoration: none; 141 | } 142 | 143 | img { 144 | border: 0; 145 | 146 | &[src=''] { 147 | opacity: 0; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/pwa/ReloadPrompt.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | 75 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v*.* 9 | 10 | jobs: 11 | deploy-docs: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install pnpm 27 | id: pnpm-install 28 | uses: pnpm/action-setup@v2.2.2 29 | 30 | - name: Get pnpm store directory 31 | id: pnpm-cache 32 | run: | 33 | echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" 34 | 35 | - uses: actions/cache@v3 36 | name: Setup pnpm cache 37 | with: 38 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} 39 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 40 | restore-keys: | 41 | ${{ runner.os }}-pnpm-store- 42 | 43 | - name: Install dependencies 44 | run: pnpm install 45 | 46 | - name: Build website 47 | run: pnpm build 48 | env: 49 | VITE_IS_VERCEL: true 50 | NODE_ENV: production 51 | 52 | # - name: Deploy 53 | # uses: JamesIves/github-pages-deploy-action@v4.3.3 54 | # with: 55 | # token: ${{ secrets.GITHUB_TOKEN }} 56 | # branch: gh-pages 57 | # folder: dist 58 | # git-config-name: yugasun 59 | # git-config-email: yuga_sun@163.com 60 | # commit-message: website deploy 61 | 62 | # - name: Deploy 63 | # uses: amondnet/vercel-action@v20 64 | # with: 65 | # vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required 66 | # github-token: ${{ secrets.GITHUB_TOKEN }} #Optional 67 | # vercel-org-id: yugasun 68 | # vercel-project-id: vue-template 69 | -------------------------------------------------------------------------------- /src/components/VueUse.vue: -------------------------------------------------------------------------------- 1 | 23 | 69 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue TypeScript Starter 2 | 3 | [![Vue3](https://img.shields.io/badge/Framework-Vue3-42b883)](https://vuejs.org/) 4 | [![TypeScript](https://img.shields.io/badge/Language-TypeScript-blue)](https://www.typescriptlang.org/) 5 | [![Vite](https://img.shields.io/badge/Develop-Vite-747bff)](https://vitejs.dev) 6 | [![Pinia](https://img.shields.io/badge/Store-Pinia-f7d336)](https://pinia.vuejs.org) 7 | [![Unocss](https://img.shields.io/badge/CSS-Unocss-858585)](https://uno.antfu.me/) 8 | [![Build](https://github.com/yugasun/vue-template/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/yugasun/vue-template/actions/workflows/deploy.yml) 9 | 10 | Vue template for starter using Vue3 + TypeScript + Vite + Pinia + Unocss 🚀 11 | 12 | ## Feature 13 | 14 | - [x] [Vue3.0](https://vuejs.org/) 15 | - [x] [Vue Router](https://github.com/vuejs/router) 16 | - [x] [TypeScript](https://www.typescriptlang.org/) 17 | - [x] [Vite](https://vitejs.dev/) Next Generation Frontend Tooling 18 | - [x] [Vue DevTools](https://devtools-next.vuejs.org) Vue DevTools - Unleash Vue Developer Experience 19 | - [x] [vite-plugin-pwa](https://github.com/antfu/vite-plugin-pwa) Zero-config PWA for Vite 20 | - [x] [Pinia](https://pinia.vuejs.org/) The Vue Store that you will enjoy using 21 | - [x] ⚙️ [Vitest](https://github.com/vitest-dev/vitest) Unit Testing with Vitest 22 | - [x] 🎉 [Element Plus](https://github.com/element-plus/element-plus) A Vue.js 3 UI Library made by Element team 23 | - [x] 🌈 [Ant Design Vue](https://github.com/vueComponent/ant-design-vue) An enterprise-class UI components based on Ant Design and Vue. 🐜 24 | - [x] [vueuse](https://github.com/vueuse/vueuse) Collection of essential Vue Composition Utilities for Vue 2 and 3 25 | - [x] [axios](https://github.com/axios/axios) Promise based HTTP client for the browser and node.js 26 | - [x] 🎨 [UnoCSS](https://github.com/antfu/unocss) - the instant on-demand atomic CSS engine 27 | - [x] 😃 [Use icons from any icon sets with classes](https://github.com/antfu/unocss/tree/main/packages/preset-icons) 28 | - [x] 🌍 [I18n ready](https://vue-i18n.intlify.dev/) Vue I18n Internationalization plugin for Vue.js 29 | - [x] [msw](https://mswjs.io/docs/) Seamless REST/GraphQL API mocking library for browser and Node.js. 30 | - [x] [ESLint](https://eslint.org/) 31 | - [x] [Prettier](https://prettier.io/) 32 | - [x] [Airbnb Style Guide](https://github.com/airbnb/javascript) 33 | - [x] [Commitlint](https://github.com/conventional-changelog/commitlint) Lint commit messages 34 | - [x] [Commitizen](https://github.com/commitizen/cz-cli) The commitizen command line utility. 35 | 36 | ## Start 37 | 38 | ```bash 39 | # 0. Clone project 40 | git clone https://github.com/yugasun/vue-ts-starter 41 | 42 | # 1. Install dependencies 43 | pnpm install 44 | 45 | # 2. Start develop server 46 | pnpm dev 47 | 48 | # 3. Build 49 | pnpm build 50 | ``` 51 | 52 | ## Customize 53 | 54 | If you want to use Ant Design Vue, just checkout the branch `antd`. 55 | 56 | ```bash 57 | git clone --branch antd https://github.com/yugasun/vue-ts-starter 58 | ``` 59 | 60 | If you don't need any UI components, just clone or checkout the branch `simple`. 61 | 62 | ```bash 63 | git clone --branch simple https://github.com/yugasun/vue-ts-starter 64 | ``` 65 | 66 | ## Recommended IDE Setup 67 | 68 | - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) 69 | 70 | ## License 71 | 72 | [MIT @yugasun](./LICENSE) 73 | -------------------------------------------------------------------------------- /src/components/LoginButton.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 101 | 111 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-property-sort-order-smacss', 5 | ], 6 | plugins: ['stylelint-order', 'stylelint-prettier'], 7 | // customSyntax: 'postcss-html', 8 | overrides: [ 9 | { 10 | files: ['**/*.(css|html|vue)'], 11 | customSyntax: 'postcss-html', 12 | }, 13 | { 14 | files: ['*.less', '**/*.less'], 15 | customSyntax: 'postcss-less', 16 | extends: [ 17 | 'stylelint-config-standard', 18 | 'stylelint-config-recommended-vue', 19 | ], 20 | }, 21 | { 22 | files: ['*.scss', '**/*.scss'], 23 | customSyntax: 'postcss-scss', 24 | extends: [ 25 | 'stylelint-config-standard-scss', 26 | 'stylelint-config-recommended-vue/scss', 27 | ], 28 | rule: { 29 | 'scss/percent-placeholder-pattern': null, 30 | 'scss/dollar-variable-pattern': null, 31 | }, 32 | }, 33 | ], 34 | rules: { 35 | 'prettier/prettier': true, 36 | 'media-feature-range-notation': null, 37 | 'selector-not-notation': null, 38 | 'import-notation': null, 39 | 'function-no-unknown': null, 40 | 'selector-class-pattern': null, 41 | 'selector-pseudo-class-no-unknown': [ 42 | true, 43 | { 44 | ignorePseudoClasses: ['global', 'deep'], 45 | }, 46 | ], 47 | 'selector-pseudo-element-no-unknown': [ 48 | true, 49 | { 50 | ignorePseudoElements: ['v-deep'], 51 | }, 52 | ], 53 | 'at-rule-no-unknown': [ 54 | true, 55 | { 56 | ignoreAtRules: [ 57 | 'tailwind', 58 | 'apply', 59 | 'variants', 60 | 'responsive', 61 | 'screen', 62 | 'function', 63 | 'if', 64 | 'each', 65 | 'include', 66 | 'mixin', 67 | 'extend', 68 | 'forward', 69 | 'use', 70 | ], 71 | }, 72 | ], 73 | 'no-empty-source': null, 74 | 'named-grid-areas-no-invalid': null, 75 | 'no-descending-specificity': null, 76 | 'font-family-no-missing-generic-family-keyword': null, 77 | 'rule-empty-line-before': [ 78 | 'always', 79 | { 80 | ignore: ['after-comment', 'first-nested'], 81 | }, 82 | ], 83 | 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }], 84 | 'order/order': [ 85 | [ 86 | 'dollar-variables', 87 | 'custom-properties', 88 | 'at-rules', 89 | 'declarations', 90 | { 91 | type: 'at-rule', 92 | name: 'supports', 93 | }, 94 | { 95 | type: 'at-rule', 96 | name: 'media', 97 | }, 98 | 'rules', 99 | ], 100 | { severity: 'error' }, 101 | ], 102 | }, 103 | ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'], 104 | }; 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-ts-starter", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "build:analyze": "cross-env vite build --mode analyze", 9 | "preview": "vite preview", 10 | "lint": "pnpm run lint:eslint && pnpm run lint:stylelint && pnpm run lint:prettier", 11 | "lint:eslint": "cross-env eslint --cache --ext=.js,.jsx,.ts,.tsx,.vue --fix --config=./eslint.config.js src", 12 | "lint:prettier": "prettier --write .", 13 | "lint:stylelint": "stylelint \"src/**/*.{vue,css,less,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/", 14 | "commit": "git-cz", 15 | "release": "npx bumpp --push --tag --commit 'release: v'", 16 | "postinstall": "npx msw init public --save", 17 | "prepare": "husky install", 18 | "test": "pnpm run test:unit", 19 | "test:unit": "vitest run", 20 | "test:unit:update": "vitest run --update", 21 | "type:check": "vue-tsc --noEmit --skipLibCheck" 22 | }, 23 | "dependencies": { 24 | "@element-plus/icons-vue": "^2.3.1", 25 | "@unocss/reset": "^0.53.6", 26 | "@vueuse/core": "^10.11.0", 27 | "@vueuse/integrations": "^10.11.0", 28 | "axios": "^1.7.2", 29 | "element-plus": "^2.7.5", 30 | "pinia": "^2.1.7", 31 | "universal-cookie": "^4.0.4", 32 | "vue": "^3.4.29", 33 | "vue-i18n": "^9.13.1", 34 | "vue-router": "^4.3.3", 35 | "workbox-core": "^7.1.0", 36 | "workbox-precaching": "^7.1.0", 37 | "workbox-routing": "^7.1.0", 38 | "workbox-window": "^7.1.0" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/cli": "^19.3.0", 42 | "@commitlint/config-conventional": "^19.2.2", 43 | "@iconify-json/carbon": "^1.1.36", 44 | "@intlify/unplugin-vue-i18n": "^4.0.0", 45 | "@pinia/testing": "^0.1.3", 46 | "@rollup/plugin-replace": "^5.0.7", 47 | "@types/node": "^20.14.6", 48 | "@typescript-eslint/eslint-plugin": "^7.13.1", 49 | "@typescript-eslint/parser": "^7.13.1", 50 | "@vitejs/plugin-vue": "^5.0.5", 51 | "@vue/test-utils": "^2.4.6", 52 | "autoprefixer": "^10.4.19", 53 | "bumpp": "^9.4.1", 54 | "commitizen": "^4.3.0", 55 | "commitlint-config-cz": "^0.13.3", 56 | "cross-env": "^7.0.3", 57 | "cz-conventional-changelog": "^3.3.0", 58 | "cz-customizable": "^7.0.0", 59 | "eslint": "^8.56.0", 60 | "eslint-config-prettier": "^9.1.0", 61 | "eslint-plugin-import": "^2.29.1", 62 | "eslint-plugin-prettier": "^5.1.3", 63 | "eslint-plugin-simple-import-sort": "^12.0.0", 64 | "eslint-plugin-vue": "^9.21.1", 65 | "husky": "^9.0.11", 66 | "jsdom": "^24.1.0", 67 | "lint-staged": "^15.2.7", 68 | "msw": "^1.3.3", 69 | "postcss": "^8.4.38", 70 | "postcss-scss": "^4.0.9", 71 | "prettier": "^3.3.2", 72 | "prettier-plugin-packagejson": "^2.5.0", 73 | "rollup": "^4.18.0", 74 | "sass": "^1.77.6", 75 | "standard-version": "^9.5.0", 76 | "stylelint": "^16.4.0", 77 | "stylelint-config-property-sort-order-smacss": "^10.0.0", 78 | "stylelint-config-recommended-scss": "^14.0.0", 79 | "stylelint-config-recommended-vue": "^1.5.0", 80 | "stylelint-config-standard": "^36.0.0", 81 | "stylelint-config-standard-scss": "^13.1.0", 82 | "stylelint-order": "^6.0.4", 83 | "stylelint-prettier": "^5.0.0", 84 | "tailwindcss": "^3.4.4", 85 | "typescript": "^5.4.5", 86 | "unocss": "^0.61.0", 87 | "unplugin-auto-import": "^0.17.6", 88 | "unplugin-vue-components": "^0.27.0", 89 | "unplugin-vue-macros": "^2.9.5", 90 | "vite": "^5.3.1", 91 | "vite-plugin-pwa": "^0.16.7", 92 | "vite-plugin-vue-devtools": "^7.3.2", 93 | "vitest": "^0.34.6", 94 | "vue-eslint-parser": "^9.4.3", 95 | "vue-tsc": "^2.0.14" 96 | }, 97 | "config": { 98 | "commitizen": { 99 | "path": "node_modules/cz-customizable" 100 | }, 101 | "cz-customizable": { 102 | "config": "./.cz-config.js" 103 | } 104 | }, 105 | "engines": { 106 | "node": ">=16.16.0" 107 | }, 108 | "packageManager": "pnpm@8.6.2", 109 | "msw": { 110 | "workerDirectory": "public" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import vue from '@vitejs/plugin-vue'; 4 | import AutoImport from 'unplugin-auto-import/vite'; 5 | import Components from 'unplugin-vue-components/vite'; 6 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 7 | import * as path from 'path'; 8 | import { ManifestOptions, VitePWA, VitePWAOptions } from 'vite-plugin-pwa'; 9 | import replace from '@rollup/plugin-replace'; 10 | import VueI18n from '@intlify/unplugin-vue-i18n/vite'; 11 | import Unocss from 'unocss/vite'; 12 | import VueDevTools from 'vite-plugin-vue-devtools'; 13 | 14 | const pwaOptions: Partial = { 15 | mode: 'development', 16 | base: '/', 17 | includeAssets: ['favicon.svg'], 18 | manifest: { 19 | name: 'PWA Router', 20 | short_name: 'PWA Router', 21 | theme_color: '#ffffff', 22 | icons: [ 23 | { 24 | src: 'pwa-192x192.png', // <== don't add slash, for testing 25 | sizes: '192x192', 26 | type: 'image/png', 27 | }, 28 | { 29 | src: '/pwa-512x512.png', // <== don't remove slash, for testing 30 | sizes: '512x512', 31 | type: 'image/png', 32 | }, 33 | { 34 | src: 'pwa-512x512.png', // <== don't add slash, for testing 35 | sizes: '512x512', 36 | type: 'image/png', 37 | purpose: 'any maskable', 38 | }, 39 | ], 40 | }, 41 | devOptions: { 42 | enabled: process.env.SW_DEV === 'true', 43 | /* when using generateSW the PWA plugin will switch to classic */ 44 | type: 'module', 45 | navigateFallback: 'index.html', 46 | }, 47 | }; 48 | 49 | const claims = process.env.CLAIMS === 'true'; 50 | const reload = process.env.RELOAD_SW === 'true'; 51 | 52 | if (process.env.SW === 'true') { 53 | pwaOptions.srcDir = 'src'; 54 | pwaOptions.filename = claims ? 'claims-sw.ts' : 'prompt-sw.ts'; 55 | pwaOptions.strategies = 'injectManifest'; 56 | (pwaOptions.manifest as Partial).name = 57 | 'PWA Inject Manifest'; 58 | (pwaOptions.manifest as Partial).short_name = 'PWA Inject'; 59 | } 60 | 61 | if (claims) pwaOptions.registerType = 'autoUpdate'; 62 | 63 | // https://vitejs.dev/config/ 64 | export default defineConfig({ 65 | resolve: { 66 | alias: { 67 | '@': path.resolve(__dirname, 'src'), 68 | }, 69 | }, 70 | build: { 71 | target: 'es2015', 72 | cssTarget: 'chrome80', 73 | rollupOptions: { 74 | output: { 75 | // 入口文件名(不能变,否则所有打包的 js hash 值全变了) 76 | entryFileNames: 'index.js', 77 | manualChunks: { 78 | vue: ['vue', 'pinia', 'vue-router'], 79 | elementplus: ['element-plus', '@element-plus/icons-vue'], 80 | }, 81 | }, 82 | }, 83 | }, 84 | css: { 85 | preprocessorOptions: { 86 | scss: { 87 | additionalData: `@use "@/assets/styles/element/index.scss" as *;`, 88 | }, 89 | }, 90 | }, 91 | plugins: [ 92 | vue(), 93 | AutoImport({ 94 | imports: [ 95 | 'vue', 96 | 'vue-router', 97 | 'vue-i18n', 98 | 'vue/macros', 99 | '@vueuse/head', 100 | '@vueuse/core', 101 | ], 102 | resolvers: [ElementPlusResolver()], 103 | dts: 'auto-imports.d.ts', 104 | vueTemplate: true, 105 | }), 106 | Components({ 107 | dts: 'components.d.ts', 108 | resolvers: [ElementPlusResolver()], 109 | }), 110 | 111 | // https://github.com/antfu/unocss 112 | // see unocss.config.ts for config 113 | Unocss(), 114 | 115 | VitePWA(pwaOptions), 116 | 117 | // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n 118 | VueI18n({ 119 | runtimeOnly: true, 120 | compositionOnly: true, 121 | /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ 122 | // @ts-ignore 123 | strictMessage: false, 124 | fullInstall: true, 125 | // do not support ts extension 126 | include: [path.resolve(__dirname, 'locales/*.{yaml,yml,json}')], 127 | }), 128 | 129 | replace({ 130 | preventAssignment: true, 131 | __DATE__: new Date().toISOString(), 132 | __RELOAD_SW__: reload ? 'true' : '', 133 | }), 134 | 135 | VueDevTools(), 136 | ], 137 | server: { 138 | port: 8080, 139 | host: '127.0.0.1', 140 | }, 141 | 142 | // https://github.com/vitest-dev/vitest 143 | test: { 144 | include: ['src/tests/**/*.test.ts'], 145 | environment: 'jsdom', 146 | server: { 147 | deps: { 148 | inline: ['@vue', '@vueuse', 'element-plus', 'pinia'], 149 | }, 150 | }, 151 | }, 152 | }); 153 | --------------------------------------------------------------------------------