本系列文章是爲學習Vue的項目練習筆記,儘量詳細記錄一下一個完整項目的開發過程。面向初學者,本人也是初學者,搬磚技術還不成熟。項目在技術上前端爲主,包含一些後端代碼,從基礎的數據庫(Sqlite)、到後端服務Node.js(Express),再到Web端的Vue,包含服務端、管理後臺、商城網站、小程序/App,分爲下面多個篇文檔。
🪧系列目錄:
- 圖書商城Vue+Element+Node+TS項目練習🔗
- 圖書商城①管理後臺Vue2+ElementUI🔗
- 圖書商城②後端服務Node+Express+Sqlite🔗
- 未完成:商城網站Vue3+TS、商城APP端Vue3+TS+uniapp
00、管理後臺Vue2+ElementUI
這是一個比較典型的管理後臺練習項目,包含登錄、框架頁、導航路由、導航標籤、數據管理、字典管理等基礎功能。管理後臺的業務大多是數據管理CRUD功能,該項目只是是簡單實現了幾個模塊。同時針對CRUD,整理了一個模板📁template。
🔸技術路線:
- Vue v2.*
- ElementUI v2.*
🔸相關組件:
vuex
:狀態管理vue-router
:前端路由axios
:HTTP調用echarts
:圖表組件,按需定製i18n
:多語言國際化vue-i18n
v8.*版本@wangeditor
:富文本編輯器Less
:CSS預處理器/語言
🔸源代碼地址:Github / KWebNote,Gitee / KWebNote,管理後臺代碼在目錄📁book_admin下。
🔸在線體驗地址🔥🔥:http://kanding.gitee.io/kwebnote (任意用戶名、密碼。通過gitee靜態頁面Gitee Pages部署的,所以這裏部署的版本是寫了個mock模擬api,路由用的hash模式)。
01、創建項目/準備
創建圖書管理後臺項目“book_admin
”,基於@vue/cli
,通過其vue ui
管理工具,可視化操作創建項目。
- 選擇項目目錄,開始創建項目。
- 可以選擇內置的多種預設模式,也可選擇“手動”模式,按需設置項目的詳細規則。
- 在手動模式下,選擇需要的組件,如Vuex、Vue-Router、Less等。
- 更多
@vue/cli
參考《Vue項目工程@vue/cli入門》
vue創建的項目已經包含了一個基礎的架子了,如下圖,主html頁面文件“index.html
”,入口JS文件“main.js
”,入口Vue文件“App.vue
”。
02、主頁面/框架頁面
管理後端是SPA單頁應用,創建主框架頁面“Main.vue”,登錄後的所有內容都在這個主頁面內呈現和管理。頁面視圖關係如下圖:
2.1、Main.vue
因此,主頁面就比較重要,是搬磚的基座,實際效果和佈局結構圖如下:
- Header:頭部區域,包含Logo、標籤欄(存放打開的頁面,類似瀏覽器的多頁籤)、系統按鈕(最右側區域)。
- 路由菜單:系統導航菜單,數據來自路由配置信息。
- 麪包屑:內容區域當前視圖的路由信息。
- 內容區域:當前視圖內容呈現區域。
- 緩存
<keep-alive>
,配合多標籤組件視圖緩存,切換標籤後視圖的狀態會被保持。 - 切換動畫
<transition>
,切換內容時的動畫效果。 - 內容滾動處理:內容區域的高度、寬度自適應鋪滿,如果內容超出該區域,則顯示滾動條,不會導致整個頁面出現滾動條。
- 緩存
- Footer:底部區域,好像也沒啥用,先放這吧!
<template>
<el-container style="height:100%">
<el-container class="main-aside">
<!-- 左側 :logo+導航菜單 -->
<el-aside :width="config.menuCollapse?'auto':'200px'">
<MenuSidebar />
</el-aside>
<!-- 右側 :頭部+主內容-->
<el-container>
<!-- 頭部 -->
<el-header :style="config.thema" class="header">
<!-- 標籤工具欄 -->
<div style="flex:1;overflow:hidden">
<TabsBar ref="tabsBar"></TabsBar>
</div>
<!-- 右側的系統操作按鈕 -->
<i class="el-icon-setting h-button" v-on:click="$refs.userConfig.show()" title="系統設置"></i>
<el-dropdown class="header-userbox" @command="handleCommand">
<span>
<img :src="$api.URL.proxy+'/file/f1.jpg'" alt="頭像" />
[ {{$store.state.user.name}} ]
<i class="el-icon-arrow-down el-icon--right" style="font-size:12px"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="pwd">修改密碼</el-dropdown-item>
<el-dropdown-item command="about">
<i class="el-icon-info"></i>關於
</el-dropdown-item>
<el-dropdown-item command="user">
<i class="el-icon-user-solid"></i>個人中心
</el-dropdown-item>
<el-divider></el-divider>
<el-dropdown-item command="logout" icon="el-icon-circle-close">退出登錄</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-header>
<!-- 主內容區域 -->
<el-main class="main-wrapper">
<!-- 麪包屑 -->
<div class="breadcrumb-bar">
<el-button type="text"
:icon="config.menuCollapse?'el-icon-s-unfold':'el-icon-s-fold'"
v-on:click="config.menuCollapse=!config.menuCollapse" ></el-button>
<el-breadcrumb separator="/" style="display:inlne-block">
<el-breadcrumb-item v-for="r in $route.matched" :key="r.name">{{r.meta?.lang ? $t('menu.' + r.meta.lang) : r.meta?.title}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 頁面內容的容器 -->
<div class="main view-scroll">
<!-- 加了切換動畫、保存頁面狀態 -->
<transition :name="config.routerAnimation?'fade-transform':''" mode="out-in">
<keep-alive :include="cacheNames">
<router-view></router-view>
</keep-alive>
</transition>
</div>
</el-main>
</el-container>
</el-container>
<!-- footer -->
<el-footer height="30px">{{$consts.footer}}</el-footer>
<UserConfig ref="userConfig"></UserConfig>
</el-container>
</template>
2.2、導航菜單&路由
每個後臺系統都會有導航菜單,支持多級展示、可收縮,效果如下圖:
👷♂️實現過程:
1、路由的配置:這裏用的是本地路由(vue-router中的路由配置信息),實際項目中路由可後臺管理,或者本地+後臺結合。本地路由配置“routes.js
”詳見Github / KWebNote。
import constants from '@/assets/constants'
import Vue from 'vue'
import VueRouter from 'vue-router'
//路由配置
import baseRoutes from './routes'
//註冊路由插件
Vue.use(VueRouter)
// 創建路由
const router = new VueRouter({
mode: 'history', //模式
base: process.env.BASE_URL,
routes: baseRoutes, //路由配置
})
// 路由全局守衛-導航前,登錄token判斷
router.beforeEach((to, from, next) => {
if (to.path === '/login')
return next();
// 除開登錄頁面,其他頁面都驗證token,如果沒有token則跳轉到登錄頁面
const token = sessionStorage.getItem('admin_token');
if (!token)
return next('/login');
else
next();
})
router.afterEach((to, from) => {
//更新網頁標題
document.title = constants.sysName + '-' + to.meta.title;
})
📢404頁面的配置:路由的匹配是從上而下的,404頁面路由放到最後即可,然後路徑使用通配符匹配所有地址,
{ path: '*', component: 404 }
2、導航菜單組件:創建“MenuSidebar.vue
”,用<el-menu>
組件顯示多級菜單,啓用路由導航router
,菜單的數據就是來自前面的路由。
3、樹形菜單:導航菜單項-遞歸,菜單項用一個“MenuItem.vue
”組件來實現遞歸路由樹,如果路由還有子節點children
,則遞歸調用組件自身。
<template>
<el-menu-item v-if="!hasChildren" :index="item.path">
<i :class="item.meta.icon"></i>
<!-- 名稱用title插槽 -->
<span slot="title">{{title(item)}}</span>
</el-menu-item>
<el-submenu v-else :index="item.path">
<template slot="title">
<i :class="item.meta.icon"></i>
<span slot="title">{{title(item)}}</span>
</template>
<MenuItem v-for="child in children" :item="child" :key="child.path"></MenuItem>
</el-submenu>
</template>
<script>
export default {
name: 'MenuItem',
props: ['item'],
computed: {
children() {
return this.item?.children?.filter(s => !s.hidden);
},
hasChildren() {
return this.item?.children?.length > 0;
},
},
methods: {
title(item) {
return item.meta?.lang ? this.$t('menu.' + item.meta.lang) : item.meta?.title;
}
}
}
</script>
2.3、配置路由動畫
就是在路由切換頁面視圖的時候,有一個過渡動畫效果,如下圖。
主要是使用Vue的<transition>
組件來實現過渡動畫,設置其name和對應CSS動畫即可,可參考另外一篇《Vue2快速上門(2)-模板語法 / Vue動畫》。
<transition :name="config.routerAnimation?'fade-transform':''" mode="out-in">
<keep-alive :include="cacheNames">
<router-view></router-view>
</keep-alive>
</transition>
<style>
// 路由切換動畫
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(-30px);
}
</style>
📢注意動畫模式
mode="out-in"
,避免佈局變更引起的顯示異常。name
值是用於動畫CSS樣式的類名,這裏的的name
用了一個用戶配置屬性,目的是可以配置是否開啓路由轉場動畫。
2.4、多標籤工具欄
如下效果圖,類似Chrome瀏覽器的多標籤效果,打開的路由視圖標題顯示在標籤欄,可以刷新、關閉、切換。切換時會保留視圖狀態,這樣就可以很方便的多標籤操作了。
🔵方案設計:
- 監測路由變化,記錄打開的頁面路由,保存在vuex的store中。同時記錄打開的頁面名稱,用於
<keep-alive>
的include
實現定向路由緩存,過濾不需要緩存的組件,也是實現視圖刷新的關鍵。 - 顯示緩存的路由列表,就是看到的標籤欄。
- 關閉按鈕,激活的路由視圖顯示關閉按鈕,關閉後自動路由到下一個頁面/標籤。
- 固定的標籤:對於如“主頁”的路由固定在標籤欄(會在路由信息中配置),在初始化的時候就顯示在標籤欄,固定的標籤不支持關閉。
- 右鍵菜單功能:刷新、關閉、關閉其他、關閉所有,更新緩存的路由列表。
👷♂️實現過程:
1、在vuex中添加一個子模塊“tabBars.js
”,單獨管理標籤的狀態信息。提供緩存路由列表的操作方法:添加、刪除、刪除其他、刪除所有、清空。
export default {
namespaced: true,
state: {
cacheRoutes: [], //緩存的路由,用於標籤欄使用
cacheNames: [], //緩存的打開的路由Name,用於Keep-alive的緩存白名單
},
mutations: {
add(state, obj) {
if (!state.cacheRoutes.some(s => s.path === obj.path)) {
//添加打開的路由,只需要path、name、mata
state.cacheRoutes.push({ path: obj.path, name: obj.name, meta: obj.meta });
state.cacheNames = state.cacheRoutes.map(s => s.name);
}
},
remove(state, obj) {
const i = state.cacheRoutes.findIndex(s => s.path === obj.path);
if (i < 0)
return;
state.cacheRoutes.splice(i, 1);
state.cacheNames = state.cacheRoutes.map(s => s.name);
},
removeName(state, obj) {
//只移除緩存名字
const i = state.cacheNames.findIndex(s => s === obj.name);
if (i < 0)
return;
state.cacheNames.splice(i, 1);
},
},
}
2、路由信息配置,來自vue-router的路由配置,添加了幾個自定義的屬性。
- 注意
name
和組件內部的name
定義應該一致,會在<keep-alive :include="cacheNames">
中使用。 meta.title
:標題meta.icon
:icon圖標meta.affix
:是否固定,固定在標籤欄
{
path: '/home',
name: 'Home',
meta: { title: '首頁', lang: 'home', icon: 'el-icon-s-home', affix: true },
component: () => import('@/views/Home.vue'),
},
{
path: '/books',
name: 'Books',
meta: { title: '圖書管理', lang: 'book', icon: 'el-icon-notebook-2' },
component: () => import('@/views/book/Books.vue'),
},
3、創建標籤欄組件TabsBar.vue
,核心功能、代碼都在這裏,標籤的顯示、功能操作,包括右鍵菜單。完整代碼:Github / KWebNote
<template>
<div class="tabs-bar">
<router-link
class="item"
v-for="r in cachedRoutes"
:to="r"
:key="r.path"
:class="isActive(r)?'active':''"
@contextmenu.prevent.native="showMenu(r,$event)"
>
<i :class="r.meta.icon"></i>
{{r.meta?.lang ? $t('menu.' + r.meta.lang) : r.meta?.title}}
<i class="el-icon-close close" v-if="!isAffix(r)" @click.prevent.stop="handleClose(r)"></i>
</router-link>
<!-- 頁籤按鈕的右鍵菜單 -->
<el-card class="menu" v-show="tagMenu.visible" :style="{left:tagMenu.left+'px',top:tagMenu.top+'px'}">
<ul>
<li @click="refresh(selectedTag)" v-show="isActive(selectedTag)">
<i class="el-icon-refresh"></i> 刷新
</li>
<li @click="handleClose()" v-show="!isAffix(selectedTag)">
<i class="el-icon-close"></i> 關閉
</li>
<li @click="handleCloseOther()">
<i class="el-icon-circle-close"></i> 關閉其他
</li>
<li @click="handleCloseAll">
<i class="el-icon-error"></i> 關閉所有
</li>
</ul>
</el-card>
</div>
</template>
❗注意:這裏的標籤關閉按鈕,一定要加上修飾符“.prevent.stop
”,阻止冒泡、及其他事件,因爲標籤本身也是有點擊事件的,開始沒加,莫名其妙沒有跳轉,被卡了好半天。
<i class="el-icon-close close" v-if="!isAffix(r)" @click.prevent.stop="handleClose(r)"></i>
複習一下:
修飾符 | 描述 |
---|---|
.stop | 調用 event.stopPropagation() ,停止向上冒泡(propagation /ˌprɒpəˈɡeɪʃn/ 傳播) |
.prevent | 調用 event.preventDefault() ,取消默認事件行爲,如checkbox、<a> 的默認事件行爲,不影響冒泡 |
🔃刷新怎麼實現?
刷新的實現稍微複雜一點點,因爲這是本地路由,不能刷新整個頁面,而當前路由視圖是用了<keep-alive>
緩存的。因此實現刷新的的基本過程:
- 去除緩存並關閉路由:去除
<keep-alive>
的緩存,就是從其include
白名單中移除。 - 重新打開路由視圖。
爲了視覺效果更佳,這裏用一箇中間頁面進行跳轉,效果如下:
設計了一箇中級頁面Redirect.vue
,作用只有一個,就是用於跳轉,跳轉目標用路由參數傳遞。
// 用於中轉跳轉的頁面
<script>
export default {
created() {
this.$router.replace({ path: '/' + this.$route.params.path, query: this.$route.query });
},
render: function (h) {
return h()
}
}
</script>
需要注意中間頁面,不緩存、不顯示標籤欄。刷新時,移除緩存,然後重定向到當前頁面,重新加載當前頁面。
refresh(tag) {
//移除去掉緩存,再重定向跳轉到當前頁面
this.$store.commit('tabBars/removeName', this.$route);
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + tag.path
})
})
},
2.5、用戶配置本地化保存
系統菜單“用戶設置”,實現用戶自定義的一些個性化配置,並本地存儲、加載。保存到localStorage
中,這樣下次進入系統可以保持個性化配置了。
🔵效果如上圖,需求分析:
- 主題樣式,只實現了標題欄顏色樣式,前景色
color
、背景色backgroundColor
,應用在標題欄Header
的樣式上。 - 路由切換動畫是否開啓。
- 多語言設置,實現了中文、英文語言,詳見下一章節。
- 導航菜單的摺疊狀態。
- 用戶配置保存到本地
localStorage
中,系統初始化時從localStorage
加載用戶配置。
👷♂️實現過程:
1、創建一個單獨的vue組件“UserConfig.vue
”管理用戶的設置項,內部用抽屜<el-drawer>
來實現從右側滑出的效果。
2、監聽數據的變化(深度監聽),如果變化則保存用戶配置數據到localStorage
,同時更新多語言的配置項。
created() {
//監聽配置變更,持久化存儲到本地
this.$watch('config', () => {
localStorage.setItem('admin-userconfig', JSON.stringify(this.config));
//手動更新語言
this.$i18n.locale = this.config.language;
}, { deep: true })
},
3、在main.js
中添加初始化代碼,從localStorage
加載上次保存的用戶配置信息。
created: function () {
LoadUserConfig();
}
function LoadUserConfig() {
let vstr = localStorage.getItem('admin-userconfig');
if (vstr) {
Object.assign(userConfig, JSON.parse(vstr));
userConfig.thema = themas.filter(s => s.name == userConfig.thema.name)[0];
//語言
i18n.locale = userConfig.language;
}
2.6、國際化多語言
實現多語言(國際化)的最主流、成熟的方案就是i18n
(internationalization /ˌɪntəˌnæʃnəlaɪˈzeɪʃn/ 國際化,首字母i
、尾字母n
加中間的18個字母),官方文檔,Vue中使用vue-i18n
插件。
安裝i18n
插件,Vue2.*不太兼容最新版的v9.*
,安裝8.*
版本:
vue add i18n
# 或者
cnpm i -S [email protected]
安裝完成後,“package.json”文件中就有了"vue-i18n": "^8.26.3"
。
vue add i18n
方式安裝,除了安裝插件,還把基本的配置、語言文件都準備好了,屬於完成了簡裝可以擰包入住了。npm指令安裝只會安裝插件,需要自己完成配置和註冊。
🔸配置i18n
i18n
的配置、使用還是比較簡單的,先配置語言信息,然後在代碼(JavaScript、Vue模板)中使用。
|- src
|-lang
|-index.js # 配置i18n
|-lang-en.js # 英文語言資源
|-lang-cn.js # 中文語言資源
1、分別創建不同的語言包文件,語言信息爲一個鍵值結構的JSON對象,鍵爲語言項的key,值爲顯示的文本內容。結構可以按照項目情況自行定義,葉子節點屬性是一個語言項。
2、註冊插件,配置i18n
實例。
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import lang_zhcn from './lang-cn'
import lang_en from './lang-en'
//註冊
Vue.use(VueI18n);
//申明i18n
const i18n = new VueI18n({
locale: 'en', //選中的語言
messages: {
en: {
...lang_en, //英文語言配置
},
zh: {
...lang_zhcn, //中文語言配置
}
}
})
locale
屬性爲當前選中的語言,更改值實現語言切換。- Element的國際化,按照官方文檔配置即可:element-國際化
3、在main.js
中引入,並注入到Vue根實例中。
import i18n from './lang'
new Vue({
router,
store,
i18n,
}).$mount('#app')
🔸使用
使用就簡單了,使用i18n
提供的方法$t('key')
即可。
<p class="title">{{$t('home.user')}}</p>
//JS
title(item) {
return item.meta?.lang ? this.$t('menu.' + item.meta.lang) : item.meta?.title;
}
03、登錄頁面/Login.vue
登錄頁面主要就是用戶名、密碼的表單,然後調用後端登錄api
接口驗證、獲得token
完成登錄。
- 表單規則:表單就需要用到輸入驗證規則
rules
,element表單驗證。
{
user: { name: '', pwd: '' },
rules: {
name: [{ required: true, message: '用戶名不能爲空' }, { min: 3, max: 8, message: "長度應爲3-8" }],
pwd: [{ required: true, message: '密碼不能爲空' }, { min: 3, max: 8, message: "長度應爲3-8" }],
},
}
- 表單驗證:表單驗證的執行是在表單
<el-form>
組件上調用validate()
方法,因此需要綁定model
對象,在表單項<el-form-item>``prop
上綁定model
對象字段名。
this.$refs.userForm.validate((valid, mes) => {
if (!valid) {
this.$message.error('輸入有誤,請修改後重新提交!');
return;
}
//提交...
- 記住用戶名:保存到本地
localStorage
中,加載該頁面的時候讀取。 - 登錄成功:獲取並保存
token
信息,保存在vuex
的store
中或本地sessionStorage
,然後跳轉到主頁面this.$router.push('/home')
。
04、首頁/Home.vue
管理類系統大概率都有一個首頁Home.vue
,作爲默認頁面,展示系統的一些概況、用戶的一些統計信息、通知信息等。爲保持各個“豆腐塊”風格一致,推薦用<el-card>
組件包裝內容。
用到了圖表組件 echarts,安裝最新版本:
$ cnpm install echarts -S
// 引入echarts
import * as echarts from 'echarts'
// 在Vue原型上掛載$echarts,在vue示例中this.$echarts
Vue.prototype.$echarts = echarts
這裏只用了2個圖表,卻引入了所有的echarts組件,打包後的JS文件6M多,實在太大了。通過官方提供的在線定製功能按需定製JS文件,然後引入該JS文件即可。
05、圖書管理模塊
圖書管理爲圖書的綜合管理,包含增、刪、改、查,是管理後臺的典型功能,如用戶管理、商品管理、活動管理、公告管理等等都類似。
功能結構如下圖:
代碼結構如下圖,圖書管理模塊包含多個頁面,其中“Books.vue
”爲入口頁面。
5.1、圖書列表
- 圖書列表由表格
<el-table>
和分頁<el-pagination>
組成。統一設計了查詢結構,分頁組件做了一個簡單的封裝。 - 表格中的狀態用到了一個自己寫的枚舉組件,詳見《前端枚舉enum的應用(Element)封裝》。
- 圖書詳情用了抽屜組件
<el-drawer>
,點擊名稱從右側彈出。 - 圖書的新增、修改用的彈框組件
<el-dialog>
,默認的BookDialog
是彈出帶遮罩的模態框,額外實現了一個Plus版本BookDialogPlus
(詳見下文)。
5.2、分頁組件
分頁是列表常用組件,Element-UI提供了分頁組件<el-pagination>
,在此基礎上做一個簡單的封裝,統一規範、簡化使用。
✔️封裝了些什麼?
- 統一風格樣式、佈局,也方便統一修改。
- 統一分頁大小配置
page-sizes="[5, 10, 20, 50]"
- 統一分頁事件
pagination
,統一處理了頁碼、頁數的變更。
<template>
<el-pagination
style="text-align:right;margin:6px 2px" background
:total="total" :current-page="currentPage" :page-size="pageSize" :page-sizes="[5, 10, 20, 50]"
@current-change="pageChanged" @size-change="pageSizeChanged"
layout="total, sizes, prev, pager, next, jumper"
></el-pagination>
</template>
<script>
export default {
props: {
//總數
total: { type: Number, default: 0, },
//頁碼,外部綁定,加修飾符.sync
size: { type: Number, default: 10, },
// 當前頁碼,外部綁定,加修飾符.sync
index: { type: Number, default: 1, }
},
computed: {
// 用修飾符“.sync”來實現更新父組件的值
currentPage: {
get() { return this.index },
set(val) { this.$emit('update:index', val) }
},
pageSize: {
get() { return this.size },
set(val) { this.$emit('update:size', val) }
}
},
methods: {
pageSizeChanged(v) {
// 修改父組件值
this.$emit('update:size', v);
// 觸發分頁事件
this.$emit('pagination');
},
pageChanged(v) {
// 修改父組件值
this.$emit('update:index', v);
// 觸發分頁事件
this.$emit('pagination');
},
}
}
</script>
這裏的頁行數size
、頁碼index
,是外部傳入的的prop值,但內部也會修改。導致了“雙重綁定”更新,這就需要用到.sync
修飾符了,其實就是基於事件通知實現的,可參考官網文檔。
- 子組件內部通過Vue的事件
this.$emit('update:myPropName', v);
觸發變更通知。 - 外部綁定的時候用
.sync
修飾符,.sync
實現了更新update事件的監聽和賦值,需注意不支持表達式,只能用property名。
🟢使用:
<Pagination :total="total" :size.sync="search.size" :index.sync="search.index" @pagination="loadData"></Pagination>
5.3、圖書編輯Plus
如下效果圖,相比常規的模態框,只是遮住了當前視圖(圖書管理),不影響其他功能操作。
用的依然是彈框組件<el-dialog>
,在此基礎上做了一點點調整。完整代碼見 Github / KWebNote。
- 取消遮罩層
:modal="false"
。 - 操作按鈕放到了標題欄上。
- 通過樣式讓彈層剛好覆蓋到當前視圖。
.dialogPlus {
position: absolute;
overflow: inherit;
.el-dialog {
min-height: 100% !important;
max-height: 100%;
display: flex;
flex-flow: column;
.el-dialog__header {
padding: 4px 10px;
}
.el-dialog__body {
overflow: auto;
max-height: 100%;
}
}
}
5.4、圖片上傳upLoad
圖片上傳使用文件上傳組件<el-upload>
,這裏涉及一些基礎通用操作,因此針對圖片上傳封裝爲一個組件“ImgUpload.vue
”,效果如下:
- 上傳接口
action
,後端文件接口實現詳見後端章節。 - 文件格式
accept="image/*"
,這裏的accept
值爲文件的類型。 - 文件數量
limit
,配置最大支持的文件個數,鉤子on-exceed
超過文件數量限制時觸發。當達到限制時,隱藏上傳按鈕。 - 上傳前
before-upload
上傳前的鉤子,可用來驗證文件的合法性。 - 上傳成功
on-success
文件上傳成功的鉤子,可用來同步上傳的文件資源。 - 文件信息
prop."value"
,組件中定義了一個value
的props,接受父組件傳入的已有文件集合(字符串,多個逗號隔開),組件內文件變化通過this.$emit('input',nval)
更新value值。 - 預覽:用一個
<el-dialog>
來展示預覽圖。
<template>
<div>
<el-upload
ref="upload" :action="$api.URL.upload" list-type="picture-card"
:multiple="true" accept="image/*" name="file"
:limit="limit" :class="{hide:uploadHide}" :file-list="fileList"
:on-exceed="onOutOfLimit" :on-success="handleSuccess"
:on-error="handleError" :before-upload="handeleBefore" >
<!-- 上傳按鈕 -->
<i slot="default" class="el-icon-plus"></i>
<!-- 提示內容 -->
<div slot="tip" style="font-size:0.8em">支持最多{{limit}}張圖片,每張圖片不超過{{maxSize}}Kb</div>
<!-- file模板 -->
<div slot="file" slot-scope="{file}" class="imgbox" :class="{success:file.status}">
<!-- 縮略圖的路徑,如果相對路徑則添加代理前綴-->
<img :src="proxyURL(file.url)" alt />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<i class="el-icon-zoom-in"></i>
</span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
<i class="el-icon-delete"></i>
</span>
</span>
</div>
</el-upload>
<!-- 嵌套的dialog,需要設置append-to-body,嵌入自身到body元素 -->
<el-dialog :visible.sync="dialogVisible" append-to-body custom-class="imgdialog">
<img :src="proxyURL(dialogImageUrl)" alt style="max-width: 100%;max-heigt: 100%;object-fit:contain" />
</el-dialog>
</div>
</template>
再加上一點點JS和CSS就完成了,完整代碼見 Github / KWebNote。。使用:
import ImgUpload from '@/components/ImgUpload.vue'
<ImgUpload v-model="book.imgs"></ImgUpload>
5.5、富文本@wangeditor
wangEditor 是一個輕量級 web 富文本編輯器,配置方便,使用簡單。安裝vue版本的“@wangeditor/editor-for-vue”:$ cnpm i @wangeditor/editor-for-vue -S
。然後使用參考官方vue使用文檔,Ctrl+CV即可。
要實現圖片、視頻上傳還需要自己配置,因此就基於@wangeditor
封裝了一個富文本編輯器Editor.vue
,效果如下。
- 調整了工具欄,排除了一些不需要的。
- 配置了圖片上傳服務,
@wangeditor
支持粘貼圖片。
🔸配置工具欄
默認提供的工具欄功能比較豐富,如果需要調整,則需要先獲取工具欄的toolbarKeys
。引入@wangeditor/editor
,在編輯器組件準備完成後獲取toolbarKeys
,如updated()
。
import { DomEditor } from '@wangeditor/editor'
updated() {
////在這裏獲取工具欄的配置toolbarKeys,用於自定義配置工具欄
const toolbar = DomEditor.getToolbar(this.editor)
console.log(toolbar.getConfig().toolbarKeys)
},
獲取到的keys如下(整理後):
[
"headerSelect", "blockquote",
"|",
"bold", "underline", "italic",
{
"key": "group-more-style", "title": "更多", "iconSvg": "",
"menuKeys": [ "through", "code", "sup", "sub", "clearStyle" ]
},
"color", "bgColor",
"|",
"fontSize", "fontFamily", "lineHeight",
"|",
"bulletedList", "numberedList", "todo",
{
"key": "group-justify", "title": "對齊", "iconSvg": "",
"menuKeys": [ "justifyLeft", "justifyRight", "justifyCenter", "justifyJustify" ]
},
{
"key": "group-indent", "title": "縮進", "iconSvg": "",
"menuKeys": [ "indent", "delIndent" ]
},
"|",
"emotion", "insertLink",
{
"key": "group-image", "title": "圖片", "iconSvg": "",
"menuKeys": [ "insertImage", "uploadImage" ]
},
{
"key": "group-video",
"title": "視頻", "iconSvg": "",
"menuKeys": [ "insertVideo", "uploadVideo" ]
},
"insertTable", "codeBlock", "divider",
"|",
"undo", "redo", "|", "fullScreen"
]
通過toolbarConfig.excludeKeys
配置不需要的工具欄按鈕:
data() {
return {
editor: null,
toolbarConfig: { excludeKeys: ['group-video', 'emotion', 'lineHeight'] },
editorConfig: { placeholder: '請輸入內容...', maxLength: 8000 },
mode: 'default', // default simple
}
},
完整代碼見 Github / KWebNote。
🔸配置圖片上傳
在Editor的配置項editorConfig
中配置圖片上傳參數,如下代碼:
editorConfig: {
placeholder: '請輸入內容...', maxLength: 8000,
MENU_CONF: {
uploadImage: { //配置圖片上傳
server: this.$api.URL.upload, //後端文件上傳地址
fieldName: 'file', //表單參數名,和後端一致
maxFileSize: 2 * 2048 * 2048, //最大文件大小
maxNumberOfFiles: 1, //每次文件個數
allowedFileTypes: ['image/*'], //文件類型:圖片
timeout: 9 * 1000, //超時時長
// 自定義插入圖片,根據後端返回的結構,加上跨域代理
customInsert: (res, insertFn) => {
const url = this.$api.URL.proxy + res.url
insertFn(url)
},
}
}
},
5.6、樹形下拉框
圖書的類型是來自字典數據(詳見後續《字典管理》章節),樹形結構,ElementUI2版本中麼有樹形的下拉框組件,Element3(ElementPlus)有。結合下拉框組件<el-select>
和樹形組件<el-tree>
封裝實現了一個樹形下拉框組件TreeSelect
,效果圖如下。
<el-tree>
作爲<el-select>
的一個選項值<el-option>
,然後JS代碼實現選擇值的同步管理即可,邏輯比較簡單。
<el-select v-model="currentText" placeholder="請選擇" @clear="handelClear" clearable>
<el-option class="option view-scroll" :value="currentItem[options.value]" :label="currentItem[options.label]">
<!-- data:數據-->
<!-- props:數據結構配置 -->
<!-- node-key:唯一標識字段 -->
<el-tree ref="tree" :data="data" :node-key="options.value" :props="options" class="tree" @current-change="handleCurrentChange"></el-tree>
</el-option>
</el-select>
完整代碼見Github / KWebNote。
06、字典管理模塊
字典管理爲一個比較通用的字典數據管理模塊Dictionary.vue
,用來管理一些可變的分類數據,如圖書分類、商品促銷類型、品牌、國家、省市區地址等。包含兩部分數據:
- 字典類別,定義有哪些字典類別,包含分類名稱、編碼、是否樹形結構等關鍵字段。
- 字典數據,每一個字典類別的字典數據,統一存儲,用字典編碼區分,樹形結構。結構:id、名稱、類別編碼、排序號、父id。
樹形結構的數據編輯時,可以選擇父級,這裏用的是<el-cascader>
級聯選擇器組件。
數據是在本地進行樹形組裝和排序的,根級節點的父idpid
爲0,用buildDicTree
方法遞歸構造一顆樹。
export function queryDicData(type, istree = false) {
return api.dicdata({ code: type }).then(res => {
if (!res.data || res.data.length <= 0) return [];
//構造樹形結構
if (!istree)
return res.data.sort(sortDicData);
let sortItems = buildDicTree(res.data, ROOT_PID);
return sortItems;
})
}
function buildDicTree(items, pid) {
let sortItems = items.filter(s => s.pid == pid);
if (!sortItems || sortItems.length <= 0) return [];
sortItems.sort(sortDicData).forEach(item => {
const res = buildDicTree(items, item.id);
if (res && res.length > 0)
item.children = res.sort(sortDicData);
});
return sortItems;
}
function sortDicData(item1, item2) {
return item1.sort - item2.sort;
}
📢需要注意的是,這裏修改字典數據時,父級節點不能選擇自己及自己的子節點,否則會導致死循環。因此需要對上面構造的樹做一個處理,把不能選擇的節點設置
disabled
屬性。
參考資料
- 在線體驗地址:http://kanding.gitee.io/kwebnote (任意用戶名、密碼。通過gitee靜態頁面Gitee Pages部署的,所以這裏部署的版本是寫了個mock模擬api,路由用的hash模式)。
- vue-element-admin vue2的版本後臺框架,比較全面,適合學習
- Vue 官方文檔
- element-ui 2
- Vue2快速上門
- Vue項目工程@vue/cli入門
©️版權申明:版權所有@安木夕,本文內容僅供學習,歡迎指正、交流,轉載請註明出處!原文編輯地址-語雀