first #4

Merged
pficv7wyt merged 1 commits from branch1 into main 10 months ago

@ -0,0 +1,155 @@
<template>
<!-- 使用 Vue 的过渡组件根据传入的 transitionName 决定过渡效果 -->
<transition :name="transitionName">
<!-- visible true 时显示返回顶部按钮应用自定义样式 -->
<div v-show="visible" :style="customStyle" class="back-to-ceiling" @click="backToTop">
<!-- 返回顶部按钮的图标 -->
<svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height:16px;width:16px"><path d="M12.036 15.59a1 1 0 0 1-.997.995H5.032a.996.996 0 0 1-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29a1.003 1.003 0 0 1 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" /></svg>
</div>
</transition>
</template>
<script>
export default {
//
name: 'BackToTop',
//
props: {
// 400
visibilityHeight: {
type: Number,
default: 400
},
// 0
backPosition: {
type: Number,
default: 0
},
//
customStyle: {
type: Object,
default: function() {
return {
right: '50px',
bottom: '50px',
width: '40px',
height: '40px',
'border-radius': '4px',
'line-height': '45px',
background: '#e7eaf1'
}
}
},
// 'fade'
transitionName: {
type: String,
default: 'fade'
}
},
//
data() {
return {
//
visible: false,
//
interval: null,
//
isMoving: false
}
},
//
mounted() {
//
window.addEventListener('scroll', this.handleScroll)
},
//
beforeDestroy() {
//
window.removeEventListener('scroll', this.handleScroll)
//
if (this.interval) {
clearInterval(this.interval)
}
},
//
methods: {
//
handleScroll() {
// visibilityHeight
this.visible = window.pageYOffset > this.visibilityHeight
},
//
backToTop() {
//
if (this.isMoving) return
//
const start = window.pageYOffset
let i = 0
//
this.isMoving = true
//
this.interval = setInterval(() => {
// 使
const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
if (next <= this.backPosition) {
//
window.scrollTo(0, this.backPosition)
//
clearInterval(this.interval)
//
this.isMoving = false
} else {
//
window.scrollTo(0, next)
}
i++
}, 16.7)
},
/**
* 缓动函数实现二次方的缓入缓出效果
* @param {number} t - 当前时间从动画开始到现在的时间
* @param {number} b - 起始值
* @param {number} c - 变化量最终值 - 起始值
* @param {number} d - 持续时间
* @returns {number} - 当前时间对应的数值
*/
easeInOutQuad(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b
return -c / 2 * (--t * (t - 2) - 1) + b
}
}
}
</script>
<style scoped>
/* 返回顶部按钮的基础样式 */
.back-to-ceiling {
position: fixed;
display: inline-block;
text-align: center;
cursor: pointer;
}
/* 返回顶部按钮的悬停样式 */
.back-to-ceiling:hover {
background: #d5dbe7;
}
/* 淡入动画的激活状态样式,过渡时间为 0.5 秒 */
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
}
/* 淡入动画的起始状态和淡出动画的结束状态样式,透明度为 0 */
.fade-enter,
.fade-leave-to {
opacity: 0
}
/* 返回顶部按钮图标的样式 */
.back-to-ceiling .Icon {
fill: #9aaabf;
background: none;
}
</style>

