初始化项目

This commit is contained in:
zx
2026-03-09 15:02:47 +08:00
commit 97b070a1a4
274 changed files with 24365 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<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>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import usePermissionStore from '@/store/modules/permission'
const route = useRoute()
const router = useRouter()
const permissionStore = usePermissionStore()
const levelList = ref([])
function getBreadcrumb() {
// only show routes with meta.title
let matched = []
const pathNum = findPathNum(route.path)
// multi-level menu
if (pathNum > 2) {
const reg = /\/\w+/gi
const pathList = route.path.match(reg).map((item, index) => {
if (index !== 0) item = item.slice(1)
return item
})
getMatched(pathList, permissionStore.defaultRoutes, matched)
} else {
matched = route.matched.filter((item) => item.meta && item.meta.title)
}
// 判断是否为首页
if (!isDashboard(matched[0])) {
matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function findPathNum(str, char = "/") {
let index = str.indexOf(char)
let num = 0
while (index !== -1) {
num++
index = str.indexOf(char, index + 1)
}
return num
}
function getMatched(pathList, routeList, matched) {
let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
if (data) {
matched.push(data)
if (data.children && pathList.length) {
pathList.shift()
getMatched(pathList, data.children, matched)
}
}
}
function isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
}
function handleLink(item) {
const { redirect, path } = item
if (redirect) {
router.push(redirect)
return
}
router.push(path)
}
watchEffect(() => {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
})
getBreadcrumb()
</script>
<style lang='scss' scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * ? / L W]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
不指定
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
周期从
<el-input-number v-model='cycle01' :min="1" :max="30" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
<el-input-number v-model='average01' :min="1" :max="30" /> 号开始
<el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="5">
每月
<el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="6">
本月最后一天
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="7">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 31" :key="item" :label="item" :value="item" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(1)
const cycle02 = ref(2)
const average01 = ref(1)
const average02 = ref(1)
const workday = ref(1)
const checkboxList = ref([])
const checkCopy = ref([1])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 30)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 30)
average02.value = props.check(average02.value, 1, 31 - average01.value)
return average01.value + '/' + average02.value
})
const workdayTotal = computed(() => {
workday.value = props.check(workday.value, 1, 31)
return workday.value + 'W'
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.day, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === "*") {
radioValue.value = 1
} else if (value === "?") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else if (value.indexOf("W") > -1) {
const indexArr = value.split("W")
workday.value = Number(indexArr[0])
radioValue.value = 5
} else if (value === "L") {
radioValue.value = 6
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 7
}
}
// 单选按钮值变化时
function onRadioChange() {
if (radioValue.value === 2 && props.cron.week === '?') {
emit('update', 'week', '*', 'day')
}
if (radioValue.value !== 2 && props.cron.week !== '?') {
emit('update', 'week', '?', 'day')
}
switch (radioValue.value) {
case 1:
emit('update', 'day', '*', 'day')
break
case 2:
emit('update', 'day', '?', 'day')
break
case 3:
emit('update', 'day', cycleTotal.value, 'day')
break
case 4:
emit('update', 'day', averageTotal.value, 'day')
break
case 5:
emit('update', 'day', workdayTotal.value, 'day')
break
case 6:
emit('update', 'day', 'L', 'day')
break
case 7:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'day', checkboxString.value, 'day')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
小时允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="0" :max="22" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="0" :max="22" /> 时开始
<el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 22)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 22)
average02.value = props.check(average02.value, 1, 23 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.hour, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (props.cron.min === '*') {
emit('update', 'min', '0', 'hour')
}
if (props.cron.second === '*') {
emit('update', 'second', '0', 'hour')
}
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'hour', '*', 'hour')
break
case 2:
emit('update', 'hour', cycleTotal.value, 'hour')
break
case 3:
emit('update', 'hour', averageTotal.value, 'hour')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'hour', checkboxString.value, 'hour')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane label="秒" v-if="shouldHide('second')">
<CrontabSecond
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronsecond"
/>
</el-tab-pane>
<el-tab-pane label="分钟" v-if="shouldHide('min')">
<CrontabMin
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmin"
/>
</el-tab-pane>
<el-tab-pane label="小时" v-if="shouldHide('hour')">
<CrontabHour
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronhour"
/>
</el-tab-pane>
<el-tab-pane label="日" v-if="shouldHide('day')">
<CrontabDay
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronday"
/>
</el-tab-pane>
<el-tab-pane label="月" v-if="shouldHide('month')">
<CrontabMonth
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronmonth"
/>
</el-tab-pane>
<el-tab-pane label="周" v-if="shouldHide('week')">
<CrontabWeek
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronweek"
/>
</el-tab-pane>
<el-tab-pane label="年" v-if="shouldHide('year')">
<CrontabYear
@update="updateCrontabValue"
:check="checkNumber"
:cron="crontabValueObj"
ref="cronyear"
/>
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">时间表达式</p>
<table>
<thead>
<tr>
<th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
</td>
<td class="result">
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
</tr>
</tbody>
</table>
</div>
<CrontabResult :ex="crontabValueString"></CrontabResult>
<div class="pop_btn">
<el-button type="primary" @click="submitFill">确定</el-button>
<el-button type="warning" @click="clearCron">重置</el-button>
<el-button @click="hidePopup">取消</el-button>
</div>
</div>
</div>
</template>
<script setup>
import CrontabSecond from "./second.vue"
import CrontabMin from "./min.vue"
import CrontabHour from "./hour.vue"
import CrontabDay from "./day.vue"
import CrontabMonth from "./month.vue"
import CrontabWeek from "./week.vue"
import CrontabYear from "./year.vue"
import CrontabResult from "./result.vue"
const { proxy } = getCurrentInstance()
const emit = defineEmits(['hide', 'fill'])
const props = defineProps({
hideComponent: {
type: Array,
default: () => [],
},
expression: {
type: String,
default: ""
}
})
const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
const tabActive = ref(0)
const hideComponent = ref([])
const expression = ref('')
const crontabValueObj = ref({
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
})
const crontabValueString = computed(() => {
const obj = crontabValueObj.value
return obj.second
+ " "
+ obj.min
+ " "
+ obj.hour
+ " "
+ obj.day
+ " "
+ obj.month
+ " "
+ obj.week
+ (obj.year === "" ? "" : " " + obj.year)
})
watch(expression, () => resolveExp())
function shouldHide(key) {
return !(hideComponent.value && hideComponent.value.includes(key))
}
function resolveExp() {
// 反解析 表达式
if (expression.value) {
const arr = expression.value.split(/\s+/)
if (arr.length >= 6) {
//6 位以上是合法表达式
let obj = {
second: arr[0],
min: arr[1],
hour: arr[2],
day: arr[3],
month: arr[4],
week: arr[5],
year: arr[6] ? arr[6] : ""
}
crontabValueObj.value = {
...obj,
}
}
} else {
// 没有传入的表达式 则还原
clearCron()
}
}
// tab切换值
function tabCheck(index) {
tabActive.value = index
}
// 由子组件触发,更改表达式组成的字段值
function updateCrontabValue(name, value, from) {
crontabValueObj.value[name] = value
}
// 表单选项的子组件校验数字格式(通过-props传递
function checkNumber(value, minLimit, maxLimit) {
// 检查必须为整数
value = Math.floor(value)
if (value < minLimit) {
value = minLimit
} else if (value > maxLimit) {
value = maxLimit
}
return value
}
// 隐藏弹窗
function hidePopup() {
emit("hide")
}
// 填充表达式
function submitFill() {
emit("fill", crontabValueString.value)
hidePopup()
}
function clearCron() {
// 还原选择项
crontabValueObj.value = {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
}
onMounted(() => {
expression.value = props.expression
hideComponent.value = props.hideComponent
})
</script>
<style lang="scss" scoped>
.pop_btn {
text-align: center;
margin-top: 20px;
}
.popup-main {
position: relative;
margin: 10px auto;
border-radius: 5px;
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
box-sizing: border-box;
line-height: 24px;
margin: 25px auto;
padding: 15px 10px 10px;
border: 1px solid #ccc;
position: relative;
}
.popup-result .title {
position: absolute;
top: -28px;
left: 50%;
width: 140px;
font-size: 14px;
margin-left: -70px;
text-align: center;
line-height: 30px;
background: #fff;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table td:not(.result) {
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 30px;
height: 30px;
white-space: nowrap;
overflow: hidden;
border: 1px solid #e8e8e8;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
分钟允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 58)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 58)
average02.value = props.check(average02.value, 1, 59 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.min, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'min', '*', 'min')
break
case 2:
emit('update', 'min', cycleTotal.value, 'min')
break
case 3:
emit('update', 'min', averageTotal.value, 'min')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'min', checkboxString.value, 'min')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 19.8rem;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="1" :max="11" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="1" :max="11" /> 月开始
<el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(1)
const cycle02 = ref(2)
const average01 = ref(1)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([1])
const monthList = ref([
{key: 1, value: '一月'},
{key: 2, value: '二月'},
{key: 3, value: '三月'},
{key: 4, value: '四月'},
{key: 5, value: '五月'},
{key: 6, value: '六月'},
{key: 7, value: '七月'},
{key: 8, value: '八月'},
{key: 9, value: '九月'},
{key: 10, value: '十月'},
{key: 11, value: '十一月'},
{key: 12, value: '十二月'}
])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 11)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 11)
average02.value = props.check(average02.value, 1, 12 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.month, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'month', '*', 'month')
break
case 2:
emit('update', 'month', cycleTotal.value, 'month')
break
case 3:
emit('update', 'month', averageTotal.value, 'month')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'month', checkboxString.value, 'month')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,540 @@
<template>
<div class="popup-result">
<p class="title">最近5次运行时间</p>
<ul class="popup-result-scroll">
<template v-if='isShow'>
<li v-for='item in resultList' :key="item">{{item}}</li>
</template>
<li v-else>计算结果中...</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps({
ex: {
type: String,
default: ''
}
})
const dayRule = ref('')
const dayRuleSup = ref('')
const dateArr = ref([])
const resultList = ref([])
const isShow = ref(false)
watch(() => props.ex, () => expressionChange())
// 表达式值变化时,开始去计算结果
function expressionChange() {
// 计算开始-隐藏结果
isShow.value = false
// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
let ruleArr = props.ex.split(' ')
// 用于记录进入循环的次数
let nums = 0
// 用于暂时存符号时间规则结果的数组
let resultArr = []
// 获取当前时间精确至[年、月、日、时、分、秒]
let nTime = new Date()
let nYear = nTime.getFullYear()
let nMonth = nTime.getMonth() + 1
let nDay = nTime.getDate()
let nHour = nTime.getHours()
let nMin = nTime.getMinutes()
let nSecond = nTime.getSeconds()
// 根据规则获取到近100年可能年数组、月数组等等
getSecondArr(ruleArr[0])
getMinArr(ruleArr[1])
getHourArr(ruleArr[2])
getDayArr(ruleArr[3])
getMonthArr(ruleArr[4])
getWeekArr(ruleArr[5])
getYearArr(ruleArr[6], nYear)
// 将获取到的数组赋值-方便使用
let sDate = dateArr.value[0]
let mDate = dateArr.value[1]
let hDate = dateArr.value[2]
let DDate = dateArr.value[3]
let MDate = dateArr.value[4]
let YDate = dateArr.value[5]
// 获取当前时间在数组中的索引
let sIdx = getIndex(sDate, nSecond)
let mIdx = getIndex(mDate, nMin)
let hIdx = getIndex(hDate, nHour)
let DIdx = getIndex(DDate, nDay)
let MIdx = getIndex(MDate, nMonth)
let YIdx = getIndex(YDate, nYear)
// 重置月日时分秒的函数(后面用的比较多)
const resetSecond = function () {
sIdx = 0
nSecond = sDate[sIdx]
}
const resetMin = function () {
mIdx = 0
nMin = mDate[mIdx]
resetSecond()
}
const resetHour = function () {
hIdx = 0
nHour = hDate[hIdx]
resetMin()
}
const resetDay = function () {
DIdx = 0
nDay = DDate[DIdx]
resetHour()
}
const resetMonth = function () {
MIdx = 0
nMonth = MDate[MIdx]
resetDay()
}
// 如果当前年份不为数组中当前值
if (nYear !== YDate[YIdx]) {
resetMonth()
}
// 如果当前月份不为数组中当前值
if (nMonth !== MDate[MIdx]) {
resetDay()
}
// 如果当前“日”不为数组中当前值
if (nDay !== DDate[DIdx]) {
resetHour()
}
// 如果当前“时”不为数组中当前值
if (nHour !== hDate[hIdx]) {
resetMin()
}
// 如果当前“分”不为数组中当前值
if (nMin !== mDate[mIdx]) {
resetSecond()
}
// 循环年份数组
goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
let YY = YDate[Yi]
// 如果到达最大值时
if (nMonth > MDate[MDate.length - 1]) {
resetMonth()
continue
}
// 循环月份数组
goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
// 赋值、方便后面运算
let MM = MDate[Mi];
MM = MM < 10 ? '0' + MM : MM
// 如果到达最大值时
if (nDay > DDate[DDate.length - 1]) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue
}
// 循环日期数组
goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
// 赋值、方便后面运算
let DD = DDate[Di]
let thisDD = DD < 10 ? '0' + DD : DD
// 如果到达最大值时
if (nHour > hDate[hDate.length - 1]) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue
}
// 判断日期的合法性,不合法的话也是跳出当前循环
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
resetDay()
continue goMonth
}
// 如果日期规则中有值时
if (dayRule.value === 'lastDay') {
// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
} else if (dayRule.value === 'workDay') {
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取达到条件的日期是星期X
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 当星期日时
if (thisWeek === 1) {
// 先找下一个日,并判断是否为月底
DD++
thisDD = DD < 10 ? '0' + DD : DD
// 判断下一日已经不是合法日期
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD -= 3
}
} else if (thisWeek === 7) {
// 当星期6时只需判断不是1号就可进行操作
if (dayRuleSup.value !== 1) {
DD--
} else {
DD += 2
}
}
} else if (dayRule.value === 'weekDay') {
// 如果指定了是星期几
// 获取当前日期是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
// 校验当前星期是否在星期池dayRuleSup
if (dayRuleSup.value.indexOf(thisWeek) < 0) {
// 如果到达最大值时
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue
}
} else if (dayRule.value === 'assWeek') {
// 如果指定了是第几周的星期几
// 获取每月1号是属于星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
if (dayRuleSup.value[1] >= thisWeek) {
DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
} else {
DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
}
} else if (dayRule.value === 'lastWeek') {
// 如果指定了每月最后一个星期几
// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
DD--
thisDD = DD < 10 ? '0' + DD : DD
}
}
// 获取月末最后一天是星期几
let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
// 找到要求中最近的那个星期几
if (dayRuleSup.value < thisWeek) {
DD -= thisWeek - dayRuleSup.value
} else if (dayRuleSup.value > thisWeek) {
DD -= 7 - (dayRuleSup.value - thisWeek)
}
}
// 判断时间值是否小于10置换成“05”这种格式
DD = DD < 10 ? '0' + DD : DD
// 循环“时”数组
goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
// 如果到达最大值时
if (nMin > mDate[mDate.length - 1]) {
resetMin()
if (hi === hDate.length - 1) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue goDay
}
continue
}
// 循环"分"数组
goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
// 如果到达最大值时
if (nSecond > sDate[sDate.length - 1]) {
resetSecond()
if (mi === mDate.length - 1) {
resetMin()
if (hi === hDate.length - 1) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue goDay
}
continue goHour
}
continue
}
// 循环"秒"数组
goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
// 添加当前时间(时间合法性在日期循环时已经判断)
if (MM !== '00' && DD !== '00') {
resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
nums++
}
// 如果条数满了就退出循环
if (nums === 5) break goYear
// 如果到达最大值时
if (si === sDate.length - 1) {
resetSecond()
if (mi === mDate.length - 1) {
resetMin()
if (hi === hDate.length - 1) {
resetHour()
if (Di === DDate.length - 1) {
resetDay()
if (Mi === MDate.length - 1) {
resetMonth()
continue goYear
}
continue goMonth
}
continue goDay
}
continue goHour
}
continue goMin
}
} //goSecond
} //goMin
}//goHour
}//goDay
}//goMonth
}
// 判断100年内的结果条数
if (resultArr.length === 0) {
resultList.value = ['没有达到条件的结果!']
} else {
resultList.value = resultArr
if (resultArr.length !== 5) {
resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
}
}
// 计算完成-显示结果
isShow.value = true
}
// 用于计算某位数字在数组中的索引
function getIndex(arr, value) {
if (value <= arr[0] || value > arr[arr.length - 1]) {
return 0
} else {
for (let i = 0; i < arr.length - 1; i++) {
if (value > arr[i] && value <= arr[i + 1]) {
return i + 1
}
}
}
}
// 获取"年"数组
function getYearArr(rule, year) {
dateArr.value[5] = getOrderArr(year, year + 100)
if (rule !== undefined) {
if (rule.indexOf('-') >= 0) {
dateArr.value[5] = getCycleArr(rule, year + 100, false)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[5] = getAverageArr(rule, year + 100)
} else if (rule !== '*') {
dateArr.value[5] = getAssignArr(rule)
}
}
}
// 获取"月"数组
function getMonthArr(rule) {
dateArr.value[4] = getOrderArr(1, 12)
if (rule.indexOf('-') >= 0) {
dateArr.value[4] = getCycleArr(rule, 12, false)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[4] = getAverageArr(rule, 12)
} else if (rule !== '*') {
dateArr.value[4] = getAssignArr(rule)
}
}
// 获取"日"数组-主要为日期规则
function getWeekArr(rule) {
// 只有当日期规则的两个值均为“”时则表达日期是有选项的
if (dayRule.value === '' && dayRuleSup.value === '') {
if (rule.indexOf('-') >= 0) {
dayRule.value = 'weekDay'
dayRuleSup.value = getCycleArr(rule, 7, false)
} else if (rule.indexOf('#') >= 0) {
dayRule.value = 'assWeek'
let matchRule = rule.match(/[0-9]{1}/g)
dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
dateArr.value[3] = [1]
if (dayRuleSup.value[1] === 7) {
dayRuleSup.value[1] = 0
}
} else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastWeek'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [31]
if (dayRuleSup.value === 7) {
dayRuleSup.value = 0
}
} else if (rule !== '*' && rule !== '?') {
dayRule.value = 'weekDay'
dayRuleSup.value = getAssignArr(rule)
}
}
}
// 获取"日"数组-少量为日期规则
function getDayArr(rule) {
dateArr.value[3] = getOrderArr(1, 31)
dayRule.value = ''
dayRuleSup.value = ''
if (rule.indexOf('-') >= 0) {
dateArr.value[3] = getCycleArr(rule, 31, false)
dayRuleSup.value = 'null'
} else if (rule.indexOf('/') >= 0) {
dateArr.value[3] = getAverageArr(rule, 31)
dayRuleSup.value = 'null'
} else if (rule.indexOf('W') >= 0) {
dayRule.value = 'workDay'
dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
dateArr.value[3] = [dayRuleSup.value]
} else if (rule.indexOf('L') >= 0) {
dayRule.value = 'lastDay'
dayRuleSup.value = 'null'
dateArr.value[3] = [31]
} else if (rule !== '*' && rule !== '?') {
dateArr.value[3] = getAssignArr(rule)
dayRuleSup.value = 'null'
} else if (rule === '*') {
dayRuleSup.value = 'null'
}
}
// 获取"时"数组
function getHourArr(rule) {
dateArr.value[2] = getOrderArr(0, 23)
if (rule.indexOf('-') >= 0) {
dateArr.value[2] = getCycleArr(rule, 24, true)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[2] = getAverageArr(rule, 23)
} else if (rule !== '*') {
dateArr.value[2] = getAssignArr(rule)
}
}
// 获取"分"数组
function getMinArr(rule) {
dateArr.value[1] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
dateArr.value[1] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[1] = getAverageArr(rule, 59)
} else if (rule !== '*') {
dateArr.value[1] = getAssignArr(rule)
}
}
// 获取"秒"数组
function getSecondArr(rule) {
dateArr.value[0] = getOrderArr(0, 59)
if (rule.indexOf('-') >= 0) {
dateArr.value[0] = getCycleArr(rule, 60, true)
} else if (rule.indexOf('/') >= 0) {
dateArr.value[0] = getAverageArr(rule, 59)
} else if (rule !== '*') {
dateArr.value[0] = getAssignArr(rule)
}
}
// 根据传进来的min-max返回一个顺序的数组
function getOrderArr(min, max) {
let arr = []
for (let i = min; i <= max; i++) {
arr.push(i)
}
return arr
}
// 根据规则中指定的零散值返回一个数组
function getAssignArr(rule) {
let arr = []
let assiginArr = rule.split(',')
for (let i = 0; i < assiginArr.length; i++) {
arr[i] = Number(assiginArr[i])
}
arr.sort(compare)
return arr
}
// 根据一定算术规则计算返回一个数组
function getAverageArr(rule, limit) {
let arr = []
let agArr = rule.split('/')
let min = Number(agArr[0])
let step = Number(agArr[1])
while (min <= limit) {
arr.push(min)
min += step
}
return arr
}
// 根据规则返回一个具有周期性的数组
function getCycleArr(rule, limit, status) {
// status--表示是否从0开始则从1开始
let arr = []
let cycleArr = rule.split('-')
let min = Number(cycleArr[0])
let max = Number(cycleArr[1])
if (min > max) {
max += limit
}
for (let i = min; i <= max; i++) {
let add = 0
if (status === false && i % limit === 0) {
add = limit
}
arr.push(Math.round(i % limit + add))
}
arr.sort(compare)
return arr
}
// 比较数字大小用于Array.sort
function compare(value1, value2) {
if (value2 - value1 > 0) {
return -1
} else {
return 1
}
}
// 格式化日期格式如2017-9-19 18:04:33
function formatDate(value, type) {
// 计算日期相关值
let time = typeof value == 'number' ? new Date(value) : value
let Y = time.getFullYear()
let M = time.getMonth() + 1
let D = time.getDate()
let h = time.getHours()
let m = time.getMinutes()
let s = time.getSeconds()
let week = time.getDay()
// 如果传递了type的话
if (type === undefined) {
return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
} else if (type === 'week') {
// 在quartz中 1为星期日
return week + 1
}
}
// 检查日期是否存在
function checkDate(value) {
let time = new Date(value)
let format = formatDate(time)
return value === format
}
onMounted(() => {
expressionChange()
})
</script>

