tangning 10 months ago
parent
commit
cf6855d4d9
95 changed files with 27101 additions and 0 deletions
  1. 10 0
      .cypress-cucumber-preprocessorrc.json
  2. 18 0
      .eslintrc
  3. 11 0
      .gitignore
  4. 2 0
      .prettierignore
  5. 7 0
      .prettierrc
  6. 12 0
      .stylelintrc
  7. 12 0
      .vscode/extensions.json
  8. 5 0
      .vscode/settings.json
  9. 45 0
      cypress.config.ts
  10. 6 0
      e2e_wsl_setup.sh
  11. 13 0
      index.html
  12. 16721 0
      package-lock.json
  13. 82 0
      package.json
  14. 8 0
      postcss.config.js
  15. BIN
      public/favicon.ico
  16. BIN
      public/fonts/InterWeb/Inter-Black.woff
  17. BIN
      public/fonts/InterWeb/Inter-Black.woff2
  18. BIN
      public/fonts/InterWeb/Inter-BlackItalic.woff
  19. BIN
      public/fonts/InterWeb/Inter-BlackItalic.woff2
  20. BIN
      public/fonts/InterWeb/Inter-Bold.woff
  21. BIN
      public/fonts/InterWeb/Inter-Bold.woff2
  22. BIN
      public/fonts/InterWeb/Inter-BoldItalic.woff
  23. BIN
      public/fonts/InterWeb/Inter-BoldItalic.woff2
  24. BIN
      public/fonts/InterWeb/Inter-ExtraBold.woff
  25. BIN
      public/fonts/InterWeb/Inter-ExtraBold.woff2
  26. BIN
      public/fonts/InterWeb/Inter-ExtraBoldItalic.woff
  27. BIN
      public/fonts/InterWeb/Inter-ExtraBoldItalic.woff2
  28. BIN
      public/fonts/InterWeb/Inter-ExtraLight.woff
  29. BIN
      public/fonts/InterWeb/Inter-ExtraLight.woff2
  30. BIN
      public/fonts/InterWeb/Inter-ExtraLightItalic.woff
  31. BIN
      public/fonts/InterWeb/Inter-ExtraLightItalic.woff2
  32. BIN
      public/fonts/InterWeb/Inter-Italic.woff
  33. BIN
      public/fonts/InterWeb/Inter-Italic.woff2
  34. BIN
      public/fonts/InterWeb/Inter-Light.woff
  35. BIN
      public/fonts/InterWeb/Inter-Light.woff2
  36. BIN
      public/fonts/InterWeb/Inter-LightItalic.woff
  37. BIN
      public/fonts/InterWeb/Inter-LightItalic.woff2
  38. BIN
      public/fonts/InterWeb/Inter-Medium.woff
  39. BIN
      public/fonts/InterWeb/Inter-Medium.woff2
  40. BIN
      public/fonts/InterWeb/Inter-MediumItalic.woff
  41. BIN
      public/fonts/InterWeb/Inter-MediumItalic.woff2
  42. BIN
      public/fonts/InterWeb/Inter-Regular.woff
  43. BIN
      public/fonts/InterWeb/Inter-Regular.woff2
  44. BIN
      public/fonts/InterWeb/Inter-SemiBold.woff
  45. BIN
      public/fonts/InterWeb/Inter-SemiBold.woff2
  46. BIN
      public/fonts/InterWeb/Inter-SemiBoldItalic.woff
  47. BIN
      public/fonts/InterWeb/Inter-SemiBoldItalic.woff2
  48. BIN
      public/fonts/InterWeb/Inter-Thin.woff
  49. BIN
      public/fonts/InterWeb/Inter-Thin.woff2
  50. BIN
      public/fonts/InterWeb/Inter-ThinItalic.woff
  51. BIN
      public/fonts/InterWeb/Inter-ThinItalic.woff2
  52. BIN
      public/fonts/InterWeb/Inter-italic.var.woff2
  53. BIN
      public/fonts/InterWeb/Inter-roman.var.woff2
  54. BIN
      public/fonts/InterWeb/Inter.var.woff2
  55. 186 0
      src/App.vue
  56. BIN
      src/assets/chronos.jpg
  57. BIN
      src/assets/logo.png
  58. 12 0
      src/components/global/index.ts
  59. 14 0
      src/components/global/vendor/icons.ts
  60. 5 0
      src/components/global/vendor/index.ts
  61. 46 0
      src/components/global/vendor/naive.ts
  62. 1 0
      src/components/index.ts
  63. 8 0
      src/env.d.ts
  64. 13 0
      src/main.ts
  65. 52 0
      src/router/index.ts
  66. 1 0
      src/store/index.ts
  67. 19 0
      src/store/main.ts
  68. 198 0
      src/styles/fonts/_inter.sass
  69. 1 0
      src/styles/fonts/index.sass
  70. 7 0
      src/styles/index.sass
  71. 3 0
      src/styles/vendor/_tailwind.css
  72. 2 0
      src/styles/vendor/index.sass
  73. 97 0
      src/views/HelloWorld.vue
  74. 23 0
      src/views/basicSettings/index.vue
  75. 23 0
      src/views/digitalHuman/index.vue
  76. 23 0
      src/views/message/index.vue
  77. 108 0
      src/views/textToaudio/index.vue
  78. 23 0
      src/views/topicNavigation/index.vue
  79. 30 0
      tailwind.config.ts
  80. 8 0
      test/e2e/.eslintrc.js
  81. 31 0
      test/e2e/cypress.d.ts
  82. 53 0
      test/e2e/plugins/index.ts
  83. 79 0
      test/e2e/specs/common/index.ts
  84. 45 0
      test/e2e/specs/features/main/HelloWorld.feature
  85. 31 0
      test/e2e/specs/features/main/HelloWorld/HelloWorld.ts
  86. 19 0
      test/e2e/support/commands.ts
  87. 18 0
      test/e2e/support/index.ts
  88. 14 0
      test/e2e/tsconfig.json
  89. 63 0
      test/unit/App.spec.ts
  90. 35 0
      test/unit/testhelper.ts
  91. 23 0
      test/unit/tsconfig.json
  92. 37 0
      test/unit/views/HelloWorld.spec.ts
  93. 28 0
      tsconfig.json
  94. 48 0
      vite.config.ts
  95. 8722 0
      yarn.lock

+ 10 - 0
.cypress-cucumber-preprocessorrc.json

@@ -0,0 +1,10 @@
+{
+    "stepDefinitions": [
+        "test/e2e/specs/features/**/*.{js,ts}",
+        "test/e2e/specs/common/**/*.{js,ts}"
+    ],
+    "json": {
+        "enabled": true,
+        "output": "test/e2e/reports/cucumber-json/chronos-e2e.cucumber.json"
+    }
+}

+ 18 - 0
.eslintrc