@ -0,0 +1,138 @@
<template>
<!-- 定义面包屑导航组件使用 Element UI el-breadcrumb 组件设置分隔符为 / -->
<el-breadcrumb class="app-breadcrumb" separator="/">
<!-- 使用过渡组件实现面包屑项切换时的动画效果动画名称为 breadcrumb -->
<transition-group name="breadcrumb">
<!-- 遍历 levelList 数组为每个元素生成一个面包屑项 -->
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<!-- 如果当前项不需要重定向或者是最后一项则显示为普通文本 -->
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<!-- 否则显示为可点击的链接点击时调用 handleLink 方法 -->
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
// pathToRegexp
import pathToRegexp from 'path-to-regexp'
export default {
/**
* 组件的数据函数返回组件的初始数据
* @returns {Object} 包含 levelList 的数据对象
*/
data() {
return {
//
levelList: null
}
},
/**
* 组件的监听器监听路由变化
*/
watch: {
/**
* 监听 $route 对象的变化
* @param {Object} route - 新的路由对象
*/
$route(route) {
//
if (route.path.startsWith('/redirect/')) {
return
}
// getBreadcrumb
this.getBreadcrumb()
}
},
/**
* 组件创建完成后的钩子函数
*/
created() {
// getBreadcrumb
this.getBreadcrumb()
},
/**
* 组件的方法
*/
methods: {
/**
* 获取面包屑导航数据
*/
getBreadcrumb() {
// meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
//
const first = matched[0]
//
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: '控制台' }}].concat(matched)
}
// meta.title meta.breadcrumb false
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
/**
* 判断路由是否为控制台页面
* @param {Object} route - 路由对象
* @returns {boolean} 是否为控制台页面
*/
isDashboard(route) {
//
const name = route && route.name
// false
if (!name) {
return false
}
//
return name.trim().toLocaleLowerCase() === '控制台'.toLocaleLowerCase()
},
/**
* 编译路径解决路径参数问题
* @param {string} path - 原始路径
* @returns {string} 编译后的路径
*/
pathCompile(path) {
//
const { params } = this.$route
// 使 pathToRegexp
var toPath = pathToRegexp.compile(path)
//
return toPath(params)
},
/**
* 处理面包屑项的点击事件
* @param {Object} item - 点击的面包屑项
*/
handleLink(item) {
//
const { redirect, path } = item
//
if (redirect) {
this.$router.push(redirect)
return
}
//
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
/* 定义面包屑导航的样式 */
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
/* 定义不可点击的面包屑项的样式 */
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

@ -0,0 +1,296 @@
<template>
<!-- 组件容器使用 app-container 类名 -->
<div class="app-container">
<!-- 筛选条件容器 -->
<div class="filter-container">
<!-- 插槽用于插入自定义筛选内容 -->
<slot name="filter-content" />
<!-- Element UI 行布局 -->
<el-row>
<!-- Element UI 列布局 -->
<el-col>
<!-- options.addRoute 存在时显示添加按钮 -->
<el-button v-if="options.addRoute" type="primary" icon="el-icon-plus" @click="handleAdd"></el-button>
</el-col>
</el-row>
</div>
<!-- multiShow true options.multiActions 存在时显示批量操作容器 -->
<div v-show="multiShow && options.multiActions" class="filter-container">
<!-- Element UI 下拉选择框用于选择批量操作 -->
<el-select v-model="multiNow" :placeholder="selectedLabel" class="filter-item" style="width: 130px" @change="handleOption">
<!-- 循环渲染批量操作选项 -->
<el-option
v-for="item in options.multiActions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<!-- Element UI 表格显示数据列表 -->
<el-table
v-loading="listLoading"
:data="dataList.records"
:header-cell-style="{'background':'#f2f3f4', 'color':'#555', 'font-weight':'bold', 'line-height':'32px'}"
border
fit
highlight-current-row
@selection-change="handleSelection"
>
<!-- options.multi true 时显示多选列 -->
<el-table-column
v-if="options.multi"
align="center"
type="selection"
width="55"
/>
<!-- 插槽用于插入自定义表格列 -->
<slot name="data-columns" />
</el-table>
<!-- 分页组件当总数据量大于 0 时显示 -->
<pagination v-show="dataList.total>0" :total="dataList.total" :page.sync="listQuery.current" :limit.sync="listQuery.size" @pagination="getList" />
</div>
</template>
<script>
// API
import { fetchList, deleteData, changeState } from '@/api/common'
//
import Pagination from '@/components/Pagination'
export default {
//
name: 'PagingTable',
//
components: { Pagination },
//
props: {
options: {
type: Object,
default: () => {
return {
//
multiActions: [],
// API
listUrl: '/exam/api',
// API
deleteUrl: '',
// API
stateUrl: '',
//
multi: false
}
}
},
//
listQuery: {
type: Object,
default: () => {
return {
//
current: 1,
//
size: 10,
//
params: {},
//
t: 0
}
}
}
},
data() {
return {
//
dataList: {
//
total: 0
},
//
listLoading: true,
// ID
selectedIds: [],
//
selectedObjs: [],
//
selectedLabel: '',
//
multiShow: false,
//
multiNow: ''
}
},
watch: {
//
listQuery: {
handler() {
this.getList()
},
deep: true
}
},
//
created() {
this.getList()
},
methods: {
/**
* 添加数据跳转方法
* 如果 options.addRoute 存在则跳转到指定路由否则打印提示信息
*/
handleAdd() {
if (this.options.addRoute) {
this.$router.push({ name: this.options.addRoute, params: {}})
return
}
console.log('未设置添加数据跳转路由!')
},
/**
* 查询数据列表方法
* 发送请求获取数据列表并更新组件数据
*/
getList() {
this.listLoading = true
this.listQuery.t = new Date().getTime()
fetchList(this.options.listUrl, this.listQuery).then(response => {
this.dataList = response.data
this.listLoading = false
})
},
/**
* 搜索方法
* 重新调用获取数据列表的方法
*/
handleFilter() {
//
this.getList()
},
/**
* 批量操作回调方法
* 根据选中的批量操作标识执行相应操作
* @param {string} v - 当前选中的批量操作标识
*/
handleOption(v) {
this.multiNow = ''
//
if (v === 'delete') {
this.handleDelete()
return
}
if (v === 'enable') {
this.handleState(0)
return
}
if (v === 'disable') {
this.handleState(1)
return
}
//
this.$emit('multi-actions', { opt: v, ids: this.selectedIds })
},
/**
* 修改状态方法用于启用或禁用数据
* 发送请求修改选中数据的状态并在成功后重新获取数据列表
* @param {number} state - 要修改成的状态值
*/
handleState(state) {
//
changeState(this.options.stateUrl, this.selectedIds, state).then(response => {
if (response.code === 0) {
this.$message({
type: 'success',
message: '状态修改成功!'
})
//
this.getList()
}
})
},
/**
* 删除数据方法
* 先检查是否有选中数据有则弹出确认对话框确认后发送删除请求并重新获取数据列表
*/
handleDelete() {
if (this.selectedIds.length === 0) {
this.$message({
message: '请至少选择一条数据!',
type: 'warning'
})
return
}
//
this.$confirm('确实要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteData(this.options.deleteUrl, this.selectedIds).then(() => {
this.$message({
type: 'success',
message: '删除成功!'
})
this.getList()
})
})
},
/**
* 列表多选操作方法
* 处理表格多选变化事件更新选中数据的相关信息并触发事件
* @param {Array} val - 当前选中的数据对象数组
*/
handleSelection(val) {
const ids = []
val.forEach(row => {
ids.push(row.id)
})
this.selectedObjs = val
this.selectedIds = ids
this.multiShow = ids.length > 0
this.selectedLabel = '已选' + ids.length + '项'
this.$emit('select-changed', { ids: this.selectedIds, objs: this.selectedObjs })
}
}
}
</script>
<style>
/* 筛选容器内筛选项的样式,设置左外边距 */
.filter-container .filter-item{
margin-left: 5px;
}
/* 筛选容器内第一个筛选项的样式,取消左外边距 */
.filter-container .filter-item:first-child{
margin-left: 0px;
}
</style>