View File

@@ -0,0 +1,128 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
周期从
<el-input-number v-model='cycle01' :min="0" :max="58" /> -
<el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始
<el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
<el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: "",
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(1)
const average01 = ref(0)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([0])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 0, 58)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 0, 58)
average02.value = props.check(average02.value, 1, 59 - average01.value)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.second, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '*') {
radioValue.value = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 3
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 4
}
}
// 单选按钮值变化时
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'second', '*', 'second')
break
case 2:
emit('update', 'second', cycleTotal.value, 'second')
break
case 3:
emit('update', 'second', averageTotal.value, 'second')
break
case 4:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'second', checkboxString.value, 'second')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<el-form>
<el-form-item>
<el-radio v-model='radioValue' :value="1">
允许的通配符[, - * ? / L #]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="2">
不指定
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="3">
周期从
<el-select clearable v-model="cycle01">
<el-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key === 7"
>{{item.value}}</el-option>
</el-select>
-
<el-select clearable v-model="cycle02">
<el-option
v-for="(item,index) of weekList"
:key="index"
:label="item.value"
:value="item.key"
:disabled="item.key <= cycle01"
>{{item.value}}</el-option>
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="4">
<el-input-number v-model='average01' :min="1" :max="4" /> 周的
<el-select clearable v-model="average02">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="5">
本月最后一个
<el-select clearable v-model="weekday">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model='radioValue' :value="6">
指定
<el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
<el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: ""
}
},
check: {
type: Function,
default: () => {
}
}
})
const radioValue = ref(2)
const cycle01 = ref(2)
const cycle02 = ref(3)
const average01 = ref(1)
const average02 = ref(2)
const weekday = ref(2)
const checkboxList = ref([])
const checkCopy = ref([2])
const weekList = ref([
{key: 1, value: '星期日'},
{key: 2, value: '星期一'},
{key: 3, value: '星期二'},
{key: 4, value: '星期三'},
{key: 5, value: '星期四'},
{key: 6, value: '星期五'},
{key: 7, value: '星期六'}
])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, 1, 6)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, 1, 4)
average02.value = props.check(average02.value, 1, 7)
return average02.value + '#' + average01.value
})
const weekdayTotal = computed(() => {
weekday.value = props.check(weekday.value, 1, 7)
return weekday.value + 'L'
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.week, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === "*") {
radioValue.value = 1
} else if (value === "?") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("#") > -1) {
const indexArr = value.split('#')
average01.value = Number(indexArr[1])
average02.value = Number(indexArr[0])
radioValue.value = 4
} else if (value.indexOf("L") > -1) {
const indexArr = value.split("L")
weekday.value = Number(indexArr[0])
radioValue.value = 5
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 6
}
}
function onRadioChange() {
if (radioValue.value === 2 && props.cron.day === '?') {
emit('update', 'day', '*', 'week')
}
if (radioValue.value !== 2 && props.cron.day !== '?') {
emit('update', 'day', '?', 'week')
}
switch (radioValue.value) {
case 1:
emit('update', 'week', '*', 'week')
break
case 2:
emit('update', 'week', '?', 'week')
break
case 3:
emit('update', 'week', cycleTotal.value, 'week')
break
case 4:
emit('update', 'week', averageTotal.value, 'week')
break
case 5:
emit('update', 'week', weekdayTotal.value, 'week')
break
case 6:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'week', checkboxString.value, 'week')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.5rem;
}
.el-select, .el-select--small {
width: 8rem;
}
.el-select.multiselect, .el-select--small.multiselect {
width: 17.8rem;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<el-form>
<el-form-item>
<el-radio :value="1" v-model='radioValue'>
不填允许的通配符[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="2" v-model='radioValue'>
每年
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="3" v-model='radioValue'>
周期从
<el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="4" v-model='radioValue'>
<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始
<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
</el-radio>
</el-form-item>
<el-form-item>
<el-radio :value="5" v-model='radioValue'>
指定
<el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script setup>
const emit = defineEmits(['update'])
const props = defineProps({
cron: {
type: Object,
default: {
second: "*",
min: "*",
hour: "*",
day: "*",
month: "*",
week: "?",
year: ""
}
},
check: {
type: Function,
default: () => {
}
}
})
const fullYear = Number(new Date().getFullYear())
const maxFullYear = fullYear + 10
const radioValue = ref(1)
const cycle01 = ref(fullYear)
const cycle02 = ref(fullYear + 1)
const average01 = ref(fullYear)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([fullYear])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
average02.value = props.check(average02.value, 1, 10)
return average01.value + '/' + average02.value
})
const checkboxString = computed(() => {
return checkboxList.value.join(',')
})
watch(() => props.cron.year, value => changeRadioValue(value))
watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
function changeRadioValue(value) {
if (value === '') {
radioValue.value = 1
} else if (value === "*") {
radioValue.value = 2
} else if (value.indexOf("-") > -1) {
const indexArr = value.split('-')
cycle01.value = Number(indexArr[0])
cycle02.value = Number(indexArr[1])
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
radioValue.value = 5
}
}
function onRadioChange() {
switch (radioValue.value) {
case 1:
emit('update', 'year', '', 'year')
break
case 2:
emit('update', 'year', '*', 'year')
break
case 3:
emit('update', 'year', cycleTotal.value, 'year')
break
case 4:
emit('update', 'year', averageTotal.value, 'year')
break
case 5:
if (checkboxList.value.length === 0) {
checkboxList.value.push(checkCopy.value[0])
} else {
checkCopy.value = checkboxList.value
}
emit('update', 'year', checkboxString.value, 'year')
break
}
}
</script>
<style lang="scss" scoped>
.el-input-number--small, .el-select, .el-select--small {
margin: 0 0.2rem;
}
.el-select, .el-select--small {
width: 18.8rem;
}
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div>
<template v-for="(item, index) in options">
<template v-if="isValueMatch(item.value)">
<span
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
:key="item.value"
:index="index"
:class="item.elTagClass"
>{{ item.label + " " }}</span>
<el-tag
v-else
:disable-transitions="true"
:key="item.value + ''"
:index="index"
:type="item.elTagType"
:class="item.elTagClass"
>{{ item.label + " " }}</el-tag>
</template>
</template>
<template v-if="unmatch && showValue">
{{ unmatchArray | handleArray }}
</template>
</div>
</template>
<script setup>
// 记录未匹配的项
const unmatchArray = ref([])
const props = defineProps({
// 数据
options: {
type: Array,
default: null,
},
// 当前的值
value: [Number, String, Array],
// 当未找到匹配的数据时显示value
showValue: {
type: Boolean,
default: true,
},
separator: {
type: String,
default: ",",
}
})
const values = computed(() => {
if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
if (typeof props.value === 'number' || typeof props.value === 'boolean') return [props.value]
return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
})
const unmatch = computed(() => {
unmatchArray.value = []
// 没有value不显示
if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
// 传入值为数组
let unmatch = false // 添加一个标志来判断是否有未匹配项
values.value.forEach(item => {
if (!props.options.some(v => v.value == item)) {
unmatchArray.value.push(item)
unmatch = true // 如果有未匹配项将标志设置为true
}
})
return unmatch // 返回标志的值
})
function handleArray(array) {
if (array.length === 0) return ""
return array.reduce((pre, cur) => {
return pre + " " + cur
})
}
function isValueMatch(itemValue) {
return values.value.some(val => val == itemValue)
}
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
name="file"
:show-file-list="false"
:headers="headers"
class="editor-img-uploader"
v-if="type == 'url'"
>
<i ref="uploadRef" class="editor-img-uploader"></i>
</el-upload>
</div>
<div class="editor">
<quill-editor
ref="quillEditorRef"
v-model:content="content"
contentType="html"
@textChange="(e) => $emit('update:modelValue', content)"
:options="options"
:style="styles"
/>
</div>
</template>
<script setup>
import axios from 'axios'
import { QuillEditor } from "@vueup/vue-quill"
import "@vueup/vue-quill/dist/vue-quill.snow.css"
import { getToken } from "@/utils/auth"
const { proxy } = getCurrentInstance()
const quillEditorRef = ref()
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
const headers = ref({
Authorization: "Bearer " + getToken()
})
const props = defineProps({
/* 编辑器的内容 */
modelValue: {
type: String,
},
/* 高度 */
height: {
type: Number,
default: null,
},
/* 最小高度 */
minHeight: {
type: Number,
default: null,
},
/* 只读 */
readOnly: {
type: Boolean,
default: false,
},
/* 上传文件大小限制(MB) */
fileSize: {
type: Number,
default: 5,
},
/* 类型base64格式、url格式 */
type: {
type: String,
default: "url",
}
})
const options = ref({
theme: "snow",
bounds: document.body,
debug: "warn",
modules: {
// 工具栏配置
toolbar: [
["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
["blockquote", "code-block"], // 引用 代码块
[{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
[{ indent: "-1" }, { indent: "+1" }], // 缩进
[{ size: ["small", false, "large", "huge"] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
[{ align: [] }], // 对齐方式
["clean"], // 清除文本格式
["link", "image", "video"] // 链接、图片、视频
],
},
placeholder: "请输入内容",
readOnly: props.readOnly
})
const styles = computed(() => {
let style = {}
if (props.minHeight) {
style.minHeight = `${props.minHeight}px`
}
if (props.height) {
style.height = `${props.height}px`
}
return style
})
const content = ref("")
watch(() => props.modelValue, (v) => {
if (v !== content.value) {
content.value = v == undefined ? "<p></p>" : v
}
}, { immediate: true })
// 如果设置了上传地址则自定义图片上传事件
onMounted(() => {
if (props.type == 'url') {
let quill = quillEditorRef.value.getQuill()
let toolbar = quill.getModule("toolbar")
toolbar.addHandler("image", (value) => {
if (value) {
proxy.$refs.uploadRef.click()
} else {
quill.format("image", false)
}
})
quill.root.addEventListener('paste', handlePasteCapture, true)
}
})
// 上传前校检格式和大小
function handleBeforeUpload(file) {
const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
const isJPG = type.includes(file.type)
//检验文件格式
if (!isJPG) {
proxy.$modal.msgError(`图片格式错误!`)
return false
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
return true
}
// 上传成功处理
function handleUploadSuccess(res, file) {
// 如果上传成功
if (res.code == 200) {
// 获取富文本实例
let quill = toRaw(quillEditorRef.value).getQuill()
// 获取光标位置
let length = quill.selection.savedRange.index
// 插入图片res.url为服务器返回的图片链接地址
quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName)
// 调整光标到最后
quill.setSelection(length + 1)
} else {
proxy.$modal.msgError("图片插入失败")
}
}
// 上传失败处理
function handleUploadError() {
proxy.$modal.msgError("图片插入失败")
}
// 复制粘贴图片处理
function handlePasteCapture(e) {
const clipboard = e.clipboardData || window.clipboardData
if (clipboard && clipboard.items) {
for (let i = 0; i < clipboard.items.length; i++) {
const item = clipboard.items[i]
if (item.type.indexOf('image') !== -1) {
e.preventDefault()
const file = item.getAsFile()
insertImage(file)
}
}
}
}
function insertImage(file) {
const formData = new FormData()
formData.append("file", file)
axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
handleUploadSuccess(res.data)
})
}
</script>
<style>
.editor-img-uploader {
display: none;
}
.editor, .ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
}
.quill-img {
display: none;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: "保存";
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
content: "32px";
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体";
}
</style>

View File

@@ -0,0 +1,256 @@
<template>
<div class="upload-file">
<el-upload
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<!-- 文件列表 -->
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">&nbsp;删除</el-link>
</div>
</li>
</transition-group>
</div>
</template>
<script setup>
import { getToken } from "@/utils/auth"
import Sortable from 'sortablejs'
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
default: "/common/upload"
},
// 上传携带的参数
data: {
type: Object
},
// 数量限制
limit: {
type: Number,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 禁用组件(仅查看文件)
disabled: {
type: Boolean,
default: false
},
// 拖动排序
drag: {
type: Boolean,
default: true
}
})
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
)
watch(() => props.modelValue, val => {
if (val) {
let temp = 1
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(',')
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item }
}
item.uid = item.uid || new Date().getTime() + temp++
return item
})
} else {
fileList.value = []
return []
}
},{ deep: true, immediate: true })
// 上传前校检格式和大小
function handleBeforeUpload(file) {
// 校检文件类型
if (props.fileType.length) {
const fileName = file.name.split('.')
const fileExt = fileName[fileName.length - 1]
const isTypeOk = props.fileType.indexOf(fileExt) >= 0
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
return false
}
}
// 校检文件名是否包含特殊字符
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading("正在上传文件,请稍候...")
number.value++
return true
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传失败
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败")
proxy.$modal.closeLoading()
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.fileUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除文件
function handleDelete(index) {
fileList.value.splice(index, 1)
emit("update:modelValue", listToString(fileList.value))
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading()
}
}
// 获取文件名称
function getFileName(name) {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1)
} else {
return name
}
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = ""
separator = separator || ","
for (let i in list) {
if (list[i].url) {
strs += list[i].url + separator
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : ''
}
// 初始化拖拽排序
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
Sortable.create(element, {
ghostClass: 'file-upload-darg',
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
.file-upload-darg {
opacity: 0.5;
background: #c8ebfb;
}
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
transition: none !important;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
fill="currentColor"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script setup>
defineProps({
isActive: {
type: Boolean,
default: false
}
})
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick')
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,252 @@
<template>
<div class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-dialog
v-model="show"
width="600"
@close="close"
:show-close="false"
append-to-body
>
<el-input
v-model="search"
ref="headerSearchSelectRef"
size="large"
@input="querySearch"
prefix-icon="Search"
placeholder="菜单搜索支持标题、URL模糊查询"
clearable
@keyup.enter="selectActiveResult"
@keydown.up.prevent="navigateResult('up')"
@keydown.down.prevent="navigateResult('down')"
>
</el-input>
<div class="result-wrap">
<el-scrollbar>
<div class="search-item" tabindex="1" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1">
<div class="left">
<svg-icon class="menu-icon" :icon-class="item.icon" />
</div>
<div class="search-info" @click="change(item)">
<div class="menu-title">
{{ item.title.join(" / ") }}
</div>
<div class="menu-path">
{{ item.path }}
</div>
</div>
<svg-icon icon-class="enter" v-show="index === activeIndex"/>
</div>
</el-scrollbar>
</div>
</el-dialog>
</div>
</template>
<script setup>
import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
const search = ref('')
const options = ref([])
const searchPool = ref([])
const activeIndex = ref(-1)
const show = ref(false)
const fuse = ref(undefined)
const headerSearchSelectRef = ref(null)
const router = useRouter()
const theme = computed(() => useSettingsStore().theme)
const routes = computed(() => usePermissionStore().defaultRoutes)
function click() {
show.value = !show.value
if (show.value) {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
options.value = searchPool.value
}
}
function close() {
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
search.value = ''
options.value = []
show.value = false
activeIndex.value = -1
}
function change(val) {
const path = val.path
const query = val.query
if (isHttp(path)) {
// http(s):// 路径新窗口打开
const pindex = path.indexOf("http")
window.open(path.substr(pindex, path.length), "_blank")
} else {
if (query) {
router.push({ path: path, query: JSON.parse(query) })
} else {
router.push(path)
}
}
search.value = ''
options.value = []
nextTick(() => {
show.value = false
})
}
function initFuse(list) {
fuse.value = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
}
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) {
let res = []
for (const r of routes) {
// skip hidden router
if (r.hidden) { continue }
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle],
icon: ''
}
if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title]
data.icon = r.meta.icon
if (r.redirect !== "noRedirect") {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
if (r.query) {
data.query = r.query
}
// recursive child routes
if (r.children) {
const tempRoutes = generateRoutes(r.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
}
function querySearch(query) {
activeIndex.value = -1
if (query !== '') {
options.value = fuse.value.search(query).map((item) => item.item) ?? searchPool.value
} else {
options.value = searchPool.value
}
}
function activeStyle(index) {
if (index !== activeIndex.value) return {}
return {
"background-color": theme.value,
"color": "#fff"
}
}
function navigateResult(direction) {
if (direction === "up") {
activeIndex.value = activeIndex.value <= 0 ? options.value.length - 1 : activeIndex.value - 1
} else if (direction === "down") {
activeIndex.value = activeIndex.value >= options.value.length - 1 ? 0 : activeIndex.value + 1
}
}
function selectActiveResult() {
if (options.value.length > 0 && activeIndex.value >= 0) {
change(options.value[activeIndex.value])
}
}
onMounted(() => {
searchPool.value = generateRoutes(routes.value)
})
watch(searchPool, (list) => {
initFuse(list)
})
</script>
<style lang='scss' scoped>
.header-search {
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
}
.result-wrap {
height: 280px;
margin: 6px 0;
.search-item {
display: flex;
height: 48px;
align-items: center;
padding-right: 10px;
.left {
width: 60px;
text-align: center;
.menu-icon {
width: 18px;
height: 18px;
}
}
.search-info {
padding-left: 5px;
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
.menu-title,
.menu-path {
height: 20px;
}
.menu-path {
color: #ccc;
font-size: 10px;
}
}
}
.search-item:hover {
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<div class="icon-body">
<el-input
v-model="iconName"
class="icon-search"
clearable
placeholder="请输入图标名称"
@clear="filterIcons"
@input="filterIcons"
>
<template #suffix><i class="el-icon-search el-input__icon" /></template>
</el-input>
<div class="icon-list">
<div class="list-container">
<div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
<div :class="['icon-item', { active: activeIcon === item }]">
<svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
<span>{{ item }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import icons from './requireIcons'
const props = defineProps({
activeIcon: {
type: String
}
})
const iconName = ref('')
const iconList = ref(icons)
const emit = defineEmits(['selected'])
function filterIcons() {
iconList.value = icons
if (iconName.value) {
iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
}
}
function selectedIcon(name) {
emit('selected', name)
document.body.click()
}
function reset() {
iconName.value = ''
iconList.value = icons
}
defineExpose({
reset
})
</script>
<style lang='scss' scoped>
.icon-body {
width: 100%;
padding: 10px;
.icon-search {
position: relative;
margin-bottom: 5px;
}
.icon-list {
height: 200px;
overflow: auto;
.list-container {
display: flex;
flex-wrap: wrap;
.icon-item-wrapper {
width: calc(100% / 3);
height: 25px;
line-height: 25px;
cursor: pointer;
display: flex;
.icon-item {
display: flex;
max-width: 100%;
height: 100%;
padding: 0 5px;
&:hover {
background: #ececec;
border-radius: 5px;
}
.icon {
flex-shrink: 0;
}
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
padding-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.icon-item.active {
background: #ececec;
border-radius: 5px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,8 @@
let icons = []
const modules = import.meta.glob('./../../assets/icons/svg/*.svg')
for (const path in modules) {
const p = path.split('assets/icons/svg/')[1].split('.svg')[0]
icons.push(p)
}
export default icons

View File

@@ -0,0 +1,92 @@
<template>
<el-image
:src="`${realSrc}`"
fit="cover"
:style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList"
preview-teleported
>
<template #error>
<div class="image-slot">
<el-icon><picture-filled /></el-icon>
</div>
</template>
</el-image>
</template>
<script setup>
import { isExternal } from "@/utils/validate"
const props = defineProps({
src: {
type: String,
default: ""
},
width: {
type: [Number, String],
default: ""
},
height: {
type: [Number, String],
default: ""
}
})
const realSrc = computed(() => {
if (!props.src) {
return
}
let real_src = props.src.split(",")[0]
if (isExternal(real_src)) {
return real_src
}
return import.meta.env.VITE_APP_BASE_API + real_src
})
const realSrcList = computed(() => {
if (!props.src) {
return
}
let real_src_list = props.src.split(",")
let srcList = []
real_src_list.forEach(item => {
if (isExternal(item)) {
return srcList.push(item)
}
return srcList.push(import.meta.env.VITE_APP_BASE_API + item)
})
return srcList
})
const realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px`
)
const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px`
)
</script>
<style lang="scss" scoped>
.el-image {
border-radius: 5px;
background-color: #ebeef5;
box-shadow: 0 0 5px 1px #ccc;
:deep(.el-image__inner) {
transition: all 0.3s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
:deep(.image-slot) {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 30px;
}
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="component-upload-image">
<el-upload
multiple
:disabled="disabled"
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
ref="imageUpload"
:before-remove="handleDelete"
:show-file-list="true"
:headers="headers"
:file-list="fileList"
:on-preview="handlePictureCardPreview"
:class="{ hide: fileList.length >= limit }"
>
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件
</div>
<el-dialog
v-model="dialogVisible"
title="预览"
width="800px"
append-to-body
>
<img
:src="dialogImageUrl"
style="display: block; max-width: 100%; margin: 0 auto"
/>
</el-dialog>
</div>
</template>
<script setup>
import { getToken } from "@/utils/auth"
import { isExternal } from "@/utils/validate"
import Sortable from 'sortablejs'
const props = defineProps({
modelValue: [String, Object, Array],
// 上传接口地址
action: {
type: String,
default: "/common/upload"
},
// 上传携带的参数
data: {
type: Object
},
// 图片数量限制
limit: {
type: Number,
default: 5
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 5
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"]
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
// 禁用组件(仅查看图片)
disabled: {
type: Boolean,
default: false
},
// 拖动排序
drag: {
type: Boolean,
default: true
}
})
const { proxy } = getCurrentInstance()
const emit = defineEmits()
const number = ref(0)
const uploadList = ref([])
const dialogImageUrl = ref("")
const dialogVisible = ref(false)
const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() })
const fileList = ref([])
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
)
watch(() => props.modelValue, val => {
if (val) {
// 首先将值转为数组
const list = Array.isArray(val) ? val : props.modelValue.split(",")
// 然后将数组转为对象数组
fileList.value = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item }
} else {
item = { name: item, url: item }
}
}
return item
})
} else {
fileList.value = []
return []
}
},{ deep: true, immediate: true })
// 上传前loading加载
function handleBeforeUpload(file) {
let isImg = false
if (props.fileType.length) {
let fileExtension = ""
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
}
isImg = props.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true
if (fileExtension && fileExtension.indexOf(type) > -1) return true
return false
})
} else {
isImg = file.type.indexOf("image") > -1
}
if (!isImg) {
proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
return false
}
if (file.name.includes(',')) {
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
return false
}
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize
if (!isLt) {
proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
return false
}
}
proxy.$modal.loading("正在上传图片,请稍候...")
number.value++
}
// 文件个数超出
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
}
// 上传成功回调
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName })
uploadedSuccessfully()
} else {
number.value--
proxy.$modal.closeLoading()
proxy.$modal.msgError(res.msg)
proxy.$refs.imageUpload.handleRemove(file)
uploadedSuccessfully()
}
}
// 删除图片
function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name)
if (findex > -1 && uploadList.value.length === number.value) {
fileList.value.splice(findex, 1)
emit("update:modelValue", listToString(fileList.value))
return false
}
}
// 上传结束处理
function uploadedSuccessfully() {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
uploadList.value = []
number.value = 0
emit("update:modelValue", listToString(fileList.value))
proxy.$modal.closeLoading()
}
}
// 上传失败
function handleUploadError() {
proxy.$modal.msgError("上传图片失败")
proxy.$modal.closeLoading()
}
// 预览
function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url
dialogVisible.value = true
}
// 对象转成指定字符串分隔
function listToString(list, separator) {
let strs = ""
separator = separator || ","
for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator
}
}
return strs != "" ? strs.substr(0, strs.length - 1) : ""
}
// 初始化拖拽排序
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
Sortable.create(element, {
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
// .el-upload--picture-card 控制加号部分
:deep(.hide .el-upload--picture-card) {
display: none;
}
:deep(.el-upload.el-upload--picture-card.is-disabled) {
display: none !important;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<div :class="{ 'hidden': hidden }" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup>
import { scrollTo } from '@/utils/scroll-to'
const props = defineProps({
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// 移动端页码按钮的数量端默认值5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
})
const emit = defineEmits()
const currentPage = computed({
get() {
return props.page
},
set(val) {
emit('update:page', val)
}
})
const pageSize = computed({
get() {
return props.limit
},
set(val){
emit('update:limit', val)
}
})
function handleSizeChange(val) {
if (currentPage.value * val > props.total) {
currentPage.value = 1
}
emit('pagination', { page: currentPage.value, limit: val })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
}
.pagination-container.hidden {
display: none;
}
</style>

View File

@@ -0,0 +1,3 @@
<template >
<router-view />
</template>

View File

@@ -0,0 +1,181 @@
<template>
<div class="top-right-btn" :style="style">
<el-row>
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
<el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="Object.keys(columns).length > 0">
<el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button circle icon="Menu" />
<template #dropdown>
<el-dropdown-menu>
<!-- 全选/反选 按钮 -->
<el-dropdown-item>
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
</el-dropdown-item>
<div class="check-line"></div>
<template v-for="(item, key) in columns" :key="item.key">
<el-dropdown-item>
<el-checkbox v-model="item.visible" @change="checkboxChange($event, key)" :label="item.label" />
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</el-row>
<el-dialog :title="title" v-model="open" append-to-body>
<el-transfer
:titles="['显示', '隐藏']"
v-model="value"
:data="transferData"
@change="dataChange"
></el-transfer>
</el-dialog>
</div>
</template>
<script setup>
const props = defineProps({
/* 是否显示检索条件 */
showSearch: {
type: Boolean,
default: true
},
/* 显隐列信息(数组格式、对象格式) */
columns: {
type: [Array, Object],
default: () => ({})
},
/* 是否显示检索图标 */
search: {
type: Boolean,
default: true
},
/* 显隐列类型transfer穿梭框、checkbox复选框 */
showColumnsType: {
type: String,
default: "checkbox"
},
/* 右外边距 */
gutter: {
type: Number,
default: 10
},
})
const emits = defineEmits(['update:showSearch', 'queryTable'])
// 显隐数据
const value = ref([])
// 弹出层标题
const title = ref("显示/隐藏")
// 是否显示弹出层
const open = ref(false)
const style = computed(() => {
const ret = {}
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`
}
return ret
})
// 是否全选/半选 状态
const isChecked = computed({
get: () => Array.isArray(props.columns) ? props.columns.every(col => col.visible) : Object.values(props.columns).every((col) => col.visible),
set: () => {}
})
const isIndeterminate = computed(() => Array.isArray(props.columns) ? props.columns.some((col) => col.visible) && !isChecked.value : Object.values(props.columns).some((col) => col.visible) && !isChecked.value)
const transferData = computed(() => Array.isArray(props.columns) ? props.columns.map((item, index) => ({ key: index, label: item.label })) : Object.keys(props.columns).map((key, index) => ({ key: index, label: props.columns[key].label })))
// 搜索
function toggleSearch() {
emits("update:showSearch", !props.showSearch)
}
// 刷新
function refresh() {
emits("queryTable")
}
// 右侧列表元素变化
function dataChange(data) {
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
const key = props.columns[item].key
props.columns[item].visible = !data.includes(key)
}
} else {
Object.keys(props.columns).forEach((key, index) => {
props.columns[key].visible = !data.includes(index)
})
}
}
// 打开显隐列dialog
function showColumn() {
open.value = true
}
if (props.showColumnsType == "transfer") {
// transfer穿梭显隐列初始默认隐藏列
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item))
}
}
} else {
Object.keys(props.columns).forEach((key, index) => {
if (props.columns[key].visible === false) {
value.value.push(index)
}
})
}
}
// 单勾选
function checkboxChange(event, key) {
if (Array.isArray(props.columns)) {
props.columns.filter(item => item.key == key)[0].visible = event
} else {
props.columns[key].visible = event
}
}
// 切换全选/反选
function toggleCheckAll() {
const newValue = !isChecked.value
if (Array.isArray(props.columns)) {
props.columns.forEach((col) => (col.visible = newValue))
} else {
Object.values(props.columns).forEach((col) => (col.visible = newValue))
}
}
</script>
<style lang='scss' scoped>
:deep(.el-transfer__button) {
border-radius: 50%;
display: block;
margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
margin-bottom: 10px;
}
:deep(.el-dropdown-menu__item) {
line-height: 30px;
padding: 0 17px;
}
.check-line {
width: 90%;
height: 1px;
background-color: #ccc;
margin: 3px auto;
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<svg-icon icon-class="question" @click="goto" />
</div>
</template>
<script setup>
const url = ref('http://doc.ruoyi.vip/ruoyi-vue')
function goto() {
window.open(url.value)
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<svg-icon icon-class="github" @click="goto" />
</div>
</template>
<script setup>
const url = ref('https://gitee.com/y_project/RuoYi-Vue')
function goto() {
window.open(url.value)
}
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
</div>
</template>
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen()
</script>
<style lang='scss' scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;
width: 20px;
height: 20px;
vertical-align: 10px;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<div>
<el-dropdown trigger="click" @command="handleSetSize">
<div class="size-icon--style">
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup>
import useAppStore from "@/store/modules/app"
const appStore = useAppStore()
const size = computed(() => appStore.size)
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const sizeOptions = ref([
{ label: "较大", value: "large" },
{ label: "默认", value: "default" },
{ label: "稍小", value: "small" },
])
function handleSetSize(size) {
proxy.$modal.loading("正在设置布局大小,请稍候...")
appStore.setSize(size)
setTimeout("window.location.reload()", 1000)
}
</script>
<style lang='scss' scoped>
.size-icon--style {
font-size: 18px;
line-height: 50px;
padding-right: 7px;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" :fill="color" />
</svg>
</template>
<script>
export default defineComponent({
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
},
color: {
type: String,
default: ''
},
},
setup(props) {
return {
iconName: computed(() => `#icon-${props.iconClass}`),
svgClass: computed(() => {
if (props.className) {
return `svg-icon ${props.className}`
}
return 'svg-icon'
})
}
}
})
</script>
<style scope lang="scss">
.sub-el-icon,
.nav-icon {
display: inline-block;
font-size: 15px;
margin-right: 12px;
position: relative;
}
.svg-icon {
width: 1em;
height: 1em;
position: relative;
fill: currentColor;
vertical-align: -2px;
}
</style>

View File

@@ -0,0 +1,10 @@
import * as components from '@element-plus/icons-vue'
export default {
install: (app) => {
for (const key in components) {
const componentConfig = components[key]
app.component(componentConfig.name, componentConfig)
}
}
}

View File

@@ -0,0 +1,215 @@
<template>
<el-menu
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
:ellipsis="false"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template #title>更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
:index="item.path"
:key="index"
v-if="index >= visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
// 顶部栏初始数
const visibleNumber = ref(null)
// 当前激活菜单的 index
const currentIndex = ref(null)
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile']
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute()
const router = useRouter()
// 主题颜色
const theme = computed(() => settingsStore.theme)
// 所有的路由信息
const routers = computed(() => permissionStore.topbarRouters)
// 顶部显示菜单
const topMenus = computed(() => {
let topMenus = []
routers.value.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === '/' && menu.children) {
topMenus.push(menu.children[0])
} else {
topMenus.push(menu)
}
}
})
return topMenus
})
// 设置子路由
const childrenMenus = computed(() => {
let childrenMenus = []
routers.value.map((router) => {
for (let item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path
} else {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path
}
}
router.children[item].parentPath = router.path
}
childrenMenus.push(router.children[item])
}
})
return constantRoutes.concat(childrenMenus)
})
// 默认激活的菜单
const activeMenu = computed(() => {
const path = route.path
let activePath = path
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length)
if (!route.meta.link) {
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
appStore.toggleSideBarHide(false)
}
} else if(!route.children) {
activePath = path
appStore.toggleSideBarHide(true)
}
activeRoutes(activePath)
return activePath
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3
visibleNumber.value = parseInt(width / 85)
}
function handleSelect(key, keyPath) {
currentIndex.value = key
const route = routers.value.find(item => item.path === key)
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank")
} else if (!route || !route.children) {
// 没有子路由路径内部打开
const routeMenu = childrenMenus.value.find(item => item.path === key)
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query)
router.push({ path: key, query: query })
} else {
router.push({ path: key })
}
appStore.toggleSideBarHide(true)
} else {
// 显示左侧联动菜单
activeRoutes(key)
appStore.toggleSideBarHide(false)
}
}
function activeRoutes(key) {
let routes = []
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item)
}
})
}
if(routes.length > 0) {
permissionStore.setSidebarRouters(routes)
} else {
appStore.toggleSideBarHide(true)
}
return routes
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
background-color: #ffffff;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
margin-right: 4px;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<div v-loading="loading" :style="'height:' + height">
<iframe
:src="url"
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto" />
</div>
</template>
<script setup>
const props = defineProps({
src: {
type: String,
required: true
}
})
const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
const loading = ref(true)
const url = computed(() => props.src)
onMounted(() => {
setTimeout(() => {
loading.value = false
}, 300)
window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + "px;"
}
})
</script>