feat: sunniejs/vue3 template project init

This commit is contained in:
fonghehe 2022-05-17 18:57:32 +08:00
commit d23e5bacb1
57 changed files with 56254 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
VITE_TOKEN_KEY=tokenKey
VITE_URL_PREFIX=/api

15
.eslintignore Normal file
View File

@ -0,0 +1,15 @@
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
Dockerfile

72
.eslintrc.js Normal file
View File

@ -0,0 +1,72 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
},
},
extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
rules: {
'vue/script-setup-uses-vars': 'error',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'vue/custom-event-name-casing': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'space-before-function-paren': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off',
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always',
},
svg: 'always',
math: 'always',
},
],
'vue/multi-word-component-names': 'off',
},
};

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.eslintcache
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

9
.prettierignore Normal file
View File

@ -0,0 +1,9 @@
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*

3
.stylelintignore Normal file
View File

@ -0,0 +1,3 @@
/dist/*
/public/*
public/*

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["johnsoncodehk.volar"]
}

21
License Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present, Fast-vue3
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# Vue 3 + Typescript + Vite
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.

42
index.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,viewport-fit=cover"
/>
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
document.addEventListener('touchstart', function (event) {
if (event.touches.length > 1) {
event.preventDefault();
}
});
var lastTouchEnd = 0;
document.addEventListener(
'touchend',
function (event) {
var now = new Date().getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
},
false
);
document.addEventListener('gesturestart', function (event) {
event.preventDefault();
});
</script>
</body>
</html>

47215
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
package.json Normal file
View File

@ -0,0 +1,88 @@
{
"name": "vue-h5-template",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged",
"deps": "yarn upgrade-interactive --latest"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"dependencies": {
"@nutui/nutui": "^3.1.20",
"@vueuse/core": "8.3.1",
"@vueuse/integrations": "8.3.1",
"axios": "0.27.2",
"pinia": "^2.0.13",
"universal-cookie": "^4.0.4",
"vant": "^3.4.8",
"vue": "^3.2.33",
"vue-i18n": "^9.1.9",
"vue-router": "^4.0.14"
},
"devDependencies": {
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"@vitejs/plugin-legacy": "^1.8.1",
"@vitejs/plugin-vue": "^2.3.1",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"consola": "^2.15.3",
"eruda": "^2.4.1",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.7.1",
"husky": "7.0.4",
"lint-staged": "12.4.1",
"mockjs": "^1.1.0",
"postcss": "^8.4.12",
"postcss-px-to-viewport-8-plugin": "^1.1.3",
"prettier": "^2.6.2",
"stylelint": "^14.8.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^7.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^25.0.0",
"stylelint-order": "^5.0.0",
"typescript": "^4.6.3",
"vite": "^2.9.6",
"vite-plugin-eruda": "^1.0.1",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-style-import": "^2.0.0",
"vue-eslint-parser": "^8.3.0",
"vue-tsc": "^0.34.10"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
"prettier --write--parser json"
],
"package.json": [
"prettier --write"
],
"*.vue": [
"eslint --fix",
"prettier --write",
"stylelint --fix"
],
"*.{scss,less,styl,html}": [
"stylelint --fix",
"prettier --write"
],
"*.md": [
"prettier --write"
]
}
}

3067
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

16
postcss.config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
plugins: {
'postcss-px-to-viewport-8-plugin': {
unitToConvert: 'px', // 要转化的单位
viewportWidth: 375, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ['*'], // 指定转换的css属性的单位*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位默认vw
minPixelValue: 1, // 默认值1小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
},
},
};

10
prettier.config.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
printWidth: 160,
semi: true,
vueIndentScriptAndStyle: true,
singleQuote: true,
trailingComma: 'all',
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

14
src/App.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<Suspense>
<template #default>
<router-view v-slot="{ Component, route }">
<keep-alive>
<component :is="Component" v-if="route.meta && route.meta.keepAlive" :key="route.meta.usePathKey ? route.fullPath : undefined" />
</keep-alive>
<component :is="Component" v-if="!(route.meta && route.meta.keepAlive)" :key="route.meta.usePathKey ? route.fullPath : undefined" />
</router-view>
</template>
<template #fallback> Loading... </template>
</Suspense>
</template>
<script setup></script>

12
src/api/index.ts Normal file
View File

@ -0,0 +1,12 @@
import useAxiosApi from '@/utils/useAxiosApi';
/**
*
* @returns UseAxiosReturn
*/
export function loginPassword() {
return useAxiosApi(`/api/login`, {
method: 'POST',
data: { name: '123' },
});
}

11
src/assets/app.css Normal file
View File

@ -0,0 +1,11 @@
html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}

536
src/assets/font/demo.css Normal file
View File

