This commit is contained in:
xushunfa459 2022-06-30 19:27:52 +08:00
parent 2a9781110e
commit bb8a597e19
84 changed files with 24338 additions and 695 deletions

2
.env Normal file
View File

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

2
.env.production Normal file
View File

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

2
.env.test 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',
},
};

21
.gitignore vendored
View File

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

7
.husky/pre-commit Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
[ -n "$CI" ] && exit 0
# Format and submit code according to lintstagedrc.js configuration
npm run lint:lint-staged

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"]
}

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"i18n-ally.localesPaths": ["src/i18n", "src/i18n/lang"],
"cSpell.words": ["consola", "eruda", "mockjs", "nutui", "pinia", "stylelint", "vant", "vite", "vitejs", "vueuse"]
}

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 xsf
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

431
README.md
View File

@ -1,36 +1,421 @@
# Vue3-Tutorial
A Tutorial for Vue 3.
# vue3-h5-template
- [x] Vue3
- [x] vue-router
- [x] Vite
- [x] setup
- [x] echarts
基于 vue3 + vite + nut ui + sass + viewport 适配方案 +axios 封装,构建手机端模板脚手架
## How to use
You should clone the repo and install the dependencies, and then npm start.That is all.
### 启动项目
```bash
$ git clone https://github.com/allan2coder/VUE3-Tutorial.git
$ cd VUE3-Tutorial
$ npm install
npm install
npm run dev
```
Then launch the project app.
<span id="top">目录</span>
- [√ vite](#)
- [√ 配置多环境变量](#env)
- [√ viewport 适配方案](#viewport)
- [√ nutUI 组件按需加载](#nutUI)
- [√ Pinia 状态管理](#Pinia)
- [√ Vue-router4](#router)
- [√ Axios 封装及接口管理](#axios)
- [√ vite.config.ts 基础配置](#base)
- [√ alias](#alias)
- [√ proxy 跨域](#proxy)
- [√ Eslint+Pettier+stylelint 统一开发规范 ](#lint)
### <span id="env">✅ 配置多环境变量 </span>
`package.json` 里的 `scripts` 配置 `dev` `dev:test` `dev:prod` ,通过 `--mode xxx` 来执行不同环境
- 通过 `npm run dev` 启动本地环境参数 , 执行 `development`
- 通过 `npm run dev:test` 启动测试环境参数 , 执行 `test`
- 通过 `npm run dev:prod` 启动正式环境参数 , 执行 `prod`
```javascript
"scripts": {
"dev": "vite",
"dev:test": "vite --mode test",
"dev:prod": "vite --mode production",
}
```
[▲ 回顶部](#top)
### <span id="viewport">✅ viewport 适配方案 </span>
不用担心,项目已经配置好了 `viewport` 适配, 下面仅做介绍:
- [postcss-px-to-viewport-8-plugin](https://github.com/xian88888888/postcss-px-to-viewport-8-plugin) 是一款 `postcss` 插件,用于将单位转化为 `vw`, 现在很多浏览器对`vw`的支持都很好。
##### PostCSS 配置
下面提供了一份基本的 `postcss` 配置,可以在此配置的基础上根据项目需求进行修改
```javascript
// https://github.com/michael-ciniawsky/postcss-load-config
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/], // 设置忽略文件,用正则做目录名匹配
},
},
};
```
更多详细信息: [vant](https://youzan.github.io/vant/#/zh-CN/quickstart#jin-jie-yong-fa)
**新手必看,老鸟跳过**
很多小伙伴会问我,适配的问题, 因为我们使用的是 Vant UI所以必须根据 Vant UI 375 的设计规范走,一般我们的设计会将 UI 图上传到蓝湖,我们就可以需要的尺寸了。下面就大搞普及一下 rem。
我们知道 `1rem` 等于 `html` 根元素设定的 `font-size``px` 值。Vant UI 设置 `rootValue: 37.5` , 你可以看到在 iPhone 6 下看到 `1rem 等于 37.5px`
```html
<html data-dpr="1" style="font-size: 37.5px;"> </html>
```
切换不同的机型,根元素可能会有不同的 `font-size` 。当你写 css px 样式时,会被程序换算成 `rem` 达到适配。
因为我们用了 Vant 的组件,需要按照 `rootValue: 37.5` 来写样式。
举个例子:设计给了你一张 750px \* 1334px 图片,在 iPhone6 上铺满屏幕, 其他机型适配。
- 当`rootValue: 75` , 样式 `width: 750px;height: 1334px;` 图片会撑满 iPhone6 屏幕,这个时候切换其他机型,图片也会跟着撑满。
- 当`rootValue: 37.5` 的时候,样式 `width: 375px;height: 667px;` 图片会撑满 iPhone6 屏幕。
也就是 iphone 6 下 375px 宽度写 CSS。其他的你就可以根据你设计图去写对应的样式就可以了。
当然,想要撑满屏幕你可以使用 100%,这里只是举例说明。
```html
<img class="image" src="https://www.sunniejs.cn/static/weapp/logo.png" />
<style>
/* rootValue: 75 */
.image {
width: 750px;
height: 1334px;
}
/* rootValue: 37.5 */
.image {
width: 375px;
height: 667px;
}
</style>
```
[▲ 回顶部](#top)
### <span id="nutUI">✅ nutUI 组件按需加载 </span>
Vite 构建工具,使用 vite-plugin-style-import 实现按需引入。
#### 安装插件
```bash
$ npm run dev
npm i vite-plugin-style-import -D
```
You should see a new browser tap opening and a page of 'index.html' in http://localhost:3000.
`vite.config.ts` 设置
## How to build the static files
``` bash
npm run build
```javascript
plugins: [
...
createStyleImportPlugin({
resolves: [NutuiResolve()],
}),
...
],
```
## Other SPA
- [React.js](https://github.com/allan2coder/React-SPA) :fire: :fire: :fire:
#### 使用组件
## License
MIT
项目在 `plugins/nutUI.ts` 下统一管理组件,用哪个引入哪个,无需在页面里重复引用
```javascript
// 按需全局引入nutUI组件
import Vue from 'vue';
import { Button, Cell, CellGroup } from '@nutui/nutui';
export const nutUiComponents = [Button, Cell, CellGroup];
// 在main.ts文件中引入
nutUiComponents.forEach((item) => {
app.use(item);
});
```
[▲ 回顶部](#top)
### <span id="Pinia">✅ Pinia 状态管理</span>
下一代 vuex使用极其方便ts 兼容好
目录结构
```bash
├── store
│ ├── modules
│ │ └── user.js
│ ├── index.js
```
使用
```html
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
userStore.login();
</script>
```
[▲ 回顶部](#top)
### <span id="router">✅ Vue-router </span>
本案例采用 `hash` 模式,开发者根据需求修改 `mode` `base`
**注意**:如果你使用了 `history` 模式, `vue.config.js` 中的 `publicPath` 要做对应的**修改**
前往:[vue.config.js 基础配置](#base)
```javascript
import Vue from 'vue';
import { createRouter, createWebHistory, Router } from 'vue-router';
Vue.use(Router);
export const router = [
{
name: 'root',
path: '/',
redirect: '/home',
component: () => import('@/layout/basic/index.vue'),
},
];
const router: Router = createRouter({
history: createWebHistory(),
routes: routes,
});
export default router;
```
更多:[Vue Router](https://router.vuejs.org/zh/introduction.html)
[▲ 回顶部](#top)
### <span id="axios">✅ Axios 封装及接口管理</span>
`utils/request.js` 封装 axios , 开发者需要根据后台接口做修改。
- `service.interceptors.request.use` 里可以设置请求头,比如设置 `token`
- `config.hideloading` 是在 api 文件夹下的接口参数里设置,下文会讲
- `service.interceptors.response.use` 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录
```javascript
import axios from 'axios';
import store from '@/store';
import { Toast } from 'vant';
// 根据环境不同引入不同api地址
import { baseApi } from '@/config';
// create an axios instance
const service = axios.create({
baseURL: baseApi, // url = base api url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
});
// request 拦截器 request interceptor
service.interceptors.request.use(
(config) => {
// 不传递默认开启loading
if (!config.hideloading) {
// loading
Toast.loading({
forbidClick: true,
});
}
if (store.getters.token) {
config.headers['X-Token'] = '';
}
return config;
},
(error) => {
// do something with request error
console.log(error); // for debug
return Promise.reject(error);
},
);
// respone拦截器
service.interceptors.response.use(
(response) => {
Toast.clear();
const res = response.data;
if (res.status && res.status !== 200) {
// 登录超时,重新登录
if (res.status === 401) {
store.dispatch('FedLogOut').then(() => {
location.reload();
});
}
return Promise.reject(res || 'error');
} else {
return Promise.resolve(res);
}
},
(error) => {
Toast.clear();
console.log('err' + error); // for debug
return Promise.reject(error);
},
);
export default service;
```
#### 接口管理
`src/api` 文件夹下统一管理接口
- 你可以建立多个模块对接接口, 比如 `home.js` 里是首页的接口这里讲解 `user.js`
- `url` 接口地址,请求的时候会拼接上 `config` 下的 `baseApi`
- `method` 请求方法
- `data` 请求参数 `qs.stringify(params)` 是对数据系列化操作
- `hideloading` 默认 `false`, 设置为 `true` 后,不显示 loading ui 交互中有些接口不需要让用户感知
```javascript
import qs from 'qs';
// axios
import request from '@/utils/request';
//user api
// 用户信息
export function getUserInfo(params) {
return request({
url: '/user/userinfo',
method: 'post',
data: qs.stringify(params),
hideloading: true, // 隐藏 loading 组件
});
}
```
#### 如何调用
```javascript
// 请求接口
import { getUserInfo } from '@/api/user.js';
const params = {
user: 'sunnie',
};
getUserInfo(params)
.then(() => {})
.catch(() => {});
```
[▲ 回顶部](#top)
### <span id="base">✅ vite.config.ts 基础配置 </span>
如果你的 `Vue Router` 模式是 hash
```javascript
publicPath: './',
```
如果你的 `Vue Router` 模式是 history 这里的 publicPath 和你的 `Vue Router` `base` **保持一直**
```javascript
publicPath: '/app/',
```
```javascript
export default function ({ command }: ConfigEnv): UserConfigExport {
const isProduction = command === 'build';
return {
server: {
host: '0.0.0.0',
},
plugins: [
vue(),
vueJsx(),
createStyleImportPlugin({
resolves: [NutuiResolve()],
}),
eruda(),
viteMockServe({
mockPath: './src/mock',
localEnabled: command === 'serve',
logger: true,
}),
],
css: {
preprocessorOptions: {
scss: {
// 配置 nutui 全局 scss 变量
additionalData: `@import "@nutui/nutui/dist/styles/variables.scss";`,
},
},
},
};
}
```
[▲ 回顶部](#top)
### <span id="alias">✅ 配置 alias 别名 </span>
```javascript
resolve: {
alias: [{
find: 'vue-i18n',
replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
},
// /@/xxxx => src/xxxx
{
find: /\/@\//,
replacement: pathResolve('src') + '/',
},
// /#/xxxx => types/xxxx
{
find: /\/#\//,
replacement: pathResolve('types') + '/',
},
],
},
```
[▲ 回顶部](#top)
### <span id="proxy">✅ 配置 proxy 跨域 </span>
```javascript
server: {
proxy: {
'/api': {
target: 'https://baidu.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
```
[▲ 回顶部](#top)
### <span id="lint">✅ Eslint+Pettier+stylelint 统一开发规范 </span>
根目录下的`.eslintrc.js``.stylelint.config.js``.prettier.config.js`内置了 lint 规则,帮助你规范地开发代码,有助于提高团队的代码质量和协作性,可以根据团队的规则进行修改

1
config/constant.ts Normal file
View File

@ -0,0 +1 @@
export const IsReport = process.env.REPORT;

View File

@ -0,0 +1,21 @@
/**
* @name AutoImportDeps
* @description
*/
import AutoImport from 'unplugin-auto-import/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export const AutoImportDeps = () => {
return AutoImport({
dts: 'types/auto-imports.d.ts',
imports: [
'vue',
'pinia',
'vue-router',
{
'@vueuse/core': [],
},
],
resolvers: [ElementPlusResolver()],
});
};

View File

@ -0,0 +1,20 @@
/**
* @name AutoRegistryComponents
* @description
*/
import Components from 'unplugin-vue-components/vite';
import { VueUseComponentsResolver } from 'unplugin-vue-components/resolvers';
export const AutoRegistryComponents = () => {
return Components({
// dirs: ['src/components'],
extensions: ['vue', 'md'],
deep: true,
dts: 'types/components.d.ts',
directoryAsNamespace: false,
globalNamespaces: [],
directives: true,
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
resolvers: [VueUseComponentsResolver()],
});
};

View File

@ -0,0 +1,18 @@
/**
* @name ConfigCompressPlugin
* @description .gz压缩
*/
import viteCompression from 'vite-plugin-compression';
export const ConfigCompressPlugin = () => {
return viteCompression({
verbose: true, // 默认即可
disable: false, //开启压缩(不禁用),默认即可
deleteOriginFile: false, //删除源文件
threshold: 10240, //压缩前最小文件大小
algorithm: 'gzip', //压缩算法
ext: '.gz', //文件类型
});
return [];
};

View File

@ -0,0 +1,5 @@
import eruda from 'vite-plugin-eruda';
export const ConfigEruda = () => {
return eruda();
};

View File

@ -0,0 +1,32 @@
import viteImagemin from 'vite-plugin-imagemin';
export function ConfigImageminPlugin() {
const plugin = viteImagemin({
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
mozjpeg: {
quality: 20,
},
optipng: {
optimizationLevel: 7,
},
pngquant: {
quality: [0.8, 0.9],
speed: 4,
},
svgo: {
plugins: [
{
name: 'removeViewBox',
},
{
name: 'removeEmptyAttrs',
active: false,
},
],
},
});
return plugin;
}

View File

@ -0,0 +1,69 @@
/**
* @name createVitePlugins
* @description plugins数组统一调用
*/
import type { Plugin } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import vueSetupExtend from 'vite-plugin-vue-setup-extend';
import { ConfigSvgIconsPlugin } from './svgIcons';
import { AutoRegistryComponents } from './component';
import { AutoImportDeps } from './autoImport';
import { ConfigMockPlugin } from './mock';
import { ConfigCompressPlugin } from './compress';
import { ConfigPagesPlugin } from './pages';
import { ConfigRestartPlugin } from './restart';
import { ConfigProgressPlugin } from './progress';
import { ConfigEruda } from './eruda';
import { ConfigStyleImport } from './styleImport';
import { ConfigImageminPlugin } from './imagemin';
import { ConfigVisualizerConfig } from './visualizer';
export function createVitePlugins(isBuild: boolean) {
const vitePlugins: (Plugin | Plugin[])[] = [
// vue支持
vue(),
// JSX支持
vueJsx(),
// setup语法糖组件名支持
vueSetupExtend(),
];
// 自动按需引入组件
vitePlugins.push(AutoRegistryComponents());
// 自动按需引入依赖
vitePlugins.push(AutoImportDeps());
// 自动生成路由
vitePlugins.push(ConfigPagesPlugin());
// 开启.gz压缩 rollup-plugin-gzip
vitePlugins.push(ConfigCompressPlugin());
// 监听配置文件改动重启
vitePlugins.push(ConfigRestartPlugin());
// 构建时显示进度条
vitePlugins.push(ConfigProgressPlugin());
//styleImport
vitePlugins.push(ConfigStyleImport());
// eruda
vitePlugins.push(ConfigEruda());
// rollup-plugin-visualizer
vitePlugins.push(ConfigVisualizerConfig());
if (isBuild) {
// vite-plugin-imagemin
vitePlugins.push(ConfigImageminPlugin());
// vite-plugin-svg-icons
vitePlugins.push(ConfigSvgIconsPlugin(isBuild));
// vite-plugin-mock
vitePlugins.push(ConfigMockPlugin(isBuild));
}
return vitePlugins;
}

View File

@ -0,0 +1,18 @@
/**
* @name ConfigMockPlugin
* @description mockjs
*/
import { viteMockServe } from 'vite-plugin-mock';
export const ConfigMockPlugin = (isBuild: boolean) => {
return viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: false, //实际开发请关闭,会影响打包体积
// https://github.com/anncwb/vite-plugin-mock/issues/9
injectCode: `
import { setupProdMockServer } from '../mock/_createProdMockServer';
setupProdMockServer();
`,
});
};

View File

@ -0,0 +1,13 @@
/**
* @name ConfigPagesPlugin
* @description
*/
import Pages from 'vite-plugin-pages';
export const ConfigPagesPlugin = () => {
return Pages({
pagesDir: [{ dir: 'src/pages', baseRoute: '' }],
extensions: ['vue', 'md'],
exclude: ['**/components/*.vue'],
nuxtStyle: true,
});
};

View File

@ -0,0 +1,9 @@
/**
* @name ConfigProgressPlugin
* @description
*/
import progress from 'vite-plugin-progress';
export const ConfigProgressPlugin = () => {
return progress() as Plugin;
};

View File

@ -0,0 +1,10 @@
/**
* @name ConfigRestartPlugin
* @description Vite
*/
import ViteRestart from 'vite-plugin-restart';
export const ConfigRestartPlugin = () => {
return ViteRestart({
restart: ['*.config.[jt]s', '**/config/*.[jt]s'],
});
};

View File

@ -0,0 +1,7 @@
import { createStyleImportPlugin, NutuiResolve, VantResolve } from 'vite-plugin-style-import';
export const ConfigStyleImport = () => {
return createStyleImportPlugin({
resolves: [NutuiResolve(), VantResolve()],
});
};

View File

@ -0,0 +1,16 @@
/**
* @name SvgIconsPlugin
* @description SVG文件
*/
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import path from 'path';
export const ConfigSvgIconsPlugin = (isBuild: boolean) => {
return createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
svgoOptions: isBuild,
});
};

View File

@ -0,0 +1,14 @@
import visualizer from 'rollup-plugin-visualizer';
import { IsReport } from '../../constant';
export function ConfigVisualizerConfig() {
if (IsReport) {
return visualizer({
filename: './node_modules/.cache/visualizer/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}) as Plugin;
}
return [];
}

20
config/vite/proxy.ts Normal file
View File

@ -0,0 +1,20 @@
import { API_BASE_URL, API_TARGET_URL, MOCK_API_BASE_URL, MOCK_API_TARGET_URL } from '../../config/constant';
import { ProxyOptions } from 'vite';
type ProxyTargetList = Record<string, ProxyOptions>;
const init: ProxyTargetList = {
// test
[API_BASE_URL]: {
target: API_TARGET_URL,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), ''),
},
// mock
[MOCK_API_BASE_URL]: {
target: MOCK_API_TARGET_URL,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${MOCK_API_BASE_URL}`), '/api'),
},
};
export default init;

View File

@ -3,11 +3,40 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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.js"></script>
<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>

10555
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,110 @@
{
"name": "vue3-vite",
"version": "0.0.0",
"name": "vue-h5-template",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
"dev:test": "vite --mode test",
"dev:prod": "vite --mode production",
"build": "vue-tsc --noEmit && vite build",
"report": "cross-env REPORT=true npm run 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",
"prepare": "husky install",
"deps": "yarn upgrade-interactive --latest"
},
"dependencies": {
"echarts": "^5.2.2",
"vue": "^3.2.27",
"vue-router": "^4.0.5"
"@nutui/nutui": "^3.1.22",
"@vueuse/core": "8.7.5",
"@vueuse/integrations": "8.7.5",
"axios": "0.27.2",
"pinia": "^2.0.14",
"universal-cookie": "^4.0.4",
"vant": "^3.5.1",
"vue": "^3.2.36",
"vue-i18n": "^9.1.10",
"vue-router": "^4.0.16"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5",
"vite": "^2.1.5"
"@types/node": "^17.0.42",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"@vitejs/plugin-legacy": "^1.8.2",
"@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"consola": "^2.15.3",
"cross-env": "^7.0.3",
"eruda": "^2.4.1",
"eslint": "^8.18.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.1.1",
"husky": "8.0.1",
"lint-staged": "13.0.3",
"mockjs": "^1.1.0",
"postcss": "^8.4.14",
"postcss-html": "1.4.1",
"postcss-less": "^6.0.0",
"postcss-px-to-viewport-8-plugin": "^1.1.3",
"prettier": "^2.7.1",
"rollup-plugin-visualizer": "^5.6.0",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^8.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^26.0.0",
"stylelint-order": "^5.0.0",
"typescript": "^4.7.4",
"unplugin-auto-import": "^0.9.1",
"unplugin-vue-components": "^0.19.9",
"vite": "^2.9.12",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eruda": "^1.0.1",
"vite-plugin-imagemin": "^0.6.1",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-pages": "^0.24.2",
"vite-plugin-progress": "^0.0.3",
"vite-plugin-restart": "^0.1.1",
"vite-plugin-style-import": "^2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-eslint-parser": "^9.0.3",
"vue-tsc": "^0.38.1"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china",
"rollup": "^2.56.3",
"gifsicle": "5.2.0"
},
"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"
]
}
}

3966
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: 140,
semi: true,
vueIndentScriptAndStyle: true,
singleQuote: true,
trailingComma: 'all',
proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict',
endOfLine: 'auto',
};

BIN
public/group.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@ -1,21 +1,14 @@
<template>
<router-link to="/">Page1</router-link>
<router-link to="/page2">Page2</router-link>
<router-view />
<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>
import { onBeforeMount } from 'vue'
onBeforeMount(() => {
//set tmp token when setting isNeedLogin false
})
</script>
<style>
a {
color: #42b983;
}
</style>
<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,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>

View File

@ -1,137 +0,0 @@
<template>
<div class="chart-wrap">
<div :id="id" :class="className" :style="{ height: height, width: width }" />
</div>
</template>
<script setup>
import * as echarts from 'echarts';
import { getCurrentInstance, onMounted, reactive, onBeforeUnmount } from 'vue';
let { proxy } = getCurrentInstance();
defineProps({
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '215px'
}
})
const state = reactive({
chart: null,
});
onMounted(() => {
initChart();
});
onBeforeUnmount(() => {
if (!state.chart) {
return
}
state.chart.dispose()
state.chart = null
})
const initChart = () => {
state.chart = echarts.init(document.getElementById(proxy.id));
let dataAxis = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬rén', '癸guǐ'];
let data = [10, 25, 10, 15, 20, 25, 10, 15, 20, 25, 10];
state.chart.resize({
width: 400,
height: 215
});
state.chart.setOption({
tooltip: {
show: true,
},
xAxis: {
data: dataAxis,
// label
axisLabel: {
color: '#8885a1',
margin: state.isH5?4:12, // 线
interval: 0,
rotate: state.isH5?45:0,
fontSize: 10,
},
axisTick: {
alignWithLabel: true,
},
// 线
axisLine: {
show: true,
lineStyle:{
color: '#3a3464',
width: 1,
type: 'solid',
},
},
},
yAxis: {
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: '#4e4b74'
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255, 255, 255, 0.15)',
width: 1,
type: 'dashed'
}
}
},
series: [
{
type: 'bar',
showBackground: false,
barWidth: state.isH5?12:16,
label: {
show: true,
position: 'top',
color: '#fff',
},
//
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#60B5FF' },
{ offset: 1, color: '#0088FF' },
])
},
data: data
}
],
});
}
</script>
<style scoped>
.chart-wrap {
width: auto;
overflow-x: auto;
height: 215px;
background: var(--linear-gradient-bg);
border-radius: 12px;
}
</style>

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

@ -0,0 +1,32 @@
import { AnyObject } from '/#/global';
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: '支持',
},
};

View File

@ -0,0 +1,42 @@
<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) => {
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

@ -1,5 +0,0 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app')

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,
];