@@ -0,0 +1,18 @@
+{
+  "env": {
+    "es2021": true,
+    "node": true,
+    "vue/setup-compiler-macros": true
+  },
+  "extends": [
+    "eslint:recommended",
+    "plugin:vue/vue3-essential",
+    "plugin:@typescript-eslint/recommended",
+    "prettier"
+  ],
+  "parser": "vue-eslint-parser",
+  "parserOptions": {
+    "parser": "@typescript-eslint/parser",
+    "sourceType": "module"
+  }
+}

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+coverage
+yarn-error.log
+/cypress
+/.nyc_output
+reports
+cucumber-messages.ndjson

+ 2 - 0
.prettierignore

@@ -0,0 +1,2 @@
+dist
+coverage

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "printWidth": 80,
+  "tabWidth": 2,
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "none"
+}

+ 12 - 0
.stylelintrc

@@ -0,0 +1,12 @@
+{
+  "extends": "stylelint-config-recommended",
+  "rules": {
+    "at-rule-no-unknown": [
+      true,
+      {
+        "ignoreAtRules": ["extends", "tailwind"]
+      }
+    ],
+    "block-no-empty": null
+  }
+}

+ 12 - 0
.vscode/extensions.json

@@ -0,0 +1,12 @@
+{
+  "recommendations": [
+    "vue.volar",
+    "zixuanchen.vitest-explorer",
+    "bradlc.vscode-tailwindcss",
+    "foxundermoon.shell-format",
+    "syler.sass-indented",
+    "esbenp.prettier-vscode",
+    "alexkrechik.cucumberautocomplete",
+    "dbaeumer.vscode-eslint"
+  ]
+}

+ 5 - 0
.vscode/settings.json

@@ -0,0 +1,5 @@
+{
+  "css.validate": false,
+  "less.validate": false,
+  "scss.validate": false
+}

+ 45 - 0
cypress.config.ts

@@ -0,0 +1,45 @@
+import { defineConfig } from 'cypress'
+import { devServer } from '@cypress/vite-dev-server'
+import { ViteDevServerConfig } from '@cypress/vite-dev-server/dist/devServer'
+import { setupNodeEvents } from './test/e2e/plugins'
+import viteConfig from './vite.config'
+
+export default defineConfig({
+  defaultCommandTimeout: 30000,
+  requestTimeout: 30000,
+  fileServerFolder: '.',
+  fixturesFolder: 'test/e2e/fixtures',
+  experimentalFetchPolyfill: true,
+  trashAssetsBeforeRuns: true,
+  viewportWidth: 1440,
+  viewportHeight: 990,
+  env: {
+    CYPRESS_COVERAGE: 'true',
+    TAGS: 'not @ignore',
+    BASE_URL: 'http://localhost:3000'
+  },
+  e2e: {
+    baseUrl: 'http://localhost:3000',
+    // We've imported your old cypress plugins here.
+    // You may want to clean this up later by importing these.
+    setupNodeEvents,
+    specPattern: 'test/e2e/specs/**/*.{feature,features}',
+    supportFile: 'test/e2e/support/index.ts',
+    excludeSpecPattern: ['*.{ts,tsx,js,jsx}']
+  },
+  component: {
+    devServer(config: ViteDevServerConfig) {
+      return devServer({
+        ...config,
+        framework: 'vue',
+        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+        //@ts-ignore
+        viteConfig: {
+          ...viteConfig
+        }
+      })
+    },
+    specPattern: 'test/**/*.{feature,features}',
+    excludeSpecPattern: ['*.{ts,tsx,js,jsx}']
+  }
+})

+ 6 - 0
e2e_wsl_setup.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+
+sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
+
+export LIBGL_ALWAYS_INDIRECT=1
+export DISPLAY=:0

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

File diff suppressed because it is too large
+ 16721 - 0
package-lock.json


+ 82 - 0
package.json

@@ -0,0 +1,82 @@
+{
+  "name": "vue-ts-vite-esc",
+  "version": "0.0.0",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vite",
+    "build": "npm run validate:types && vite build",
+    "validate:types": "vue-tsc --noEmit",
+    "serve": "vite preview",
+    "test": "vitest run --silent",
+    "test:watch": "vitest",
+    "test:e2e:wsl": "env LIBGL_ALWAYS_INDIRECT=1 DISPLAY=:0 npm run test:e2e",
+    "test:e2e": "env CYPRESS_TEST=true concurrently -r \"npm run dev\" \"cypress open\"",
+    "test:e2e:headless": "env CYPRESS_TEST=true concurrently -k -r -s first \"npm run dev\" \"cypress run\"",
+    "coverage": "npm run test -- --coverage",
+    "lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore --fix src",
+    "format": "prettier . --write"
+  },
+  "dependencies": {
+    "@headlessui/vue": "^1.7.20",
+    "@heroicons/vue": "^2.1.3",
+    "@popperjs/core": "^2.11.8",
+    "@vicons/ionicons5": "^0.12.0",
+    "body-scroll-lock": "^4.0.0-beta.0",
+    "pinia": "^2.1.7",
+    "vue": "^3.4.23",
+    "vue-router": "^4.3.2"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.24.4",
+    "@babel/preset-env": "^7.24.7",
+    "@babel/types": "^7.24.9",
+    "@badeball/cypress-cucumber-preprocessor": "^20.0.3",
+    "@bahmutov/cypress-esbuild-preprocessor": "^2.2.0",
+    "@cypress/browserify-preprocessor": "^3.0.2",
+    "@cypress/code-coverage": "^3.12.35",
+    "@cypress/vite-dev-server": "^5.0.7",
+    "@cypress/vue": "^6.0.0",
+    "@pinia/testing": "^0.1.3",
+    "@tailwindcss/aspect-ratio": "^0.4.2",
+    "@tailwindcss/forms": "^0.5.7",
+    "@tailwindcss/typography": "^0.5.12",
+    "@types/node": "^20.12.7",
+    "@typescript-eslint/eslint-plugin": "^7.7.0",
+    "@typescript-eslint/parser": "^7.7.0",
+    "@vicons/fa": "^0.12.0",
+    "@vitejs/plugin-vue": "^5.0.4",
+    "@vitest/coverage-v8": "^1.6.0",
+    "@vitest/ui": "^1.6.0",
+    "@vue/compiler-dom": "^3.4.23",
+    "@vue/compiler-sfc": "^3.4.23",
+    "@vue/server-renderer": "^3.4.33",
+    "@vue/test-utils": "^2.4.5",
+    "autoprefixer": "^10.4.19",
+    "babel-loader": "^9.1.3",
+    "c8": "^9.1.0",
+    "concurrently": "^8.2.2",
+    "cypress": "^13.8.0",
+    "esbuild": "^0.20.2",
+    "eslint": "^8.57.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-vue": "^9.25.0",
+    "happy-dom": "^14.7.1",
+    "naive-ui": "^2.38.1",
+    "postcss": "^8.4.38",
+    "postcss-import": "^16.1.0",
+    "postcss-scss": "^4.0.9",
+    "prettier": "^3.2.5",
+    "sass": "^1.75.0",
+    "stylelint": "^16.3.1",
+    "stylelint-config-recommended": "^14.0.0",
+    "tailwindcss": "^3.4.3",
+    "typescript": "^5.4.5",
+    "vfonts": "^0.0.3",
+    "vite": "^5.2.10",
+    "vite-plugin-istanbul": "^6.0.0",
+    "vite-tsconfig-paths": "^4.3.2",
+    "vitest": "^1.6.0",
+    "vue-tsc": "^2.0.26",
+    "webpack": "^5.91.0"
+  }
+}