@ -0,0 +1,536 @@
/* Logo 字体 */
@font-face {
font-family: 'iconfont logo';
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: 'iconfont logo';
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown > p,
.markdown > blockquote,
.markdown > .highlight,
.markdown > ol,
.markdown > ul {
width: 80%;
}
.markdown ul > li {
list-style: circle;
}
.markdown > ul li,
.markdown blockquote ul > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown > ul li p,
.markdown > ol li p {
margin: 0.6em 0;
}
.markdown ol > li {
list-style: decimal;
}
.markdown > ol li,
.markdown blockquote ol > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown > table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown > table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown > table th,
.markdown > table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown > table th {
background: #f7f7f7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown > br,
.markdown > p > br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*='language-'],
pre[class*='language-'] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*='language-'],
pre[class*='language-'] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #dd4a68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,37 @@
@font-face {
font-family: 'iconfont'; /* Project id 3210904 */
src: url('iconfont.woff2?t=1646452970429') format('woff2'), url('iconfont.woff?t=1646452970429') format('woff'),
url('iconfont.ttf?t=1646452970429') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-custom-ok:before {
content: '\e631';
}
.icon-github-fill:before {
content: '\e885';
}
.icon-l-search:before {
content: '\e79e';
}
.icon-home:before {
content: '\e603';
}
.icon-member:before {
content: '\e602';
}
.icon-list:before {
content: '\e601';
}

View File

@ -0,0 +1,51 @@
{
"id": "3210904",
"name": "fast-vue3",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "517495",
"name": "ok",
"font_class": "custom-ok",
"unicode": "e631",
"unicode_decimal": 58929
},
{
"icon_id": "4937000",
"name": "github-fill",
"font_class": "github-fill",
"unicode": "e885",
"unicode_decimal": 59525
},
{
"icon_id": "12932129",
"name": "l-search",
"font_class": "l-search",
"unicode": "e79e",
"unicode_decimal": 59294
},
{
"icon_id": "109751",
"name": "home",
"font_class": "home",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "663138",
"name": "member",
"font_class": "member",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "21513638",
"name": "list",
"font_class": "list",
"unicode": "e601",
"unicode_decimal": 58881
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,43 @@
<template>
<div class="main-page">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<nut-tabbar unactive-color="#364636" active-color="#1989fa" @tab-switch="tabSwitch" bottom>
<nut-tabbar-item :tab-title="$t('tabbar.home')" font-class-name="iconfont" class-prefix="icon" icon="home" />
<nut-tabbar-item :tab-title="$t('tabbar.list')" font-class-name="iconfont" class-prefix="icon" icon="list" />
<nut-tabbar-item :tab-title="$t('tabbar.member')" font-class-name="iconfont" class-prefix="icon" icon="member" />
</nut-tabbar>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const tabSwitch = (item, index) => {
console.log(item, index);
switch (index) {
case 0:
router.push('/home');
break;
case 1:
router.push('/list');
break;
case 2:
router.push('/member');
break;
}
};
</script>
<style scoped lang="scss">
.main-page {
height: calc(100vh - 50px);
overflow-y: scroll;
overflow-x: hidden;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<div class="main-page">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<nut-tabbar unactive-color="#364636" active-color="#1989fa" @tab-switch="tabSwitch">
<nut-tabbar-item :tab-title="$t('tabbar.home')" font-class-name="iconfont" class-prefix="icon" icon="home" />
<nut-tabbar-item :tab-title="$t('tabbar.list')" font-class-name="iconfont" class-prefix="icon" icon="list" />
<nut-tabbar-item :tab-title="$t('tabbar.member')" font-class-name="iconfont" class-prefix="icon" icon="member" />
</nut-tabbar>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const tabSwitch = (item, index) => {
console.log(item, index);
switch (index) {
case 0:
router.push('/home');
break;
case 1:
router.push('/list');
break;
case 2:
router.push('/member');
break;
}
};
</script>
<style scoped lang="scss">
.main-page {
height: calc(100vh - 50px);
overflow-y: scroll;
overflow-x: hidden;
}
.tabbar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 50px;
border: none;
box-shadow: 0 0 20px -5px #9a9a9a;
}
</style>

31
src/i18n/index.ts Normal file
View File

@ -0,0 +1,31 @@
import { createI18n } from 'vue-i18n';
export function loadLang() {
const context = import.meta.globEager('./lang/*.ts');
const messages: AnyObject = {};
const langs = Object.keys(context);
for (const key of langs) {
if (key === './index.ts') return;
const lang = context[key].lang;
const name = key.replace(/(\.\/lang\/|\.ts)/g, '');
messages[name] = lang;
}
return messages;
}
export const i18n = createI18n({
// globalInjection: true,
// legacy: false,
locale: 'zh-cn',
fallbackLocale: 'zh-cn',
messages: loadLang(),
});
export const i18nt = i18n.global.t;
export function setLang(locale: string) {
i18n.global.locale = locale;
}

12
src/i18n/lang/en-us.ts Normal file
View File

@ -0,0 +1,12 @@
export const lang = {
tabbar: {
home: 'Home',
list: 'List',
member: 'Member',
},
language: {
en: 'English',
zh: 'Chinese',
},
introduction: 'A rapid development vue3 of mobile terminal template',
};

15
src/i18n/lang/zh-cn.ts Normal file
View File

@ -0,0 +1,15 @@
export const lang = {
tabbar: {
home: '首页',
list: '列表',
member: '我的',
},
language: {
en: '英文',
zh: '中文',
},
introduction: '一个快速开发vue3的移动端模板',
home: {
support: '支持',
},
};

20
src/main.ts Normal file
View File

@ -0,0 +1,20 @@
import { createApp } from 'vue';
import App from './App.vue';
import { nutUiComponents } from './plugins/nutUI';
import { i18n } from '@/i18n';
import router from './router';
import { setupStore } from '@/store';
import './assets/font/iconfont.css';
import './assets/app.css';
const app = createApp(App);
app.use(router);
setupStore(app);
app.use(i18n);
app.mount('#app');
// nutUi按需加载
nutUiComponents.forEach((item) => {
app.use(item);
});

22
src/mock/index.ts Normal file
View File

@ -0,0 +1,22 @@
import { MockMethod, Recordable } from 'vite-plugin-mock';
interface response {
body: Recordable;
query: Recordable;
}
export default [
{
url: '/api/login',
method: 'post',
response: ({ body, query }: response) => {
console.log('body>>>>>>>>', body);
console.log('query>>>>>>>>', query);
return {
code: 200,
message: 'ok',
data: { name: 'Evan', age: 26 },
};
},
},
] as MockMethod[];

71
src/plugins/nutUI.ts Normal file
View File

@ -0,0 +1,71 @@
// nutui按需加载
import {
Button,
Cell,
CellGroup,
Icon,
Input,
Tabbar,
TabbarItem,
Toast,
ShortPassword,
Price,
Layout,
Rate,
Popup,
Calendar,
Video,
NoticeBar,
NumberKeyboard,
CountDown,
Tag,
Badge,
SearchBar,
Avatar,
Menu,
MenuItem,
Popover,
Pagination,
Form,
FormItem,
Navbar,
Card,
Grid,
GridItem,
} from '@nutui/nutui';
export const nutUiComponents = [
Button,
Cell,
CellGroup,
Form,
FormItem,
Icon,
Input,
Tabbar,
TabbarItem,
Toast,
ShortPassword,
Price,
Layout,
Rate,
Popup,
Calendar,
Video,
NoticeBar,
NumberKeyboard,
CountDown,
Tag,
Badge,
SearchBar,
Avatar,
Menu,
MenuItem,
Popover,
Pagination,
Navbar,
Card,
Grid,
GridItem,
];

13
src/router/index.ts Normal file
View File

@ -0,0 +1,13 @@
import { createRouter, createWebHistory, Router } from 'vue-router';
import routes from './routes';
const router: Router = createRouter({
history: createWebHistory(),
routes: routes,
});
router.beforeEach(async (to, from, next) => {
next();
});
export default router;

48
src/router/routes.ts Normal file
View File

@ -0,0 +1,48 @@
const routes = [
{
name: 'root',
path: '/',
redirect: '/home',
component: () => import('@/components/Basic/index.vue'),
children: [
{
name: 'Home',
path: 'home',
component: () => import('@/views/Home/index.vue'),
meta: {
title: '',
keepAlive: true,
},
},
{
name: 'List',
path: 'list',
component: () => import('@/views/List/index.vue'),
meta: {
title: '',
keepAlive: true,
},
},
{
name: 'Member',
path: 'member',
component: () => import('@/views/Member/index.vue'),
meta: {
title: '',
keepAlive: true,
},
},
],
},
{
name: 'Login',
path: '/login',
component: () => import('@/views/Login/index.vue'),
meta: {
title: '',
keepAlive: true,
},
},
];
export default routes;

10
src/store/index.ts Normal file
View File

@ -0,0 +1,10 @@
import type { App } from 'vue';
import { createPinia } from 'pinia';
const store = createPinia();
export function setupStore(app: App<Element>) {
app.use(store);
}
export { store };

40
src/store/modules/user.ts Normal file
View File

@ -0,0 +1,40 @@
import { loginPassword } from '@/api';
import { useCookies } from '@vueuse/integrations/useCookies';
import { defineStore } from 'pinia';
import { watch } from 'vue';
const { VITE_TOKEN_KEY } = import.meta.env;
const token = useCookies().get(VITE_TOKEN_KEY as string);
interface StoreUser {
token: string;
info: AnyObject;
}
export const useUserStore = defineStore({
id: 'app-user',
state: (): StoreUser => ({
token: token,
info: {},
}),
getters: {
getUserInfo(): any {
return this.info || {};
},
},
actions: {
setInfo(info: any) {
this.info = info ? info : '';
},
login() {
return new Promise((resolve) => {
const { data } = loginPassword();
watch(data, () => {
this.setInfo(data.value);
// useCookies().set(VITE_TOKEN_KEY as string, data.value.token);
resolve(data.value);
});
});
},
},
});

20
src/utils/index.ts Normal file
View File

@ -0,0 +1,20 @@
/**
*
*/
export function typeCheck(param: any) {
return Object.prototype.toString.call(param);
}
/**
* stage
*/
export function mutateState(state: AnyObject, payload: AnyObject) {
if (typeCheck(state) === '[object Object]' && typeCheck(payload) === '[object Object]') {
for (const key in payload) {
state[key] = payload[key];
}
} else {
console.error('expected plain Object');
}
}

72
src/utils/useAxiosApi.ts Normal file
View File

@ -0,0 +1,72 @@
import { useAxios } from '@vueuse/integrations/useAxios';
import axios, { AxiosRequestConfig } from 'axios';
import { Toast } from 'vant';
// create an axios instance
const instance = axios.create({
withCredentials: false,
timeout: 5000,
});
// request interceptor
instance.interceptors.request.use(
(config) => {
// do something before request is sent
// const token = store.state.user.token;
// if (token) {
// // let each request carry token
// config.headers = {
// ...config.headers,
// Authorization: `Bearer ${token}`
// };
// }
return config;
},
(error) => {
// do something with request error
console.log(error); // for debug
return Promise.reject(error);
},
);
// response interceptor
instance.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
(response) => {
const res = response.data;
// if the custom code is not 200, it is judged as an error.
if (res.code !== 200) {
Toast(res.msg);
// 412: Token expired;
if (res.code === 412) {
// store.dispatch('user/userLogout');
}
return Promise.reject(res.msg || 'Error');
} else {
return res;
}
},
(error) => {
console.log('err' + error);
Toast(error.message);
return Promise.reject(error.message);
},
);
/**
* reactive useFetchApi
*/
export default function useAxiosApi(url: string, config: AxiosRequestConfig) {
return useAxios(url, config, instance);
}

41
src/utils/useFetchApi.ts Normal file
View File

@ -0,0 +1,41 @@
import { createFetch } from '@vueuse/core';
import { Notify } from 'vant';
const useFetchApi = createFetch({
baseUrl: '',
options: {
async beforeFetch({ options }) {
const myToken = 'token';
options.headers = {
...options.headers,
Authorization: `Bearer ${myToken}`,
};
return { options };
},
afterFetch(ctx) {
console.log(ctx);
const { data, response } = ctx;
if (response.status >= 200 && response.status < 300) {
try {
console.log(response);
const jsonObj = data;
if (jsonObj.code != 200) {
Notify({ type: 'danger', message: jsonObj.message || 'Error' });
}
ctx.data = jsonObj.data;
} catch (error) {
console.error(error);
ctx.data = null;
}
} else {
Notify({ type: 'danger', message: response.statusText || 'Error' });
ctx.data = null;
}
return ctx;
},
},
});
export default useFetchApi;

18
src/views/List/index.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<nut-navbar :left-show="false" :title="$t('tabbar.list')" />
<nut-card :img-url="state.imgUrl" :title="state.title" :price="state.price" :vip-price="state.vipPrice" :shop-name="state.shopName" />
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
let state = reactive({
imgUrl: '//img10.360buyimg.com/n2/s240x240_jfs/t1/210890/22/4728/163829/6163a590Eb7c6f4b5/6390526d49791cb9.jpg!q70.jpg',
title: '活蟹】湖塘煙雨 阳澄湖大闸蟹公4.5两 母3.5两 4对8只 鲜活生鲜螃蟹现货水产礼盒海鲜水',
price: '388',
vipPrice: '378',
shopDesc: '自营',
delivery: '厂商配送',
shopName: '阳澄湖大闸蟹自营店>',
});
</script>

View File

@ -0,0 +1,57 @@
<template>
<nut-navbar :left-show="false" :title="$t('tabbar.member')" />
<div class="avatar-wrap">
<nut-avatar
class="avatar"
size="large"
icon="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
/>
<div class="member-detail">
<p class="nickname"> 昵称<nut-button shape="square" size="small" type="default" @click="goLogin"> 去登录 </nut-button> </p>
<p class="info"> 个人其他信息后续补充.... </p>
</div>
</div>
<nut-grid>
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
<nut-grid-item icon="dongdong" text="文字" />
</nut-grid>
</template>
<script lang="ts" setup>
// import { useUserStore } from '@/store/modules/user';
import { useRouter } from 'vue-router';
const router = useRouter();
// const userStore = useUserStore();
// const getUserInfo = computed(() => {
// const { name = '' } = userStore.getUserInfo || {};
// return name;
// });
const goLogin = () => {
router.push('/login');
};
</script>
<style lang="scss">
.avatar-wrap {
display: flex;
margin: 30px;
height: 50px;
align-items: center;
.member-detail {
margin-left: 20px;
.nickname {
font-size: 16px;
font-weight: bold;
}
.info {
}
}
}
</style>

85
src/views/home/index.vue Normal file
View File

@ -0,0 +1,85 @@
<template>
<nut-navbar :left-show="false" :title="$t('tabbar.home')" />
<header class="header">
<img width="30" src="https://v3.cn.vuejs.org/logo.png" />
<h4 class="intro-title"> Fast-Vue3 </h4>
</header>
<p class="intro-header">
{{ $t('introduction') }}
<a href="https://github.com/study-vue3/fast-vue3">
<nut-icon class="github-icon" name="github" />
</a>
</p>
<nut-cell-group :title="$t('home.support')" class="supportList">
<nut-cell title="Vue3" icon="Check" />
<nut-cell title="Vue-router" icon="Check" />
<nut-cell title="Axios" icon="Check" />
<nut-cell title="Pinia" icon="Check" />
<nut-cell title="NutUI" icon="Check" />
<nut-cell title="Vue-i18n" icon="Check" />
<nut-cell title="Jsx" icon="Check" />
</nut-cell-group>
<div class="btn-wrap">
<nut-button shape="square" size="small" type="default" @click="changeLang('zh-cn')">
{{ $t('language.zh') }}
</nut-button>
<nut-button shape="square" size="small" type="default" @click="changeLang('en-us')">
{{ $t('language.en') }}
</nut-button>
</div>
{{ getUserInfo }}
<Tabbar />
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useUserStore } from '@/store/modules/user';
import { setLang } from '@/i18n';
const userStore = useUserStore();
const getUserInfo = computed(() => {
const { name = '' } = userStore.getUserInfo || {};
return name;
});
const changeLang = (type) => {
setLang(type);
};
</script>
<style lang="scss">
.header {
display: flex;
justify-content: center;
margin: 26px 0 10px;
padding: 0 20px;
height: 50px;
height: 30px;
font-size: 20px;
}
.intro-title {
text-align: center;
}
.intro-header {
height: 30px;
font-size: 16px;
text-align: center;
}
.supportList {
margin: 0 16px;
.nut-icon {
color: green;
}
}
.github-icon {
margin-top: 4px;
font-size: 24px;
}
.btn-wrap {
margin: 20px;
}
</style>