View File

@ -1,23 +0,0 @@
import { createWebHistory, createRouter } from "vue-router";
import Page1 from "../views/Page1.vue";
import Page2 from "../views/Page2.vue";
const routes = [
{
path: "/",
name: "page1",
component: Page1,
},
{
path: "/page2",
name: "Page2",
component: Page2,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;

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

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

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

@ -0,0 +1,44 @@
const routes = [
{
path: '/',
redirect: '/home',
component: () => import('/@/layout/basic/index.vue'),
children: [
{
path: 'home',
component: () => import('/@/views/home/index.vue'),
meta: {
title: '',
keepAlive: true,
},
},
{
path: 'list',
component: () => import('/@/views/list/index.vue'),
meta: {
title: '',
keepAlive: true,
},
},
{
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 };

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

@ -0,0 +1,41 @@
import { loginPassword } from '/@/api';
import { useCookies } from '@vueuse/integrations/useCookies';
import { defineStore } from 'pinia';
import { watch } from 'vue';
import { AnyObject } from '/#/global';
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);
});
});
},
},
});

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

@ -0,0 +1,18 @@
import { AnyObject } from '/#/global';
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/lib/toast';
// 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;

View File

@ -1,12 +0,0 @@
<template>
<!-- use components -->
<Chart />
</template>
<script setup>
// import components
import Chart from '../components/chart.vue'
</script>
<style></style>