+ 8 - 0
postcss.config.js

@@ -0,0 +1,8 @@
+module.exports = {
+  plugins: [
+    require('postcss-import'),
+    require('tailwindcss/nesting'),
+    require('tailwindcss'),
+    require('autoprefixer')
+  ]
+}

BIN
public/favicon.ico


BIN
public/fonts/InterWeb/Inter-Black.woff


BIN
public/fonts/InterWeb/Inter-Black.woff2


BIN
public/fonts/InterWeb/Inter-BlackItalic.woff


BIN
public/fonts/InterWeb/Inter-BlackItalic.woff2


BIN
public/fonts/InterWeb/Inter-Bold.woff


BIN
public/fonts/InterWeb/Inter-Bold.woff2


BIN
public/fonts/InterWeb/Inter-BoldItalic.woff


BIN
public/fonts/InterWeb/Inter-BoldItalic.woff2


BIN
public/fonts/InterWeb/Inter-ExtraBold.woff


BIN
public/fonts/InterWeb/Inter-ExtraBold.woff2


BIN
public/fonts/InterWeb/Inter-ExtraBoldItalic.woff


BIN
public/fonts/InterWeb/Inter-ExtraBoldItalic.woff2


BIN
public/fonts/InterWeb/Inter-ExtraLight.woff


BIN
public/fonts/InterWeb/Inter-ExtraLight.woff2


BIN
public/fonts/InterWeb/Inter-ExtraLightItalic.woff


BIN
public/fonts/InterWeb/Inter-ExtraLightItalic.woff2


BIN
public/fonts/InterWeb/Inter-Italic.woff


BIN
public/fonts/InterWeb/Inter-Italic.woff2


BIN
public/fonts/InterWeb/Inter-Light.woff


BIN
public/fonts/InterWeb/Inter-Light.woff2


BIN
public/fonts/InterWeb/Inter-LightItalic.woff


BIN
public/fonts/InterWeb/Inter-LightItalic.woff2


BIN
public/fonts/InterWeb/Inter-Medium.woff


BIN
public/fonts/InterWeb/Inter-Medium.woff2


BIN
public/fonts/InterWeb/Inter-MediumItalic.woff


BIN
public/fonts/InterWeb/Inter-MediumItalic.woff2


BIN
public/fonts/InterWeb/Inter-Regular.woff


BIN
public/fonts/InterWeb/Inter-Regular.woff2


BIN
public/fonts/InterWeb/Inter-SemiBold.woff


BIN
public/fonts/InterWeb/Inter-SemiBold.woff2


BIN
public/fonts/InterWeb/Inter-SemiBoldItalic.woff


BIN
public/fonts/InterWeb/Inter-SemiBoldItalic.woff2


BIN
public/fonts/InterWeb/Inter-Thin.woff


BIN
public/fonts/InterWeb/Inter-Thin.woff2


BIN
public/fonts/InterWeb/Inter-ThinItalic.woff


BIN
public/fonts/InterWeb/Inter-ThinItalic.woff2


BIN
public/fonts/InterWeb/Inter-italic.var.woff2


BIN
public/fonts/InterWeb/Inter-roman.var.woff2


BIN
public/fonts/InterWeb/Inter.var.woff2


+ 186 - 0
src/App.vue

@@ -0,0 +1,186 @@
+<template>
+  <n-config-provider data-cy="app" :theme="theme" :class="{ dark: mode }">
+    <n-layout position="absolute" style="height: 100vh">
+      <n-layout-header style="padding: 24px" bordered>
+        <div class="flex justify-between items-center">
+          <div class="back" style="position: relative; top: 4px">
+            <n-icon size="24" :component="ChevronBackOutline" />
+            <span style="position: relative; top: -6px">返回我的场景</span>
+          </div>
+          <n-button type="primary"> 保存 </n-button>
+          <!-- <n-switch data-cy="dark-mode" v-model:value="mode">
+            <template #checked> Dark </template>
+            <template #unchecked> Light </template>
+            <template #checked-icon>
+              <n-icon>
+                <moon />
+              </n-icon>
+            </template>
+            <template #unchecked-icon>
+              <n-icon>
+                <sun />
+              </n-icon>
+            </template>
+          </n-switch> -->
+        </div>
+      </n-layout-header>
+      <n-layout
+        class="bg-white dark:bg-gray-800 dark:text-white text-gray-800 h-screen w-screen"
+        position="absolute"
+        style="top: 83px; bottom: 64px"
+        has-sider
+      >
+        <!-- collapse-mode="transform" 
+          @mouseover="collapsed = false"
+          @mouseleave="collapsed = true" -->
+        <n-layout-sider
+          data-cy="sidebar"
+          :width="180"
+          :collapsed="collapsed"
+          :native-scrollbar="false"
+          bordered
+        >
+          <n-menu :options="menuOptions" @update:value="handleUpdateValue" />
+        </n-layout-sider>
+        <n-layout-content
+          content-class="layoutContent"
+          :native-scrollbar="false"
+        >
+          <router-view />
+        </n-layout-content>
+      </n-layout>
+    </n-layout>
+  </n-config-provider>
+</template>
+
+<script setup lang="ts">
+import { darkTheme, lightTheme } from 'naive-ui'
+import { computed, ref, watchEffect, h } from 'vue'
+import type { Component } from 'vue'
+import {
+  ChevronBackOutline,
+  Layers,
+  Cog,
+  HomeOutline as HomeIcon,
+  RepeatOutline,
+  PeopleOutline,
+  ChatboxEllipses
+} from '@vicons/ionicons5'
+const darkStore = localStorage.getItem('dark')
+import { RouterLink } from 'vue-router'
+import { NIcon, useMessage } from 'naive-ui'
+const prefersDark: boolean = darkStore
+  ? darkStore === 'true'
+  : window.matchMedia('(prefers-color-scheme: dark)').matches
+
+const mode = ref<boolean>(prefersDark)
+const theme = computed(() => (mode.value ? darkTheme : lightTheme))
+
+watchEffect(() => {
+  localStorage.setItem('dark', `${mode.value}`)
+})
+function renderIcon(icon: Component) {
+  return () => h(NIcon, null, { default: () => h(icon) })
+}
+const collapsed = ref(false)
+const menuOptions = [
+  {
+    label: () =>
+      h(
+        RouterLink,
+        {
+          to: {
+            name: 'basicSettings'
+            // params: {
+            //   lang: 'zh-CN'
+            // }
+          }
+        },
+        { default: () => '基础设置' }
+      ),
+    key: 'basicSettings',
+    icon: renderIcon(Cog)
+  },
+  {
+    label: () =>
+      h(
+        RouterLink,
+        {
+          to: {
+            name: 'digitalHuman'
+            // params: {
+            //   lang: 'zh-CN'
+            // }
+          }
+        },
+        { default: () => '数字人播报' }
+      ),
+    key: 'digitalHuman',
+    icon: renderIcon(PeopleOutline)
+  },
+  {
+    label: () =>
+      h(
+        RouterLink,
+        {
+          to: {
+            name: 'textToaudio'
+            // params: {
+            //   lang: 'zh-CN'
+            // }
+          }
+        },
+        { default: () => '文字语音互转' }
+      ),
+    key: 'textToaudio',
+    icon: renderIcon(RepeatOutline)
+  },
+  {
+    label: () =>
+      h(
+        RouterLink,
+        {
+          to: {
+            name: 'message'
+            // params: {
+            //   lang: 'zh-CN'
+            // }
+          }
+        },
+        { default: () => '留言互动' }
+      ),
+    key: 'message',
+    icon: renderIcon(ChatboxEllipses)
+  },
+  {
+    label: () =>
+      h(
+        RouterLink,
+        {
+          to: {
+            name: 'topicNavigation'
+            // params: {
+            //   lang: 'zh-CN'
+            // }
+          }
+        },
+        { default: () => '专题导航' }
+      ),
+    key: 'topicNavigation',
+    icon: renderIcon(Layers)
+  }
+]
+const handleUpdateValue = (value: string) => {
+  console.log(value)
+}
+</script>
+
+<style lang="sass">
+#app
+  font-family: Inter, Avenir, Helvetica, Arial, sans-serif
+  -webkit-font-smoothing: antialiased
+  -moz-osx-font-smoothing: grayscale
+.layoutContent
+  padding: 10px
+  max-height: calc(100vh - 83px)
+</style>