61
src/views/login/index.vue Normal file
View File

@ -0,0 +1,61 @@
<template>
<div class="login">
<h2>登录</h2>
<nut-form ref="ruleForm" :model-value="formData">
<nut-form-item required prop="name" :rules="[{ required: true, message: '请输入用户名' }]">
<input v-model="formData.name" class="nut-input-text" placeholder="请输入用户名" type="text" />
</nut-form-item>
<nut-form-item required prop="pwd" :rules="[{ required: true, message: '请填写联系电话' }]">
<input v-model="formData.pwd" class="nut-input-text" placeholder="请输入密码" type="password" />
</nut-form-item>
<nut-button block type="info" @click="submit"> 登录 </nut-button>
</nut-form>
</div>
</template>
<script lang="ts" setup>
import router from '@/router';
import { ComponentInternalInstance, getCurrentInstance, reactive, ref } from 'vue';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
const formData = reactive({
name: '',
pwd: '',
});
const ruleForm = ref<any>(null);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const submit = () => {
ruleForm.value.validate().then(async ({ valid, errors }: any) => {
if (valid) {
const userInfo = await userStore.login();
if (userInfo) {
router.push({ name: 'Home' });
}
} else {
console.log('error submit!!', errors);
}
});
};
</script>
<style scoped lang="scss">
.login {
padding: 20px;
h2 {
text-align: center;
letter-spacing: 10px;
}
.nut-form-item {
background: #f2f3f5;
border-radius: 20px;
margin-bottom: 20px;
input {
background: transparent;
}
}
}
</style>

