Я всегда хотел закончить и написать большое резюме статьи.Чувствую, что слишком много тяну и всегда хочется написать ее идеально.Тогда многие заметки мертвы в редакторе.В будущем буду обновлять в соответствии к разделу за разделом, маленькими шагами. Запустите 😂, сначала отправьте его, а потом повторяйте.
Недавно мы участвовали в разработке (года назад) проекта бизнес-аналитики. В интерфейсе использовалось семейство Vue Bucket. В основном была разработана функция проекта, а остальное было переделано. Процесс разработки прошел гладко. В течение периода мы столкнулись с много проблем и записал их. Основная идея заключается в том, как использовать API, предоставленный Vue, для реализации функции, чтобы избежать слишком долгого пребывания всех в одной и той же яме. Если есть лучшая идея реализации, вы можете общаться и обсуждать вместе 😎🤗.
Front-end и back-end разработка отдельных форм,vue+vueRouter+vueX+iviewUI+elementUI
, Мы используем iviewUI для большинства функций, а elementUI для некоторых компонентов, таких как формы и плагины календаря, у нас нет mock инструментов, интерфейс общается в виде документов, атмосфера в команде относительно гармоничная, три PHP и три внешних интерфейса, эффективные Все в порядке, два внешних партнера довольно мощные.В первый раз, когда я использовал vue, я взял на себя 90% рабочих задач по разработке.Я побежал домой, чтобы взять отпуск по уходу за ребенком, прежде чем я вышел в интернет.Я я особенно благодарен моим коллегам за их поддержку, так что я могу пойти домой посмотреть на ребенка.
Внешний интерфейс на самом деле не слишком сложен, но пока вы используете Vue для разработки, вы в основном столкнетесь с несколькими проблемами, такими как многоуровневая вложенность компонентов меню, выбор текущего элемента после обновления,
Здесь задействовано несколько моментов: слияние тела таблицы заголовков, загрузка файлов, редактор форматированного текста, дерево разрешений и т. д.
Введение в проект
Основная функция системы - просмотр данных отчетов для различных отделов.Бэкенд-студенты очень мощные, и они могут обобщать все данные группы и различные крутые технологии больших данных;
Функция меню:
- Канбан данных:Фильтрация, отображение дат и нумерация таблиц
- Деловой отчет:Тип отчета, фильтрация по дате, нумерация таблиц
- Извлечение данных:Связывание элементов фильтра, подкачка таблиц
- Карта потерь:Элементы скрининга, диаграмма взаимосвязей разъемов
- Развернуть анализ:Фильтры, Категории, Карточки, Таблицы
- системное сообщение:Выпуск версии, пошаговая панель, редактирование форматированного текста
- Загрузка источника данных:Ручная загрузка, табличное отображение
- Управление властью:Управление пользователями, управление ролями (конфигурация меню разрешений)
Предварительный просмотр проекта:
Отметьте как обновленный.
- 1. Используйте v-if для решения асинхронной передачи параметров
- 2. Используйте $refs для вызова методов дочерних компонентов
- 3. Компонент рекурсивно реализует многоуровневое меню
- 4. Используйте часы для мониторинга параметров маршрутизации для получения данных
- 5. После обновления страницы Меню выбирает текущий пункт меню по адресу
- 6. Используйте унифицированный код состояния Axios для оценки и равномерного увеличения поля токена.
- 7. Щелкните левое меню, чтобы выбрать элемент, и щелкните, чтобы обновить страницу.
- 8. Используйте Axios.CancelToken для переключения маршрута для отмены запроса
- 9. Используйте компонент таблицы элемента, чтобы объединить заголовок и тело
- 10. Компонент меню iview + vuex реализует навигацию по хлебным крошкам
- 11. Ручная загрузка компонента загрузки iview и доступ к текстовому редактору.
- 12. Получить данные таблицы с помощью cheerio
- 13. сохранить кеш компонентов
- 14. Держите поток данных в одном направлении (не манипулируйте данными родительского компонента в дочернем компоненте)
1. Используйте v-if для решения асинхронной перерисовки параметров
Большинство процессов взаимодействия представляют собой «данные запроса ajax => рендеринг входящего компонента». Многие свойства необходимо асинхронно передавать подкомпонентам, а затем выполнять соответствующие вычисления. , а некоторые Сцена не требует использования вычислений и просмотра, нам нужно получить ее только один раз при ее первоначальном создании.
В следующем примере gif нажмите TAB выше, чтобы обновить компонент полилинии:
<!--模板-->
<mapBox v-if="mapData" :data="mapData"></mapBox>
<!--点击搜索后执行-->
let This = this
// setp1 重点
this.mapData = false
this.$http
.post('/api/show/mapcondition',{key:key,type:type})
.then(function(response){
// setp2 重点
this.mapData = response.data
})
Иногда элементы DOM не синхронизированы с данными, вы можете использовать другие методы, чтобы сделать DOM сильным.
- setTimeou
- $forceUpdate()
- $nextTick()
- $set()
2. Используйте $refs для вызова методов дочерних компонентов
Иногда это включает родительский компонент, вызывающий метод дочернего компонента, например, компонент дерева, предоставляемый iview.getCheckedAndIndeterminateNodes
Для метода, пожалуйста, обратитесь к официальной документации веб-сайтаlink.
<!--模板-->
<Tree v-if="menu" :data="menu" show-checkbox multiple ref="Tree"></Tree>
let rules = this.$refs.Tree.getCheckedAndIndeterminateNodes();
3. Компонент рекурсивно реализует многоуровневое меню
Рекурсивные компоненты используются очень часто.В нашем левом меню также есть бесконечно разделенные слияния таблиц, все из которых используют рекурсивные компоненты.Подробности см. по официальной ссылке на сайт.link.
Изображение эффекта:
Общая идея состоит в том, чтобы сначала создать дочерний компонент, а затем создать родительский компонент, циклическую ссылку, взять левое меню для объяснения, код выглядит следующим образом, структура данных также находится в родительском компоненте.
<!--index.vue 父组件 数据接口在default中-->
<template>
<Menu width="auto"
theme="dark"
:active-name="activeName"
:open-names="openNames"
@on-select="handleSelect"
:accordion="true"
>
<template v-for="(item,index) in items">
<side-menu-item
v-if="item.children&&item.children.length!==0"
:parent-item="item"
:name="index+''"
:index="index"
>
</side-menu-item>
<menu-item v-else
:name="index+''"
:to="item.path"
>
<Icon :type="item.icon" :size="15"/>
<span>{{ item.title }}</span>
</menu-item>
</template>
</Menu>
</template>
<script>
import sideMenuItem from '@/components/Menu/side-menu-item.vue'
export default {
name: 'sideMenu',
props: {
activeName: {
type: String,
default: 'auth'
},
openNames: {
type: Array,
default: () => [
'other',
'role',
'auth'
]
},
items: {
type: Array,
default: () => [
{
name : 'system',
title : '数据看板',
icon : 'ios-analytics',
children: [
{ name : 'user', title : '用户管理', icon : 'outlet',
children : [
{ name : 'auth', title : '权限管理1', icon : 'outlet' },
{ name : 'auth', title : '权限管理', icon : 'outlet',
children:[
{ name : '334', title : '子菜单', icon : 'outlet' },
{ name : '453', title : '子菜单', icon : 'outlet' }
]
}
]
}
]
},
{
name : 'other',
title: '其他管理',
icon : 'outlet',
}
]
}
},
components: {
sideMenuItem
},
methods: {
handleSelect(name) {
this.$emit('on-select', name)
}
}
}
</script>
<!--side-menu-item.vue 子组件-->
<template>
<Submenu :name="index+''">
<template slot="title" >
<Icon :type="parentItem.icon" :size="10"/>
<span>{{ parentItem.title }}</span>
</template>
<template v-for="(item,i) in parentItem.children">
<side-menu-item
v-if="item.children&&item.children.length!==0"
:parent-item="item"
:to="item.path"
:name="index+'-'+i"
:index="index+'-'+i"
>
</side-menu-item>
<menu-item v-else
:name="index+'-'+i" :to="item.path">
<Icon :type="item.icon" :size="15" />
<span>{{ item.title }}</span>
</menu-item>
</template>
</Submenu>
</template>
<script>
export default {
name: 'sideMenuItem',
props: {
parentItem: {
type: Object,
default: () => {}
},
index:{}
},
created:function(){
}
}
</script>
4. Используйте часы для мониторинга параметров маршрутизации для получения данных
Многие пункты меню просто разные, это не будет пересматривать бизнес-логику, мы используем watch для мониторинга $router и повторно запрашиваем новые данные, если они изменяются.
export default {
watch: {
'$route':'isChange'
},
methods:{
getData(){
// Do something
},
isChange(){
this.getData()
},
}
}
5. Обновить: выберите текущий пункт меню по адресу
После обновления страницы выбранный по умолчанию пункт в левом меню не соответствует странице.Используем метод beforeEnter $router для суждения, получаем ключ маршрута по адресу (каждый маршрут имеет ключевой параметр) , и сохранить его в localStorage, а затем компонент меню берет ключ из localStorage, а затем проходит и сопоставляет выбранный в данный момент элемент.Излишне получать данные меню один раз в beforeEnter, а затем снова получать данные из компонента меню , запросив интерфейс дважды.
step1 router.js中设置beforeEnter方法,获得地址栏中的key 存储到localStorage
step2 菜单组件取出localStorage中key,递归匹配
6. Унифицированное определение кода состояния Axios, единое поле токена увеличения
Метод перехватчиков Axios имеет два метода: запрос и ответ, которые единообразно обрабатывают входные параметры и возвращают результаты запроса.
<!--request 除登录请求外,其他均增加token字段 -->
axios.interceptors.request.use(function (config) {
let token = localStorage.getItem('token')
if(token== null && router.currentRoute.path == '/login'){// 本地无token,未登录 跳转至登录页面
router.push('/login')
}else{
if(config.data==undefined){
config.data = {
"token":token
}
}else{
Object.assign(config.data,{"token":token})
}
}
return config
}, function (error) {
iView.Message.error('请求失败')
return Promise.reject(error)
})
<!--response 返回状态统一处理 -->
axios.interceptors.response.use(function (response) {
if(response.hasOwnProperty("data") && typeof response.data == "object"){
if(response.data.code === 998){// 登录超时 跳转至登录页面
iView.Message.error(response.data.msg)
router.push('/login')
return Promise.reject(response)
}else if (response.data.code === 1000) {// 成功
return Promise.resolve(response)
} else if (response.data.code === 1060){ //数据定制中
return Promise.resolve(response)
}else {// 失败
iView.Message.error(response.data.msg)
return Promise.reject(response)
}
} else {
return Promise.resolve(response)
}
}, function (error) {
iView.Message.error('请求失败')
// 请求错误时做些事
return Promise.reject(error)
})
7. Щелкните левое меню, чтобы выбрать элемент, и щелкните, чтобы обновить страницу.
Тестовые одноклассники выявили ошибки.После выбора левого меню, щелкните выбранный элемент еще раз, и он не обновляется.Пользовательский опыт не очень хороший.Одноклассники по продукту единогласно прошли его. Установите событие выбора для компонента меню, сохраните путь к текущему выбранному элементу после щелчка и каждый раз сравнивайте текущий путь с сохраненным путем.Если они совпадают, перейдите на пустую страницу, а затем вернитесь к текущая страница. Поддельное обновление, примечание: я не знаю, есть ли у router.push контроль дросселирования или что происходит. Он не работает без setTimeout.
<!--菜单的handleSelect事件-->
handleSelect(name) {
let This = this
if((this.selectIndex == 'reset') || (name == this.selectIndex)){
// 点击再次刷新
setTimeout(function function_name(argument) {
This.$router.push({
path: '/Main/about',
query: {
t: Date.now()
}
})
},1)
}
this.selectIndex = name
this.$emit('on-select', name)
},
<!--空白页-->
created(){
let This = this
setTimeout(function function_name(argument) {
This.$router.go(-1);
},1)
}
8. Используйте Axios.CancelToken для переключения маршрута для отмены запроса
В некоторых случаях при переключении маршрутов меняются только параметры.Как упоминалось в "4. Используйте часы для мониторинга параметров маршрутизации для повторного получения данных", также есть некоторые функции, которые очень медленно возвращают данные интерфейса.После появления меню переключения ,данные только доступны.После загрузки нужно добавить меню переключателя и отменить первоначальный запрос.Setp1,2 и 3 идут в порядке комментариев кода.
export default {
data(){
return {
// setp1 创建data公共的source变量
source:''
}
},
created:function(){
// 获取搜索数据
this.getData()
},
watch:{
'$route':'watchGetSearchData',
},
methods:{
getData(){
// setp2 请求时创建source实例
let CancelToken = this.$http.CancelToken
this.source = CancelToken.source();
},
watchGetSearchData(){
// setp3 切换路由时取消source实例
this.source.cancel('0000')
this.getData()
this.$http
.post('/api/show/map',data,{cancelToken:this.source.token})
.then(function(response){
})
}
}
}
9. Табличный компонент элемента реализует слияние тела таблицы заголовков
В нашем проекте используются два типа таблиц компонентов: одна — это таблица с iview, таблица с кнопками операций, которая поддерживает заголовки строк и столбцов, а другой — компонент таблицы элементов, который отображает только данные и поддерживает заголовки и заголовки. , По строкам и столбцам.
Табличный компонент элемента поддерживает слияние заголовка и заголовка таблицы.Мы определяем структуру данных, включающую три части: заголовок таблицы, тело таблицы и элементы слияния тела таблицы. Заголовок таблицы может быть вложен непосредственно с рекурсивными компонентами.Данные тела таблицы напрямую передаются в компонент таблицы, а данные объединенного элемента просматриваются и объединяются с помощью метода cellMerge.Код выглядит следующим образом.
структура данных
data:{
historyColumns:[ // 表头数据
{
"title": " ",
"key": "column"
},
{
"title": "指标",
"key": "target"
},
{
"title": "11/22",
"key": "11/22"
},
{
"title": "日环比",
"key": "日环比"
},
{
"title": "当周值",
"key": "当周值"
},
{
"title": "上周同期",
"key": "上周同期"
},
{
"title": "周环比",
"key": "周环比"
},
{
"title": "近7日累计",
"key": "近7日累计"
},
{
"title": "当月累计",
"key": "当月累计"
}
],
histories:[ // 表体数据
{
"target": "在售量",
"11/22": 912,
"日环比": "-",
"当周值": 912,
"上周同期": 0,
"周环比": "100%",
"近7日累计": 912,
"当月累计": 912,
"column": "基础指标"
},
{
"target": "-在售外库车量",
"11/22": 29,
"日环比": "-",
"当周值": 29,
"上周同期": 0,
"周环比": "100%",
"近7日累计": 29,
"当月累计": 29,
"column": "基础指标"
}
],
merge:[ // 表体合并项
{
"rowNum": 0,
"colNum": 0,
"ropSpan": 1,
"copSpan": 4
},
{
"rowNum": 4,
"colNum": 0,
"ropSpan": 1,
"copSpan": 27
}
]
}
Описание объединения тела таблицы:В таблице есть метод cellMerge, который выполняется при рендеринге каждого td.Данные слияния просматриваются в cellMerge, а td находится в соответствии со строкой и столбцом входного параметра cellMerge.Если это таблица для слияния, верните количество объединяемых строк и количество столбцов, если они находятся в объединенном диапазоне, вернуть [0,0], чтобы скрыть текущий td.
Например, чтобы объединить данные rowNum A, B, C, D, rowNum A, столбец A, ropSpan со значением 2 и copSpan со значением 2 в методе cellMerge, если координата — это ячейка A, вернуть ropSpan и copSpan,Если координаты B, C, D, верните [0,0], чтобы скрыть, иначе будет путаница в таблице..
код метода слияния:// 表格合并主方法 row:行数组 column:列数据 rowIndex、columnIndex行列索引
cellMerge({ row, column, rowIndex, columnIndex }) {
let This = this;
if(This.configJson){
for(let i = 0; i < This.configJson.length; i++){
let rowNum = This.configJson[i].rowNum // 行
let colNum = This.configJson[i].colNum // 列
let ropSpan = This.configJson[i].ropSpan // 跨列数
let copSpan = This.configJson[i].copSpan // 跨行数
if(rowIndex == rowNum && columnIndex == colNum ){// 当前表格index 合并项
return [copSpan,ropSpan]
// 隐藏范围内容的单元格
// 行范围 rowNum <= rowIndex && rowIndex < (rowNum+copSpan)
// 列范围 colNum <= columnIndex && columnIndex < (colNum+ropSpan)
}else if( rowNum <= rowIndex && rowIndex < (rowNum+copSpan) && colNum <= columnIndex && columnIndex < (colNum+ropSpan) ){
return [0,0]
}
}
}
}
** Инструкции по слиянию заголовков: ** Формат данных слияния заголовков элемента и iview может быть одинаковым, как в рекурсивной форме, разница в том, что табличный компонент iview может напрямую передавать данные в компонент, а элемент должен инкапсулировать заголовок сам по себе.
// 子组件
<template>
<el-table-column :prop="thList.key" :label="thList.title" align="center">
<template v-for="(item,i) in thList.children" >
<tableItem v-if="item.children&&item.children.length!==0"
:thList="item" /></tableItem>
<el-table-column align="center" v-else
:prop="item.key"
:label="item.title"
:formatter="toThousands"
>
</el-table-column>
</template>
</el-table-column>
</template>
<script>
export default {
name: 'tableItem',
props: {
thList: {
type: Object,
default: () => {}
},
},
}
</script>
Компонент инкапсулированной таблицы:
<template>
<div>
<el-table :data="Tbody" :stripe="stripe" :border="true" :span-method="cellMerge" align="center" :header-cell-style="tableHeaderColor" height="600" >
<template v-for="(item,i) in Thead">
<template v-if="item.children&&item.children.length!==0" >
<tableItem :thList="item" />
</template>
<template v-else >
<el-table-column align="center"
:prop="item.key"
:label="item.title"
:formatter="toThousands"
>
</el-table-column>
</template>
</template>
</el-table>
</div>
</template>
<script>
import tableItem from '@/components/table/tableHeader/table-Item.vue'
export default {
name: 'table-header',
props: {
Thead: {
type: Array,
default: () => {}
},
Tbody:{
type: Array,
default: () => {}
},
stripe:{
type:Boolean,
default:false
},
cellMerge:Function,
default:()=>{}
},
created:function(){
},
components:{
tableItem
},
methods:{
tableHeaderColor({ row, column, rowIndex, columnIndex }) {
if (rowIndex === 0) {
return 'background-color: #f8f8f9;'
}
}
}
}
</script>
Таблица повторного использования других страниц
<!--引入-->
import TableList from '@/components/table/tableHeader/index.vue'
<!--调用-->
<TableList :Thead="historyColumns" :Tbody="historyData" :cellMerge="cellMerge" />
10. Компонент меню iview + vuex реализует навигацию по хлебным крошкам
Компонент меню iview имеет метод выбора, который может получить имя выбранного элемента.Наше имя просматривается в соответствии с индексом данных, таким как трехуровневое меню, которое вернется после выбора.2-0-1
Такая строка представляет второй пункт меню в первом подменю в третьем меню древовидного меню, а затем отфильтровывает массив через эту строку.['业务报表','B2C报表','成交明细']
Соответствует заголовку меню, а затем отправляет его в Store.state vuex, а затем компонент хлебной крошки прослушивает Store.state, вычисляя свойство данных для отображения свойства.
<!-- 根据字符串筛出title数组 发给$store -->
toBreadcrumb(arrIndex){
let This = this;
let mapIndex = arrIndex.split('-');
// 获取对应name
let box={};
let mapText = mapIndex.map(function(item,index){
if(index == 0){
box = This.MenuData[eval(item)];
}else{
box = box.children[eval(item)];
}
return box.title;
});
this.$store.commit('toBreadcrumb',mapText)
}
код vueX
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
Breadcrumb:[], // 面包屑导航
userName: '',
readyData:""
},
mutations: {
toBreadcrumb(state,arr){
state.Breadcrumb = arr;
}
},
getters: {
getBreadcrumb: state => {
return state.Breadcrumb
}
}
})
компонент хлебных крошек
<template>
<Header style="background: #fff;">
<Row>
<Col span="12">
<!-- {{doneTodosCount}} -->
<Breadcrumb>
<BreadcrumbItem v-for="item in doneTodosCount">{{item}}</BreadcrumbItem>
</Breadcrumb>
</Col>
<Col span="12">
<Login />
</Col>
</Row>
</Header>
</template>
<script>
import Login from '@/components/Login'
export default {
data(){
return {
}
},
created:function(){
this.$store.commit('toBreadcrumb',['首页'])
},
computed: {
doneTodosCount () {
return this.$store.state.Breadcrumb
}
},
components:{
Login
}
}
</script>
11. Ручная загрузка компонента загрузки iview, доступ к текстовому редактору
Компоненты, предоставляемые iview, очень богаты. Когда мы загружаем изображения, нам нужно загружать их вручную. Нам нужно вызвать файловый объект подкомпонента, чтобы отправить его на сервер с помощью собственного метода публикации. ActionDate — это файл. данные, а затем они загружаются через обратную связь об успешном выполнении. успех или неудача.Ручная загрузка:
<Upload
ref="upload"
:data= "actionDate"
:on-success="handleSuccess"
:format="['png','jpg']"
action="/api/upload/ccupload">
<Button icon="ios-cloud-upload-outline">点击上传文件</Button>
</Upload>
<div v-if="file !== null">
上传文件: {{ file.name }}
<Button type="text" @click="upload" :loading="loadingStatus">{{ loadingStatus ? 'Uploading' : '上传' }}</Button>
</div>
// upload 方法
let uploadFile = this.$refs.upload.file
this.$refs.upload.data = this.actionDate;
this.$refs.upload.post(uploadFile);
this.loadingStatus = true;
// handleSuccess 方法
this.loadingStatus = false;
if(res.code == 1000){
this.$Message.success('上传成功')
}else{
this.$Message.error('上传失败')
}
В процессе совместной отладки бэкенд сказал, что файл не может быть получен, мы могли только использовать node, чтобы проверить, есть ли проблема с компонентом, поэтому мы написали загрузку файла, используя экспресс.
var express = require('express');
var router = express.Router();
let fs = require('fs')
var formidable = require('formidable');//表单控件
var path = require('path');
var app = express();
app.use(express.static('/public/'));
router.post('/test',(req,res)=>{
var imgPath = path.dirname(__dirname) + '/public';
var form = new formidable.IncomingForm();
form.encoding = 'utf-8'; //设置编辑
form.uploadDir = imgPath; //设置上传目录
form.keepExtensions = true; //保留后缀
form.maxFieldsSize = 2 * 1024 * 1024; //文件大小
form.type = true;
form.parse(req, function(err, fields, files){
let src = files.img.path.split('/');
let urlString = src[src.length-1]
if (err) {
console.log(err);
req.flash('error','图片上传失败');
return;
}
res.json({
code: '200',
type:'single',
url:'http://10.70.74.167:3000/'+urlString
})
});
});
module.exports = router;
Мы добавили конфигурацию пересылки для теста изображения во время тестирования, а затем заменили адрес действия компонента на/test/
Все, с про тестом [коварное лицо] проблем нет.vue.config.js
module.exports = {
baseUrl: baseUrl,
devServer: {
proxy: {
'/api': { // 开发服务器
target: ' http://*******',
changeOrigin: true,
},
'/test': { // 图片上传测试
target: ' http://10.70.74.167:3000',
changeOrigin: true,
}
}
},
productionSourceMap: false,
}
Существует два режима загрузки изображений в текстовом редакторе: один — преобразовать изображение в формат base64 и отправить html-контент на сервер через один интерфейс, другой — загрузить изображение на сервер через два интерфейса, а затем вернуть строку URL-адреса в редактор, а затем сохранить html в редакторе на сервер, редактор, который мы используем,vue-quill-editor
, используйте второй режим для автоматической загрузки картинок с помощью компонента el-upload элемента, а затем вставьте обратный адрес в редактор.
import {format} from '@/lib/js/utils.js'
import {quillEditor} from 'vue-quill-editor'
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
[{'header': 1}, {'header': 2}], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
[{'direction': 'rtl'}], // text direction
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['link', 'image'],
['clean']
]
export default {
data () {
return {
options2:{},
quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
content:'', // 富文本内容
title:'新建',
editorOption:{
placeholder: '',
theme: 'snow', // or 'bubble'
modules:{
toolbar: {
container: toolbarOptions,
handlers: {
'image': function (value) {
if (value) {
// 触发input框选择图片文件
document.querySelector('.avatar-uploader input').click()
} else {
this.quill.format('image', false);
}
}
}
}
}
},
serverUrl: '/api/add/upload?key='+this.$route.params.key, // 这里写你要上传的图片服务器地址
header: {
// token: sessionStorage.token
},
current: 0,
formValidate: {
device_name: '集团BI',
versions: '',
publish_time: '',
desc: '',
},
ruleValidate: {
device_name: [
{ required: true, message: '请选择系统名称', trigger: 'change' }
],
versions: [
{ required: true, message: '请输入版本信息', trigger: 'blur' }
],
publish_time: [
{ required: true, type: 'date', message: '请选择发版时间', trigger: 'change' }
],
desc: [
{ required: true, message: '请输入对于该版本的总体描述', trigger: 'blur' },
{ type: 'string', min: 20, message: '版本的总体描述不少于20个字', trigger: 'blur' }
]
},
isFirst: true,
isSecond: false,
isThird: false,
versionid:''
}
},
created:function(){
this.limit();
this.initialization();
},
methods: {
limit(){
this.options2 = {
disabledDate (date) {
return (date && date.valueOf() > new Date().getTime()) || (date && date.valueOf() < new Date("2017-12-31"))
}
}
},
//初始判定是新增/修改
initialization(){
let id = this.$route.params.id;
if(id !=0){
this.title = "编辑";
let obj = {};
obj.version_id = this.$route.params.id;
obj.key = this.$route.params.key;
this.$http
.post('/api/show/version',obj).then(response => (
this.formValidate.device_name = response.data.data.device_name,
this.formValidate.versions = response.data.data.versions,
this.formValidate.publish_time = response.data.data.publish_time,
this.formValidate.desc = response.data.data.desc,
this.content = response.data.data.pc_html
))
}else{
this.title = "新建";
}
},
//第一步基本信息(发布)
firstSubmit(name){
this.$refs[name].validate((valid) => {
if (valid) {
this.$Message.success('信息添加成功');
this.current += 1;
this.isFirst = !this.isFirst;
this.isSecond = !this.isSecond;
}else{
this.$Message.error('请完善必填信息');
}
})
},
//第二步的表单数据提交(发布)
save(){
let id = this.$route.params.id;
let addObj = this.formValidate;
addObj.publish_time = format(this.formValidate.publish_time);
addObj.pc_html = this.content;
addObj.key = this.$route.params.key;
if(this.$route.params.id != 0){
addObj.version_id = id;
}
this.$http
.post('/api/add/version',addObj).then(response => (
this.secondSubmit(response.data.version_id)
))
},
//第二步提交成功后转至第三步(发布)
secondSubmit(id){
this.current += 1;
this.isSecond = false;
this.isThird = !this.isThird;
this.versionid = id;
},
//第三步跳转至[预览]
preview(){
this.$router.push({ path:"/Main/VersionManagementInfo/system_versions/"+this.versionid});
},
//第三步发布
release(){
let status = this.$route.params.status;
if(status != 2){
let obj = {};
if(this.$route.params.id == 0){
obj.version_id = this.versionid;
}else{
obj.version_id = this.$route.params.id;
}
obj.key = this.$route.params.key;
this.$http
.post('/api/edit/publish/version',obj).then(response => (
this.releaseLink()
))
}else{
this.releaseLink()
}
},
//第三步发布跳转
releaseLink(){
this.$router.push({ path:"/Main/VersionManagement/system_versions"});
},
//上一步操作
returns () {
if (this.current != 0) {
this.current -= 1;
this.isFirst = true;
this.isSecond = false;
}
},
//富文本内容改变事件
onEditorChange({editor, html, text}) {
this.content = html
},
//富文本图片上传前
beforeUpload() {
// 显示loading动画
this.quillUpdateImg = true
},
//富文本图片上传成功
uploadSuccess(res, file) {
// res为图片服务器返回的数据
// 获取富文本组件实例
console.log(res,file);
let quill = this.$refs.myQuillEditor.quill
// 如果上传成功
if (res.code == 1000 ) {
// 获取光标所在位置
let length = quill.getSelection().index;
// 插入图片 res.url为服务器返回的图片地址
quill.insertEmbed(length, 'image', res.data)
// 调整光标到最后
quill.setSelection(length + 1)
} else {
this.$message.error('图片插入失败')
}
// loading动画消失
this.quillUpdateImg = false
},
// 富文本图片上传失败
uploadError() {
// loading动画消失
this.quillUpdateImg = false
this.$message.error('图片插入失败')
},
}
}
12. Используйте cheerio для отображения таблицы строк
Часть данных таблицы трудно обработать. Серверная часть напрямую преобразует файл xlsx в строку и отправляет ее во внешний интерфейс. Cheerio может преобразовать строку в виртуальный DOM, аналогичный объекту jquery, а затем использовать jquery API для работы с виртуальным DOM.
import cheerio from "cheerio"
this.$http.post('/api/list/statement-table',p).then(function(response){
if(response.data==""){
This.isShow=false;This.content=true;This.title=false//无数据时数据加载中和标题数据的盒子隐藏
This.message="<div style='text-align:center'>暂无数据</div>"
}else{
//console.log(response)
This.isShow=false;
This.content=true;//有数据时 数据加载中隐藏 标题和表体显示
let $ = cheerio.load(response.data);
//删除自带的行内样式
$("body style").remove();
$("body table").css({"border":"1px solid #e8eaec" });
$("body table td").css({"border":"1px solid #e8eaec","padding":"10px","color":"#515a6e"});
//全文匹配 剔除&quot;
This.message = $("body").html().replace(/&quot;/g,"");
}
})
13. сохранить кеш компонентов
Требование продукта — нажать кнопку просмотра на странице списка, чтобы перейти на страницу сведений, а затем нажать, чтобы вернуться на страницу сведений.Если страницу списка невозможно обновить, компоненты необходимо кэшировать.
Кэш компонентов добавляется напрямуюkeep-live
Все в порядке, более хлопотным является то, что мы оцениваем три ситуации в этом компоненте: 1. Ввод в первый раз 2. Ввод из других столбцов 3. Ввод со страницы сведений, если две ситуации 1 и 2, нам нужно Обновить страница, если она 3, не обновлять.
Идея такова:created
увеличение крючкаisFirstEnter
логотип,beforeRouteEnter
Оценка того, возвращается ли она, возвращается ли страница сведений, если даmeta.isBack
логотип, вactivated
В хуке он оценивается в первых нескольких случаях, если это 1 или 2, данные страницы списка будут повторно запрошены, если это 3, никаких действий не требуется.
router.js
Увеличьте идентификацию метаkeepAlive
а такжеisBack
/******** 业务报表 Start ********/
{
path: '/Main/BusinessReport/:key', // 业务报表-列表
name: 'BusinessReport',
meta: { keepAlive: true,isBack:false},
component: () => import('./pages/BusinessReport/index.vue'),
},
{
path: '/Main/BusinessReportInfo/:sn/:is_check/:key/:type/:cmd5/:time/:is_down', // 业务报表-详情
name: 'BusinessReportInfo',
component: () => import('./pages/BusinessReportInfo/index.vue'),
},
/******** 业务报表 End ********/
согласно сmate.keepAlive
делать разныеrouter-view
(Забыл, почему так написали, очень низко).
<keep-alive>
<router-view v-if="$route.meta.keepAlive" ></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" >
<!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>
Событие ловушки кода компонентаcreated
,beforeRouteEnter
,activated
метод
data(){
return{
isFirstEnter:false
}
},
created:function(){
this.isFirstEnter = true;
},
beforeRouteEnter(to, from, next) {
if(from.name === 'BusinessReportInfo') { //判断是从哪个路由过来的,若是BusinessReportInfo页面不需要刷新获取新数据,直接用之前缓存的数据即可
to.meta.isBack = true
}
next();
},
activated() {
if(!this.$route.meta.isBack || this.isFirstEnter) {
this.data=""
//如果isBack是false,表明需要获取新数据,否则就不再请求,直接使用缓存的数据
this.getPath(); // ajax获取数据方法
}
this.$route.meta.isBack = false;
this.isFirstEnter=false;
//恢复成默认的false,避免isBack一直是true,导致下次无法获取数据
}
14. Не манипулируйте данными родительского компонента в дочернем компоненте.
Модифицировать данные родительского компонента в дочернем компоненте действительно можно, но настоятельно не рекомендуется манипулировать данными родительского компонента в дочернем компоненте.В этот период я взял на себя функцию и долго прочесывал логику время, но я не мог найти, где была точка срабатывания.Она оказалась в дочернем компоненте. Манипулирование данными родительского компонента не способствует обслуживанию.Я сам сделал имя, чтобы данные шли в одном направлении Я не знаю, можно ли это определить как принцип единичных данных.
В процессе развития мы обнаружили, что кодовые стили бизнес-компонентов, написанных всеми, несовместимыми. Какова последовательность? Есть ли хорошие нормы или принципы для бизнес-компонентов? Я также надеюсь, что каждый может дать некоторую информацию и предложения. Большое спасибо.