BIN
src/assets/chronos.jpg


BIN
src/assets/logo.png


+ 12 - 0
src/components/global/index.ts

@@ -0,0 +1,12 @@
+import vendorPlugins from './vendor'
+import { App, Plugin } from 'vue'
+
+const globalPlugins = [...vendorPlugins]
+
+export const globalComponents = {
+  install(app: App) {
+    for (const { install } of globalPlugins) {
+      install?.(app)
+    }
+  }
+} as Plugin

+ 14 - 0
src/components/global/vendor/icons.ts

@@ -0,0 +1,14 @@
+import { Moon, Sun } from '@vicons/fa'
+import { App, Component } from 'vue'
+
+const icons: Record<string, Component> = {
+  Moon,
+  Sun
+}
+
+export default {
+  install(app: App) {
+    for (const [name, component] of Object.entries(icons))
+      app.component(name, component)
+  }
+}

+ 5 - 0
src/components/global/vendor/index.ts

@@ -0,0 +1,5 @@
+import naive from './naive'
+import icons from './icons'
+import { Plugin } from 'vue'
+
+export default [naive, icons] as Array<Plugin>

+ 46 - 0
src/components/global/vendor/naive.ts

@@ -0,0 +1,46 @@
+import {
+  create,
+  NButton,
+  NConfigProvider,
+  NH1,
+  NH2,
+  NH3,
+  NH4,
+  NH5,
+  NH6,
+  NHr,
+  NIcon,
+  NLayout,
+  NLayoutContent,
+  NLayoutFooter,
+  NLayoutHeader,
+  NLayoutSider,
+  NMenu,
+  NScrollbar,
+  NSwitch,
+  NDataTable,
+} from 'naive-ui'
+
+export default create({
+  components: [
+    NButton,
+    NConfigProvider,
+    NH1,
+    NH2,
+    NH3,
+    NH4,
+    NH5,
+    NH6,
+    NHr,
+    NIcon,
+    NLayout,
+    NLayoutHeader,
+    NLayoutContent,
+    NLayoutFooter,
+    NLayoutSider,
+    NMenu,
+    NScrollbar,
+    NSwitch,
+    NDataTable
+  ]
+})

+ 1 - 0
src/components/index.ts

@@ -0,0 +1 @@
+export * from './global'

+ 8 - 0
src/env.d.ts

@@ -0,0 +1,8 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import { DefineComponent } from 'vue'
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 13 - 0
src/main.ts

@@ -0,0 +1,13 @@
+import { createApp } from 'vue'
+import App from '@/App.vue'
+import '@/styles/index.sass'
+import router from '@/router'
+import { createPinia } from 'pinia'
+import { globalComponents } from '@/components'
+
+const app = createApp(App)
+app.use(router)
+app.use(createPinia())
+app.use(globalComponents)
+
+app.mount('#app')

+ 52 - 0
src/router/index.ts

@@ -0,0 +1,52 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes: [
+    {
+      path: '/',
+      name: 'home',
+      component: () => import('@/views/HelloWorld.vue'),
+      props: {
+        msg: 'Hello Vue 3 + TypeScript + Vite + Tailwind CSS/UI + Pinia + Vue Router'
+      }
+    },{
+      path: '/basicSettings',
+      name: 'basicSettings',
+      component: () => import('@/views/basicSettings/index.vue'),
+      props: {
+        msg: 'Hello Vue 3 + TypeScript + Vite + Tailwind CSS/UI + Pinia + Vue Router'
+      }
+    },{
+      path: '/digitalHuman',
+      name: 'digitalHuman',
+      component: () => import('@/views/digitalHuman/index.vue'),
+      props: {
+        msg: 'Hello Vue 3 + TypeScript + Vite + Tailwind CSS/UI + Pinia + Vue Router'
+      }
+    },{
+      path: '/textToaudio',
+      name: 'textToaudio',
+      component: () => import('@/views/textToaudio/index.vue'),
+      props: {
+        msg: 'Hello Vue 3 + TypeScript + Vite + Tailwind CSS/UI + Pinia + Vue Router'
+      }
+    },{
+      path: '/message',
+      name: 'message',
+      component: () => import('@/views/message/index.vue'),
+      props: {
+        msg: 'Hello Vue 3 + TypeScript + Vite + Tailwind CSS/UI + Pinia + Vue Router'
+      }
+    },{
+      path: '/topicNavigation',
+      name: 'topicNavigation',
+      component: () => import('@/views/topicNavigation/index.vue'),
+      props: {
+        msg: 'Hello Vue 3 + TypeScript + Vite + Tailwind CSS/UI + Pinia + Vue Router'
+      }
+    }
+  ]
+})
+
+export default router

+ 1 - 0
src/store/index.ts

@@ -0,0 +1 @@
+export * from './main'

+ 19 - 0
src/store/main.ts