89
stylelint.config.js Normal file
View File

@ -0,0 +1,89 @@
module.exports = {
root: true,
plugins: ['stylelint-order'],
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
customSyntax: 'postcss-html',
rules: {
'function-no-unknown': null,
'selector-class-pattern': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global'],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep'],
},
],
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen', 'function', 'if', 'each', 'include', 'mixin'],
},
],
'no-empty-source': null,
'string-quotes': null,
'named-grid-areas-no-invalid': null,
'unicode-bom': 'never',
'no-descending-specificity': null,
'font-family-no-missing-generic-family-keyword': null,
'declaration-colon-space-after': 'always-single-line',
'declaration-colon-space-before': 'never',
// 'declaration-block-trailing-semicolon': 'always',
'rule-empty-line-before': [
'always',
{
ignore: ['after-comment', 'first-nested'],
},
],
'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
'order/order': [
[
'dollar-variables',
'custom-properties',
'at-rules',
'declarations',
{
type: 'at-rule',
name: 'supports',
},
{
type: 'at-rule',
name: 'media',
},
'rules',
],
{ severity: 'warning' },
],
},
ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
overrides: [
{
files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
extends: ['stylelint-config-recommended'],
rules: {
'keyframes-name-pattern': null,
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['deep', 'global'],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
},
],
},
},
{
files: ['*.less', '**/*.less'],
customSyntax: 'postcss-less',
extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
},
],
};