@ -0,0 +1,250 @@
<!-- 树状选择器 -->
<template>
<!-- Element UI 弹出框组件用于显示树状菜单 -->
<el-popover
ref="popover"
placement="bottom-start"
trigger="click"
@show="onShowPopover"
@hide="onHidePopover"
>
<!-- Element UI 树组件用于显示部门树状结构 -->
<el-tree
ref="tree"
:style="`min-width: ${treeWidth}`"
:data="data"
:props="props"
:expand-on-click-node="false"
:filter-node-method="filterNode"
placeholder="选择部门"
class="select-tree"
:check-strictly="false"
highlight-current
default-expand-all
@node-click="onClickNode"
/>
<!-- Element UI 输入框组件作为弹出框的触发元素 -->
<el-input
slot="reference"
ref="input"
v-model="labelModel"
:style="`width: ${width}px`"
:class="{ 'rotate': showStatus }"
:placeholder="placeholder"
clearable
suffix-icon="el-icon-arrow-down"
/>
</el-popover>
</template>
<script>
export default {
//
name: 'DepartTree',
// prop value selected
model: {
prop: 'value',
event: 'selected'
},
//
props: {
//
value: String,
//
width: String,
//
options: {
type: Array,
required: true
},
//
placeholder: {
type: String,
required: false,
default: '请选择'
},
//
props: {
type: Object,
required: false,
default: () => ({
parent: 'parentId',
value: 'rowGuid',
label: 'areaName',
children: 'children'
})
}
},
//
data() {
return {
//
showStatus: false,
//
treeWidth: 'auto',
//
labelModel: '',
//
valueModel: '0'
}
},
//
computed: {
//
dataType() {
const jsonStr = JSON.stringify(this.options)
return jsonStr.indexOf(this.props.children) !== -1
},
//
data() {
return this.dataType ? this.options : this.switchTree()
}
},
//
watch: {
//
labelModel(val) {
if (!val) {
this.valueModel = ''
}
this.$refs.tree.filter(val)
},
//
value(val) {
this.labelModel = this.queryTree(this.data, val)
}
},
//
created() {
//
if (this.value) {
this.labelModel = this.queryTree(this.data, this.value)
}
// DOM
this.$nextTick(() => {
this.treeWidth = `${(this.width || this.$refs.input.$refs.input.clientWidth) - 24}px`
})
},
//
methods: {
//
onClickNode(node) {
//
this.labelModel = node[this.props.label]
//
this.valueModel = node[this.props.value]
//
this.onCloseTree()
},
//
switchTree() {
return this.cleanChildren(this.buildTree(this.options, '0'))
},
//
onCloseTree() {
this.$refs.popover.showPopper = false
},
//
onShowPopover() {
//
this.showStatus = true
//
this.$refs.tree.filter(false)
},
//
onHidePopover() {
//
this.showStatus = false
// selected
this.$emit('selected', this.valueModel)
},
//
filterNode(query, data) {
//
if (!query) return true
// label
return data[this.props.label].indexOf(query) !== -1
},
// ID label
queryTree(tree, id) {
let stark = []
stark = stark.concat(tree)
while (stark.length) {
const temp = stark.shift()
if (temp[this.props.children]) {
stark = stark.concat(temp[this.props.children])
}
if (temp[this.props.value] === id) {
return temp[this.props.label]
}
}
return ''
},
//
buildTree(data, id = '0') {
const fa = (parentId) => {
const temp = []
for (let i = 0; i < data.length; i++) {
const n = data[i]
if (n[this.props.parent] === parentId) {
n.children = fa(n.rowGuid)
temp.push(n)
}
}
return temp
}
return fa(id)
},
// children
cleanChildren(data) {
const fa = (list) => {
list.map((e) => {
if (e.children.length) {
fa(e.children)
} else {
delete e.children
}
return e
})
return list
}
return fa(data)
}
}
}
</script>
<style>
/* 带后缀图标的输入框样式 */
.el-input.el-input--suffix {
cursor: pointer;
overflow: hidden;
}
/* 带后缀图标且旋转状态的输入框样式,使后缀图标旋转 180 度 */
.el-input.el-input--suffix.rotate .el-input__suffix {
transform: rotate(180deg);
}
/* 树状选择器样式,设置最大高度和垂直滚动条 */
.select-tree {
max-height: 350px;
overflow-y: scroll;
}
/* 菜单滚动条样式 */
.select-tree::-webkit-scrollbar {
z-index: 11;
width: 6px;
}
.select-tree::-webkit-scrollbar-track,
.select-tree::-webkit-scrollbar-corner {
background: #fff;
}
.select-tree::-webkit-scrollbar-thumb {
border-radius: 5px;
width: 6px;
background: #b4bccc;
}
.select-tree::-webkit-scrollbar-track-piece {
background: #fff;
width: 6px;
}
</style>
Loading…
Cancel
Save