@@ -0,0 +1,19 @@
+import { defineStore } from 'pinia'
+
+// useStore could be anything like useUser, useCart
+// the first argument is a unique id of the store across your application
+export const useMainStore = defineStore('main', {
+  state: () => {
+    return {
+      counter: 0
+    }
+  },
+  getters: {
+    count: ({ counter }) => counter
+  },
+  actions: {
+    incrementCounter(count: number) {
+      this.counter += count
+    }
+  }
+})

+ 198 - 0
src/styles/fonts/_inter.sass

@@ -0,0 +1,198 @@
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 100
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Thin.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Thin.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 100
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-ThinItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-ThinItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 200
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-ExtraLight.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-ExtraLight.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 200
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-ExtraLightItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-ExtraLightItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 300
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Light.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Light.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 300
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-LightItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-LightItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 400
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Regular.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Regular.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 400
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Italic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Italic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 500
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Medium.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Medium.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 500
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-MediumItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-MediumItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 600
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-SemiBold.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-SemiBold.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 600
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-SemiBoldItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-SemiBoldItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 700
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Bold.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Bold.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 700
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-BoldItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-BoldItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 800
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-ExtraBold.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-ExtraBold.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 800
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-ExtraBoldItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-ExtraBoldItalic.woff") format("woff")
+
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  normal
+  font-weight: 900
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-Black.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-Black.woff") format("woff")
+
+@font-face 
+  font-family: 'Inter'
+  font-style:  italic
+  font-weight: 900
+  font-display: swap
+  src: url("/fonts/InterWeb/Inter-BlackItalic.woff2") format("woff2")
+  src: url("/fonts/InterWeb/Inter-BlackItalic.woff") format("woff")
+
+
+// -------------------------------------------------------
+// Variable font.
+// Usage:
+
+//   html  font-family: 'Inter', sans-serif
+//   @supports (font-variation-settings: normal) 
+//     html  font-family: 'Inter var', sans-serif
+
+@font-face 
+  font-family: 'Inter var'
+  font-weight: 100 900
+  font-display: swap
+  font-style: normal
+  font-named-instance: 'Regular'
+  src: url("/fonts/InterWeb/Inter-roman.var.woff2") format("woff2")
+
+@font-face 
+  font-family: 'Inter var'
+  font-weight: 100 900
+  font-display: swap
+  font-style: italic
+  font-named-instance: 'Italic'
+  src: url("/fonts/InterWeb/Inter-italic.var.woff2") format("woff2")
+
+
+
+// --------------------------------------------------------------------------
+// [EXPERIMENTAL] Multi-axis, single variable font.
+
+// Slant axis is not yet widely supported (as of February 2019) and thus this
+// multi-axis single variable font is opt-in rather than the default.
+
+// When using this, you will probably need to set font-variation-settings
+// explicitly, e.g.
+
+//   *  font-variation-settings: "slnt" 0deg 
+//   .italic  font-variation-settings: "slnt" 10deg 
+
+@font-face 
+  font-family: 'Inter var experimental'
+  font-weight: 100 900
+  font-display: swap
+  font-style: oblique 0deg 10deg
+  src: url("/fonts/InterWeb/Inter.var.woff2") format("woff2")
+

+ 1 - 0
src/styles/fonts/index.sass

@@ -0,0 +1 @@
+@import "_inter.sass"

+ 7 - 0
src/styles/index.sass

@@ -0,0 +1,7 @@
+// VENDOR
+
+@import "vendor/index.sass"
+
+// FONTS    
+
+@import "fonts/index.sass"

+ 3 - 0
src/styles/vendor/_tailwind.css

@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

+ 2 - 0
src/styles/vendor/index.sass

@@ -0,0 +1,2 @@
+// TAILWIND
+@import "_tailwind"

+ 97 - 0
src/views/HelloWorld.vue

@@ -0,0 +1,97 @@
+<template>
+  <img
+    class="pt-10 mx-auto mb-4"
+    alt="Chronos logo"
+    data-cy="chronos-logo"
+    src="@/assets/chronos.jpg"
+  />
+  <h1>{{ msg }}</h1>
+  <p>
+    Recommended IDE setup:
+    <a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
+    +
+    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
+  </p>
+
+  <p>See <code>README.md</code> for more information.</p>
+
+  <p>
+    <a href="https://vitejs.dev/guide/features.html" target="_blank">
+      Vite Docs
+    </a>
+    |
+    <a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
+    |
+    <a href="https://pinia.vuejs.org/introduction.html" target="_blank">
+      Pinia Docs
+    </a>
+    |
+    <a href="https://next.router.vuejs.org/guide/" target="_blank">
+      Vue Router Next Docs
+    </a>
+    |
+    <a href="https://tailwindui.com" target="_blank">Tailwind UI Docs</a>
+    |
+    <a href="https://tailwindcss.com/docs" target="_blank">Tailwind CSS Docs</a>
+    |
+    <a
+      href="https://www.naiveui.com/en-US/dark/docs/introduction"
+      target="_blank"
+    >
+      Naive UI Docs
+    </a>
+  </p>
+  <NButton
+    data-cy="click-me"
+    :type="variant"
+    :disabled="variant === 'error'"
+    class="capitalize mx-auto my-2"
+    @click="incrementCounter(1)"
+  >
+    <b>{{ message }}</b>
+  </NButton>
+  <p>
+    Edit
+    <code>views/HelloWorld.vue</code> to test hot module replacement.
+  </p>
+</template>
+
+<script setup lang="ts">
+import { useMainStore } from '@/store'
+import { computed } from 'vue'
+
+defineProps<{ msg: string }>()
+
+const main = useMainStore()
+
+const { incrementCounter } = main
+
+const message = computed(() => {
+  if (main.count >= 60) return "It's Broken!"
+  if (main.count > 50) return 'Uh-oh'
+  if (main.count > 30) return 'Slow Down..'
+  if (main.count > 10) return 'Great Job!'
+  return 'Click Me'
+})
+
+const variant = computed(() => {
+  if (main.count >= 60) return 'error'
+  if (main.count > 10) return 'success'
+  return 'default'
+})
+</script>
+
+<style lang="sass" scoped>
+a
+  color: #42b983
+
+label
+  margin: 0 0.5em
+  font-weight: bold
+
+code
+  background-color: #eee
+  padding: 2px 4px
+  border-radius: 4px
+  color: #304455
+</style>

+ 23 - 0
src/views/basicSettings/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>基础设置页面</div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ msg: string }>()
+
+</script>
+
+<style lang="sass" scoped>
+a
+  color: #42b983
+
+label
+  margin: 0 0.5em
+  font-weight: bold
+
+code
+  background-color: #eee
+  padding: 2px 4px
+  border-radius: 4px
+  color: #304455
+</style>

