{{wikiTitle}}
组件添加说明
目录:
添加组件说明
概述
CRMEB多店系统的组件分为两大类:移动端(uni-app)组件 和 后台管理端(Vue + iView)组件。本文档详细介绍如何在这两种场景下开发和集成新的组件。
一、移动端组件开发(uni-app)
1.1 组件目录结构
view/uniapp/components/
├── BaseMoney.vue # 金额显示组件
├── BaseTag.vue # 标签组件
├── NavBar.vue # 导航栏组件
├── emptyPage.vue # 空状态组件
├── skeleton/ # 骨架屏组件
│ └── index.vue
├── countDown/ # 倒计时组件
│ └── index.vue
├── productWindow/ # 商品弹窗组件
│ └── productWindow.vue
├── couponWindow/ # 优惠券弹窗组件
│ └── couponWindow.vue
├── addressWindow/ # 地址选择组件
│ └── addressWindow.vue
├── authorize/ # 授权组件
│ └── index.vue
└── ... # 更多组件
1.2 创建基础组件
组件文件模板
<!-- components/myComponent/index.vue -->
<template>
<view class="my-component" :style="customStyle">
<!-- 组件内容 -->
<view class="header" v-if="showHeader">
<text class="title">{{ title }}</text>
<slot name="header-right"></slot>
</view>
<view class="body">
<!-- 默认插槽 -->
<slot></slot>
</view>
<view class="footer" v-if="$slots.footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script>
export default {
name: 'MyComponent',
// 组件属性定义
props: {
// 标题
title: {
type: String,
default: ''
},
// 是否显示头部
showHeader: {
type: Boolean,
default: true
},
// 自定义样式
customStyle: {
type: [String, Object],
default: ''
},
// 数据列表
list: {
type: Array,
default: () => []
},
// 数值类型
count: {
type: Number,
default: 0
}
},
// 组件数据
data() {
return {
innerValue: '',
loading: false
};
},
// 计算属性
computed: {
// 计算显示内容
displayText() {
return this.title || '默认标题';
},
// 处理后的列表
processedList() {
return this.list.filter(item => item.show);
}
},
// 监听器
watch: {
// 监听属性变化
count: {
handler(newVal, oldVal) {
this.handleCountChange(newVal);
},
immediate: true
}
},
// 生命周期
created() {
this.init();
},
mounted() {
this.bindEvents();
},
beforeDestroy() {
this.unbindEvents();
},
// 方法定义
methods: {
// 初始化
init() {
// 初始化逻辑
},
// 处理数量变化
handleCountChange(val) {
// 处理逻辑
},
// 绑定事件
bindEvents() {
// 事件绑定
},
// 解绑事件
unbindEvents() {
// 事件解绑
},
// 触发自定义事件
handleClick(data) {
// 向父组件发送事件
this.$emit('click', data);
},
// 带回调的事件
handleChange(value) {
this.$emit('change', value);
// 支持v-model
this.$emit('input', value);
},
// 暴露给父组件的方法
refresh() {
this.loading = true;
// 刷新逻辑
setTimeout(() => {
this.loading = false;
}, 1000);
}
}
};
</script>
<style lang="scss" scoped>
.my-component {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1rpx solid #f5f5f5;
.title {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
}
.body {
padding: 24rpx;
}
.footer {
padding: 24rpx;
border-top: 1rpx solid #f5f5f5;
}
}
</style>
1.3 实战示例:倒计时组件
参考项目中的 countDown 组件实现:
<!-- components/countDown/index.vue -->
<template>
<view class="count-down" :style="justifyLeft">
<!-- 提示文字 -->
<text class="tip-text" v-if="tipText.trim()">{{ tipText }}</text>
<!-- 天数 -->
<text class="time-block" :style="blockStyle" v-if="isDay">
{{ day }}
<text class="day-text">{{ inDayText }}</text>
</text>
<text class="separator" :style="separatorStyle" v-if="dayText">{{ dayText }}</text>
<!-- 小时 -->
<text class="time-block" :style="blockStyle">{{ hour }}</text>
<text class="separator" :style="separatorStyle" v-if="hourText">{{ hourText }}</text>
<!-- 分钟 -->
<text class="time-block" :style="blockStyle">{{ minute }}</text>
<text class="separator" :style="separatorStyle" v-if="minuteText">{{ minuteText }}</text>
<!-- 秒数 -->
<text class="time-block" :style="blockStyle" v-if="isSecond">{{ second }}</text>
<text class="separator" :style="separatorStyle" v-if="secondText">{{ secondText }}</text>
<!-- 底部插槽 -->
<slot name="bottom"></slot>
</view>
</template>
<script>
export default {
name: 'CountDown',
props: {
// 对齐方式样式
justifyLeft: {
type: String,
default: ''
},
// 提示文字
tipText: {
type: String,
default: '倒计时'
},
// 天数单位(块内)
inDayText: {
type: String,
default: '天'
},
// 天数分隔符
dayText: {
type: String,
default: '天'
},
// 小时分隔符
hourText: {
type: String,
default: '时'
},
// 分钟分隔符
minuteText: {
type: String,
default: '分'
},
// 秒数分隔符
secondText: {
type: String,
default: '秒'
},
// 目标时间戳
datatime: {
type: Number,
default: 0
},
// 是否显示天
isDay: {
type: Boolean,
default: true
},
// 是否显示秒
isSecond: {
type: Boolean,
default: true
},
// 时间块背景色
bgColor: {
type: String,
default: ''
},
// 时间数字颜色
colors: {
type: String,
default: '#e93323'
},
// 分隔符颜色
dotColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
day: '00',
hour: '00',
minute: '00',
second: '00',
timer: null
};
},
computed: {
// 时间块样式
blockStyle() {
return `background-color: ${this.bgColor}; color: ${this.colors};`;
},
// 分隔符样式
separatorStyle() {
return `color: ${this.dotColor};`;
}
},
watch: {
datatime: {
handler(val) {
this.startCountDown();
},
immediate: true
}
},
beforeDestroy() {
this.clearTimer();
},
methods: {
// 开始倒计时
startCountDown() {
this.clearTimer();
this.updateTime();
this.timer = setInterval(() => {
this.updateTime();
}, 1000);
},
// 更新时间
updateTime() {
const now = Date.parse(new Date()) / 1000;
let diff = this.datatime - now;
if (diff > 0) {
let day = 0, hour = 0, minute = 0, second = 0;
if (this.isDay) {
day = Math.floor(diff / (60 * 60 * 24));
}
hour = Math.floor(diff / (60 * 60)) - day * 24;
minute = Math.floor(diff / 60) - day * 24 * 60 - hour * 60;
second = Math.floor(diff) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
this.day = day;
this.hour = hour <= 9 ? '0' + hour : hour;
this.minute = minute <= 9 ? '0' + minute : minute;
this.second = second <= 9 ? '0' + second : second;
} else {
this.day = '00';
this.hour = '00';
this.minute = '00';
this.second = '00';
this.clearTimer();
// 倒计时结束事件
this.$emit('endTime');
}
},
// 清除定时器
clearTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
};
</script>
<style lang="scss" scoped>
.count-down {
display: flex;
justify-content: center;
align-items: center;
.tip-text {
color: #e93323;
margin-right: 8rpx;
}
.time-block {
min-width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align: center;
border-radius: 6rpx;
font-size: 24rpx;
.day-text {
font-size: 20rpx;
margin-left: 4rpx;
}
}
.separator {
margin: 0 4rpx;
font-size: 24rpx;
}
}
</style>
1.4 使用组件
全局注册
在 main.js 中全局注册组件:
// main.js
import Vue from 'vue';
import MyComponent from './components/myComponent/index.vue';
import CountDown from './components/countDown/index.vue';
// 全局注册
Vue.component('MyComponent', MyComponent);
Vue.component('CountDown', CountDown);
局部引入
在页面中局部引入:
<template>
<view>
<!-- 使用倒计时组件 -->
<count-down
:datatime="endTime"
tip-text="距结束"
bg-color="#ff5500"
colors="#ffffff"
@endTime="handleEnd">
</count-down>
<!-- 使用自定义组件 -->
<my-component
title="我的组件"
:list="dataList"
@click="handleClick">
<text>内容区域</text>
<template #footer>
<button>底部按钮</button>
</template>
</my-component>
</view>
</template>
<script>
import CountDown from '@/components/countDown/index.vue';
import MyComponent from '@/components/myComponent/index.vue';
export default {
components: {
CountDown,
MyComponent
},
data() {
return {
endTime: Math.floor(Date.now() / 1000) + 3600, // 1小时后
dataList: []
};
},
methods: {
handleEnd() {
uni.showToast({ title: '倒计时结束', icon: 'none' });
},
handleClick(data) {
console.log('clicked:', data);
}
}
};
</script>
1.5 实战示例:空状态组件
<!-- components/emptyPage.vue -->
<template>
<view class="empty-page">
<!-- 空状态图片 -->
<image class="empty-img" :src="imgSrc"></image>
<!-- 未登录提示 -->
<view v-if="!isLogin" class="title">暂未登录</view>
<!-- 提示文字 -->
<view class="desc">{{ title }}</view>
<!-- 登录按钮 -->
<view class="login-btn" v-if="!isLogin" @tap="goLogin">
立即登录
</view>
<!-- 底部插槽 -->
<slot name="bottom"></slot>
</view>
</template>
<script>
import { HTTP_REQUEST_URL } from '@/config/app';
import { toLogin } from '@/libs/login.js';
export default {
name: 'EmptyPage',
props: {
// 提示文字
title: {
type: String,
default: '暂无记录'
},
// 图片路径
src: {
type: String,
default: '/statics/images/empty-box.gif'
},
// 是否已登录
isLogin: {
type: Boolean,
default: true
}
},
computed: {
imgSrc() {
return HTTP_REQUEST_URL + this.src;
}
},
methods: {
goLogin() {
toLogin();
}
}
};
</script>
<style lang="scss" scoped>
.empty-page {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 82rpx 0 160rpx;
background-color: #fff;
border-radius: 24rpx;
.empty-img {
width: 440rpx;
height: 360rpx;
}
.title {
font-size: 28rpx;
font-weight: 500;
color: #282828;
margin-top: 20rpx;
}
.desc {
font-size: 26rpx;
color: #999;
line-height: 36rpx;
margin-top: 16rpx;
}
.login-btn {
width: 360rpx;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 500;
border: 1px solid var(--view-theme);
color: var(--view-theme);
margin-top: 48rpx;
}
}
</style>
二、后台管理端组件开发(Vue + iView)
2.1 组件目录结构
view/admin/src/components/
├── cards/ # 卡片组件
├── copyright/ # 版权组件
├── couponList/ # 优惠券列表
├── customerInfo/ # 客户信息
├── echarts/ # 图表组件
├── from/ # 表单组件
├── fromBuild/ # 动态表单构建
├── goodsList/ # 商品列表选择器
├── uploadPictures/ # 图片上传组件
├── uploadVideo/ # 视频上传组件
├── searchFrom/ # 搜索表单
├── wangEditor/ # 富文本编辑器
├── map/ # 地图组件
└── ... # 更多组件
2.2 创建基础组件
<!-- src/components/myAdminComponent/index.vue -->
<template>
<div class="my-admin-component">
<!-- 头部区域 -->
<div class="header" v-if="showHeader">
<span class="title">{{ title }}</span>
<div class="actions">
<slot name="header-actions"></slot>
</div>
</div>
<!-- 内容区域 -->
<div class="content">
<slot></slot>
</div>
<!-- 加载状态 -->
<Spin fix v-if="loading">
<Icon type="ios-loading" size="18" class="spin-icon"></Icon>
<div>加载中...</div>
</Spin>
</div>
</template>
<script>
export default {
name: 'MyAdminComponent',
props: {
// 标题
title: {
type: String,
default: ''
},
// 是否显示头部
showHeader: {
type: Boolean,
default: true
},
// 加载状态
loading: {
type: Boolean,
default: false
}
},
data() {
return {
innerData: null
};
},
methods: {
// 刷新
refresh() {
this.$emit('refresh');
},
// 提交
submit(data) {
this.$emit('submit', data);
}
}
};
</script>
<style lang="stylus" scoped>
.my-admin-component
background-color #fff
border-radius 4px
padding 20px
.header
display flex
justify-content space-between
align-items center
margin-bottom 20px
padding-bottom 15px
border-bottom 1px solid #e8eaec
.title
font-size 16px
font-weight 500
color #17233d
.content
min-height 200px
.spin-icon
animation spin 1s linear infinite
@keyframes spin
from
transform rotate(0deg)
to
transform rotate(360deg)
</style>
2.3 实战示例:商品选择器组件
参考项目中的 goodsList 组件,实现一个完整的商品选择器:
<!-- src/components/goodsList/index.vue -->
<template>
<div class="goods-list-selector">
<!-- 搜索表单 -->
<Form
ref="formValidate"
:model="formValidate"
:label-width="labelWidth"
:label-position="labelPosition"
inline
class="search-form">
<!-- 商品分类 -->
<FormItem label="商品分类:">
<Cascader
:data="categoryList"
placeholder="请选择商品分类"
change-on-select
filterable
class="input-add"
@on-change="handleCategoryChange">
</Cascader>
</FormItem>
<!-- 商品名称搜索 -->
<FormItem label="商品搜索:">
<Input
v-model="formValidate.keyword"
placeholder="请输入商品名称/关键字/编号"
class="input-add mr14"
/>
<Button type="primary" @click="handleSearch">查询</Button>
</FormItem>
</Form>
<!-- 商品表格 -->
<Table
ref="table"
:columns="columns"
:data="tableData"
:loading="loading"
no-data-text="暂无商品数据"
@on-select="handleSelect"
@on-select-cancel="handleSelectCancel"
@on-select-all="handleSelectAll"
@on-select-all-cancel="handleSelectAllCancel"
height="500">
<!-- 商品名称列 -->
<template slot-scope="{ row }" slot="store_name">
<Tooltip max-width="200" placement="bottom">
<span class="line2">{{ row.store_name }}</span>
<p slot="content">{{ row.store_name }}</p>
</Tooltip>
</template>
<!-- 商品图片列 -->
<template slot-scope="{ row }" slot="image">
<div class="table-img">
<img v-lazy="row.image" />
</div>
</template>
<!-- 商品类型列 -->
<template slot-scope="{ row }" slot="product_type">
<span v-if="row.product_type === 0">普通商品</span>
<span v-else-if="row.product_type === 1">卡密商品</span>
<span v-else-if="row.product_type === 3">虚拟商品</span>
<span v-else-if="row.product_type === 4">次卡商品</span>
<span v-else-if="row.product_type === 5">卡项商品</span>
<span v-else-if="row.product_type === 6">预约商品</span>
</template>
</Table>
<!-- 分页 -->
<div class="page-wrapper">
<Page
:total="total"
:current="formValidate.page"
:page-size="formValidate.limit"
show-elevator
show-total
@on-change="handlePageChange"
/>
</div>
<!-- 提交按钮(多选模式) -->
<div class="footer" v-if="multiple">
<Button
type="primary"
size="large"
long
:loading="submitting"
@click="handleSubmit">
确定选择(已选{{ selectedList.length }}件)
</Button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { cascaderListApi, changeListApi } from '@/api/product';
export default {
name: 'GoodsListSelector',
props: {
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 场景类型
chooseType: {
type: Number,
default: 0
},
// 已选商品
selectedIds: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
submitting: false,
categoryList: [],
tableData: [],
selectedList: [],
total: 0,
formValidate: {
page: 1,
limit: 10,
cate_id: '',
keyword: '',
choose_type: this.chooseType
},
columns: []
};
},
computed: {
...mapState('admin/layout', ['isMobile']),
labelWidth() {
return this.isMobile ? undefined : 100;
},
labelPosition() {
return this.isMobile ? 'top' : 'right';
}
},
created() {
this.initColumns();
this.getCategoryList();
this.getGoodsList();
},
methods: {
// 初始化表格列
initColumns() {
const selectionColumn = {
type: 'selection',
width: 60,
align: 'center'
};
const radioColumn = {
title: '选择',
width: 70,
align: 'center',
render: (h, params) => {
return h('Radio', {
props: {
value: this.selectedList.some(item => item.id === params.row.id)
},
on: {
'on-change': () => {
this.selectedList = [params.row];
this.$emit('select', params.row);
}
}
});
}
};
const dataColumns = [
{ title: '商品ID', key: 'id', width: 100 },
{ title: '图片', slot: 'image', width: 80 },
{ title: '商品名称', slot: 'store_name', minWidth: 200 },
{ title: '商品类型', slot: 'product_type', width: 120 },
{ title: '商品分类', key: 'cate_name', minWidth: 150 }
];
this.columns = this.multiple
? [selectionColumn, ...dataColumns]
: [radioColumn, ...dataColumns];
},
// 获取商品分类
async getCategoryList() {
try {
const res = await cascaderListApi({ type: 0, relation_id: 0 });
this.categoryList = res.data;
} catch (error) {
this.$Message.error(error.msg || '获取分类失败');
}
},
// 获取商品列表
async getGoodsList() {
this.loading = true;
try {
const res = await changeListApi(this.formValidate);
this.tableData = res.data.list || [];
this.total = res.data.count;
// 回显已选中状态
this.restoreSelection();
} catch (error) {
this.$Message.error(error.msg || '获取商品失败');
} finally {
this.loading = false;
}
},
// 恢复选中状态
restoreSelection() {
if (this.selectedList.length) {
this.tableData.forEach(item => {
if (this.selectedList.some(s => s.id === item.id)) {
item._checked = true;
}
});
}
},
// 分类变化
handleCategoryChange(value) {
this.formValidate.cate_id = value[value.length - 1] || '';
this.formValidate.page = 1;
this.getGoodsList();
},
// 搜索
handleSearch() {
this.formValidate.page = 1;
this.getGoodsList();
},
// 分页变化
handlePageChange(page) {
this.formValidate.page = page;
this.getGoodsList();
},
// 选中行
handleSelect(selection, row) {
if (!this.selectedList.some(item => item.id === row.id)) {
this.selectedList.push(row);
}
},
// 取消选中
handleSelectCancel(selection, row) {
const index = this.selectedList.findIndex(item => item.id === row.id);
if (index > -1) {
this.selectedList.splice(index, 1);
}
},
// 全选
handleSelectAll(selection) {
selection.forEach(row => {
if (!this.selectedList.some(item => item.id === row.id)) {
this.selectedList.push(row);
}
});
},
// 取消全选
handleSelectAllCancel() {
this.tableData.forEach(row => {
const index = this.selectedList.findIndex(item => item.id === row.id);
if (index > -1) {
this.selectedList.splice(index, 1);
}
});
},
// 提交选择
handleSubmit() {
if (!this.selectedList.length) {
this.$Message.warning('请先选择商品');
return;
}
this.$emit('confirm', this.selectedList);
},
// 清空选择
clearSelection() {
this.selectedList = [];
this.$refs.table.selectAll(false);
}
}
};
</script>
<style lang="stylus" scoped>
.goods-list-selector
.search-form
margin-bottom 20px
/deep/ .ivu-form-item
margin-bottom 16px
.input-add
width 200px
.mr14
margin-right 14px
.table-img
width 36px
height 36px
border-radius 4px
overflow hidden
img
width 100%
height 100%
object-fit cover
.line2
display -webkit-box
-webkit-line-clamp 2
-webkit-box-orient vertical
overflow hidden
.page-wrapper
display flex
justify-content flex-end
margin-top 20px
.footer
margin-top 20px
padding-top 20px
border-top 1px solid #e8eaec
</style>
2.4 使用后台组件
<template>
<div class="my-page">
<!-- 使用商品选择器 -->
<Modal
v-model="showGoodsModal"
title="选择商品"
width="900"
:mask-closable="false">
<goods-list-selector
ref="goodsSelector"
:multiple="true"
:choose-type="1"
@confirm="handleGoodsConfirm"
/>
</Modal>
<Button type="primary" @click="showGoodsModal = true">
选择商品
</Button>
<!-- 已选商品展示 -->
<div class="selected-goods" v-if="selectedGoods.length">
<div class="goods-item" v-for="item in selectedGoods" :key="item.id">
<img :src="item.image" />
<span>{{ item.store_name }}</span>
<Icon type="ios-close" @click="removeGoods(item.id)" />
</div>
</div>
</div>
</template>
<script>
import GoodsListSelector from '@/components/goodsList/index.vue';
export default {
components: {
GoodsListSelector
},
data() {
return {
showGoodsModal: false,
selectedGoods: []
};
},
methods: {
handleGoodsConfirm(goods) {
this.selectedGoods = goods;
this.showGoodsModal = false;
},
removeGoods(id) {
const index = this.selectedGoods.findIndex(item => item.id === id);
if (index > -1) {
this.selectedGoods.splice(index, 1);
}
}
}
};
</script>
2.5 全局注册后台组件
在 main.js 中全局注册:
// src/main.js
import Vue from 'vue';
import GoodsList from '@/components/goodsList/index.vue';
import UploadPictures from '@/components/uploadPictures/index.vue';
// 全局注册
Vue.component('GoodsList', GoodsList);
Vue.component('UploadPictures', UploadPictures);
三、组件开发最佳实践
3.1 Props 设计原则
props: {
// 1. 明确类型
count: {
type: Number,
default: 0
},
// 2. 多类型支持
value: {
type: [String, Number],
default: ''
},
// 3. 必填校验
id: {
type: Number,
required: true
},
// 4. 自定义校验
status: {
type: String,
validator: (value) => {
return ['pending', 'success', 'error'].includes(value);
}
},
// 5. 对象/数组默认值用工厂函数
list: {
type: Array,
default: () => []
},
options: {
type: Object,
default: () => ({})
}
}
3.2 事件命名规范
// 使用 kebab-case 命名事件
this.$emit('update:value', newValue); // 支持.sync修饰符
this.$emit('input', newValue); // 支持v-model
this.$emit('change', newValue); // 值变化事件
this.$emit('click', data); // 点击事件
this.$emit('select', item); // 选择事件
this.$emit('confirm', result); // 确认事件
this.$emit('cancel'); // 取消事件
this.$emit('load-more'); // 加载更多事件
3.3 插槽使用
<template>
<div class="my-component">
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="header"></slot>
<slot name="footer"></slot>
<!-- 作用域插槽 -->
<slot name="item" :data="item" :index="index"></slot>
</div>
</template>
<!-- 使用方 -->
<my-component>
<!-- 默认插槽内容 -->
<div>默认内容</div>
<!-- 具名插槽 -->
<template #header>
<h1>头部内容</h1>
</template>
<!-- 作用域插槽 -->
<template #item="{ data, index }">
<div>{{ index }}: {{ data.name }}</div>
</template>
</my-component>
3.4 组件通信方式
// 1. Props / Events(父子通信)
// 父组件
<child :value="data" @change="handleChange"></child>
// 子组件
this.$emit('change', newValue);
// 2. v-model(双向绑定)
// 子组件
props: ['value'],
methods: {
updateValue(val) {
this.$emit('input', val);
}
}
// 3. .sync 修饰符
// 父组件
<child :visible.sync="isVisible"></child>
// 子组件
this.$emit('update:visible', false);
// 4. provide / inject(跨层级)
// 祖先组件
provide() {
return {
theme: this.theme
};
}
// 后代组件
inject: ['theme']
// 5. EventBus(全局事件)
// 发送
this.$eventHub.$emit('eventName', data);
// 接收
this.$eventHub.$on('eventName', handler);
四、常见问题
Q1: 如何实现组件懒加载?
// 路由懒加载
const MyComponent = () => import('@/components/MyComponent.vue');
// 组件懒加载
components: {
MyComponent: () => import('@/components/MyComponent.vue')
}
Q2: 如何在组件中使用全局方法?
// main.js 挂载
Vue.prototype.$toast = (msg) => {
uni.showToast({ title: msg, icon: 'none' });
};
// 组件中使用
this.$toast('提示信息');
Q3: 组件如何响应主题变化?
<script>
import colors from '@/mixins/color.js';
export default {
mixins: [colors],
computed: {
themeStyle() {
return {
'--primary-color': this.colorStyle['--view-theme'],
'--bg-color': this.colorStyle['--view-minorColorTwo']
};
}
}
};
</script>
Q4: 如何创建递归组件?
<!-- TreeNode.vue -->
<template>
<div class="tree-node">
<div class="node-content">{{ node.name }}</div>
<div class="node-children" v-if="node.children">
<!-- 递归调用自身 -->
<tree-node
v-for="child in node.children"
:key="child.id"
:node="child">
</tree-node>
</div>
</div>
</template>
<script>
export default {
name: 'TreeNode', // 必须定义name用于递归
props: {
node: Object
}
};
</script>
Q5: 如何优化大型组件的性能?
export default {
// 1. 使用计算属性缓存
computed: {
processedList() {
return this.list.filter(item => item.visible);
}
},
// 2. 使用v-once渲染静态内容
// <div v-once>{{ staticContent }}</div>
// 3. 使用v-memo缓存(Vue3)
// 4. 组件按需加载
components: {
HeavyComponent: () => import('./HeavyComponent.vue')
},
// 5. 长列表使用虚拟滚动
// 6. 避免深层响应式
data() {
return {
// 使用Object.freeze()冻结大数据
bigData: Object.freeze(largeArray)
};
}
};
评论({{cateWiki.comment_num}})
{{commentWhere.order ? '评论从旧到新':'评论从新到旧'}}
{{cateWiki.page_view_num}}人看过该文档
评论(0)
{{commentWhere.order ? '评论从旧到新':'评论从新到旧'}}
15人看过该文档
{{item.user ? item.user.nickname : ''}}
(自评)
{{item.content}}
{{item.create_time}}
删除
搜索结果
为您找到{{wikiCount}}条结果
{{item.page_view_num}}
{{item.like ? item.like.like_num : 0}}
{{item.comment ? item.comment.comment_num : 0}}
位置:
{{path.name}}
{{(i+1) == item.catalogue.path_data.length ? '':'/'}}