44
tsconfig.json Normal file
View File

@ -0,0 +1,44 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"noLib": false,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"strictFunctionTypes": false,
"jsx": "preserve",
"baseUrl": ".",
"allowJs": true,
"sourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"experimentalDecorators": true,
"lib": ["dom", "esnext"],
"noImplicitAny": false,
"skipLibCheck": true,
"types": ["vite/client"],
"removeComments": true,
"paths": {
"@/*": ["src/*"],
"#/*": ["types/*"]
}
},
"include": [
"tests/**/*.ts",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/**/*.d.ts",
"types/**/*.ts",
"build/**/*.ts",
"build/**/*.d.ts",
"mock/**/*.ts",
"vite.config.ts"
],
"exclude": ["node_modules", "tests/server/**/*.ts", "dist", "**/*.js"]
}

53
types/axios.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
export interface RequestOptions {
// Splicing request parameters to url
joinParamsToUrl?: boolean;
// Format request parameter time
formatDate?: boolean;
// Whether to process the request result
isTransformResponse?: boolean;
// Whether to return native response headers
// For example: use this attribute when you need to get the response headers
isReturnNativeResponse?: boolean;
// Whether to join url
joinPrefix?: boolean;
// Interface address, use the default apiUrl if you leave it blank
apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
// Error message prompt type
errorMessageMode?: ErrorMessageMode;
// Whether to add a timestamp
joinTime?: boolean;
ignoreCancelToken?: boolean;
// Whether to send token in header
withToken?: boolean;
// 请求重试机制
retryRequest?: RetryRequest;
}
export interface RetryRequest {
isOpenRetry: boolean;
count: number;
waitTime: number;
}
export interface Result<T = any> {
code: number;
type: 'success' | 'error' | 'warning';
message: string;
result: T;
}
// multipart/form-data: upload file
export interface UploadFileParams {
// Other parameters
data?: Recordable;
// File parameter interface field name
name?: string;
// file name
file: File | Blob;
// file name
filename?: string;
[key: string]: any;
}