+ 23 - 0
src/views/digitalHuman/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>数字人播报</div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ msg: string }>()
+
+</script>
+
+<style lang="sass" scoped>
+a
+  color: #42b983
+
+label
+  margin: 0 0.5em
+  font-weight: bold
+
+code
+  background-color: #eee
+  padding: 2px 4px
+  border-radius: 4px
+  color: #304455
+</style>

+ 23 - 0
src/views/message/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>留言互动</div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ msg: string }>()
+
+</script>
+
+<style lang="sass" scoped>
+a
+  color: #42b983
+
+label
+  margin: 0 0.5em
+  font-weight: bold
+
+code
+  background-color: #eee
+  padding: 2px 4px
+  border-radius: 4px
+  color: #304455
+</style>

+ 108 - 0
src/views/textToaudio/index.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="textToaudio m-4">
+    <div class="tablehader flex justify-between items-center mb-2.5">
+      <div class="title">文字语音互转</div>
+      <div class="bottomList">
+        <n-space>
+          <n-button type="primary"> 音转文 </n-button>
+          <n-button type="primary"> 文转音 </n-button>
+        </n-space>
+      </div>
+    </div>
+    <!-- <n-scrollbar style="max-height: calc(100vh - 100px)" trigger="none"> -->
+    <n-data-table
+      pagination-behavior-on-filter="first"
+      :columns="columns"
+      :data="data"
+      :pagination="paginationReactive"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, h, reactive } from 'vue'
+import { NButton,  } from 'naive-ui'
+defineProps<{ msg: string }>()
+const paginationReactive = reactive({
+  page: 2,
+  pageSize: 5,
+  showSizePicker: true,
+  pageSizes: [3, 5, 7],
+  onChange: (page) => {
+    paginationReactive.page = page
+  },
+  onUpdatePageSize: (pageSize) => {
+    paginationReactive.pageSize = pageSize
+    paginationReactive.page = 1
+  }
+})
+const columns = [
+  {
+    title: 'Name',
+    key: 'name'
+  },
+  {
+    title: 'Age',
+    key: 'age'
+  },
+  {
+    title: 'Address',
+    key: 'address',
+    defaultFilterOptionValues: [],
+    filterOptions: [
+      {
+        label: 'London',
+        value: 'London'
+      },
+      {
+        label: 'New York',
+        value: 'New York'
+      }
+    ],
+    filter(value, row) {
+      return !!~row.address.indexOf(String(value))
+    }
+  }
+]
+const data = [
+  {
+    key: 1,
+    name: 'John Brown',
+    age: 32,
+    address: 'New York No. 1 Lake Park'
+  },
+  {
+    key: 2,
+    name: 'Jim Green',
+    age: 42,
+    address: 'London No. 1 Lake Park'
+  },
+  {
+    key: 3,
+    name: 'Joe Black',
+    age: 32,
+    address: 'Sidney No. 1 Lake Park'
+  },
+  {
+    key: 4,
+    name: 'Jim Red',
+    age: 32,
+    address: 'London No. 2 Lake Park'
+  }
+]
+</script>
+
+<style lang="sass" scoped>
+a
+  color: #42b983
+
+label
+  margin: 0 0.5em
+  font-weight: bold
+
+code
+  background-color: #eee
+  padding: 2px 4px
+  border-radius: 4px
+  color: #304455
+</style>

+ 23 - 0
src/views/topicNavigation/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>专题导航</div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ msg: string }>()
+
+</script>
+
+<style lang="sass" scoped>
+a
+  color: #42b983
+
+label
+  margin: 0 0.5em
+  font-weight: bold
+
+code
+  background-color: #eee
+  padding: 2px 4px
+  border-radius: 4px
+  color: #304455
+</style>

+ 30 - 0
tailwind.config.ts

@@ -0,0 +1,30 @@
+import forms from '@tailwindcss/forms'
+import type { Config } from 'tailwindcss'
+import colors from 'tailwindcss/colors'
+
+const deprecations = [
+  'lightBlue',
+  'warmGray',
+  'trueGray',
+  'coolGray',
+  'blueGray'
+]
+
+for (const color of Object.keys(colors)) {
+  if (deprecations.includes(color)) delete colors[color]
+}
+
+export default {
+  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
+  theme: {
+    extend: {},
+    colors: {
+      ...colors
+      // override default colors here
+    }
+  },
+  plugins: [forms],
+  corePlugins: {
+    preflight: false // disables default styles from overriding other custom styles
+  }
+} satisfies Config

+ 8 - 0
test/e2e/.eslintrc.js

@@ -0,0 +1,8 @@
+module.exports = {
+  plugins: ['cypress'],
+  env: {
+    mocha: true,
+    chai: true,
+    'cypress/globals': true
+  }
+}

+ 31 - 0
test/e2e/cypress.d.ts

@@ -0,0 +1,31 @@
+// in cypress/support/index.ts
+// load type definitions that come with Cypress module
+/// <reference types="cypress" />
+/// <reference types="@badeball/cypress-cucumber-preprocessor" />
+
+import {
+  dataCy,
+  getTestElement,
+  getTestElementByClass
+} from './support/commands'
+
+declare global {
+  namespace Cypress {
+    interface Chainable {
+      /**
+       * Custom command to select DOM element by data-cy attribute.
+       * @example cy.dataCy('greeting')
+       */
+      dataCy: typeof dataCy
+      /**
+       * Custom Command - getTestElement
+       * Gets elements via the data-test-id-attribute
+       * fails if more than one of the same attribute name found
+       */
+      getTestElement: typeof getTestElement
+      getTestElementByClass: typeof getTestElementByClass
+    }
+  }
+}
+
+export {}

+ 53 - 0
test/e2e/plugins/index.ts

@@ -0,0 +1,53 @@
+// in cypress/support/index.ts
+// load type definitions that come with Cypress module
+import path from 'path'
+const rootDir = path.resolve(__dirname, '..', '..', '..')
+
+import { devServer } from '@cypress/vite-dev-server'
+import coverage from '@cypress/code-coverage/task'
+import browserify from '@cypress/browserify-preprocessor'
+import createBundler from '@bahmutov/cypress-esbuild-preprocessor'
+import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'
+import createEsbuildPlugin from '@badeball/cypress-cucumber-preprocessor/esbuild'
+
+const { default: viteConfig } = require(path.resolve(rootDir, 'vite.config.ts'))
+// need to use require for now as importing default throws a type error at runtime
+
+export const setupNodeEvents = async (
+  on: Cypress.PluginEvents,
+  config: Cypress.PluginConfigOptions
+) => {
+  const cucumberOptions = {
+    ...browserify.defaultOptions,
+    typescript: path.join(rootDir, 'node_modules/typescript')
+  }
+
+  coverage(on, config)
+  // This is required for the preprocessor to be able to generate JSON reports after each run, and more,
+  await addCucumberPreprocessorPlugin(on, { ...config, ...cucumberOptions })
+
+  on(
+    'file:preprocessor',
+    createBundler({
+      plugins: [createEsbuildPlugin(config)]
+    })
+  )
+
+  on('dev-server:start', (options: Cypress.DevServerConfig) => {
+    return devServer({
+      ...options,
+      viteConfig: {
+        ...viteConfig,
+        // configFile: path.resolve(rootDir, 'vite.config.ts'),
+        define: {
+          'process.env': process.env
+        },
+        optimizeDeps: {
+          include: ['tailwind-merge'] // we need to include the tailwind-merge dependency otherwise the first run of the tests will fail
+        }
+      }
+    })
+  })
+
+  return config
+}

