├── 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 |
10 |
11 |
12 |
13 |
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 |
13 |
19 |
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 |
15 |
16 |
17 |
18 |
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 |
11 | {{ t('detail.loginTip') }}
12 |
13 | {{
14 | t('common.welcome', {
15 | name: userStore.userInfo?.username,
16 | })
17 | }}
18 |
19 |
20 |
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 |
12 | {{ msg }}
13 |
14 | {{
15 | t('common.welcome', {
16 | name: userStore.userInfo?.username,
17 | })
18 | }}
19 |
20 |
21 | {{ t('home.recommendIde') }}:
22 | VS Code
23 | +
24 | Volar
27 |
28 |
29 |
30 |
31 |
32 |
48 |
--------------------------------------------------------------------------------
/src/components/shared/Footer.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
35 |
36 |
46 |
--------------------------------------------------------------------------------
/src/components/Menu.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
33 |
34 |
45 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Built at: {{ date }} ({{ timeAgo }})
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
29 |
30 | Ping Web Worker
31 |
32 | Reset Message
33 |
34 |
35 |
36 | Response from web worker:
37 |
38 | Message: {{ pong }}
39 |
40 |
41 |
42 | Using ENV mode: {{ mode }}
44 |
45 |
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 |
11 |
34 |
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 |
31 |
36 |
37 | App ready to work offline
38 |
39 | New content available, click on reload button to update.
40 |
41 |
42 |
43 | Reload
44 |
45 |
Close
46 |
47 |
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 |
24 |
25 |
26 | VueUse {{ t('common.example') }}: ColorMode
27 |
28 |
29 |
36 |
43 |
50 |
57 |
64 |
65 | {{ mode }}
66 |
67 |
68 |
69 |
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue TypeScript Starter
2 |
3 | [](https://vuejs.org/)
4 | [](https://www.typescriptlang.org/)
5 | [](https://vitejs.dev)
6 | [](https://pinia.vuejs.org)
7 | [](https://uno.antfu.me/)
8 | [](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 |
52 |
53 | Logout
54 |
55 | Login
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 |
83 |
84 |
85 |
86 |
87 |
88 | Do you confirm to logout?
89 |
90 |
98 |
99 |
100 |
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 |
--------------------------------------------------------------------------------