View File

@ -1,21 +0,0 @@
<template>
<div class="btn" @click="state.count++">Click me</div>
<h4>{{ state.count }}</h4>
</template>
<script setup>
import { reactive } from 'vue';
// reactive data
const state = reactive({ count: 0 })
</script>
<style>
.btn {
margin-top: 50px;
}
h4{
text-align: center;
}
</style>

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

@ -0,0 +1,75 @@
<template>
<nut-navbar :left-show="false" :title="$t('tabbar.home')" />
<p class="intro-header">{{ $t('introduction') }}</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 }}
</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>

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>

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

@ -0,0 +1,60 @@
<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 { reactive, ref } from 'vue';
import { useUserStore } from '/@/store/modules/user';
const userStore = useUserStore();
const formData = reactive({
name: '',
pwd: '',
});
const ruleForm = ref<any>(null);
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>

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>

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"]
}

69
types/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,69 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}

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;
}

14
types/components.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TitleBar: typeof import('./../src/components/TitleBar/index.vue')['default']
}
}
export {}

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;
}
}
}
export 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;
}
}

View File

@ -1,6 +0,0 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()]
})

49
vite.config.ts Normal file
View File

@ -0,0 +1,49 @@
import { createVitePlugins } from './config/vite/plugins';
import { resolve } from 'path';
import { ConfigEnv, UserConfigExport } from 'vite';
// import { viteMockServe } from 'vite-plugin-mock';
const pathResolve = (dir: string) => {
return resolve(process.cwd(), '.', dir);
};
// https://vitejs.dev/config/
export default function ({ command }: ConfigEnv): UserConfigExport {
const isProduction = command === 'build';
const root = process.cwd();
return {
root,
resolve: {
alias: [
{
find: 'vue-i18n',
replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
},
// /@/xxxx => src/xxxx
{
find: /\/@\//,
replacement: pathResolve('src') + '/',
},
// /#/xxxx => types/xxxx
{
find: /\/#\//,
replacement: pathResolve('types') + '/',
},
],
},
server: {
host: true,
hmr: true,
},
plugins: createVitePlugins(isProduction),
css: {
preprocessorOptions: {
scss: {
// 配置 nutui 全局 scss 变量
additionalData: `@import "@nutui/nutui/dist/styles/variables.scss";`,
},
},
},
};
}

7178
yarn.lock Normal file

File diff suppressed because it is too large Load Diff