+ 79 - 0
test/e2e/specs/common/index.ts

@@ -0,0 +1,79 @@
+import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
+
+Given('I open the home page', () => {
+  cy.visit(Cypress.env('BASE_URL'), {
+    onBeforeLoad(win) {
+      cy.stub(win, 'matchMedia')
+        .callThrough()
+        .withArgs('(prefers-color-scheme: dark)')
+        .returns({
+          matches: false,
+          addListener() {}
+        })
+    }
+  })
+})
+
+Given('I refresh the page', () => {
+  cy.reload()
+})
+
+Given('I can see the {string} element', (attr: string) => {
+  cy.dataCy(attr)
+})
+
+When('I click on the {string} element', (attr: string) => {
+  const el = cy.dataCy(attr)
+
+  el.click()
+})
+
+When(
+  'I trigger the {string} event on the {string} element',
+  (event: string, attr: string) => {
+    const el = cy.dataCy(attr)
+
+    el.trigger(event)
+  }
+)
+
+Then('I see {string} in the title', (val: string) => {
+  cy.title().should('include', val)
+})
+
+Then(
+  'The {string} style on the {string} element should be {string}',
+  (style: string, attr: string, val: string) => {
+    const el = cy.dataCy(attr)
+
+    el.should('have.css', style, val)
+  }
+)
+
+Then(
+  'The {string} element should have class {string}',
+  (attr: string, val: string) => {
+    const el = cy.dataCy(attr)
+
+    el.should('have.class', val)
+  }
+)
+
+Then(
+  'The {string} element should not have class {string}',
+  (attr: string, val: string) => {
+    const el = cy.dataCy(attr)
+
+    el.should('not.have.class', val)
+  }
+)
+
+Then('I should see the {string} element', (attr: string) => {
+  const el = cy.dataCy(attr)
+
+  el.should('be.visible')
+})
+
+Cypress.on('uncaught:exception', (err, _runnable) => {
+  console.error(err)
+})

+ 45 - 0
test/e2e/specs/features/main/HelloWorld.feature

@@ -0,0 +1,45 @@
+Feature: HelloWorld
+
+    I want to open the home page
+
+    Scenario: Navigating Home
+        Given I open the home page
+        Then I see "Vite App" in the title
+        Then I should see the "chronos-logo" element
+
+    @ignore
+    Scenario: This Test should be skipped
+
+    Scenario: Clicking on the button enough times should break it
+        Given I open the home page
+        And I can see the "click-me" element
+        When I click on the "click-me" button 60 times
+        Then The "click-me" button should say "It's Broken!" and be disabled
+
+    Scenario: Switching Dark Mode
+        Given I open the home page
+        And I can see the "dark-mode" element
+        When I click on the "dark-mode" element
+        Then The "dark-mode" switch should say "Dark"
+        Then The "app" element should have class "dark"
+        Given I refresh the page
+        Then The "app" element should have class "dark"
+
+    Scenario: Switching Light Mode
+        Given I open the home page
+        And I can see the "dark-mode" element
+        When I click on the "dark-mode" element
+        Then The "dark-mode" switch should say "Dark"
+        When I click on the "dark-mode" element
+        Then The "dark-mode" switch should say "Light"
+        Then The "app" element should not have class "dark"
+        Given I refresh the page
+        Then The "app" element should not have class "dark"
+
+    Scenario: Mouseover/leave the sidebar
+        Given I open the home page
+        And I can see the "sidebar" element
+        When I trigger the "mouseover" event on the "sidebar" element
+        Then The "max-width" style on the "sidebar" element should be "240px"
+        When I trigger the "mouseleave" event on the "sidebar" element
+        Then The "max-width" style on the "sidebar" element should be "48px"

+ 31 - 0
test/e2e/specs/features/main/HelloWorld/HelloWorld.ts

@@ -0,0 +1,31 @@
+import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor'
+
+When(
+  'I click on the {string} button {int} times',
+  (attr: string, times: number) => {
+    const el = cy.dataCy(attr)
+
+    for (let i = 0; i < times; i++) {
+      el.click()
+    }
+  }
+)
+
+Then(
+  'The {string} button should say {string} and be disabled',
+  (attr: string, message: string) => {
+    const el = cy.dataCy(attr)
+
+    el.should('have.text', message)
+    el.should('be.disabled')
+  }
+)
+
+Then(
+  'The {string} switch should say {string}',
+  (attr: string, message: string) => {
+    const el = cy.dataCy(attr)
+
+    el.should('include.text', message)
+  }
+)

+ 19 - 0
test/e2e/support/commands.ts

@@ -0,0 +1,19 @@
+export function getTestElement(id: string) {
+  const el = cy.get(`[data-test-id=${id}]`)
+  el.should('have.length', 1)
+  return el
+}
+
+export function getTestElementByClass(name: string) {
+  return cy.get(`[data-test-class=${name}]`)
+}
+
+export function dataCy(val: string) {
+  const el = cy.get(`[data-cy=${val}]`)
+  el.should('have.length', 1)
+  return el
+}
+
+Cypress.Commands.add('getTestElement', getTestElement)
+Cypress.Commands.add('getTestElementByClass', getTestElementByClass)
+Cypress.Commands.add('dataCy', dataCy)

+ 18 - 0
test/e2e/support/index.ts

@@ -0,0 +1,18 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+import '@cypress/code-coverage/support'

+ 14 - 0
test/e2e/tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "lib": ["esnext", "dom"],
+    "types": ["cypress", "node"],
+    "esModuleInterop": true,
+    "noImplicitAny": false,
+    "importHelpers": true,
+    "experimentalDecorators": true,
+    "allowSyntheticDefaultImports": true,
+    "moduleResolution": "node"
+  },
+  "include": ["**/*.ts", "**/*.d.ts", "node_modules/cypress"]
+}

+ 63 - 0
test/unit/App.spec.ts

