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

23
.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
*.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
@ -18,4 +18,4 @@ 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.
SOFTWARE.

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