161
types/config.d.ts vendored Normal file
View File

@ -0,0 +1,161 @@
import { MenuTypeEnum, MenuModeEnum, TriggerEnum, MixSidebarTriggerEnum } from '/@/enums/menuEnum';
import {
ContentEnum,
PermissionModeEnum,
ThemeEnum,
RouterTransitionEnum,
SettingButtonPositionEnum,
SessionTimeoutProcessingEnum,
} from '/@/enums/appEnum';
import { CacheTypeEnum } from '/@/enums/cacheEnum';
export type LocaleType = 'zh_CN' | 'en' | 'ru' | 'ja' | 'ko';
export interface MenuSetting {
bgColor: string;
fixed: boolean;
collapsed: boolean;
canDrag: boolean;
show: boolean;
hidden: boolean;
split: boolean;
menuWidth: number;
mode: MenuModeEnum;
type: MenuTypeEnum;
theme: ThemeEnum;
topMenuAlign: 'start' | 'center' | 'end';
trigger: TriggerEnum;
accordion: boolean;
closeMixSidebarOnChange: boolean;
collapsedShowTitle: boolean;
mixSideTrigger: MixSidebarTriggerEnum;
mixSideFixed: boolean;
}
export interface MultiTabsSetting {
cache: boolean;
show: boolean;
showQuick: boolean;
canDrag: boolean;
showRedo: boolean;
showFold: boolean;
}
export interface HeaderSetting {
bgColor: string;
fixed: boolean;
show: boolean;
theme: ThemeEnum;
// Turn on full screen
showFullScreen: boolean;
// Whether to show the lock screen
useLockPage: boolean;
// Show document button
showDoc: boolean;
// Show message center button
showNotice: boolean;
showSearch: boolean;
}
export interface LocaleSetting {
showPicker: boolean;
// Current language
locale: LocaleType;
// default language
fallback: LocaleType;
// available Locales
availableLocales: LocaleType[];
}
export interface TransitionSetting {
// Whether to open the page switching animation
enable: boolean;
// Route basic switching animation
basicTransition: RouterTransitionEnum;
// Whether to open page switching loading
openPageLoading: boolean;
// Whether to open the top progress bar
openNProgress: boolean;
}
export interface ProjectConfig {
// Storage location of permission related information
permissionCacheType: CacheTypeEnum;
// Whether to show the configuration button
showSettingButton: boolean;
// Whether to show the theme switch button
showDarkModeToggle: boolean;
// Configure where the button is displayed
settingButtonPosition: SettingButtonPositionEnum;
// Permission mode
permissionMode: PermissionModeEnum;
// Session timeout processing
sessionTimeoutProcessing: SessionTimeoutProcessingEnum;
// Website gray mode, open for possible mourning dates
grayMode: boolean;
// Whether to turn on the color weak mode
colorWeak: boolean;
// Theme color
themeColor: string;
// The main interface is displayed in full screen, the menu is not displayed, and the top
fullContent: boolean;
// content width
contentMode: ContentEnum;
// Whether to display the logo
showLogo: boolean;
// Whether to show the global footer
showFooter: boolean;
// menuType: MenuTypeEnum;
headerSetting: HeaderSetting;
// menuSetting
menuSetting: MenuSetting;
// Multi-tab settings
multiTabsSetting: MultiTabsSetting;
// Animation configuration
transitionSetting: TransitionSetting;
// pageLayout whether to enable keep-alive
openKeepAlive: boolean;
// Lock screen time
lockTime: number;
// Show breadcrumbs
showBreadCrumb: boolean;
// Show breadcrumb icon
showBreadCrumbIcon: boolean;
// Use error-handler-plugin
useErrorHandle: boolean;
// Whether to open back to top
useOpenBackTop: boolean;
// Is it possible to embed iframe pages
canEmbedIFramePage: boolean;
// Whether to delete unclosed messages and notify when switching the interface
closeMessageOnSwitch: boolean;
// Whether to cancel the http request that has been sent but not responded when switching the interface.
removeAllHttpPending: boolean;
}
export interface GlobConfig {
// Site title
title: string;
// Service interface url
apiUrl: string;
// Upload url
uploadUrl?: string;
// Service interface url prefix
urlPrefix?: string;
// Project abbreviation
shortName: string;
}
export interface GlobEnvConfig {
// Site title
VITE_GLOB_APP_TITLE: string;
// Service interface url
VITE_GLOB_API_URL: string;
// Service interface url prefix
VITE_GLOB_API_URL_PREFIX?: string;
// Project abbreviation
VITE_GLOB_APP_SHORT_NAME: string;
// Upload url
VITE_GLOB_UPLOAD_URL?: string;
}