@@ -0,0 +1,63 @@
+import App from '@/App.vue'
+import router from '@/router'
+import HelloWorld from '@/views/HelloWorld.vue'
+import { mountComponent } from '@test/unit/testhelper'
+import { VueWrapper } from '@vue/test-utils'
+import { NConfigProvider, NSwitch, NLayoutSider } from 'naive-ui'
+import { ComponentPublicInstance } from 'vue'
+
+describe('App.vue', () => {
+  let wrapper: VueWrapper<ComponentPublicInstance<typeof App>>
+  beforeEach(() => {
+    wrapper = mountComponent<InstanceType<typeof App>>(App, {}, {}, true)
+  })
+
+  afterEach(() => {
+    wrapper?.unmount()
+  })
+
+  test('should mount', async () => {
+    expect(wrapper.exists()).toBe(true)
+
+    router.push({ name: 'home' })
+    await router.isReady()
+
+    expect(wrapper.findComponent(HelloWorld).exists()).toBe(true)
+  })
+
+  test('should switch to dark mode', async () => {
+    const button = wrapper.findComponent(NSwitch)
+
+    expect(button.exists()).toBe(true)
+
+    button.trigger('click')
+
+    await wrapper.vm.$nextTick()
+
+    const confComp = wrapper.findComponent(NConfigProvider)
+
+    expect(confComp.classes().includes('dark')).toBe(true)
+  })
+
+  test('should expand sider when hovered, and collapse when left', async () => {
+    let sider = wrapper.findComponent(NLayoutSider)
+
+    expect(sider.exists()).toBe(true)
+
+    sider.trigger('mouseover')
+
+    await wrapper.vm.$nextTick()
+
+    sider = wrapper.findComponent(NLayoutSider)
+
+    expect(sider.classes().includes('n-layout-sider--show-content')).toBe(true)
+
+    sider.trigger('mouseleave')
+
+    await wrapper.vm.$nextTick()
+
+    sider = wrapper.findComponent(NLayoutSider)
+
+    expect(sider.classes().includes('n-layout-sider--collapsed')).toBe(true)
+  })
+})

+ 35 - 0
test/unit/testhelper.ts

@@ -0,0 +1,35 @@
+import { globalComponents } from '@/components'
+import router from '@/router'
+import { ComponentMountingOptions, mount } from '@vue/test-utils'
+import { createTestingPinia, TestingOptions } from '@pinia/testing'
+import { ComponentPublicInstance, Plugin } from 'vue'
+import { createPinia, setActivePinia } from 'pinia'
+
+export function mountComponent<T extends ComponentPublicInstance>(
+  component: T,
+  options: ComponentMountingOptions<T> = { shallow: false },
+  mockStore?: TestingOptions,
+  useRouter = false
+) {
+  const pinia = mockStore
+    ? createTestingPinia({ createSpy: vi.fn, ...mockStore })
+    : createPinia()
+  setActivePinia(pinia)
+
+  const plugins: Array<Plugin> = [pinia, globalComponents]
+
+  if (useRouter) {
+    plugins.push(router)
+  }
+
+  if (options.global?.plugins) {
+    options.global.plugins.push(...plugins)
+  } else {
+    options.global = {
+      ...(options.global || {}),
+      plugins
+    }
+  }
+
+  return mount(component, options)
+}

+ 23 - 0
test/unit/tsconfig.json

@@ -0,0 +1,23 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "types": ["vite/client", "vitest/globals", "naive-ui/volar", "node"],
+    "paths": {
+      "@/*": ["src/*"],
+      "@test/*": ["test/*"]
+    },
+    "baseUrl": "../.."
+  },
+  "include": [
+    "../../src/**/*.ts",
+    "../../src/**/*.d.ts",
+    "../../src/**/*.tsx",
+    "../../src/**/*.vue",
+    "../../tests/unit/**.ts",
+    "../../tests/unit/**.spec.ts",
+    "**/*.ts",
+    "**/*.d.ts",
+    "**/*.tsx",
+    "**/*.vue"
+  ]
+}

+ 37 - 0
test/unit/views/HelloWorld.spec.ts

@@ -0,0 +1,37 @@
+import HelloWorld from '@/views/HelloWorld.vue'
+import { mountComponent } from '@test/unit/testhelper'
+import { VueWrapper } from '@vue/test-utils'
+import { NButton } from 'naive-ui'
+import { ComponentPublicInstance } from 'vue'
+
+describe('HelloWorld.vue', () => {
+  let wrapper: VueWrapper<ComponentPublicInstance<typeof HelloWorld>>
+  beforeEach(() => {
+    wrapper = mountComponent<InstanceType<typeof HelloWorld>>(HelloWorld, {
+      props: { msg: 'foo bar' }
+    })
+  })
+
+  afterEach(() => {
+    wrapper?.unmount()
+  })
+
+  test('should mount', () => {
+    expect(wrapper.findComponent(HelloWorld).exists()).toBe(true)
+  })
+
+  test('should disable button after 60 clicks', async () => {
+    const nButton: VueWrapper<InstanceType<typeof NButton>> =
+      wrapper.findComponent(NButton)
+
+    expect(nButton.exists()).toBe(true)
+    expect(nButton.attributes()).not.toHaveProperty('disabled')
+
+    for (let i = 0; i < 61; i++) {
+      ;(nButton.element as HTMLButtonElement).click()
+      await wrapper.vm.$nextTick()
+    }
+
+    expect(nButton.attributes()).toHaveProperty('disabled')
+  })
+})

+ 28 - 0
tsconfig.json

@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "outDir": "./dist",
+    "skipLibCheck": true,
+    "target": "esnext",
+    "useDefineForClassFields": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "strict": true,
+    "jsx": "preserve",
+    "allowJs": true,
+    "checkJs": true,
+    "noImplicitAny": false,
+    "importHelpers": true,
+    "experimentalDecorators": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "lib": ["esnext", "dom"],
+    "types": ["vite/client", "naive-ui/volar", "node"],
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "exclude": ["node_modules/**/*", "dist/**/*"]
+}

+ 48 - 0
vite.config.ts

@@ -0,0 +1,48 @@
+/// <reference types="vitest" />
+
+import { defineConfig, Plugin } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import tsconfigPaths from 'vite-tsconfig-paths'
+
+// import IstanbulPlugin from 'vite-plugin-istanbul'
+
+const plugins: Array<Plugin> = [vue(), tsconfigPaths()]
+
+if (process.env.CYPRESS_TEST === 'true') {
+  // console.info('instrumenting code coverage for e2e tests...')
+  // plugins.push(
+  //   IstanbulPlugin({
+  //     cypress: true,
+  //     checkProd: true,
+  //     exclude: ['dist', '.nyc_output', 'node_modules', 'coverage', 'test'],
+  //     include: ['src/*']
+  //   })
+  // )
+}
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => ({
+  plugins,
+  test: {
+    server: {
+      deps: {
+        inline: ['date-fns']
+      }
+    },
+    environment: 'happy-dom',
+    globals: true,
+    coverage: {
+      all: true,
+      exclude: ['*.config.{ts,js}', '**/*.d.ts', 'src/main.ts', 'dist', 'test'],
+      functions: 80,
+      branches: 80,
+      statements: 80
+    }
+  },
+  build: {
+    sourcemap: mode === 'production' ? false : 'inline'
+  },
+  server: {
+    port: 3000
+  }
+}))

File diff suppressed because it is too large
+ 8722 - 0
yarn.lock