基本パッケージをインストールする
npm create vite@latest
# 这里选择的是Vue+Typescript的组合
cd vue-admin
npm install
# 先安装基础包
npm install vue-router@4
npm i pinia
npm i axios
npm install sass --save-dev
npm install element-plus --save
npm install @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
npm i eslint -D
# 提交规范
npm i lint-staged husky --save-dev
npm install @commitlint/cli @commitlint/config-conventional -D
コード仕様
npm init @eslint/config
たくさんのプロンプトが表示されます。選択肢は次のとおりです。
Need to install the following packages:
@eslint/create-config
Ok to proceed? (y)
√ How would you like to use ESLint? · style
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:
eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.50.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-plugin-vue@latest, eslint-config-standard-with-typescript@latest, @typescript-eslint/eslint-plugin@^5.50.0, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, typescript@*
ファイルがプロジェクト内に生成され.eslintrc.cjs
、それを検証するようにスクリプトを構成します。
"lint": "eslint src/**/*.{js,jsx,vue,ts,tsx} --fix"
ただし、実行時にエラーが報告されました。現在サポートされている"typescript": "^5.1.3",
バージョン範囲は:なので、ダウングレードする必要があります: 。eslint を構成する途中で多くの問題があり、解決策が直接提供されます。@typescript-eslint/typescript-estree
ts
=3.3.1 <5.1.0
[email protected]
1 つ目は、以下を変更することです.eslintrc.cjs
。
module.exports = {
env: {
browser: true,
es2021: true
},
extends: [
'plugin:vue/vue3-essential',
'standard-with-typescript'
],
parser: "vue-eslint-parser",
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ["./tsconfig.json"],
parser: "@typescript-eslint/parser",
extraFileExtensions: ['.vue']
},
plugins: [
'vue'
],
rules: {
'space-before-function-paren': [2, {
anonymous: 'always',
named: 'never',
asyncArrow: 'always'
}],
'vue/multi-word-component-names': 0,
"space-before-function-paren": 0,
"@typescript-eslint/consistent-type-assertions": 0,
"@typescript-eslint/ban-types": [
"error",
{
"extendDefaults": true,
"types": {
"{}": false
}
}
]
}
}
vite-env.d.ts
チェックを無視するには、コメント行を追加します。
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />
構成で発生した問題についてはeslint
、この偉人が書いた文書を参照して詳細を確認してください: Eslint: eslint (標準ルール) を vue3 プロジェクトに追加します。
コミット仕様
git init
次のコードを に追加しpackage.json
、それを使用して eslint と stylelint を呼び出し、一時記憶領域内のコードを確認します。
"lint-staged": {
"*.{vue,js}": [
"npm run lint"
]
}
埋め込む:
npm pkg set scripts.postinstall="husky install"
# 等同于执行npm i,执行过程中会生成.husky文件夹
npm run postinstall
npx husky add .husky/pre-commit "npm lint"
git add .husky/pre-commit
このようにすると、実行git commit
時に自動的に実行されますnpm lint
。
実行中のプロセス中に、ノードが内部コマンドまたは外部コマンドではないというエラーが報告されました。問題はありません。おそらく nvm ツールの問題であるため、 node のバージョン管理を行うためにvolta をnode -v
後で変更しました。
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
新しいcommitlint.config.cjs
:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', // 新增功能
'update', // 更新功能
'ui', // 样式改动
'fix', // 修复功能bug
'merge', // 合并分支
'refactor', // 重构功能
'perf', // 性能优化
'revert', // 回退提交
'style', // 不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等)
'build', // 修改项目构建工具(例如 glup,webpack,rollup 的配置等)的提交
'docs', // 文档新增、改动
'test', // 增加测试、修改测试
'chore' // 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
]],
'scope-empty': [0],
// 'scope-empty': [2, 'never'], 作用域不为空
'scope-case': [0],
'subject-full-stop': [0],
'subject-case': [0]
}
}
変更tsconfig.json
:
"include": [
//...
"commitlint.config.cjs"
],
変更.eslintrc.cjs
:
project: ["./tsconfig.json", "./commitlint.config.cjs"],
git add .
# 失败
git commit -m "commit校验"
# 成功
git commit -m "feat: commit校验"
パスのエイリアスを設定する
import {
defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
const resolve = (dist) => path.resolve(__dirname, dist)
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve('src')
},
// 顺便把可以省略的后缀配置一下,在vite中不支持省略.vue
extensions: [".js", ".ts", ".tsx", ".jsx"]
}
})
変更tsconfig.json
および追加:
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
リセットスタイル
次の下にassets
新しいフォルダーを作成しますstyles/reset.css
。
/**
* Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
* http://cssreset.com
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video{
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
font-weight: normal;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section{
display: block;
}
ol, ul, li{
list-style: none;
}
blockquote, q{
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after{
content: '';
content: none;
}
table{
border-collapse: collapse;
border-spacing: 0;
}
/* custom */
a{
color: #7e8c8d;
text-decoration: none;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
::-webkit-scrollbar{
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track-piece{
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical{
height: 5px;
background-color: rgba(125, 125, 125, 0.7);
border-radius: 6px;
-webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal{
width: 5px;
background-color: rgba(125, 125, 125, 0.7);
border-radius: 6px;
-webkit-border-radius: 6px;
}
html, body{
width: 100%;
font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", "微软雅黑", sans-serif;
}
body{
line-height: 1;
-webkit-text-size-adjust: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html{
overflow-y: scroll;
}
/*清除浮动*/
.clearfix:before,
.clearfix:after{
content: " ";
display: inline-block;
height: 0;
clear: both;
visibility: hidden;
}
.clearfix{
*zoom: 1;
}
/*隐藏*/
.dn{
display: none;
}
SSSを使用する
.scss
Vite は、、、、およびファイルの組み込みサポートを提供し.sass
ます。それらに特定のプラグインをインストールする必要はありませんが、対応するプリプロセッサの依存関係をインストールする必要があります。.less
.styl
.stylus
Vite
通常、プロジェクト内でいくつかのテーマカラーを定義します。
// variable.scss
$font-color-gray:rgb(147,147,147);
または、いくつかのカプセル化されたコレクション スタイル:
// mixins.scss
@mixin line-clamp($lines) {
word-break: break-all;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
}
@mixin ellipsis() {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
次に、次vite.config.js
のように設定します。
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
}
}
}
ディレクトリを削除しstyle.css
、新しいディレクトリを作成しますstyles/common.scss
。
// common.scss
@import url('./reset.css');
次に、次のようmain.ts
にインポートします。
import '@/assets/styles/common.scss'
このようにして、グローバル スタイルが初期化されます。次に、合計が機能するかどうかをテストしvariable.scss
、灰色になっていて 2 行が省略されているかどうかを確認するためにmixins.scss
変更します。HelloWorld.vue
<script setup lang="ts">
</script>
<template>
<div class="box">没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。</div>
</template>
<style scoped lang="scss">
.box {
color: $font-color-gray;
height: 40px;
line-height: 20px;
width: 200px;
@include line-clamp(2);
}
</style>
ルーティングを構成する
次にルーティングを設定すると、次の結果が得られると思います。
- 登录页
- 带菜单栏的框架
- 主页
- 人员管理
- 客户管理
- 员工管理
- 404页
したがって、次のファイルを作成します。
このプロセスでは、いくつかの問題が発生します。
- ルートを記述するときは、サフィックスが必要なコンポーネントを導入します
.vue
。 import xxx from '@/xxx'
上記のように合計値tsconfg.json
を設定していないため、エラーが報告されます。baseUrl
paths
layout
サブルートはファイル内でネストされるため、layout
以下を追加する必要がありますrouter-view
。
<template>
<div>布局</div>
<router-view></router-view>
</template>
他のファイルは次のように記述するだけです。
<template>
<p>主页</p>
</template>
新しいrouter
フォルダー:
- router
-hooks # 后期做登录校验和鉴权用的
- routes
- index.ts # 总输出文件
- others.ts # 不需要layout这一层的路由均可以放在这里
- person.ts # 人员管理模块
- index.ts # 总输出文件
各ファイルの内容は次のとおりです。
// person.ts
export default [
{
path: '/person',
name: 'Person',
meta: {
title: '人员管理' },
redirect: '/person/customer',
children: [
{
path: '/person/customer',
name: 'PersonCustomer',
meta: {
title: '客户管理' },
component: () => import('@/views/person/customer/index.vue')
},
{
path: '/person/staff',
name: 'PersonStaff',
meta: {
title: '员工管理' },
component: () => import('@/views/person/staff/index.vue')
}
]
}
];
// others.ts
export default [
{
path: '/login',
name: 'Login',
meta: {
title: '登录' },
component: () => import('@/views/login/index.vue')
}
];
// router/routes/index.ts
import Layout from '@/views/layout/index.vue';
import personRoutes from './person';
import otherRoutes from './others';
export default [
{
path: '/',
name: 'Layout',
component: Layout,
children: [
{
path: '/',
name: 'Index',
meta: {
title: '主页' },
component: () => import('@/views/index/index.vue')
},
...personRoutes,
]
},
...otherRoutes,
{
path: '/404',
name: 'NotFound',
meta: {
title: '404' },
component: () => import('@/views/404/index.vue')
},
{
path: "/:pathMatch(.*)",
redirect: "/404",
name:'ErrorPage',
meta: {
title: '' },
}
];
最初にフォルダーを脇に置きhooks
、単に次のように記述しますindex.ts
。
// router/index.ts
import routes from "./routes";
export default routes;
新しいものを作成しますsrc/plugins/index.ts
。以前にコンテンツを登録したとき、それを main.ts に直接配置しましたが、これは保守が容易ではないため、将来的にはここに一律にマウントします。
// src/plugins/index.ts
import {
createRouter, createWebHashHistory } from 'vue-router';
import routes from '@/router/index';
export default (app: any) => {
// 注册路由
const router = createRouter({
history: createWebHashHistory(),
routes
})
app.use(router);
}
変更することを忘れないでくださいApp.vue
:
<template>
<router-view></router-view>
</template>
import {
createApp } from 'vue'
import '@/assets/styles/common.scss'
import App from './App.vue'
import installPlugins from '@/plugins';
const app = createApp(App);
installPlugins(app);
app.mount('#app')
これにより、以下のテストが可能になります。
http://127.0.0.1:5173/#/
http://127.0.0.1:5173/#/login
http://127.0.0.1:5173/#/person
http://127.0.0.1:5173/#/person/customer
http://127.0.0.1:5173/#/person/staff
構成プロセス中にエラーが見つかった場合: モジュール " xxx.vue
" またはそれに対応する型宣言が見つからない場合は、次をvite-env.d.ts
追加します。
declare module '*.vue' {
import type {
DefineComponent } from 'vue';
const vueComponent: DefineComponent<{
}, {
}, any>;
export default vueComponent;
}
要素プラスを使用する
を使用する場合はVolar
、tsconfig.json
でグローバル コンポーネント タイプを指定しますcompilerOptions.type
。
// tsconfig.json
{
"compilerOptions": {
// ...
// 然而这个配置在后期打包的时候报错了...
"types": ["element-plus/global"]
}
}
ここではオンデマンドインポートの方法を採用していますが、量を追求しない場合は完全インポートを使用することもできます。
// vite.config.ts
import {
defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const resolve = (dist) => path.resolve(__dirname, dist)
export default defineConfig({
plugins: [
vue(),
// 新增
AutoImport({
resolvers: [ElementPlusResolver()]
}),
// 新增
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': resolve('./src')
},
extensions: [".js", ".ts", ".tsx", ".jsx"]
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
}
}
}
})
element plus
中国語の日付などのコンポーネントはデフォルトでは英語なので、コンポーネントを中国語に変更します。
変更App.vue
:
<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
</script>
<template>
<el-config-provider :locale="locale">
<router-view></router-view>
</el-config-provider>
</template>
これは中国語で表示されます。
導入した 2 つのプラグインについては、次のように説明します。
unplugin-vue-components
Vue テンプレートで使用されるコンポーネントを自動的に識別し、オンデマンドで自動的にインポートして登録するために使用されます。unplugin-auto-import
vite、webpack
一般的に使用される構成ライブラリは、他の環境にオンデマンドで自動的にインポートできますAPI
。たとえばVue
、ref
手動作業は必要ないimport
ため、構成ライブラリを構成し、いくつかの API の導入を削除できます。
export default defineConfig({
plugins: [
// ...
AutoImport({
imports: [
'vue',
'vue-router',
'pinia'
],
eslintrc: {
enabled: true,
filepath: './.eslintrc-auto-import.json',
globalsPropValue: true
},
resolvers: [ElementPlusResolver()]
}),
// ...
],
})
保存が有効になると、auto-imports.d.ts
コンテンツが自動的に入力され、.eslintrc-auto-import.json
eslint グローバル変数構成がプロジェクトのルート ディレクトリに生成されます。
次に、変更してtsconfg.json
次のようにします.eslintrc.cjs
。
// tsconfg.json
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "commitlint.config.cjs", "auto-imports.d.ts"],
// .eslintrc.cjs
project: ["./tsconfig.json", "./commitlint.config.cjs", './.eslintrc-auto-import.json'],
auto-imports.d.ts
ESLint 検証を無視する
# .eslintignore
auto-imports.d.ts
ここで注意する必要があります。
- Vue Router などのすべての API が
createRouter
インポートされるわけではありません。自動的にインポートできる特定の API については、unplugin-auto-import/src/presetsを参照してください。 - ファイルの生成
.eslintrc-auto-import.json
後に設定を追加する必要がない場合は、enabled: true
に設定することをお勧めしますfalse
。そうしないと、このファイルが毎回生成されます。
設定後、ページ内の一部の参照を削除しても問題ないことがわかりました。
<script lang='ts' setup>
// import { storeToRefs } from 'pinia'
// import { useRouter } from 'vue-router'
// ...
</script>
コンポーネントをテストします。
<template>
<p><el-button>测试</el-button></p>
</template>
これにより、ページ上にボタンが表示されます。
自動オンデマンド インポートの原則は、<template>
識別に使用されるコンポーネントを自動的にインポートすることです。これは、JS でメソッドを直接呼び出すコンポーネントと同様です。ElMessage
プラグインは自動インポートを認識して完了しないため、手動でインポートする必要があります (このような境界の問題を回避するために、完全なスタイル ファイルをインポートすることをお勧めします)。
変更しないvite-env.d.ts
と、 ts に導入されたときにエラーが報告されますelement plus
。
declare module "element-plus";
中小規模でplugins/element-plus.ts
試してください:
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import {
ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'
const options = {
size: 'small',
zIndex: 3000
}
const components = [
ElLoading,
ElMessage,
ElMessageBox,
ElNotification
]
export default function install (app: any): void {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
components.forEach((component) => {
app.use(component, options)
})
}
// plugins/index.ts
export default (app: any) => {
// ...
// 注册element-plus
installElementPlus(app);
}
テストを受けてください:
<template>
<p><el-button @click="onTest">测试</el-button></p>
</template>
<script setup lang="ts">
const onTest = () => {
ElLoading.service({
fullscreen: true });
}
</script>
グローバルプロパティ
以前の習慣によれば、loading
の呼び出しは確かに上記のメソッドではなく、グローバル にマウントされていますVue.prototype
が、今回のプロジェクトではオンデマンドインポートを使用しており、Vue3
では書き方が変わっています。
app.config.globalProperties.$loading = ElLoading;
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$msgBox = ElMessageBox;
app.config.globalProperties.$notification = ElNotification;
次に、使用の過程で次のようになります。
<script setup lang="ts">
const instance = getCurrentInstance()
onMounted(() => {
instance.proxy.$message.success('setup - getCurrentInstance() 成功使用')
// 也可以使用 appContext
console.log(instance.appContext.config.globalProperties.$message === instance.proxy.$message) // true
})
</script>
しかし、公式ドキュメントを参照したところ、そのような方法はなくgetCurrentInstance
、おそらく仕様を満たしていません。そこで、グローバルメソッドの注入を採用しましたprovide/inject
。
App.vue
:
<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
import {
ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
provide('$loading', ElLoading)
provide('$message', ElMessage)
provide('$messagebox', ElMessageBox)
provide('$notification', ElNotification)
</script>
<template>
<el-config-provider :locale="locale">
<router-view></router-view>
</el-config-provider>
</template>
変更element-plus.ts
:
import {
App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'
// const options = {
// size: 'small',
// zIndex: 3000
// }
// const components = [
// ElLoading,
// ElMessage,
// ElMessageBox,
// ElNotification
// ]
export default function install (app: App): void {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// components.forEach((component) => {
// app.use(component, options)
// })
}
追加src/types/global.d.ts
:
import {
ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
export interface ComponentCustomProperties {
$message: typeof ElMessage
$msgBox: typeof ElMessageBox
$loading: typeof ElLoading
$notification: typeof ElNotification
}
テスト:
<template>
<p><el-button @click="onTest">测试</el-button></p>
</template>
<script setup lang="ts">
const $loading = inject('$loading') as any
const onTest = () => {
$loading.service({
lock: true,
text: 'Loading',
})
}
</script>
いくつかの一般的な TS 記述方法を確認する
開発サーバーとパッケージャーは、ts
型チェックを行わずにファイルの構文エスケープのみを実行するため、使用時にvite
開発サーバーts
が常に高速であることが保証されます。
一般的な例をいくつか示します。
<!-- 对props的类型声明和默认值 -->
<script setup lang="ts">
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
</script>
<!-- 另一种方式 -->
<script setup lang="ts">
interface Book {
title?: string
author: string
}
const props = defineProps({
book: Object as PropType<Book>
})
</script>
<!-- 对emits进行声明 -->
<script setup lang="ts">
// 1.运行时
const emit = defineEmits(['change', 'update'])
// 2.基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
<!-- 为computed指定返回类型 --->
<script setup lang="ts">
const double = computed<number>(() => {
/** return number */ })
</script>
<!-- 为函数参数标注类型 -->
<script setup lang="ts">
const onClick = (e: Event) => {
console.log((event.target as HTMLInputElement).value)
}
</script>
<!-- project和inject 然而我很少用到 -->
<script setup lang="ts">
import {
provide, inject } from 'vue'
import type {
InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo')
const foo = inject<string>('foo', 'bar')
</script>
<!-- 为模板引用标注类型 -->
<script setup lang="ts">
import {
ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
el.value?.focus()
})
</script>
<template>
<input ref="el" />
</template>
<!-- 为组件模板引用标注类型 -->
<script setup lang="ts">
import MyModal from './MyModal.vue'
const modal = ref<InstanceType<typeof MyModal> | null>(null)
const openModal = () => {
modal.value?.open()
}
</script>
静的ページにログインする
コンポーネントはログイン ページで使用されますel-input
。一般に、この種のフォーム コンポーネントの場合、ポスト管理の頻度は依然として非常に高いため、再パッケージ化して再利用する傾向があります。通常、その前後のスペースは最適化され、分解されますtextarea
。
<script lang='ts' setup>
import {
computed } from 'vue';
const emits = defineEmits<{
(e: 'update:value', value: string): void;
(e: 'blur'): void;
(e: 'focus'): void;
(e: 'change', value: string): void;
(e: 'clear'): void;
}>()
const props = defineProps({
type: {
type: String,
default: 'string'
},
value: {
type: [String, Number],
default: '',
required: true
},
maxlength: [String, Number],
minlength: [String, Number],
placeholder: {
type: String,
default: '请输入',
},
clearable: {
type: Boolean,
default: true
},
showPassword: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
prefixIcon: {
type: String,
default: ''
},
suffixIcon: {
type: String,
default: ''
},
inputStyle: [String, Object],
showWordLimit: {
type: Boolean,
default: true
},
rows: Number,
autosize: [Boolean, Object]
})
const input = computed({
get(){
console.log(props.value)
return props.value;
},
set(val: any){
if(typeof val === 'string') {
val = val ? val.trim() : val;
}
emits('update:value', val);
}
})
const onFocus = () => {
emits('focus');
}
const onBlur = () => {
emits('blur');
}
const onClear = () => {
emits('clear');
}
const onChange = (val: string) => {
emits('change', val);
}
</script>
<template>
<el-input
v-if="type === 'textarea'"
v-model="input"
:rows="rows"
type="textarea"
:placeholder="placeholder"
:maxlength="maxlength"
:minlength="minlength"
:show-word-limit="showWordLimit"
:disabled="disabled"
:prefixIcon="prefixIcon"
:suffixIcon="suffixIcon"
:autosize="autosize"
:inputStyle="inputStyle"
@focus="onFocus"
@blur="onBlur"
@change="onChange"
/>
<el-input
v-else
v-model="input"
:type="type"
:placeholder="placeholder"
:maxlength="maxlength"
:minlength="minlength"
:clearable="clearable"
:showPassword="showPassword"
:disabled="disabled"
:prefixIcon="prefixIcon"
:suffixIcon="suffixIcon"
:inputStyle="inputStyle"
@focus="onFocus"
@blur="onBlur"
@clear="onClear"
@change="onChange" />
</template>
<style scoped lang='scss'>
</style>
以下の下にplugins/components.ts
グローバルに登録します。
import type {
Component } from 'vue'
import ArInput from '@/components/form/input/index.vue';
const componentObj: {
[propName: string]: Component} = {
ArInput
};
export default function install(app: any) {
Object.keys(componentObj).forEach((key) => {
app.component(key, componentObj[key])
})
}
忘れずplugins/index.ts
に追加してください:
import installComponents from './components'
export default (app: any) => {
// ...
// 注册自定义组件
installComponents(app);
}
login.vue
ソースコード:
<script lang="ts" setup>
import type {
FormInstance, FormRules } from 'element-plus'
import {
ref, reactive } from 'vue'
const formRef = ref<FormInstance>();
const form = reactive({
name: '',
password: ''
})
const rules = ref<FormRules>({
name: [
{
required: true, message: '请输入账号', trigger: 'blur' },
{
min: 8, max: 12, message: '账号长度为8-12', trigger: 'blur' },
],
password: [
{
required: true, message: '请输入密码', trigger: 'blur' },
]
})
const onSubmit = async() => {
await formRef.value.validate((valid, fields) => {
if (valid) {
console.log('submit!')
} else {
console.log('error submit!', fields)
}
})
}
</script>
<template>
<div class="login">
<div class="login-inner">
<el-form ref="formRef" :model="form" :rules="rules">
<el-form-item label="账号" prop="name" required>
<ArInput v-model:value="form.name" prefix-icon="User" />
</el-form-item>
<el-form-item label="密码" prop="password" required>
<ArInput v-model:value="form.password" type="password" prefix-icon="Lock" />
</el-form-item>
<el-button type="primary" class="login-btn" @click="onSubmit">登录</el-button>
</el-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.login {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
background-color: #ccc;
&-inner{
margin-top: 20%;
width: 300px;
padding: 32px;
background-color: #fff;
border-radius: 4px;
}
&-btn {
width: 100%;
}
}
</style>
最終ページの効果は次のとおりです。
環境変数
ページ送信アクションを完了する前に、まず環境変数の問題を解決します。テスト サービス、プレリリースまたは実稼働環境では常に異なる変数がいくつかあるため、環境を区別する必要があります。
新しいフォルダーを作成しenv
、その中に 3 つまたは 4 つのファイルを追加します。
.env # 所有情况下都会加载
.env.development # 开发环境
.env.release # 预发布环境
.env.production # 正服环境
例えば:
# .env.development
VITE_ENV = devalopment
# 请求接口
VITE_API_URL = https://api.vvhan.com/testapi/saorao
このようにして、変数をさまざまな環境に設定でき、環境を区別するためにスクリプト コマンドを変更します。
"scripts": {
"watch": "vite",
"watch:release": "vite --mode release",
"watch:production": "vite --mode production",
"build:development": "vue-tsc && vite build --mode development",
"build:release": "vue-tsc && vite build --mode release",
"build:production": "vue-tsc && vite build --mode production",
// ...
},
ルート ディレクトリに新しいファイルを作成しますbuild/utils.ts
。
// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable) {
const result: any = {
}
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, '\n')
realName =
realName === 'true' ? true : realName === 'false' ? false : realName
result[envName] = realName
if (typeof realName === 'string') {
process.env[envName] = realName
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName)
}
}
return result
}
次に、vite.config.js
定義した変数を変更して挿入します。
import {
defineConfig, loadEnv, UserConfig, ConfigEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import {
wrapperEnv } from './build/utils'
const resolve = (dist) => path.resolve(__dirname, dist)
// https://vitejs.dev/config/
export default ({
command, mode }: ConfigEnv): UserConfig => {
const env = loadEnv(mode, './env')
wrapperEnv(env)
return {
plugins: [
vue(),
AutoImport({
imports: [
'vue',
'vue-router',
'pinia'
],
eslintrc: {
enabled: false,
filepath: './.eslintrc-auto-import.json',
globalsPropValue: true
},
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
resolve: {
alias: {
'@': resolve('./src')
},
extensions: [".js", ".ts", ".tsx", ".jsx"]
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
}
}
}
}
}
このようにして、axios
以下のパッケージで使用できます。そうそう、TS エラーが少しあるかもしれません。変更してくださいtsconfig.node.json
:
"include": ["vite.config.ts", "build**/*.ts"]
アクシオスパッケージ
新しいsrc/utils/request.ts
:
import axios, {
AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import {
ElMessage } from 'element-plus'
const request: AxiosInstance | any = axios.create({
timeout: 100000,
headers: {
post: {
'Content-Type': 'application/x-www-form-urlencoded'
}
},
withCredentials: true
});
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
let token = localStorage.getItem("token");
if (token && token !== '') {
config.headers['Authorization'] = token;
}
// 获取环境变量!!!
const projectUrlPrefix = import.meta.env.VITE_API_URL;
// 这样更支持多域名接口的情况
if (config && config.url && !/^(http(|s):\/\/)|^\/\//.test(config.url)) {
config.url = projectUrlPrefix + config.url;
}
return config;
});
request.interceptors.response.use((res: any) => {
if (res.data.status && res.data.status !== 200) {
ElMessage.error(res.data.msg || '请求失败,请稍后重试')
return Promise.reject(res.data)
}
// 如果这里是登录信息过期,那么应该给个弹窗提示什么的,最后都应该重定向到登录页面
return res.data
}, (error: any) => {
console.log(`%c 接口异常 `, 'background-color:orange;color: #FFF;border-radius: 4px;', error);
})
export default request;
export const $get = (url: string, params = {
}) => {
return request.get(url, {
params
})
}
export const $post = (url: string, params = {
}) => {
return request.post(url, params)
}
ログイン ページに移動してテストを実行します。
<script lang="ts" setup>
const $post = inject('$post') as any
// ...
const onSubmit = async () => {
// ...
const res = await $post('/login', form)
// ...
}
</script >
異なる環境でドメイン名のプレフィックスをテストします。異なる場合、構成は成功しています~
https://api.vvhan.com/testapi/saorao/login
https://api.vvhan.com/releaseapi/saorao/login
https://api.vvhan.com/api/saorao/login
全体的な配置
- ルーティングモジュール内のパスを左側に表示されるメニューに変換します。
- 上の部分はユーザー情報であり、終了できます。
- ルートを切り替えると、ブラウザに似たタブが表示されます。タブをクリックして切り替えるか、現在のページを閉じることができます。
- 最後に、ページのメインコンテンツが表示されます。
まずメニュー コンポーネントを見てください。
<script lang='ts' setup>
import {
ref, computed, watch } from 'vue';
import {
useRoute, useRouter } from 'vue-router'
import {
storeToRefs } from 'pinia'
import routes from '@/router/index';
import {
IRouterItem } from '@/types/menu'
import {
useTagViewsStore } from '@/store/tagViews';
const route = useRoute();
const router = useRouter();
const isCollapse = ref(false);
const defaultActive = ref('0')
const tagViewsStore = useTagViewsStore();
const {
visitedViews } = storeToRefs(tagViewsStore);
// 1. 从声明的路由中获取当前显示的菜单栏(还可以过滤一些不是菜单栏的页面)
const currentMenu = computed(() => {
if(routes && routes.length) {
const routesArr: any = routes.filter((route) => route.name === 'Layout');
if(routesArr && routesArr[0] && routesArr[0].children){
const res = routesArr[0].children as IRouterItem[];
return res;
}else {
return [];
}
}else {
return []
}
});
// 获取路由对应的菜单下标(如果一打开是客户列表页,则高亮客户列表)
const currentMenuToObj = computed(() => {
const routes = currentMenu.value;
if(routes && routes.length) {
let obj: {
[key: string]: any} = {
};
for(let i = 0; i < routes.length; i++) {
const item = routes[i];
if(item.children) {
for(let j = 0; j< item.children.length; j++) {
const subItem = item.children[j];
obj[subItem.path] = {
index: `${
i}-${
j}`,
item: subItem
};
}
}else {
obj[item.path] = {
index: '' + i,
item
};
}
}
return obj;
}else {
return {
};
}
})
// 监听路由获取当前高亮的值,store的使用在后面详细说一下
watch(
() => route.path,
(val: string) => {
if(!visitedViews.value.length) {
const item = {
path: '/',
name: 'Index',
meta: {
title: '主页' },
}
tagViewsStore.addVisitedViews(item)
tagViewsStore.setActivitedView(item)
}
if(val) {
const obj = currentMenuToObj.value[val];
defaultActive.value = obj.index;
tagViewsStore.addVisitedViews(obj.item)
tagViewsStore.setActivitedView(obj.item)
}
}, {
immediate: true
}
)
// 点击菜单栏进行跳转
const onToPage = (item: IRouterItem) => {
if(route.path === item.path) return;
tagViewsStore.addVisitedViews(item)
tagViewsStore.setActivitedView(item)
router.push(item.path)
}
</script>
<template>
<div class="menu">
<div class="menu-logo">Logo</div>
<div class="menu-main">
<el-menu
:default-active="defaultActive"
:collapse="isCollapse"
background-color="#191919"
text-color="#7e7e7e"
active-text-color="#ffffff"
>
<template v-for="(item, index) in currentMenu" :key="index">
<el-menu-item :index="'' + index" v-if="!item.children || !item.children.length" @click="onToPage(item)">
<template #title>{
{
item.meta.title }}</template>
</el-menu-item>
<el-sub-menu :index="'' + index" v-else>
<template #title>
<span>{
{
item.meta.title }}</span>
</template>
<el-menu-item v-for="(subItem, subIndex) in item.children" :key="`${index}-${subIndex}`" :index="`${index}-${subIndex}`" @click="onToPage(subItem)">{
{
subItem.meta.title }}</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</div>
</div>
</template>
<style scoped lang='scss'>
.menu {
height: 100vh;
max-width: 280px;
background-color: #191919;
&-logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
&-main {
height: calc(100vh - 100px);
}
}
</style>
<style lang="scss">
.menu .el-menu {
border: none !important;
}
</style>
次に上部のバーの情報です。
<script lang='ts' setup>
</script>
<template>
<div class="nav">
<div class="nav-left"></div>
<div class="nav-right">
<div class="nav-right-item">
<el-dropdown>
<span class="el-dropdown-link">
您好,XXX
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</template>
<style scoped lang='scss'>
.nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
height: 60px;
border-bottom: solid 1px var(--el-menu-border-color);
background-color: #fff;
&-right{
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
&-item {
cursor: pointer;
}
}
}
</style>
次にナビゲーションバーの開発ですがtab
、ここでは状態管理にpiniaを使用します。インストール後、まず以下plugins/index.ts
に登録します。
import {
createPinia } from 'pinia';
// ...
// 注册store (建议这个放在所有注册的首位,方便其他插件可能会用到它)
app.use(createPinia());
ナビゲーションに関するステートメントtab
、store
次のとおりですsrc/store/tagViews.ts
。
import {
defineStore } from 'pinia';
import {
IRouterItem } from '@/types/menu'
export const useTagViewsStore = defineStore('tagViews', {
state: () => {
return {
// 访问过的页面
visitedViews: [] as IRouterItem[],
// 当前访问的页面
activitedView: {
} as IRouterItem
}
},
actions: {
// 新增页面
addVisitedViews(view: IRouterItem){
const item = this.visitedViews.find((item) => item.path === view.path)
if(item) return;
this.visitedViews.push(view);
},
// 删除页面
deleteVisitedViews(index: number) {
this.visitedViews.splice(index, 1);
},
// 高亮某个页面
setActivitedView(view: IRouterItem) {
this.activitedView = view
}
}
})
tagViews
コンポーネントのソースコードは次のとおりです。
<script lang='ts' setup>
import {
storeToRefs } from 'pinia';
import {
useRouter } from 'vue-router'
import {
useTagViewsStore } from '@/store/tagViews';
import {
IRouterItem } from '@/types/menu'
const router = useRouter();
const tagViewsStore = useTagViewsStore();
const {
visitedViews, activitedView } = storeToRefs(tagViewsStore);
// 关闭页面
const onDel = (item: IRouterItem) => {
const index = visitedViews.value.findIndex((view) => view.path === item.path);
if(index === -1) return;
tagViewsStore.deleteVisitedViews(index);
if(item.path === activitedView.value.path) {
const obj = visitedViews.value[index - 1]
tagViewsStore.setActivitedView(obj);
router.push(obj.path)
}
}
// 切换页面
const onChange = (item: IRouterItem) => {
tagViewsStore.setActivitedView(item)
router.push(item.path)
}
</script>
<template>
<el-scrollbar class="tags-scrollbar">
<div class="tags">
<div v-for="item in visitedViews" :key="item.path" :class="['tags-item', { active: activitedView.path === item.path }]" @click="onChange(item)" >
<span class="tags-item-title">{
{ item.meta ? item.meta.title : '' }}</span>
<el-icon v-if="item.path !== '/'" @click.stop="onDel(item)"><Close /></el-icon>
</div>
</div>
</el-scrollbar>
</template>
<style scoped lang='scss'>
.tags-scrollbar {
height: 30px;
overflow: hidden;
}
.tags {
display: flex;
background: #f3f3f3;
border: 1px solid #f2f2f2;
border-right: none;
margin: -1px 0 0 -1px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&-item {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&-title {
margin-right: 4px;
}
&:first-of-type {
margin-left: 5px;
}
&:last-of-type {
margin-right: 5px;
}
&.active {
background-color: #e25050;
color: #fff;
border-color: #e25050;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
</style>
pinia
の使用率はvuex
非常に単純ですが、最大の違いは * mutations
* が存在しなくなったことです。
ルートインターセプト
通常、ログイン後、pinia
Web サイト上のユーザーに関する情報が更新され、一部の情報は暗号化されて Web サイトに保存されますlocalstorage
。ログインしていないユーザーの場合は、システムへのアクセスを傍受し、ログイン ページにリダイレクトする必要があります。(実際のプロジェクトではページ権限の傍受も考慮する必要があります)
// router/hooks/index.ts
import type {
Router } from 'vue-router'
import {
USERINFO } from '@/constants/localstorage'
const routerHook = (router: Router) => {
router.beforeEach(to => {
if(to.path === '/login') {
// 可以做一些清空登录信息的操作, 比如跟pinia相关的等操作
localStorage.removeItem(USERINFO);
return true;
}else{
// 在这里可以判断用户是否登录,跳转的某个页面是否有权限,这里只是粗略写一下
const info = localStorage.getItem(USERINFO);
if(!info) {
return {
name: 'Login' }
}
}
})
}
export default routerHook;
import {
createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import routerHook from './hooks/index'
// 注册路由
const router = createRouter({
history: createWebHashHistory(),
routes
})
routerHook(router)
export default router
ログイン ページを変更してシミュレートします。
const onSubmit = async () => {
await formRef.value.validate(async (valid: boolean, fields: {
[key: string]: any}) => {
if (valid) {
// 一般这种情况下,localStorage中存储的信息不能太重要,且需要加密,还应该更新pinia中的用户信息
const info = {
name: 'Armouy'
}
localStorage.setItem(USERINFO, JSON.stringify(info))
router.push('/')
} else {
console.log('error submit!', fields)
}
})
}
ログインしていない場合は、ログインページにリダイレクトされます。
パック
npm run build:production
npm run preview
参考リンク
間違いがあればご指摘ください、読んでいただきありがとうございます~