96
types/global.d.ts vendored Normal file
View File

@ -0,0 +1,96 @@
import type { ComponentRenderProxy, VNode, VNodeChild, ComponentPublicInstance, FunctionalComponent, PropType as VuePropType } from 'vue';
declare global {
const __APP_INFO__: {
pkg: {
name: string;
version: string;
dependencies: Recordable<string>;
devDependencies: Recordable<string>;
};
lastBuildTime: string;
};
// declare interface Window {
// // Global vue app instance
// __APP__: App<Element>;
// }
// vue
declare type PropType<T> = VuePropType<T>;
declare type VueNode = VNodeChild | JSX.Element;
export type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
declare type Nullable<T> = T | null;
declare type NonNullable<T> = T extends null | undefined ? never : T;
declare type Recordable<T = any> = Record<string, T>;
declare type ReadonlyRecordable<T = any> = {
readonly [key: string]: T;
};
declare type Indexable<T = any> = {
[key: string]: T;
};
declare type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
declare type TimeoutHandle = ReturnType<typeof setTimeout>;
declare type IntervalHandle = ReturnType<typeof setInterval>;
declare interface ChangeEvent extends Event {
target: HTMLInputElement;
}
declare interface WheelEvent {
path?: EventTarget[];
}
interface ImportMetaEnv extends ViteEnv {
__: unknown;
}
declare interface ViteEnv {
VITE_PORT: number;
VITE_USE_MOCK: boolean;
VITE_USE_PWA: boolean;
VITE_PUBLIC_PATH: string;
VITE_PROXY: [string, string][];
VITE_GLOB_APP_TITLE: string;
VITE_GLOB_APP_SHORT_NAME: string;
VITE_USE_CDN: boolean;
VITE_DROP_CONSOLE: boolean;
VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
VITE_LEGACY: boolean;
VITE_USE_IMAGEMIN: boolean;
VITE_GENERATE_UI: string;
}
declare function parseInt(s: string | number, radix?: number): number;
declare function parseFloat(string: string | number): number;
namespace JSX {
// tslint:disable no-empty-interface
type Element = VNode;
// tslint:disable no-empty-interface
type ElementClass = ComponentRenderProxy;
interface ElementAttributesProperty {
$props: any;
}
interface IntrinsicElements {
[elem: string]: any;
}
interface IntrinsicAttributes {
[elem: string]: any;
}
}
}
interface AnyObject {
[key: string]: any;
}
declare module 'vue' {
export type JSXComponent<Props = any> = { new (): ComponentPublicInstance<Props> } | FunctionalComponent<Props>;
}

27
types/index.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
declare interface Fn<T = any, R = T> {
(...arg: T[]): R;
}
declare interface PromiseFn<T = any, R = T> {
(...arg: T[]): Promise<R>;
}
declare type RefType<T> = T | null;
declare type LabelValueOptions = {
label: string;
value: any;
[key: string]: string | number | boolean;
}[];
declare type EmitType = (event: string, ...args: any[]) => void;
declare type TargetContext = '_self' | '_blank';
declare interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
$el: T;
}
declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null;
declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>;

16
types/module.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
declare module '*.vue' {
import { DefineComponent } from 'vue';
const Component: DefineComponent<{}, {}, any>;
export default Component;
}
declare module 'ant-design-vue/es/locale/*' {
import { Locale } from 'ant-design-vue/types/locale-provider';
const locale: Locale & ReadonlyRecordable;
export default locale as Locale & ReadonlyRecordable;
}
declare module 'virtual:*' {
const result: any;
export default result;
}

48
types/store.d.ts vendored Normal file
View File

@ -0,0 +1,48 @@
import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { RoleInfo } from '/@/api/sys/model/userModel';
// Lock screen information
export interface LockInfo {
// Password required
pwd?: string | undefined;
// Is it locked?
isLock?: boolean;
}
// Error-log information
export interface ErrorLogInfo {
// Type of error
type: ErrorTypeEnum;
// Error file
file: string;
// Error name
name?: string;
// Error message
message: string;
// Error stack
stack?: string;
// Error detail
detail: string;
// Error url
url: string;
// Error time
time?: string;
}
export interface UserInfo {
userId: string | number;
username: string;
realName: string;
avatar: string;
desc?: string;
homePath?: string;
roles: RoleInfo[];
}
export interface BeforeMiniState {
menuCollapsed?: boolean;
menuSplit?: boolean;
menuMode?: MenuModeEnum;
menuType?: MenuTypeEnum;
}

5
types/utils.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { ComputedRef, Ref } from 'vue';
export type DynamicProps<T> = {
[P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
};

45
types/vue-router.d.ts vendored Normal file
View File

@ -0,0 +1,45 @@
export {};
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, unknown> {
orderNo?: number;
// title
title: string;
// dynamic router level.
dynamicLevel?: number;
// dynamic router real route path (For performance).
realPath?: string;
// Whether to ignore permissions
ignoreAuth?: boolean;
// role info
roles?: RoleEnum[];
// Whether not to cache
ignoreKeepAlive?: boolean;
// Is it fixed on tab
affix?: boolean;
// icon on tab
icon?: string;
frameSrc?: string;
// current page transition
transitionName?: string;
// Whether the route has been dynamically added
hideBreadcrumb?: boolean;
// Hide submenu
hideChildrenInMenu?: boolean;
// Carrying parameters
carryParam?: boolean;
// Used internally to mark single-level menus
single?: boolean;
// Currently active menu
currentActiveMenu?: string;
// Never show in tab
hideTab?: boolean;
// Never show in menu
hideMenu?: boolean;
isLink?: boolean;
// only build for Menu
ignoreRoute?: boolean;
// Hide path for children
hidePathForChildren?: boolean;
}
}

50
vite.config.ts Normal file
View File

@ -0,0 +1,50 @@
import legacy from '@vitejs/plugin-legacy';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { resolve } from 'path';
import { ConfigEnv, UserConfigExport } from 'vite';
import { createStyleImportPlugin, NutuiResolve } from 'vite-plugin-style-import';
import { viteMockServe } from 'vite-plugin-mock';
import eruda from 'vite-plugin-eruda';
const pathResolve = (dir: string) => resolve(__dirname, dir);
// https://vitejs.dev/config/
export default function ({ command }: ConfigEnv): UserConfigExport {
const isProduction = command === 'build';
console.log(isProduction);
return {
server: {
host: '0.0.0.0',
},
plugins: [
vue(),
vueJsx(),
createStyleImportPlugin({
resolves: [NutuiResolve()],
}),
legacy({
targets: ['defaults', 'not IE 11'],
}),
eruda(),
viteMockServe({
mockPath: './src/mock',
localEnabled: command === 'serve',
logger: true,
}),
],
css: {
preprocessorOptions: {
scss: {
// 配置 nutui 全局 scss 变量
additionalData: `@import "@nutui/nutui/dist/styles/variables.scss";`,
},
},
},
resolve: {
alias: {
'@': pathResolve('./src'),
},
},
};
}

3628
yarn.lock Normal file

File diff suppressed because it is too large Load Diff