wangxx месяцев назад: 4
Родитель
Сommit
7c2c92ff0f
35 измененных файлов с 4549 добавлено и 0 удалено
  1. 98 0
      src/components/Breadcrumb/index.vue
  2. 174 0
      src/components/Crontab/day.vue
  3. 133 0
      src/components/Crontab/hour.vue
  4. 313 0
      src/components/Crontab/index.vue
  5. 126 0
      src/components/Crontab/min.vue
  6. 141 0
      src/components/Crontab/month.vue
  7. 540 0
      src/components/Crontab/result.vue
  8. 128 0
      src/components/Crontab/second.vue
  9. 197 0
      src/components/Crontab/week.vue
  10. 143 0
      src/components/Crontab/year.vue
  11. 82 0
      src/components/DictTag/index.vue
  12. 276 0
      src/components/Editor/index.vue
  13. 256 0
      src/components/FileUpload/index.vue
  14. 42 0
      src/components/Hamburger/index.vue
  15. 252 0
      src/components/HeaderSearch/index.vue
  16. 111 0
      src/components/IconSelect/index.vue
  17. 8 0
      src/components/IconSelect/requireIcons.js
  18. 92 0
      src/components/ImagePreview/index.vue
  19. 274 0
      src/components/ImageUpload/index.vue
  20. 105 0
      src/components/Pagination/index.vue
  21. 3 0
      src/components/ParentView/index.vue
  22. 157 0
      src/components/RightToolbar/index.vue
  23. 13 0
      src/components/RuoYi/Doc/index.vue
  24. 13 0
      src/components/RuoYi/Git/index.vue
  25. 22 0
      src/components/Screenfull/index.vue
  26. 45 0
      src/components/SizeSelect/index.vue
  27. 53 0
      src/components/SvgIcon/index.vue
  28. 10 0
      src/components/SvgIcon/svgicon.js
  29. 217 0
      src/components/TopNav/index.vue
  30. 87 0
      src/components/UserSelect/index.vue
  31. 5 0
      src/components/VueCountTo/index.js
  32. 46 0
      src/components/VueCountTo/requestAnimationFrame.js
  33. 191 0
      src/components/VueCountTo/vue-countTo.vue
  34. 31 0
      src/components/iFrame/index.vue
  35. 165 0
      src/components/sqlEdit/index.vue

+ 98 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,98 @@
1
+<template>
2
+  <el-breadcrumb class="app-breadcrumb" separator="/">
3
+    <transition-group name="breadcrumb">
4
+      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
5
+        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
6
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
7
+      </el-breadcrumb-item>
8
+    </transition-group>
9
+  </el-breadcrumb>
10
+</template>
11
+
12
+<script setup>
13
+import usePermissionStore from '@/store/modules/permission'
14
+
15
+const route = useRoute()
16
+const router = useRouter()
17
+const permissionStore = usePermissionStore()
18
+const levelList = ref([])
19
+
20
+function getBreadcrumb() {
21
+  // only show routes with meta.title
22
+  let matched = []
23
+  const pathNum = findPathNum(route.path)
24
+  // multi-level menu
25
+  if (pathNum > 2) {
26
+    const reg = /\/\w+/gi
27
+    const pathList = route.path.match(reg).map((item, index) => {
28
+      if (index !== 0) item = item.slice(1)
29
+      return item
30
+    })
31
+    getMatched(pathList, permissionStore.defaultRoutes, matched)
32
+  } else {
33
+    matched = route.matched.filter((item) => item.meta && item.meta.title)
34
+  }
35
+  // 判断是否为首页
36
+  if (!isDashboard(matched[0])) {
37
+    matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
38
+  }
39
+  levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
40
+}
41
+function findPathNum(str, char = "/") {
42
+  let index = str.indexOf(char)
43
+  let num = 0
44
+  while (index !== -1) {
45
+    num++
46
+    index = str.indexOf(char, index + 1)
47
+  }
48
+  return num
49
+}
50
+function getMatched(pathList, routeList, matched) {
51
+  let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
52
+  if (data) {
53
+    matched.push(data)
54
+    if (data.children && pathList.length) {
55
+      pathList.shift()
56
+      getMatched(pathList, data.children, matched)
57
+    }
58
+  }
59
+}
60
+function isDashboard(route) {
61
+  const name = route && route.name
62
+  if (!name) {
63
+    return false
64
+  }
65
+  return name.trim() === 'Index'
66
+}
67
+function handleLink(item) {
68
+  const { redirect, path } = item
69
+  if (redirect) {
70
+    router.push(redirect)
71
+    return
72
+  }
73
+  router.push(path)
74
+}
75
+
76
+watchEffect(() => {
77
+  // if you go to the redirect page, do not update the breadcrumbs
78
+  if (route.path.startsWith('/redirect/')) {
79
+    return
80
+  }
81
+  getBreadcrumb()
82
+})
83
+getBreadcrumb()
84
+</script>
85
+
86
+<style lang='scss' scoped>
87
+.app-breadcrumb.el-breadcrumb {
88
+  display: inline-block;
89
+  font-size: 14px;
90
+  line-height: 50px;
91
+  margin-left: 8px;
92
+
93
+  .no-redirect {
94
+    color: #97a8be;
95
+    cursor: text;
96
+  }
97
+}
98
+</style>

+ 174 - 0
src/components/Crontab/day.vue

@@ -0,0 +1,174 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio v-model='radioValue' :value="1">
5
+                日,允许的通配符[, - * ? / L W]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio v-model='radioValue' :value="2">
11
+                不指定
12
+            </el-radio>
13
+        </el-form-item>
14
+
15
+        <el-form-item>
16
+            <el-radio v-model='radioValue' :value="3">
17
+                周期从
18
+                <el-input-number v-model='cycle01' :min="1" :max="30" /> -
19
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" /> 日
20
+            </el-radio>
21
+        </el-form-item>
22
+
23
+        <el-form-item>
24
+            <el-radio v-model='radioValue' :value="4">
25
+                从
26
+                <el-input-number v-model='average01' :min="1" :max="30" /> 号开始,每
27
+                <el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
28
+            </el-radio>
29
+        </el-form-item>
30
+
31
+        <el-form-item>
32
+            <el-radio v-model='radioValue' :value="5">
33
+                每月
34
+                <el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
35
+            </el-radio>
36
+        </el-form-item>
37
+
38
+        <el-form-item>
39
+            <el-radio v-model='radioValue' :value="6">
40
+                本月最后一天
41
+            </el-radio>
42
+        </el-form-item>
43
+
44
+        <el-form-item>
45
+            <el-radio v-model='radioValue' :value="7">
46
+                指定
47
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
48
+                    <el-option v-for="item in 31" :key="item" :label="item" :value="item" />
49
+                </el-select>
50
+            </el-radio>
51
+        </el-form-item>
52
+    </el-form>
53
+</template>
54
+<script setup>
55
+const emit = defineEmits(['update'])
56
+const props = defineProps({
57
+    cron: {
58
+        type: Object,
59
+        default: {
60
+            second: "*",
61
+            min: "*",
62
+            hour: "*",
63
+            day: "*",
64
+            month: "*",
65
+            week: "?",
66
+            year: "",
67
+        }
68
+    },
69
+    check: {
70
+        type: Function,
71
+        default: () => {
72
+        }
73
+    }
74
+})
75
+const radioValue = ref(1)
76
+const cycle01 = ref(1)
77
+const cycle02 = ref(2)
78
+const average01 = ref(1)
79
+const average02 = ref(1)
80
+const workday = ref(1)
81
+const checkboxList = ref([])
82
+const checkCopy = ref([1])
83
+const cycleTotal = computed(() => {
84
+    cycle01.value = props.check(cycle01.value, 1, 30)
85
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
86
+    return cycle01.value + '-' + cycle02.value
87
+})
88
+const averageTotal = computed(() => {
89
+    average01.value = props.check(average01.value, 1, 30)
90
+    average02.value = props.check(average02.value, 1, 31 - average01.value)
91
+    return average01.value + '/' + average02.value
92
+})
93
+const workdayTotal = computed(() => {
94
+    workday.value = props.check(workday.value, 1, 31)
95
+    return workday.value + 'W'
96
+})
97
+const checkboxString = computed(() => {
98
+    return checkboxList.value.join(',')
99
+})
100
+watch(() => props.cron.day, value => changeRadioValue(value))
101
+watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
102
+function changeRadioValue(value) {
103
+    if (value === "*") {
104
+        radioValue.value = 1
105
+    } else if (value === "?") {
106
+        radioValue.value = 2
107
+    } else if (value.indexOf("-") > -1) {
108
+        const indexArr = value.split('-')
109
+        cycle01.value = Number(indexArr[0])
110
+        cycle02.value = Number(indexArr[1])
111
+        radioValue.value = 3
112
+    } else if (value.indexOf("/") > -1) {
113
+        const indexArr = value.split('/')
114
+        average01.value = Number(indexArr[0])
115
+        average02.value = Number(indexArr[1])
116
+        radioValue.value = 4
117
+    } else if (value.indexOf("W") > -1) {
118
+        const indexArr = value.split("W")
119
+        workday.value = Number(indexArr[0])
120
+        radioValue.value = 5
121
+    } else if (value === "L") {
122
+        radioValue.value = 6
123
+    } else {
124
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
125
+        radioValue.value = 7
126
+    }
127
+}
128
+// 单选按钮值变化时
129
+function onRadioChange() {
130
+    if (radioValue.value === 2 && props.cron.week === '?') {
131
+        emit('update', 'week', '*', 'day')
132
+    }
133
+    if (radioValue.value !== 2 && props.cron.week !== '?') {
134
+        emit('update', 'week', '?', 'day')
135
+    }
136
+    switch (radioValue.value) {
137
+        case 1:
138
+            emit('update', 'day', '*', 'day')
139
+            break
140
+        case 2:
141
+            emit('update', 'day', '?', 'day')
142
+            break
143
+        case 3:
144
+            emit('update', 'day', cycleTotal.value, 'day')
145
+            break
146
+        case 4:
147
+            emit('update', 'day', averageTotal.value, 'day')
148
+            break
149
+        case 5:
150
+            emit('update', 'day', workdayTotal.value, 'day')
151
+            break
152
+        case 6:
153
+            emit('update', 'day', 'L', 'day')
154
+            break
155
+        case 7:
156
+            if (checkboxList.value.length === 0) {
157
+                checkboxList.value.push(checkCopy.value[0])
158
+            } else {
159
+                checkCopy.value = checkboxList.value
160
+            }
161
+            emit('update', 'day', checkboxString.value, 'day')
162
+            break
163
+    }
164
+}
165
+</script>
166
+
167
+<style lang="scss" scoped>
168
+.el-input-number--small, .el-select, .el-select--small {
169
+    margin: 0 0.2rem;
170
+}
171
+.el-select, .el-select--small {
172
+    width: 18.8rem;
173
+}
174
+</style>

+ 133 - 0
src/components/Crontab/hour.vue

@@ -0,0 +1,133 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio v-model='radioValue' :value="1">
5
+                小时,允许的通配符[, - * /]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio v-model='radioValue' :value="2">
11
+                周期从
12
+                <el-input-number v-model='cycle01' :min="0" :max="22" /> -
13
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" /> 时
14
+            </el-radio>
15
+        </el-form-item>
16
+
17
+        <el-form-item>
18
+            <el-radio v-model='radioValue' :value="3">
19
+                从
20
+                <el-input-number v-model='average01' :min="0" :max="22" /> 时开始,每
21
+                <el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
22
+            </el-radio>
23
+        </el-form-item>
24
+
25
+        <el-form-item>
26
+            <el-radio v-model='radioValue' :value="4">
27
+                指定
28
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
29
+                    <el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
30
+                </el-select>
31
+            </el-radio>
32
+        </el-form-item>
33
+    </el-form>
34
+</template>
35
+
36
+<script setup>
37
+const emit = defineEmits(['update'])
38
+const props = defineProps({
39
+    cron: {
40
+        type: Object,
41
+        default: {
42
+            second: "*",
43
+            min: "*",
44
+            hour: "*",
45
+            day: "*",
46
+            month: "*",
47
+            week: "?",
48
+            year: "",
49
+        }
50
+    },
51
+    check: {
52
+        type: Function,
53
+        default: () => {
54
+        }
55
+    }
56
+})
57
+const radioValue = ref(1)
58
+const cycle01 = ref(0)
59
+const cycle02 = ref(1)
60
+const average01 = ref(0)
61
+const average02 = ref(1)
62
+const checkboxList = ref([])
63
+const checkCopy = ref([0])
64
+const cycleTotal = computed(() => {
65
+    cycle01.value = props.check(cycle01.value, 0, 22)
66
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
67
+    return cycle01.value + '-' + cycle02.value
68
+})
69
+const averageTotal = computed(() => {
70
+    average01.value = props.check(average01.value, 0, 22)
71
+    average02.value = props.check(average02.value, 1, 23 - average01.value)
72
+    return average01.value + '/' + average02.value
73
+})
74
+const checkboxString = computed(() => {
75
+    return checkboxList.value.join(',')
76
+})
77
+watch(() => props.cron.hour, value => changeRadioValue(value))
78
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
79
+function changeRadioValue(value) {
80
+    if (props.cron.min === '*') {
81
+        emit('update', 'min', '0', 'hour')
82
+    }
83
+    if (props.cron.second === '*') {
84
+        emit('update', 'second', '0', 'hour')
85
+    }
86
+    if (value === '*') {
87
+        radioValue.value = 1
88
+    } else if (value.indexOf('-') > -1) {
89
+        const indexArr = value.split('-')
90
+        cycle01.value = Number(indexArr[0])
91
+        cycle02.value = Number(indexArr[1])
92
+        radioValue.value = 2
93
+    } else if (value.indexOf('/') > -1) {
94
+        const indexArr = value.split('/')
95
+        average01.value = Number(indexArr[0])
96
+        average02.value = Number(indexArr[1])
97
+        radioValue.value = 3
98
+    } else {
99
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
100
+        radioValue.value = 4
101
+    }
102
+}
103
+function onRadioChange() {
104
+    switch (radioValue.value) {
105
+        case 1:
106
+            emit('update', 'hour', '*', 'hour')
107
+            break
108
+        case 2:
109
+            emit('update', 'hour', cycleTotal.value, 'hour')
110
+            break
111
+        case 3:
112
+            emit('update', 'hour', averageTotal.value, 'hour')
113
+            break
114
+        case 4:
115
+            if (checkboxList.value.length === 0) {
116
+                checkboxList.value.push(checkCopy.value[0])
117
+            } else {
118
+                checkCopy.value = checkboxList.value
119
+            }
120
+            emit('update', 'hour', checkboxString.value, 'hour')
121
+            break
122
+    }
123
+}
124
+</script>
125
+
126
+<style lang="scss" scoped>
127
+.el-input-number--small, .el-select, .el-select--small {
128
+    margin: 0 0.2rem;
129
+}
130
+.el-select, .el-select--small {
131
+    width: 18.8rem;
132
+}
133
+</style>

+ 313 - 0
src/components/Crontab/index.vue

@@ -0,0 +1,313 @@
1
+<template>
2
+    <div>
3
+        <el-tabs type="border-card">
4
+            <el-tab-pane label="秒" v-if="shouldHide('second')">
5
+                <CrontabSecond
6
+                    @update="updateCrontabValue"
7
+                    :check="checkNumber"
8
+                    :cron="crontabValueObj"
9
+                    ref="cronsecond"
10
+                />
11
+            </el-tab-pane>
12
+
13
+            <el-tab-pane label="分钟" v-if="shouldHide('min')">
14
+                <CrontabMin
15
+                    @update="updateCrontabValue"
16
+                    :check="checkNumber"
17
+                    :cron="crontabValueObj"
18
+                    ref="cronmin"
19
+                />
20
+            </el-tab-pane>
21
+
22
+            <el-tab-pane label="小时" v-if="shouldHide('hour')">
23
+                <CrontabHour
24
+                    @update="updateCrontabValue"
25
+                    :check="checkNumber"
26
+                    :cron="crontabValueObj"
27
+                    ref="cronhour"
28
+                />
29
+            </el-tab-pane>
30
+
31
+            <el-tab-pane label="日" v-if="shouldHide('day')">
32
+                <CrontabDay
33
+                    @update="updateCrontabValue"
34
+                    :check="checkNumber"
35
+                    :cron="crontabValueObj"
36
+                    ref="cronday"
37
+                />
38
+            </el-tab-pane>
39
+
40
+            <el-tab-pane label="月" v-if="shouldHide('month')">
41
+                <CrontabMonth
42
+                    @update="updateCrontabValue"
43
+                    :check="checkNumber"
44
+                    :cron="crontabValueObj"
45
+                    ref="cronmonth"
46
+                />
47
+            </el-tab-pane>
48
+
49
+            <el-tab-pane label="周" v-if="shouldHide('week')">
50
+                <CrontabWeek
51
+                    @update="updateCrontabValue"
52
+                    :check="checkNumber"
53
+                    :cron="crontabValueObj"
54
+                    ref="cronweek"
55
+                />
56
+            </el-tab-pane>
57
+
58
+            <el-tab-pane label="年" v-if="shouldHide('year')">
59
+                <CrontabYear
60
+                    @update="updateCrontabValue"
61
+                    :check="checkNumber"
62
+                    :cron="crontabValueObj"
63
+                    ref="cronyear"
64
+                />
65
+            </el-tab-pane>
66
+        </el-tabs>
67
+
68
+        <div class="popup-main">
69
+            <div class="popup-result">
70
+                <p class="title">时间表达式</p>
71
+                <table>
72
+                    <thead>
73
+                        <tr>
74
+                            <th v-for="item of tabTitles" :key="item">{{item}}</th>
75
+                            <th>Cron 表达式</th>
76
+                        </tr>
77
+                    </thead>
78
+                    <tbody>
79
+                        <tr>
80
+                            <td>
81
+                                <span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
82
+                                <el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
83
+                            </td>
84
+                            <td>
85
+                                <span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
86
+                                <el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
87
+                            </td>
88
+                            <td>
89
+                                <span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
90
+                                <el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
91
+                            </td>
92
+                            <td>
93
+                                <span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
94
+                                <el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
95
+                            </td>
96
+                            <td>
97
+                                <span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
98
+                                <el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
99
+                            </td>
100
+                            <td>
101
+                                <span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
102
+                                <el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
103
+                            </td>
104
+                            <td>
105
+                                <span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
106
+                                <el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
107
+                            </td>
108
+                            <td class="result">
109
+                                <span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
110
+                                <el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
111
+                            </td>
112
+                        </tr>
113
+                    </tbody>
114
+                </table>
115
+            </div>
116
+            <CrontabResult :ex="crontabValueString"></CrontabResult>
117
+
118
+            <div class="pop_btn">
119
+                <el-button type="primary" @click="submitFill">确定</el-button>
120
+                <el-button type="warning" @click="clearCron">重置</el-button>
121
+                <el-button @click="hidePopup">取消</el-button>
122
+            </div>
123
+        </div>
124
+    </div>
125
+</template>
126
+
127
+<script setup>
128
+import CrontabSecond from "./second.vue"
129
+import CrontabMin from "./min.vue"
130
+import CrontabHour from "./hour.vue"
131
+import CrontabDay from "./day.vue"
132
+import CrontabMonth from "./month.vue"
133
+import CrontabWeek from "./week.vue"
134
+import CrontabYear from "./year.vue"
135
+import CrontabResult from "./result.vue"
136
+const { proxy } = getCurrentInstance()
137
+const emit = defineEmits(['hide', 'fill'])
138
+const props = defineProps({
139
+    hideComponent: {
140
+        type: Array,
141
+        default: () => [],
142
+    },
143
+    expression: {
144
+        type: String,
145
+        default: ""
146
+    }
147
+})
148
+const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
149
+const tabActive = ref(0)
150
+const hideComponent = ref([])
151
+const expression = ref('')
152
+const crontabValueObj = ref({
153
+    second: "*",
154
+    min: "*",
155
+    hour: "*",
156
+    day: "*",
157
+    month: "*",
158
+    week: "?",
159
+    year: "",
160
+})
161
+const crontabValueString = computed(() => {
162
+    const obj = crontabValueObj.value
163
+    return obj.second
164
+        + " "
165
+        + obj.min
166
+        + " "
167
+        + obj.hour
168
+        + " "
169
+        + obj.day
170
+        + " "
171
+        + obj.month
172
+        + " "
173
+        + obj.week
174
+        + (obj.year === "" ? "" : " " + obj.year)
175
+})
176
+watch(expression, () => resolveExp())
177
+function shouldHide(key) {
178
+    return !(hideComponent.value && hideComponent.value.includes(key))
179
+}
180
+function resolveExp() {
181
+    // 反解析 表达式
182
+    if (expression.value) {
183
+        const arr = expression.value.split(/\s+/)
184
+        if (arr.length >= 6) {
185
+            //6 位以上是合法表达式
186
+            let obj = {
187
+                second: arr[0],
188
+                min: arr[1],
189
+                hour: arr[2],
190
+                day: arr[3],
191
+                month: arr[4],
192
+                week: arr[5],
193
+                year: arr[6] ? arr[6] : ""
194
+            }
195
+            crontabValueObj.value = {
196
+                ...obj,
197
+            }
198
+        }
199
+    } else {
200
+        // 没有传入的表达式 则还原
201
+        clearCron()
202
+    }
203
+}
204
+// tab切换值
205
+function tabCheck(index) {
206
+    tabActive.value = index
207
+}
208
+// 由子组件触发,更改表达式组成的字段值
209
+function updateCrontabValue(name, value, from) {
210
+    crontabValueObj.value[name] = value
211
+}
212
+// 表单选项的子组件校验数字格式(通过-props传递)
213
+function checkNumber(value, minLimit, maxLimit) {
214
+    // 检查必须为整数
215
+    value = Math.floor(value)
216
+    if (value < minLimit) {
217
+        value = minLimit
218
+    } else if (value > maxLimit) {
219
+        value = maxLimit
220
+    }
221
+    return value
222
+}
223
+// 隐藏弹窗
224
+function hidePopup() {
225
+    emit("hide")
226
+}
227
+// 填充表达式
228
+function submitFill() {
229
+    emit("fill", crontabValueString.value)
230
+    hidePopup()
231
+}
232
+function clearCron() {
233
+    // 还原选择项
234
+    crontabValueObj.value = {
235
+        second: "*",
236
+        min: "*",
237
+        hour: "*",
238
+        day: "*",
239
+        month: "*",
240
+        week: "?",
241
+        year: "",
242
+    }
243
+}
244
+onMounted(() => {
245
+    expression.value = props.expression
246
+    hideComponent.value = props.hideComponent
247
+})
248
+</script>
249
+
250
+<style lang="scss" scoped>
251
+.pop_btn {
252
+    text-align: center;
253
+    margin-top: 20px;
254
+}
255
+.popup-main {
256
+    position: relative;
257
+    margin: 10px auto;
258
+    border-radius: 5px;
259
+    font-size: 12px;
260
+    overflow: hidden;
261
+}
262
+.popup-title {
263
+    overflow: hidden;
264
+    line-height: 34px;
265
+    padding-top: 6px;
266
+    background: #f2f2f2;
267
+}
268
+.popup-result {
269
+    box-sizing: border-box;
270
+    line-height: 24px;
271
+    margin: 25px auto;
272
+    padding: 15px 10px 10px;
273
+    border: 1px solid #ccc;
274
+    position: relative;
275
+}
276
+.popup-result .title {
277
+    position: absolute;
278
+    top: -28px;
279
+    left: 50%;
280
+    width: 140px;
281
+    font-size: 14px;
282
+    margin-left: -70px;
283
+    text-align: center;
284
+    line-height: 30px;
285
+    background: #fff;
286
+}
287
+.popup-result table {
288
+    text-align: center;
289
+    width: 100%;
290
+    margin: 0 auto;
291
+}
292
+.popup-result table td:not(.result) {
293
+    width: 3.5rem;
294
+    min-width: 3.5rem;
295
+    max-width: 3.5rem;
296
+}
297
+.popup-result table span {
298
+    display: block;
299
+    width: 100%;
300
+    font-family: arial;
301
+    line-height: 30px;
302
+    height: 30px;
303
+    white-space: nowrap;
304
+    overflow: hidden;
305
+    border: 1px solid #e8e8e8;
306
+}
307
+.popup-result-scroll {
308
+    font-size: 12px;
309
+    line-height: 24px;
310
+    height: 10em;
311
+    overflow-y: auto;
312
+}
313
+</style>

+ 126 - 0
src/components/Crontab/min.vue

@@ -0,0 +1,126 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio v-model='radioValue' :value="1">
5
+                分钟,允许的通配符[, - * /]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio v-model='radioValue' :value="2">
11
+                周期从
12
+                <el-input-number v-model='cycle01' :min="0" :max="58" /> -
13
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
14
+            </el-radio>
15
+        </el-form-item>
16
+
17
+        <el-form-item>
18
+            <el-radio v-model='radioValue' :value="3">
19
+                从
20
+                <el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始, 每
21
+                <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
22
+            </el-radio>
23
+        </el-form-item>
24
+
25
+        <el-form-item>
26
+            <el-radio v-model='radioValue' :value="4">
27
+                指定
28
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
29
+                    <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
30
+                </el-select>
31
+            </el-radio>
32
+        </el-form-item>
33
+    </el-form>
34
+</template>
35
+<script setup>
36
+const emit = defineEmits(['update'])
37
+const props = defineProps({
38
+    cron: {
39
+        type: Object,
40
+        default: {
41
+            second: "*",
42
+            min: "*",
43
+            hour: "*",
44
+            day: "*",
45
+            month: "*",
46
+            week: "?",
47
+            year: "",
48
+        }
49
+    },
50
+    check: {
51
+        type: Function,
52
+        default: () => {
53
+        }
54
+    }
55
+})
56
+const radioValue = ref(1)
57
+const cycle01 = ref(0)
58
+const cycle02 = ref(1)
59
+const average01 = ref(0)
60
+const average02 = ref(1)
61
+const checkboxList = ref([])
62
+const checkCopy = ref([0])
63
+const cycleTotal = computed(() => {
64
+    cycle01.value = props.check(cycle01.value, 0, 58)
65
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
66
+    return cycle01.value + '-' + cycle02.value
67
+})
68
+const averageTotal = computed(() => {
69
+    average01.value = props.check(average01.value, 0, 58)
70
+    average02.value = props.check(average02.value, 1, 59 - average01.value)
71
+    return average01.value + '/' + average02.value
72
+})
73
+const checkboxString = computed(() => {
74
+    return checkboxList.value.join(',')
75
+})
76
+watch(() => props.cron.min, value => changeRadioValue(value))
77
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
78
+function changeRadioValue(value) {
79
+    if (value === '*') {
80
+        radioValue.value = 1
81
+    } else if (value.indexOf('-') > -1) {
82
+        const indexArr = value.split('-')
83
+        cycle01.value = Number(indexArr[0])
84
+        cycle02.value = Number(indexArr[1])
85
+        radioValue.value = 2
86
+    } else if (value.indexOf('/') > -1) {
87
+        const indexArr = value.split('/')
88
+        average01.value = Number(indexArr[0])
89
+        average02.value = Number(indexArr[1])
90
+        radioValue.value = 3
91
+    } else {
92
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
93
+        radioValue.value = 4
94
+    }
95
+}
96
+function onRadioChange() {
97
+    switch (radioValue.value) {
98
+        case 1:
99
+            emit('update', 'min', '*', 'min')
100
+            break
101
+        case 2:
102
+            emit('update', 'min', cycleTotal.value, 'min')
103
+            break
104
+        case 3:
105
+            emit('update', 'min', averageTotal.value, 'min')
106
+            break
107
+        case 4:
108
+            if (checkboxList.value.length === 0) {
109
+                checkboxList.value.push(checkCopy.value[0])
110
+            } else {
111
+                checkCopy.value = checkboxList.value
112
+            }
113
+            emit('update', 'min', checkboxString.value, 'min')
114
+            break
115
+    }
116
+}
117
+</script>
118
+
119
+<style lang="scss" scoped>
120
+.el-input-number--small, .el-select, .el-select--small {
121
+    margin: 0 0.2rem;
122
+}
123
+.el-select, .el-select--small {
124
+    width: 19.8rem;
125
+}
126
+</style>

+ 141 - 0
src/components/Crontab/month.vue

@@ -0,0 +1,141 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio v-model='radioValue' :value="1">
5
+                月,允许的通配符[, - * /]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio v-model='radioValue' :value="2">
11
+                周期从
12
+                <el-input-number v-model='cycle01' :min="1" :max="11" /> -
13
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" /> 月
14
+            </el-radio>
15
+        </el-form-item>
16
+
17
+        <el-form-item>
18
+            <el-radio v-model='radioValue' :value="3">
19
+                从
20
+                <el-input-number v-model='average01' :min="1" :max="11" /> 月开始,每
21
+                <el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
22
+            </el-radio>
23
+        </el-form-item>
24
+
25
+        <el-form-item>
26
+            <el-radio v-model='radioValue' :value="4">
27
+                指定
28
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
29
+                    <el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
30
+                </el-select>
31
+            </el-radio>
32
+        </el-form-item>
33
+    </el-form>
34
+</template>
35
+
36
+<script setup>
37
+const emit = defineEmits(['update'])
38
+const props = defineProps({
39
+    cron: {
40
+        type: Object,
41
+        default: {
42
+            second: "*",
43
+            min: "*",
44
+            hour: "*",
45
+            day: "*",
46
+            month: "*",
47
+            week: "?",
48
+            year: "",
49
+        }
50
+    },
51
+    check: {
52
+        type: Function,
53
+        default: () => {
54
+        }
55
+    }
56
+})
57
+const radioValue = ref(1)
58
+const cycle01 = ref(1)
59
+const cycle02 = ref(2)
60
+const average01 = ref(1)
61
+const average02 = ref(1)
62
+const checkboxList = ref([])
63
+const checkCopy = ref([1])
64
+const monthList = ref([
65
+    {key: 1, value: '一月'},
66
+    {key: 2, value: '二月'},
67
+    {key: 3, value: '三月'},
68
+    {key: 4, value: '四月'},
69
+    {key: 5, value: '五月'},
70
+    {key: 6, value: '六月'},
71
+    {key: 7, value: '七月'},
72
+    {key: 8, value: '八月'},
73
+    {key: 9, value: '九月'},
74
+    {key: 10, value: '十月'},
75
+    {key: 11, value: '十一月'},
76
+    {key: 12, value: '十二月'}
77
+])
78
+const cycleTotal = computed(() => {
79
+    cycle01.value = props.check(cycle01.value, 1, 11)
80
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
81
+    return cycle01.value + '-' + cycle02.value
82
+})
83
+const averageTotal = computed(() => {
84
+    average01.value = props.check(average01.value, 1, 11)
85
+    average02.value = props.check(average02.value, 1, 12 - average01.value)
86
+    return average01.value + '/' + average02.value
87
+})
88
+const checkboxString = computed(() => {
89
+    return checkboxList.value.join(',')
90
+})
91
+watch(() => props.cron.month, value => changeRadioValue(value))
92
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
93
+function changeRadioValue(value) {
94
+    if (value === '*') {
95
+        radioValue.value = 1
96
+    } else if (value.indexOf('-') > -1) {
97
+        const indexArr = value.split('-')
98
+        cycle01.value = Number(indexArr[0])
99
+        cycle02.value = Number(indexArr[1])
100
+        radioValue.value = 2
101
+    } else if (value.indexOf('/') > -1) {
102
+        const indexArr = value.split('/')
103
+        average01.value = Number(indexArr[0])
104
+        average02.value = Number(indexArr[1])
105
+        radioValue.value = 3
106
+    } else {
107
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
108
+        radioValue.value = 4
109
+    }
110
+}
111
+function onRadioChange() {
112
+    switch (radioValue.value) {
113
+        case 1:
114
+            emit('update', 'month', '*', 'month')
115
+            break
116
+        case 2:
117
+            emit('update', 'month', cycleTotal.value, 'month')
118
+            break
119
+        case 3:
120
+            emit('update', 'month', averageTotal.value, 'month')
121
+            break
122
+        case 4:
123
+            if (checkboxList.value.length === 0) {
124
+                checkboxList.value.push(checkCopy.value[0])
125
+            } else {
126
+                checkCopy.value = checkboxList.value
127
+            }
128
+            emit('update', 'month', checkboxString.value, 'month')
129
+            break
130
+    }
131
+}
132
+</script>
133
+
134
+<style lang="scss" scoped>
135
+.el-input-number--small, .el-select, .el-select--small {
136
+    margin: 0 0.2rem;
137
+}
138
+.el-select, .el-select--small {
139
+    width: 18.8rem;
140
+}
141
+</style>

+ 540 - 0
src/components/Crontab/result.vue

@@ -0,0 +1,540 @@
1
+<template>
2
+	<div class="popup-result">
3
+		<p class="title">最近5次运行时间</p>
4
+		<ul class="popup-result-scroll">
5
+			<template v-if='isShow'>
6
+				<li v-for='item in resultList' :key="item">{{item}}</li>
7
+			</template>
8
+			<li v-else>计算结果中...</li>
9
+		</ul>
10
+	</div>
11
+</template>
12
+
13
+<script setup>
14
+const props = defineProps({
15
+    ex: {
16
+        type: String,
17
+        default: ''
18
+    }
19
+})
20
+const dayRule = ref('')
21
+const dayRuleSup = ref('')
22
+const dateArr = ref([])
23
+const resultList = ref([])
24
+const isShow = ref(false)
25
+watch(() => props.ex, () => expressionChange())
26
+// 表达式值变化时,开始去计算结果
27
+function expressionChange() {
28
+    // 计算开始-隐藏结果
29
+    isShow.value = false
30
+    // 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
31
+    let ruleArr = props.ex.split(' ')
32
+    // 用于记录进入循环的次数
33
+    let nums = 0
34
+    // 用于暂时存符号时间规则结果的数组
35
+    let resultArr = []
36
+    // 获取当前时间精确至[年、月、日、时、分、秒]
37
+    let nTime = new Date()
38
+    let nYear = nTime.getFullYear()
39
+    let nMonth = nTime.getMonth() + 1
40
+    let nDay = nTime.getDate()
41
+    let nHour = nTime.getHours()
42
+    let nMin = nTime.getMinutes()
43
+    let nSecond = nTime.getSeconds()
44
+    // 根据规则获取到近100年可能年数组、月数组等等
45
+    getSecondArr(ruleArr[0])
46
+    getMinArr(ruleArr[1])
47
+    getHourArr(ruleArr[2])
48
+    getDayArr(ruleArr[3])
49
+    getMonthArr(ruleArr[4])
50
+    getWeekArr(ruleArr[5])
51
+    getYearArr(ruleArr[6], nYear)
52
+    // 将获取到的数组赋值-方便使用
53
+    let sDate = dateArr.value[0]
54
+    let mDate = dateArr.value[1]
55
+    let hDate = dateArr.value[2]
56
+    let DDate = dateArr.value[3]
57
+    let MDate = dateArr.value[4]
58
+    let YDate = dateArr.value[5]
59
+    // 获取当前时间在数组中的索引
60
+    let sIdx = getIndex(sDate, nSecond)
61
+    let mIdx = getIndex(mDate, nMin)
62
+    let hIdx = getIndex(hDate, nHour)
63
+    let DIdx = getIndex(DDate, nDay)
64
+    let MIdx = getIndex(MDate, nMonth)
65
+    let YIdx = getIndex(YDate, nYear)
66
+    // 重置月日时分秒的函数(后面用的比较多)
67
+    const resetSecond = function () {
68
+        sIdx = 0
69
+        nSecond = sDate[sIdx]
70
+    }
71
+    const resetMin = function () {
72
+        mIdx = 0
73
+        nMin = mDate[mIdx]
74
+        resetSecond()
75
+    }
76
+    const resetHour = function () {
77
+        hIdx = 0
78
+        nHour = hDate[hIdx]
79
+        resetMin()
80
+    }
81
+    const resetDay = function () {
82
+        DIdx = 0
83
+        nDay = DDate[DIdx]
84
+        resetHour()
85
+    }
86
+    const resetMonth = function () {
87
+        MIdx = 0
88
+        nMonth = MDate[MIdx]
89
+        resetDay()
90
+    }
91
+    // 如果当前年份不为数组中当前值
92
+    if (nYear !== YDate[YIdx]) {
93
+        resetMonth()
94
+    }
95
+    // 如果当前月份不为数组中当前值
96
+    if (nMonth !== MDate[MIdx]) {
97
+        resetDay()
98
+    }
99
+    // 如果当前“日”不为数组中当前值
100
+    if (nDay !== DDate[DIdx]) {
101
+        resetHour()
102
+    }
103
+    // 如果当前“时”不为数组中当前值
104
+    if (nHour !== hDate[hIdx]) {
105
+        resetMin()
106
+    }
107
+    // 如果当前“分”不为数组中当前值
108
+    if (nMin !== mDate[mIdx]) {
109
+        resetSecond()
110
+    }
111
+    // 循环年份数组
112
+    goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
113
+        let YY = YDate[Yi]
114
+        // 如果到达最大值时
115
+        if (nMonth > MDate[MDate.length - 1]) {
116
+            resetMonth()
117
+            continue
118
+        }
119
+        // 循环月份数组
120
+        goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
121
+            // 赋值、方便后面运算
122
+            let MM = MDate[Mi];
123
+            MM = MM < 10 ? '0' + MM : MM
124
+            // 如果到达最大值时
125
+            if (nDay > DDate[DDate.length - 1]) {
126
+                resetDay()
127
+                if (Mi === MDate.length - 1) {
128
+                    resetMonth()
129
+                    continue goYear
130
+                }
131
+                continue
132
+            }
133
+            // 循环日期数组
134
+            goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
135
+                // 赋值、方便后面运算
136
+                let DD = DDate[Di]
137
+                let thisDD = DD < 10 ? '0' + DD : DD
138
+                // 如果到达最大值时
139
+                if (nHour > hDate[hDate.length - 1]) {
140
+                    resetHour()
141
+                    if (Di === DDate.length - 1) {
142
+                        resetDay()
143
+                        if (Mi === MDate.length - 1) {
144
+                            resetMonth()
145
+                            continue goYear
146
+                        }
147
+                        continue goMonth
148
+                    }
149
+                    continue
150
+                }
151
+                // 判断日期的合法性,不合法的话也是跳出当前循环
152
+                if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
153
+                    resetDay()
154
+                    continue goMonth
155
+                }
156
+                // 如果日期规则中有值时
157
+                if (dayRule.value === 'lastDay') {
158
+                    // 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
159
+                    if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
160
+                        while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
161
+                            DD--
162
+                            thisDD = DD < 10 ? '0' + DD : DD
163
+                        }
164
+                    }
165
+                } else if (dayRule.value === 'workDay') {
166
+                    // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
167
+                    if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
168
+                        while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
169
+                            DD--
170
+                            thisDD = DD < 10 ? '0' + DD : DD
171
+                        }
172
+                    }
173
+                    // 获取达到条件的日期是星期X
174
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
175
+                    // 当星期日时
176
+                    if (thisWeek === 1) {
177
+                        // 先找下一个日,并判断是否为月底
178
+                        DD++
179
+                        thisDD = DD < 10 ? '0' + DD : DD
180
+                        // 判断下一日已经不是合法日期
181
+                        if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
182
+                            DD -= 3
183
+                        }
184
+                    } else if (thisWeek === 7) {
185
+                        // 当星期6时只需判断不是1号就可进行操作
186
+                        if (dayRuleSup.value !== 1) {
187
+                            DD--
188
+                        } else {
189
+                            DD += 2
190
+                        }
191
+                    }
192
+                } else if (dayRule.value === 'weekDay') {
193
+                    // 如果指定了是星期几
194
+                    // 获取当前日期是属于星期几
195
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
196
+                    // 校验当前星期是否在星期池(dayRuleSup)中
197
+                    if (dayRuleSup.value.indexOf(thisWeek) < 0) {
198
+                        // 如果到达最大值时
199
+                        if (Di === DDate.length - 1) {
200
+                            resetDay()
201
+                            if (Mi === MDate.length - 1) {
202
+                                resetMonth()
203
+                                continue goYear
204
+                            }
205
+                            continue goMonth
206
+                        }
207
+                        continue
208
+                    }
209
+                } else if (dayRule.value === 'assWeek') {
210
+                    // 如果指定了是第几周的星期几
211
+                    // 获取每月1号是属于星期几
212
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
213
+                    if (dayRuleSup.value[1] >= thisWeek) {
214
+                        DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
215
+                    } else {
216
+                        DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
217
+                    }
218
+                } else if (dayRule.value === 'lastWeek') {
219
+                    // 如果指定了每月最后一个星期几
220
+                    // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
221
+                    if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
222
+                        while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
223
+                            DD--
224
+                            thisDD = DD < 10 ? '0' + DD : DD
225
+                        }
226
+                    }
227
+                    // 获取月末最后一天是星期几
228
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
229
+                    // 找到要求中最近的那个星期几
230
+                    if (dayRuleSup.value < thisWeek) {
231
+                        DD -= thisWeek - dayRuleSup.value
232
+                    } else if (dayRuleSup.value > thisWeek) {
233
+                        DD -= 7 - (dayRuleSup.value - thisWeek)
234
+                    }
235
+                }
236
+                // 判断时间值是否小于10置换成“05”这种格式
237
+                DD = DD < 10 ? '0' + DD : DD
238
+                // 循环“时”数组
239
+                goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
240
+                    let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
241
+                    // 如果到达最大值时
242
+                    if (nMin > mDate[mDate.length - 1]) {
243
+                        resetMin()
244
+                        if (hi === hDate.length - 1) {
245
+                            resetHour()
246
+                            if (Di === DDate.length - 1) {
247
+                                resetDay()
248
+                                if (Mi === MDate.length - 1) {
249
+                                    resetMonth()
250
+                                    continue goYear
251
+                                }
252
+                                continue goMonth
253
+                            }
254
+                            continue goDay
255
+                        }
256
+                        continue
257
+                    }
258
+                    // 循环"分"数组
259
+                    goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
260
+                        let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
261
+                        // 如果到达最大值时
262
+                        if (nSecond > sDate[sDate.length - 1]) {
263
+                            resetSecond()
264
+                            if (mi === mDate.length - 1) {
265
+                                resetMin()
266
+                                if (hi === hDate.length - 1) {
267
+                                    resetHour()
268
+                                    if (Di === DDate.length - 1) {
269
+                                        resetDay()
270
+                                        if (Mi === MDate.length - 1) {
271
+                                            resetMonth()
272
+                                            continue goYear
273
+                                        }
274
+                                        continue goMonth
275
+                                    }
276
+                                    continue goDay
277
+                                }
278
+                                continue goHour
279
+                            }
280
+                            continue
281
+                        }
282
+                        // 循环"秒"数组
283
+                        goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
284
+                            let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
285
+                            // 添加当前时间(时间合法性在日期循环时已经判断)
286
+                            if (MM !== '00' && DD !== '00') {
287
+                                resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
288
+                                nums++
289
+                            }
290
+                            // 如果条数满了就退出循环
291
+                            if (nums === 5) break goYear
292
+                            // 如果到达最大值时
293
+                            if (si === sDate.length - 1) {
294
+                                resetSecond()
295
+                                if (mi === mDate.length - 1) {
296
+                                    resetMin()
297
+                                    if (hi === hDate.length - 1) {
298
+                                        resetHour()
299
+                                        if (Di === DDate.length - 1) {
300
+                                            resetDay()
301
+                                            if (Mi === MDate.length - 1) {
302
+                                                resetMonth()
303
+                                                continue goYear
304
+                                            }
305
+                                            continue goMonth
306
+                                        }
307
+                                        continue goDay
308
+                                    }
309
+                                    continue goHour
310
+                                }
311
+                                continue goMin
312
+                            }
313
+                        } //goSecond
314
+                    } //goMin
315
+                }//goHour
316
+            }//goDay
317
+        }//goMonth
318
+    }
319
+    // 判断100年内的结果条数
320
+    if (resultArr.length === 0) {
321
+        resultList.value = ['没有达到条件的结果!']
322
+    } else {
323
+        resultList.value = resultArr
324
+        if (resultArr.length !== 5) {
325
+            resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
326
+        }
327
+    }
328
+    // 计算完成-显示结果
329
+    isShow.value = true
330
+}
331
+// 用于计算某位数字在数组中的索引
332
+function getIndex(arr, value) {
333
+    if (value <= arr[0] || value > arr[arr.length - 1]) {
334
+        return 0
335
+    } else {
336
+        for (let i = 0; i < arr.length - 1; i++) {
337
+            if (value > arr[i] && value <= arr[i + 1]) {
338
+                return i + 1
339
+            }
340
+        }
341
+    }
342
+}
343
+// 获取"年"数组
344
+function getYearArr(rule, year) {
345
+    dateArr.value[5] = getOrderArr(year, year + 100)
346
+    if (rule !== undefined) {
347
+        if (rule.indexOf('-') >= 0) {
348
+            dateArr.value[5] = getCycleArr(rule, year + 100, false)
349
+        } else if (rule.indexOf('/') >= 0) {
350
+            dateArr.value[5] = getAverageArr(rule, year + 100)
351
+        } else if (rule !== '*') {
352
+            dateArr.value[5] = getAssignArr(rule)
353
+        }
354
+    }
355
+}
356
+// 获取"月"数组
357
+function getMonthArr(rule) {
358
+    dateArr.value[4] = getOrderArr(1, 12)
359
+    if (rule.indexOf('-') >= 0) {
360
+        dateArr.value[4] = getCycleArr(rule, 12, false)
361
+    } else if (rule.indexOf('/') >= 0) {
362
+        dateArr.value[4] = getAverageArr(rule, 12)
363
+    } else if (rule !== '*') {
364
+        dateArr.value[4] = getAssignArr(rule)
365
+    }
366
+}
367
+// 获取"日"数组-主要为日期规则
368
+function getWeekArr(rule) {
369
+    // 只有当日期规则的两个值均为“”时则表达日期是有选项的
370
+    if (dayRule.value === '' && dayRuleSup.value === '') {
371
+        if (rule.indexOf('-') >= 0) {
372
+            dayRule.value = 'weekDay'
373
+            dayRuleSup.value = getCycleArr(rule, 7, false)
374
+        } else if (rule.indexOf('#') >= 0) {
375
+            dayRule.value = 'assWeek'
376
+            let matchRule = rule.match(/[0-9]{1}/g)
377
+            dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
378
+            dateArr.value[3] = [1]
379
+            if (dayRuleSup.value[1] === 7) {
380
+                dayRuleSup.value[1] = 0
381
+            }
382
+        } else if (rule.indexOf('L') >= 0) {
383
+            dayRule.value = 'lastWeek'
384
+            dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
385
+            dateArr.value[3] = [31]
386
+            if (dayRuleSup.value === 7) {
387
+                dayRuleSup.value = 0
388
+            }
389
+        } else if (rule !== '*' && rule !== '?') {
390
+            dayRule.value = 'weekDay'
391
+            dayRuleSup.value = getAssignArr(rule)
392
+        }
393
+    }
394
+}
395
+// 获取"日"数组-少量为日期规则
396
+function getDayArr(rule) {
397
+    dateArr.value[3] = getOrderArr(1, 31)
398
+    dayRule.value = ''
399
+    dayRuleSup.value = ''
400
+    if (rule.indexOf('-') >= 0) {
401
+        dateArr.value[3] = getCycleArr(rule, 31, false)
402
+        dayRuleSup.value = 'null'
403
+    } else if (rule.indexOf('/') >= 0) {
404
+        dateArr.value[3] = getAverageArr(rule, 31)
405
+        dayRuleSup.value = 'null'
406
+    } else if (rule.indexOf('W') >= 0) {
407
+        dayRule.value = 'workDay'
408
+        dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
409
+        dateArr.value[3] = [dayRuleSup.value]
410
+    } else if (rule.indexOf('L') >= 0) {
411
+        dayRule.value = 'lastDay'
412
+        dayRuleSup.value = 'null'
413
+        dateArr.value[3] = [31]
414
+    } else if (rule !== '*' && rule !== '?') {
415
+        dateArr.value[3] = getAssignArr(rule)
416
+        dayRuleSup.value = 'null'
417
+    } else if (rule === '*') {
418
+        dayRuleSup.value = 'null'
419
+    }
420
+}
421
+// 获取"时"数组
422
+function getHourArr(rule) {
423
+    dateArr.value[2] = getOrderArr(0, 23)
424
+    if (rule.indexOf('-') >= 0) {
425
+        dateArr.value[2] = getCycleArr(rule, 24, true)
426
+    } else if (rule.indexOf('/') >= 0) {
427
+        dateArr.value[2] = getAverageArr(rule, 23)
428
+    } else if (rule !== '*') {
429
+        dateArr.value[2] = getAssignArr(rule)
430
+    }
431
+}
432
+// 获取"分"数组
433
+function getMinArr(rule) {
434
+    dateArr.value[1] = getOrderArr(0, 59)
435
+    if (rule.indexOf('-') >= 0) {
436
+        dateArr.value[1] = getCycleArr(rule, 60, true)
437
+    } else if (rule.indexOf('/') >= 0) {
438
+        dateArr.value[1] = getAverageArr(rule, 59)
439
+    } else if (rule !== '*') {
440
+        dateArr.value[1] = getAssignArr(rule)
441
+    }
442
+}
443
+// 获取"秒"数组
444
+function getSecondArr(rule) {
445
+    dateArr.value[0] = getOrderArr(0, 59)
446
+    if (rule.indexOf('-') >= 0) {
447
+        dateArr.value[0] = getCycleArr(rule, 60, true)
448
+    } else if (rule.indexOf('/') >= 0) {
449
+        dateArr.value[0] = getAverageArr(rule, 59)
450
+    } else if (rule !== '*') {
451
+        dateArr.value[0] = getAssignArr(rule)
452
+    }
453
+}
454
+// 根据传进来的min-max返回一个顺序的数组
455
+function getOrderArr(min, max) {
456
+    let arr = []
457
+    for (let i = min; i <= max; i++) {
458
+        arr.push(i)
459
+    }
460
+    return arr
461
+}
462
+// 根据规则中指定的零散值返回一个数组
463
+function getAssignArr(rule) {
464
+    let arr = []
465
+    let assiginArr = rule.split(',')
466
+    for (let i = 0; i < assiginArr.length; i++) {
467
+        arr[i] = Number(assiginArr[i])
468
+    }
469
+    arr.sort(compare)
470
+    return arr
471
+}
472
+// 根据一定算术规则计算返回一个数组
473
+function getAverageArr(rule, limit) {
474
+    let arr = []
475
+    let agArr = rule.split('/')
476
+    let min = Number(agArr[0])
477
+    let step = Number(agArr[1])
478
+    while (min <= limit) {
479
+        arr.push(min)
480
+        min += step
481
+    }
482
+    return arr
483
+}
484
+// 根据规则返回一个具有周期性的数组
485
+function getCycleArr(rule, limit, status) {
486
+    // status--表示是否从0开始(则从1开始)
487
+    let arr = []
488
+    let cycleArr = rule.split('-')
489
+    let min = Number(cycleArr[0])
490
+    let max = Number(cycleArr[1])
491
+    if (min > max) {
492
+        max += limit
493
+    }
494
+    for (let i = min; i <= max; i++) {
495
+        let add = 0
496
+        if (status === false && i % limit === 0) {
497
+            add = limit
498
+        }
499
+        arr.push(Math.round(i % limit + add))
500
+    }
501
+    arr.sort(compare)
502
+    return arr
503
+}
504
+// 比较数字大小(用于Array.sort)
505
+function compare(value1, value2) {
506
+    if (value2 - value1 > 0) {
507
+        return -1
508
+    } else {
509
+        return 1
510
+    }
511
+}
512
+// 格式化日期格式如:2017-9-19 18:04:33
513
+function formatDate(value, type) {
514
+    // 计算日期相关值
515
+    let time = typeof value == 'number' ? new Date(value) : value
516
+    let Y = time.getFullYear()
517
+    let M = time.getMonth() + 1
518
+    let D = time.getDate()
519
+    let h = time.getHours()
520
+    let m = time.getMinutes()
521
+    let s = time.getSeconds()
522
+    let week = time.getDay()
523
+    // 如果传递了type的话
524
+    if (type === undefined) {
525
+        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)
526
+    } else if (type === 'week') {
527
+        // 在quartz中 1为星期日
528
+        return week + 1
529
+    }
530
+}
531
+// 检查日期是否存在
532
+function checkDate(value) {
533
+    let time = new Date(value)
534
+    let format = formatDate(time)
535
+    return value === format
536
+}
537
+onMounted(() => {
538
+    expressionChange()
539
+})
540
+</script>

+ 128 - 0
src/components/Crontab/second.vue

@@ -0,0 +1,128 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio v-model='radioValue' :value="1">
5
+                秒,允许的通配符[, - * /]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio v-model='radioValue' :value="2">
11
+                周期从
12
+                <el-input-number v-model='cycle01' :min="0" :max="58" /> -
13
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 秒
14
+            </el-radio>
15
+        </el-form-item>
16
+
17
+        <el-form-item>
18
+            <el-radio v-model='radioValue' :value="3">
19
+                从
20
+                <el-input-number v-model='average01' :min="0" :max="58" /> 秒开始,每
21
+                <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
22
+            </el-radio>
23
+        </el-form-item>
24
+
25
+        <el-form-item>
26
+            <el-radio v-model='radioValue' :value="4">
27
+                指定
28
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
29
+                    <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
30
+                </el-select>
31
+            </el-radio>
32
+        </el-form-item>
33
+    </el-form>
34
+</template>
35
+
36
+<script setup>
37
+const emit = defineEmits(['update'])
38
+const props = defineProps({
39
+    cron: {
40
+        type: Object,
41
+        default: {
42
+            second: "*",
43
+            min: "*",
44
+            hour: "*",
45
+            day: "*",
46
+            month: "*",
47
+            week: "?",
48
+            year: "",
49
+        }
50
+    },
51
+    check: {
52
+        type: Function,
53
+        default: () => {
54
+        }
55
+    }
56
+})
57
+const radioValue = ref(1)
58
+const cycle01 = ref(0)
59
+const cycle02 = ref(1)
60
+const average01 = ref(0)
61
+const average02 = ref(1)
62
+const checkboxList = ref([])
63
+const checkCopy = ref([0])
64
+const cycleTotal = computed(() => {
65
+    cycle01.value = props.check(cycle01.value, 0, 58)
66
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
67
+    return cycle01.value + '-' + cycle02.value
68
+})
69
+const averageTotal = computed(() => {
70
+    average01.value = props.check(average01.value, 0, 58)
71
+    average02.value = props.check(average02.value, 1, 59 - average01.value)
72
+    return average01.value + '/' + average02.value
73
+})
74
+const checkboxString = computed(() => {
75
+    return checkboxList.value.join(',')
76
+})
77
+watch(() => props.cron.second, value => changeRadioValue(value))
78
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
79
+function changeRadioValue(value) {
80
+    if (value === '*') {
81
+        radioValue.value = 1
82
+    } else if (value.indexOf('-') > -1) {
83
+        const indexArr = value.split('-')
84
+        cycle01.value = Number(indexArr[0])
85
+        cycle02.value = Number(indexArr[1])
86
+        radioValue.value = 2
87
+    } else if (value.indexOf('/') > -1) {
88
+        const indexArr = value.split('/')
89
+        average01.value = Number(indexArr[0])
90
+        average02.value = Number(indexArr[1])
91
+        radioValue.value = 3
92
+    } else {
93
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
94
+        radioValue.value = 4
95
+    }
96
+}
97
+// 单选按钮值变化时
98
+function onRadioChange() {
99
+    switch (radioValue.value) {
100
+        case 1:
101
+            emit('update', 'second', '*', 'second')
102
+            break
103
+        case 2:
104
+            emit('update', 'second', cycleTotal.value, 'second')
105
+            break
106
+        case 3:
107
+            emit('update', 'second', averageTotal.value, 'second')
108
+            break
109
+        case 4:
110
+            if (checkboxList.value.length === 0) {
111
+                checkboxList.value.push(checkCopy.value[0])
112
+            } else {
113
+                checkCopy.value = checkboxList.value
114
+            }
115
+            emit('update', 'second', checkboxString.value, 'second')
116
+            break
117
+    }
118
+}
119
+</script>
120
+
121
+<style lang="scss" scoped>
122
+.el-input-number--small, .el-select, .el-select--small {
123
+    margin: 0 0.2rem;
124
+}
125
+.el-select, .el-select--small {
126
+    width: 18.8rem;
127
+}
128
+</style>

+ 197 - 0
src/components/Crontab/week.vue

@@ -0,0 +1,197 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio v-model='radioValue' :value="1">
5
+                周,允许的通配符[, - * ? / L #]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio v-model='radioValue' :value="2">
11
+                不指定
12
+            </el-radio>
13
+        </el-form-item>
14
+
15
+        <el-form-item>
16
+            <el-radio v-model='radioValue' :value="3">
17
+                周期从
18
+                <el-select clearable v-model="cycle01">
19
+                    <el-option
20
+                        v-for="(item,index) of weekList"
21
+                        :key="index"
22
+                        :label="item.value"
23
+                        :value="item.key"
24
+                        :disabled="item.key === 7"
25
+                    >{{item.value}}</el-option>
26
+                </el-select>
27
+                -
28
+                <el-select clearable v-model="cycle02">
29
+                    <el-option
30
+                        v-for="(item,index) of weekList"
31
+                        :key="index"
32
+                        :label="item.value"
33
+                        :value="item.key"
34
+                        :disabled="item.key <= cycle01"
35
+                    >{{item.value}}</el-option>
36
+                </el-select>
37
+            </el-radio>
38
+        </el-form-item>
39
+
40
+        <el-form-item>
41
+            <el-radio v-model='radioValue' :value="4">
42
+                第
43
+                <el-input-number v-model='average01' :min="1" :max="4" /> 周的
44
+                <el-select clearable v-model="average02">
45
+                    <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
46
+                </el-select>
47
+            </el-radio>
48
+        </el-form-item>
49
+
50
+        <el-form-item>
51
+            <el-radio v-model='radioValue' :value="5">
52
+                本月最后一个
53
+                <el-select clearable v-model="weekday">
54
+                    <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
55
+                </el-select>
56
+            </el-radio>
57
+        </el-form-item>
58
+
59
+        <el-form-item>
60
+            <el-radio v-model='radioValue' :value="6">
61
+                指定
62
+                <el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
63
+                    <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
64
+                </el-select>
65
+            </el-radio>
66
+        </el-form-item>
67
+
68
+    </el-form>
69
+</template>
70
+
71
+<script setup>
72
+const emit = defineEmits(['update'])
73
+const props = defineProps({
74
+    cron: {
75
+        type: Object,
76
+        default: {
77
+            second: "*",
78
+            min: "*",
79
+            hour: "*",
80
+            day: "*",
81
+            month: "*",
82
+            week: "?",
83
+            year: ""
84
+        }
85
+    },
86
+    check: {
87
+        type: Function,
88
+        default: () => {
89
+        }
90
+    }
91
+})
92
+const radioValue = ref(2)
93
+const cycle01 = ref(2)
94
+const cycle02 = ref(3)
95
+const average01 = ref(1)
96
+const average02 = ref(2)
97
+const weekday = ref(2)
98
+const checkboxList = ref([])
99
+const checkCopy = ref([2])
100
+const weekList = ref([
101
+    {key: 1, value: '星期日'},
102
+    {key: 2, value: '星期一'},
103
+    {key: 3, value: '星期二'},
104
+    {key: 4, value: '星期三'},
105
+    {key: 5, value: '星期四'},
106
+    {key: 6, value: '星期五'},
107
+    {key: 7, value: '星期六'}
108
+])
109
+const cycleTotal = computed(() => {
110
+    cycle01.value = props.check(cycle01.value, 1, 6)
111
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
112
+    return cycle01.value + '-' + cycle02.value
113
+})
114
+const averageTotal = computed(() => {
115
+    average01.value = props.check(average01.value, 1, 4)
116
+    average02.value = props.check(average02.value, 1, 7)
117
+    return average02.value + '#' + average01.value
118
+})
119
+const weekdayTotal = computed(() => {
120
+    weekday.value = props.check(weekday.value, 1, 7)
121
+    return weekday.value + 'L'
122
+})
123
+const checkboxString = computed(() => {
124
+    return checkboxList.value.join(',')
125
+})
126
+watch(() => props.cron.week, value => changeRadioValue(value))
127
+watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
128
+function changeRadioValue(value) {
129
+    if (value === "*") {
130
+        radioValue.value = 1
131
+    } else if (value === "?") {
132
+        radioValue.value = 2
133
+    } else if (value.indexOf("-") > -1) {
134
+        const indexArr = value.split('-')
135
+        cycle01.value = Number(indexArr[0])
136
+        cycle02.value = Number(indexArr[1])
137
+        radioValue.value = 3
138
+    } else if (value.indexOf("#") > -1) {
139
+        const indexArr = value.split('#')
140
+        average01.value = Number(indexArr[1])
141
+        average02.value = Number(indexArr[0])
142
+        radioValue.value = 4
143
+    } else if (value.indexOf("L") > -1) {
144
+        const indexArr = value.split("L")
145
+        weekday.value = Number(indexArr[0])
146
+        radioValue.value = 5
147
+    } else {
148
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
149
+        radioValue.value = 6
150
+    }
151
+}
152
+function onRadioChange() {
153
+    if (radioValue.value === 2 && props.cron.day === '?') {
154
+        emit('update', 'day', '*', 'week')
155
+    }
156
+    if (radioValue.value !== 2 && props.cron.day !== '?') {
157
+        emit('update', 'day', '?', 'week')
158
+    }
159
+    switch (radioValue.value) {
160
+        case 1:
161
+            emit('update', 'week', '*', 'week')
162
+            break
163
+        case 2:
164
+            emit('update', 'week', '?', 'week')
165
+            break
166
+        case 3:
167
+            emit('update', 'week', cycleTotal.value, 'week')
168
+            break
169
+        case 4:
170
+            emit('update', 'week', averageTotal.value, 'week')
171
+            break
172
+        case 5:
173
+            emit('update', 'week', weekdayTotal.value, 'week')
174
+            break
175
+        case 6:
176
+            if (checkboxList.value.length === 0) {
177
+                checkboxList.value.push(checkCopy.value[0])
178
+            } else {
179
+                checkCopy.value = checkboxList.value
180
+            }
181
+            emit('update', 'week', checkboxString.value, 'week')
182
+            break
183
+    }
184
+}
185
+</script>
186
+
187
+<style lang="scss" scoped>
188
+.el-input-number--small, .el-select, .el-select--small {
189
+    margin: 0 0.5rem;
190
+}
191
+.el-select, .el-select--small {
192
+    width: 8rem;
193
+}
194
+.el-select.multiselect, .el-select--small.multiselect {
195
+    width: 17.8rem;
196
+}
197
+</style>

+ 143 - 0
src/components/Crontab/year.vue

@@ -0,0 +1,143 @@
1
+<template>
2
+    <el-form>
3
+        <el-form-item>
4
+            <el-radio :value="1" v-model='radioValue'>
5
+                不填,允许的通配符[, - * /]
6
+            </el-radio>
7
+        </el-form-item>
8
+
9
+        <el-form-item>
10
+            <el-radio :value="2" v-model='radioValue'>
11
+                每年
12
+            </el-radio>
13
+        </el-form-item>
14
+
15
+        <el-form-item>
16
+            <el-radio :value="3" v-model='radioValue'>
17
+                周期从
18
+                <el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
19
+                <el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
20
+            </el-radio>
21
+        </el-form-item>
22
+
23
+        <el-form-item>
24
+            <el-radio :value="4" v-model='radioValue'>
25
+                从
26
+                <el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始,每
27
+                <el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
28
+            </el-radio>
29
+
30
+        </el-form-item>
31
+
32
+        <el-form-item>
33
+            <el-radio :value="5" v-model='radioValue'>
34
+                指定
35
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
36
+                    <el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
37
+                </el-select>
38
+            </el-radio>
39
+        </el-form-item>
40
+    </el-form>
41
+</template>
42
+
43
+<script setup>
44
+const emit = defineEmits(['update'])
45
+const props = defineProps({
46
+    cron: {
47
+        type: Object,
48
+        default: {
49
+            second: "*",
50
+            min: "*",
51
+            hour: "*",
52
+            day: "*",
53
+            month: "*",
54
+            week: "?",
55
+            year: ""
56
+        }
57
+    },
58
+    check: {
59
+        type: Function,
60
+        default: () => {
61
+        }
62
+    }
63
+})
64
+
65
+const fullYear = Number(new Date().getFullYear())
66
+const maxFullYear = fullYear + 10
67
+const radioValue = ref(1)
68
+const cycle01 = ref(fullYear)
69
+const cycle02 = ref(fullYear + 1)
70
+const average01 = ref(fullYear)
71
+const average02 = ref(1)
72
+const checkboxList = ref([])
73
+const checkCopy = ref([fullYear])
74
+
75
+const cycleTotal = computed(() => {
76
+    cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
77
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
78
+    return cycle01.value + '-' + cycle02.value
79
+})
80
+const averageTotal = computed(() => {
81
+    average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
82
+    average02.value = props.check(average02.value, 1, 10)
83
+    return average01.value + '/' + average02.value
84
+})
85
+const checkboxString = computed(() => {
86
+    return checkboxList.value.join(',')
87
+})
88
+watch(() => props.cron.year, value => changeRadioValue(value))
89
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
90
+function changeRadioValue(value) {
91
+    if (value === '') {
92
+        radioValue.value = 1
93
+    } else if (value === "*") {
94
+        radioValue.value = 2
95
+    } else if (value.indexOf("-") > -1) {
96
+        const indexArr = value.split('-')
97
+        cycle01.value = Number(indexArr[0])
98
+        cycle02.value = Number(indexArr[1])
99
+        radioValue.value = 3
100
+    } else if (value.indexOf("/") > -1) {
101
+        const indexArr = value.split('/')
102
+        average01.value = Number(indexArr[0])
103
+        average02.value = Number(indexArr[1])
104
+        radioValue.value = 4
105
+    } else {
106
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
107
+        radioValue.value = 5
108
+    }
109
+}
110
+function onRadioChange() {
111
+    switch (radioValue.value) {
112
+        case 1:
113
+            emit('update', 'year', '', 'year')
114
+            break
115
+        case 2:
116
+            emit('update', 'year', '*', 'year')
117
+            break
118
+        case 3:
119
+            emit('update', 'year', cycleTotal.value, 'year')
120
+            break
121
+        case 4:
122
+            emit('update', 'year', averageTotal.value, 'year')
123
+            break
124
+        case 5:
125
+            if (checkboxList.value.length === 0) {
126
+                checkboxList.value.push(checkCopy.value[0])
127
+            } else {
128
+                checkCopy.value = checkboxList.value
129
+            }
130
+            emit('update', 'year', checkboxString.value, 'year')
131
+            break
132
+    }
133
+}
134
+</script>
135
+
136
+<style lang="scss" scoped>
137
+.el-input-number--small, .el-select, .el-select--small {
138
+    margin: 0 0.2rem;
139
+}
140
+.el-select, .el-select--small {
141
+    width: 18.8rem;
142
+}
143
+</style>

+ 82 - 0
src/components/DictTag/index.vue

@@ -0,0 +1,82 @@
1
+<template>
2
+  <div>
3
+    <template v-for="(item, index) in options">
4
+      <template v-if="values.includes(item.value)">
5
+        <span
6
+          v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
7
+          :key="item.value"
8
+          :index="index"
9
+          :class="item.elTagClass"
10
+        >{{ item.label + " " }}</span>
11
+        <el-tag
12
+          v-else
13
+          :disable-transitions="true"
14
+          :key="item.value + ''"
15
+          :index="index"
16
+          :type="item.elTagType"
17
+          :class="item.elTagClass"
18
+        >{{ item.label + " " }}</el-tag>
19
+      </template>
20
+    </template>
21
+    <template v-if="unmatch && showValue">
22
+      {{ unmatchArray | handleArray }}
23
+    </template>
24
+  </div>
25
+</template>
26
+
27
+<script setup>
28
+// 记录未匹配的项
29
+const unmatchArray = ref([])
30
+
31
+const props = defineProps({
32
+  // 数据
33
+  options: {
34
+    type: Array,
35
+    default: null,
36
+  },
37
+  // 当前的值
38
+  value: [Number, String, Array],
39
+  // 当未找到匹配的数据时,显示value
40
+  showValue: {
41
+    type: Boolean,
42
+    default: true,
43
+  },
44
+  separator: {
45
+    type: String,
46
+    default: ",",
47
+  }
48
+})
49
+
50
+const values = computed(() => {
51
+  if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
52
+  return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
53
+})
54
+
55
+const unmatch = computed(() => {
56
+  unmatchArray.value = []
57
+  // 没有value不显示
58
+  if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
59
+  // 传入值为数组
60
+  let unmatch = false // 添加一个标志来判断是否有未匹配项
61
+  values.value.forEach(item => {
62
+    if (!props.options.some(v => v.value === item)) {
63
+      unmatchArray.value.push(item)
64
+      unmatch = true // 如果有未匹配项,将标志设置为true
65
+    }
66
+  })
67
+  return unmatch // 返回标志的值
68
+})
69
+
70
+function handleArray(array) {
71
+  if (array.length === 0) return ""
72
+  return array.reduce((pre, cur) => {
73
+    return pre + " " + cur
74
+  })
75
+}
76
+</script>
77
+
78
+<style scoped>
79
+.el-tag + .el-tag {
80
+  margin-left: 10px;
81
+}
82
+</style>

+ 276 - 0
src/components/Editor/index.vue

@@ -0,0 +1,276 @@
1
+<template>
2
+  <div>
3
+    <el-upload
4
+      :action="uploadUrl"
5
+      :before-upload="handleBeforeUpload"
6
+      :on-success="handleUploadSuccess"
7
+      :on-error="handleUploadError"
8
+      name="file"
9
+      :show-file-list="false"
10
+      :headers="headers"
11
+      class="editor-img-uploader"
12
+      v-if="type == 'url'"
13
+    >
14
+      <i ref="uploadRef" class="editor-img-uploader"></i>
15
+    </el-upload>
16
+  </div>
17
+  <div class="editor">
18
+    <quill-editor
19
+      ref="quillEditorRef"
20
+      v-model:content="content"
21
+      contentType="html"
22
+      @textChange="(e) => $emit('update:modelValue', content)"
23
+      :options="options"
24
+      :style="styles"
25
+    />
26
+  </div>
27
+</template>
28
+
29
+<script setup>
30
+import axios from 'axios'
31
+import { QuillEditor } from "@vueup/vue-quill"
32
+import "@vueup/vue-quill/dist/vue-quill.snow.css"
33
+import { getToken } from "@/utils/auth"
34
+
35
+const { proxy } = getCurrentInstance()
36
+
37
+const quillEditorRef = ref()
38
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload") // 上传的图片服务器地址
39
+const headers = ref({
40
+  Authorization: "Bearer " + getToken()
41
+})
42
+
43
+const props = defineProps({
44
+  /* 编辑器的内容 */
45
+  modelValue: {
46
+    type: String,
47
+  },
48
+  /* 高度 */
49
+  height: {
50
+    type: Number,
51
+    default: null,
52
+  },
53
+  /* 最小高度 */
54
+  minHeight: {
55
+    type: Number,
56
+    default: null,
57
+  },
58
+  /* 只读 */
59
+  readOnly: {
60
+    type: Boolean,
61
+    default: false,
62
+  },
63
+  /* 上传文件大小限制(MB) */
64
+  fileSize: {
65
+    type: Number,
66
+    default: 5,
67
+  },
68
+  /* 类型(base64格式、url格式) */
69
+  type: {
70
+    type: String,
71
+    default: "url",
72
+  }
73
+})
74
+
75
+const options = ref({
76
+  theme: "snow",
77
+  bounds: document.body,
78
+  debug: "warn",
79
+  modules: {
80
+    // 工具栏配置
81
+    toolbar: [
82
+      ["bold", "italic", "underline", "strike"],      // 加粗 斜体 下划线 删除线
83
+      ["blockquote", "code-block"],                   // 引用  代码块
84
+      [{ list: "ordered" }, { list: "bullet" }],      // 有序、无序列表
85
+      [{ indent: "-1" }, { indent: "+1" }],           // 缩进
86
+      [{ size: ["small", false, "large", "huge"] }],  // 字体大小
87
+      [{ header: [1, 2, 3, 4, 5, 6, false] }],        // 标题
88
+      [{ color: [] }, { background: [] }],            // 字体颜色、字体背景颜色
89
+      [{ align: [] }],                                // 对齐方式
90
+      ["clean"],                                      // 清除文本格式
91
+      ["link", "image", "video"]                      // 链接、图片、视频
92
+    ],
93
+  },
94
+  placeholder: "请输入内容",
95
+  readOnly: props.readOnly
96
+})
97
+
98
+const styles = computed(() => {
99
+  let style = {}
100
+  if (props.minHeight) {
101
+    style.minHeight = `${props.minHeight}px`
102
+  }
103
+  if (props.height) {
104
+    style.height = `${props.height}px`
105
+  }
106
+  return style
107
+})
108
+
109
+const content = ref("")
110
+watch(() => props.modelValue, (v) => {
111
+  if (v !== content.value) {
112
+    content.value = v == undefined ? "<p></p>" : v
113
+  }
114
+}, { immediate: true })
115
+
116
+// 如果设置了上传地址则自定义图片上传事件
117
+onMounted(() => {
118
+  if (props.type == 'url') {
119
+    let quill = quillEditorRef.value.getQuill()
120
+    let toolbar = quill.getModule("toolbar")
121
+    toolbar.addHandler("image", (value) => {
122
+      if (value) {
123
+        proxy.$refs.uploadRef.click()
124
+      } else {
125
+        quill.format("image", false)
126
+      }
127
+    })
128
+    quill.root.addEventListener('paste', handlePasteCapture, true)
129
+  }
130
+})
131
+
132
+// 上传前校检格式和大小
133
+function handleBeforeUpload(file) {
134
+  const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"]
135
+  const isJPG = type.includes(file.type)
136
+  //检验文件格式
137
+  if (!isJPG) {
138
+    proxy.$modal.msgError(`图片格式错误!`)
139
+    return false
140
+  }
141
+  // 校检文件大小
142
+  if (props.fileSize) {
143
+    const isLt = file.size / 1024 / 1024 < props.fileSize
144
+    if (!isLt) {
145
+      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
146
+      return false
147
+    }
148
+  }
149
+  return true
150
+}
151
+
152
+// 上传成功处理
153
+function handleUploadSuccess(res, file) {
154
+  // 如果上传成功
155
+  if (res.code == 200) {
156
+    // 获取富文本实例
157
+    let quill = toRaw(quillEditorRef.value).getQuill()
158
+    // 获取光标位置
159
+    let length = quill.selection.savedRange.index
160
+    // 插入图片,res.url为服务器返回的图片链接地址
161
+    quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName)
162
+    // 调整光标到最后
163
+    quill.setSelection(length + 1)
164
+  } else {
165
+    proxy.$modal.msgError("图片插入失败")
166
+  }
167
+}
168
+
169
+// 上传失败处理
170
+function handleUploadError() {
171
+  proxy.$modal.msgError("图片插入失败")
172
+}
173
+
174
+// 复制粘贴图片处理
175
+function handlePasteCapture(e) {
176
+  const clipboard = e.clipboardData || window.clipboardData
177
+  if (clipboard && clipboard.items) {
178
+    for (let i = 0; i < clipboard.items.length; i++) {
179
+      const item = clipboard.items[i]
180
+      if (item.type.indexOf('image') !== -1) {
181
+        e.preventDefault()
182
+        const file = item.getAsFile()
183
+        insertImage(file)
184
+      }
185
+    }
186
+  }
187
+}
188
+
189
+function insertImage(file) {
190
+  const formData = new FormData()
191
+  formData.append("file", file)
192
+  axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
193
+    handleUploadSuccess(res.data)
194
+  })
195
+}
196
+</script>
197
+
198
+<style>
199
+.editor-img-uploader {
200
+  display: none;
201
+}
202
+.editor, .ql-toolbar {
203
+  white-space: pre-wrap !important;
204
+  line-height: normal !important;
205
+}
206
+.quill-img {
207
+  display: none;
208
+}
209
+.ql-snow .ql-tooltip[data-mode="link"]::before {
210
+  content: "请输入链接地址:";
211
+}
212
+.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
213
+  border-right: 0px;
214
+  content: "保存";
215
+  padding-right: 0px;
216
+}
217
+.ql-snow .ql-tooltip[data-mode="video"]::before {
218
+  content: "请输入视频地址:";
219
+}
220
+.ql-snow .ql-picker.ql-size .ql-picker-label::before,
221
+.ql-snow .ql-picker.ql-size .ql-picker-item::before {
222
+  content: "14px";
223
+}
224
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
225
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
226
+  content: "10px";
227
+}
228
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
229
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
230
+  content: "18px";
231
+}
232
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
233
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
234
+  content: "32px";
235
+}
236
+.ql-snow .ql-picker.ql-header .ql-picker-label::before,
237
+.ql-snow .ql-picker.ql-header .ql-picker-item::before {
238
+  content: "文本";
239
+}
240
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
241
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
242
+  content: "标题1";
243
+}
244
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
245
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
246
+  content: "标题2";
247
+}
248
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
249
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
250
+  content: "标题3";
251
+}
252
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
253
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
254
+  content: "标题4";
255
+}
256
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
257
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
258
+  content: "标题5";
259
+}
260
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
261
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
262
+  content: "标题6";
263
+}
264
+.ql-snow .ql-picker.ql-font .ql-picker-label::before,
265
+.ql-snow .ql-picker.ql-font .ql-picker-item::before {
266
+  content: "标准字体";
267
+}
268
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
269
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
270
+  content: "衬线字体";
271
+}
272
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
273
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
274
+  content: "等宽字体";
275
+}
276
+</style>

+ 256 - 0
src/components/FileUpload/index.vue

@@ -0,0 +1,256 @@
1
+<template>
2
+  <div class="upload-file">
3
+    <el-upload
4
+      multiple
5
+      :action="uploadFileUrl"
6
+      :before-upload="handleBeforeUpload"
7
+      :file-list="fileList"
8
+      :data="data"
9
+      :limit="limit"
10
+      :on-error="handleUploadError"
11
+      :on-exceed="handleExceed"
12
+      :on-success="handleUploadSuccess"
13
+      :show-file-list="false"
14
+      :headers="headers"
15
+      class="upload-file-uploader"
16
+      ref="fileUpload"
17
+      v-if="!disabled"
18
+    >
19
+      <!-- 上传按钮 -->
20
+      <el-button type="primary">选取文件</el-button>
21
+    </el-upload>
22
+    <!-- 上传提示 -->
23
+    <div class="el-upload__tip" v-if="showTip && !disabled">
24
+      请上传
25
+      <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
26
+      <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
27
+      的文件
28
+    </div>
29
+    <!-- 文件列表 -->
30
+    <transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
31
+      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
32
+        <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
33
+          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
34
+        </el-link>
35
+        <div class="ele-upload-list__item-content-action">
36
+          <el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">&nbsp;删除</el-link>
37
+        </div>
38
+      </li>
39
+    </transition-group>
40
+  </div>
41
+</template>
42
+
43
+<script setup>
44
+import { getToken } from "@/utils/auth"
45
+import Sortable from 'sortablejs'
46
+
47
+const props = defineProps({
48
+  modelValue: [String, Object, Array],
49
+  // 上传接口地址
50
+  action: {
51
+    type: String,
52
+    default: "/common/upload"
53
+  },
54
+  // 上传携带的参数
55
+  data: {
56
+    type: Object
57
+  },
58
+  // 数量限制
59
+  limit: {
60
+    type: Number,
61
+    default: 5
62
+  },
63
+  // 大小限制(MB)
64
+  fileSize: {
65
+    type: Number,
66
+    default: 5
67
+  },
68
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
69
+  fileType: {
70
+    type: Array,
71
+    default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
72
+  },
73
+  // 是否显示提示
74
+  isShowTip: {
75
+    type: Boolean,
76
+    default: true
77
+  },
78
+  // 禁用组件(仅查看文件)
79
+  disabled: {
80
+    type: Boolean,
81
+    default: false
82
+  },
83
+  // 拖动排序
84
+  drag: {
85
+    type: Boolean,
86
+    default: true
87
+  }
88
+})
89
+
90
+const { proxy } = getCurrentInstance()
91
+const emit = defineEmits()
92
+const number = ref(0)
93
+const uploadList = ref([])
94
+const baseUrl = import.meta.env.VITE_APP_BASE_API
95
+const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传文件服务器地址
96
+const headers = ref({ Authorization: "Bearer " + getToken() })
97
+const fileList = ref([])
98
+const showTip = computed(
99
+  () => props.isShowTip && (props.fileType || props.fileSize)
100
+)
101
+
102
+watch(() => props.modelValue, val => {
103
+  if (val) {
104
+    let temp = 1
105
+    // 首先将值转为数组
106
+    const list = Array.isArray(val) ? val : props.modelValue.split(',')
107
+    // 然后将数组转为对象数组
108
+    fileList.value = list.map(item => {
109
+      if (typeof item === "string") {
110
+        item = { name: item, url: item }
111
+      }
112
+      item.uid = item.uid || new Date().getTime() + temp++
113
+      return item
114
+    })
115
+  } else {
116
+    fileList.value = []
117
+    return []
118
+  }
119
+},{ deep: true, immediate: true })
120
+
121
+// 上传前校检格式和大小
122
+function handleBeforeUpload(file) {
123
+  // 校检文件类型
124
+  if (props.fileType.length) {
125
+    const fileName = file.name.split('.')
126
+    const fileExt = fileName[fileName.length - 1]
127
+    const isTypeOk = props.fileType.indexOf(fileExt) >= 0
128
+    if (!isTypeOk) {
129
+      proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`)
130
+      return false
131
+    }
132
+  }
133
+  // 校检文件名是否包含特殊字符
134
+  if (file.name.includes(',')) {
135
+    proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
136
+    return false
137
+  }
138
+  // 校检文件大小
139
+  if (props.fileSize) {
140
+    const isLt = file.size / 1024 / 1024 < props.fileSize
141
+    if (!isLt) {
142
+      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
143
+      return false
144
+    }
145
+  }
146
+  proxy.$modal.loading("正在上传文件,请稍候...")
147
+  number.value++
148
+  return true
149
+}
150
+
151
+// 文件个数超出
152
+function handleExceed() {
153
+  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
154
+}
155
+
156
+// 上传失败
157
+function handleUploadError(err) {
158
+  proxy.$modal.msgError("上传文件失败")
159
+  proxy.$modal.closeLoading()
160
+}
161
+
162
+// 上传成功回调
163
+function handleUploadSuccess(res, file) {
164
+  if (res.code === 200) {
165
+    uploadList.value.push({ name: res.fileName, url: res.fileName })
166
+    uploadedSuccessfully()
167
+  } else {
168
+    number.value--
169
+    proxy.$modal.closeLoading()
170
+    proxy.$modal.msgError(res.msg)
171
+    proxy.$refs.fileUpload.handleRemove(file)
172
+    uploadedSuccessfully()
173
+  }
174
+}
175
+
176
+// 删除文件
177
+function handleDelete(index) {
178
+  fileList.value.splice(index, 1)
179
+  emit("update:modelValue", listToString(fileList.value))
180
+}
181
+
182
+// 上传结束处理
183
+function uploadedSuccessfully() {
184
+  if (number.value > 0 && uploadList.value.length === number.value) {
185
+    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
186
+    uploadList.value = []
187
+    number.value = 0
188
+    emit("update:modelValue", listToString(fileList.value))
189
+    proxy.$modal.closeLoading()
190
+  }
191
+}
192
+
193
+// 获取文件名称
194
+function getFileName(name) {
195
+  // 如果是url那么取最后的名字 如果不是直接返回
196
+  if (name.lastIndexOf("/") > -1) {
197
+    return name.slice(name.lastIndexOf("/") + 1)
198
+  } else {
199
+    return name
200
+  }
201
+}
202
+
203
+// 对象转成指定字符串分隔
204
+function listToString(list, separator) {
205
+  let strs = ""
206
+  separator = separator || ","
207
+  for (let i in list) {
208
+    if (list[i].url) {
209
+      strs += list[i].url + separator
210
+    }
211
+  }
212
+  return strs != '' ? strs.substr(0, strs.length - 1) : ''
213
+}
214
+
215
+// 初始化拖拽排序
216
+onMounted(() => {
217
+  if (props.drag && !props.disabled) {
218
+    nextTick(() => {
219
+      const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
220
+      Sortable.create(element, {
221
+        ghostClass: 'file-upload-darg',
222
+        onEnd: (evt) => {
223
+          const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
224
+          fileList.value.splice(evt.newIndex, 0, movedItem)
225
+          emit('update:modelValue', listToString(fileList.value))
226
+        }
227
+      })
228
+    })
229
+  }
230
+})
231
+</script>
232
+<style scoped lang="scss">
233
+.file-upload-darg {
234
+  opacity: 0.5;
235
+  background: #c8ebfb;
236
+}
237
+.upload-file-uploader {
238
+  margin-bottom: 5px;
239
+}
240
+.upload-file-list .el-upload-list__item {
241
+  border: 1px solid #e4e7ed;
242
+  line-height: 2;
243
+  margin-bottom: 10px;
244
+  position: relative;
245
+  transition: none !important;
246
+}
247
+.upload-file-list .ele-upload-list__item-content {
248
+  display: flex;
249
+  justify-content: space-between;
250
+  align-items: center;
251
+  color: inherit;
252
+}
253
+.ele-upload-list__item-content-action .el-link {
254
+  margin-right: 10px;
255
+}
256
+</style>

+ 42 - 0
src/components/Hamburger/index.vue

@@ -0,0 +1,42 @@
1
+<template>
2
+  <div style="padding: 0 15px;" @click="toggleClick">
3
+    <svg
4
+      :class="{'is-active':isActive}"
5
+      class="hamburger"
6
+      viewBox="0 0 1024 1024"
7
+      xmlns="http://www.w3.org/2000/svg"
8
+      width="64"
9
+      height="64"
10
+      fill="currentColor"
11
+    >
12
+      <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" />
13
+    </svg>
14
+  </div>
15
+</template>
16
+
17
+<script setup>
18
+defineProps({
19
+  isActive: {
20
+    type: Boolean,
21
+    default: false
22
+  }
23
+})
24
+
25
+const emit = defineEmits()
26
+const toggleClick = () => {
27
+  emit('toggleClick')
28
+}
29
+</script>
30
+
31
+<style scoped>
32
+.hamburger {
33
+  display: inline-block;
34
+  vertical-align: middle;
35
+  width: 20px;
36
+  height: 20px;
37
+}
38
+
39
+.hamburger.is-active {
40
+  transform: rotate(180deg);
41
+}
42
+</style>

+ 252 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,252 @@
1
+<template>
2
+  <div class="header-search">
3
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
4
+    <el-dialog
5
+      v-model="show"
6
+      width="600"
7
+      @close="close"
8
+      :show-close="false"
9
+      append-to-body
10
+    >
11
+      <el-input
12
+        v-model="search"
13
+        ref="headerSearchSelectRef"
14
+        size="large"
15
+        @input="querySearch"
16
+        prefix-icon="Search"
17
+        placeholder="菜单搜索,支持标题、URL模糊查询"
18
+        clearable
19
+        @keyup.enter="selectActiveResult"
20
+        @keydown.up.prevent="navigateResult('up')"
21
+        @keydown.down.prevent="navigateResult('down')"
22
+      >
23
+      </el-input>
24
+
25
+      <div class="result-wrap">
26
+        <el-scrollbar>
27
+          <div class="search-item" tabindex="1" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1">
28
+            <div class="left">
29
+              <svg-icon class="menu-icon" :icon-class="item.icon" />
30
+            </div>
31
+            <div class="search-info" @click="change(item)">
32
+              <div class="menu-title">
33
+                {{ item.title.join(" / ") }}
34
+              </div>
35
+              <div class="menu-path">
36
+                {{ item.path }}
37
+              </div>
38
+            </div>
39
+            <svg-icon icon-class="enter" v-show="index === activeIndex"/>
40
+          </div>
41
+        </el-scrollbar>
42
+      </div>
43
+    </el-dialog>
44
+  </div>
45
+</template>
46
+
47
+<script setup>
48
+import Fuse from 'fuse.js'
49
+import { getNormalPath } from '@/utils/ruoyi'
50
+import { isHttp } from '@/utils/validate'
51
+import useSettingsStore from '@/store/modules/settings'
52
+import usePermissionStore from '@/store/modules/permission'
53
+
54
+const search = ref('')
55
+const options = ref([])
56
+const searchPool = ref([])
57
+const activeIndex = ref(-1)
58
+const show = ref(false)
59
+const fuse = ref(undefined)
60
+const headerSearchSelectRef = ref(null)
61
+const router = useRouter()
62
+const theme = computed(() => useSettingsStore().theme)
63
+const routes = computed(() => usePermissionStore().defaultRoutes)
64
+
65
+function click() {
66
+  show.value = !show.value
67
+  if (show.value) {
68
+    headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
69
+    options.value = searchPool.value
70
+  }
71
+}
72
+
73
+function close() {
74
+  headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
75
+  search.value = ''
76
+  options.value = []
77
+  show.value = false
78
+  activeIndex.value = -1
79
+}
80
+
81
+function change(val) {
82
+  const path = val.path
83
+  const query = val.query
84
+  if (isHttp(path)) {
85
+    // http(s):// 路径新窗口打开
86
+    const pindex = path.indexOf("http")
87
+    window.open(path.substr(pindex, path.length), "_blank")
88
+  } else {
89
+    if (query) {
90
+      router.push({ path: path, query: JSON.parse(query) })
91
+    } else {
92
+      router.push(path)
93
+    }
94
+  }
95
+
96
+  search.value = ''
97
+  options.value = []
98
+  nextTick(() => {
99
+    show.value = false
100
+  })
101
+}
102
+
103
+function initFuse(list) {
104
+  fuse.value = new Fuse(list, {
105
+    shouldSort: true,
106
+    threshold: 0.4,
107
+    location: 0,
108
+    distance: 100,
109
+    minMatchCharLength: 1,
110
+    keys: [{
111
+      name: 'title',
112
+      weight: 0.7
113
+    }, {
114
+      name: 'path',
115
+      weight: 0.3
116
+    }]
117
+  })
118
+}
119
+
120
+// Filter out the routes that can be displayed in the sidebar
121
+// And generate the internationalized title
122
+function generateRoutes(routes, basePath = '', prefixTitle = []) {
123
+  let res = []
124
+
125
+  for (const r of routes) {
126
+    // skip hidden router
127
+    if (r.hidden) { continue }
128
+    const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
129
+    const data = {
130
+      path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
131
+      title: [...prefixTitle],
132
+      icon: ''
133
+    }
134
+
135
+    if (r.meta && r.meta.title) {
136
+      data.title = [...data.title, r.meta.title]
137
+      data.icon = r.meta.icon
138
+      if (r.redirect !== "noRedirect") {
139
+        // only push the routes with title
140
+        // special case: need to exclude parent router without redirect
141
+        res.push(data)
142
+      }
143
+    }
144
+    if (r.query) {
145
+      data.query = r.query
146
+    }
147
+
148
+    // recursive child routes
149
+    if (r.children) {
150
+      const tempRoutes = generateRoutes(r.children, data.path, data.title)
151
+      if (tempRoutes.length >= 1) {
152
+        res = [...res, ...tempRoutes]
153
+      }
154
+    }
155
+  }
156
+  return res
157
+}
158
+
159
+function querySearch(query) {
160
+  activeIndex.value = -1
161
+  if (query !== '') {
162
+    options.value = fuse.value.search(query).map((item) => item.item) ?? searchPool.value
163
+  } else {
164
+    options.value = searchPool.value
165
+  }
166
+}
167
+
168
+function activeStyle(index) {
169
+  if (index !== activeIndex.value) return {}
170
+  return {
171
+    "background-color": theme.value,
172
+    "color": "#fff"
173
+  }
174
+}
175
+
176
+function navigateResult(direction) {
177
+  if (direction === "up") {
178
+    activeIndex.value = activeIndex.value <= 0 ? options.value.length - 1 : activeIndex.value - 1
179
+  } else if (direction === "down") {
180
+    activeIndex.value = activeIndex.value >= options.value.length - 1 ? 0 : activeIndex.value + 1
181
+  }
182
+}
183
+
184
+function selectActiveResult() {
185
+  if (options.value.length > 0 && activeIndex.value >= 0) {
186
+    change(options.value[activeIndex.value])
187
+  }
188
+}
189
+
190
+onMounted(() => {
191
+  searchPool.value = generateRoutes(routes.value)
192
+})
193
+
194
+watch(searchPool, (list) => {
195
+  initFuse(list)
196
+})
197
+</script>
198
+
199
+<style lang='scss' scoped>
200
+.header-search {
201
+  .search-icon {
202
+    cursor: pointer;
203
+    font-size: 18px;
204
+    vertical-align: middle;
205
+  }
206
+}
207
+
208
+.result-wrap {	
209
+  height: 280px;
210
+  margin: 6px 0;
211
+
212
+  .search-item {
213
+    display: flex;
214
+    height: 48px;
215
+    align-items: center;
216
+    padding-right: 10px;
217
+
218
+    .left {
219
+      width: 60px;
220
+      text-align: center;
221
+
222
+      .menu-icon {
223
+        width: 18px;
224
+        height: 18px;
225
+      }
226
+    }
227
+
228
+    .search-info {
229
+      padding-left: 5px;
230
+      margin-top: 10px;
231
+      width: 100%;
232
+      display: flex;
233
+      flex-direction: column;
234
+      justify-content: flex-start;
235
+      flex: 1;
236
+
237
+      .menu-title,
238
+      .menu-path {
239
+        height: 20px;
240
+      }
241
+      .menu-path {
242
+        color: #ccc;
243
+        font-size: 10px;
244
+      }
245
+    }
246
+  }
247
+
248
+  .search-item:hover {
249
+    cursor: pointer;
250
+  }
251
+}
252
+</style>

+ 111 - 0
src/components/IconSelect/index.vue

@@ -0,0 +1,111 @@
1
+<template>
2
+  <div class="icon-body">
3
+    <el-input
4
+      v-model="iconName"
5
+      class="icon-search"
6
+      clearable
7
+      placeholder="请输入图标名称"
8
+      @clear="filterIcons"
9
+      @input="filterIcons"
10
+    >
11
+      <template #suffix><i class="el-icon-search el-input__icon" /></template>
12
+    </el-input>
13
+    <div class="icon-list">
14
+      <div class="list-container">
15
+        <div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
16
+          <div :class="['icon-item', { active: activeIcon === item }]">
17
+            <svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
18
+            <span>{{ item }}</span>
19
+          </div>
20
+        </div>
21
+      </div>
22
+    </div>
23
+  </div>
24
+</template>
25
+
26
+<script setup>
27
+import icons from './requireIcons'
28
+
29
+const props = defineProps({
30
+  activeIcon: {
31
+    type: String
32
+  }
33
+})
34
+
35
+const iconName = ref('')
36
+const iconList = ref(icons)
37
+const emit = defineEmits(['selected'])
38
+
39
+function filterIcons() {
40
+  iconList.value = icons
41
+  if (iconName.value) {
42
+    iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
43
+  }
44
+}
45
+
46
+function selectedIcon(name) {
47
+  emit('selected', name)
48
+  document.body.click()
49
+}
50
+
51
+function reset() {
52
+  iconName.value = ''
53
+  iconList.value = icons
54
+}
55
+
56
+defineExpose({
57
+  reset
58
+})
59
+</script>
60
+
61
+<style lang='scss' scoped>
62
+   .icon-body {
63
+    width: 100%;
64
+    padding: 10px;
65
+    .icon-search {
66
+      position: relative;
67
+      margin-bottom: 5px;
68
+    }
69
+    .icon-list {
70
+      height: 200px;
71
+      overflow: auto;
72
+      .list-container {
73
+        display: flex;
74
+        flex-wrap: wrap;
75
+        .icon-item-wrapper {
76
+          width: calc(100% / 3);
77
+          height: 25px;
78
+          line-height: 25px;
79
+          cursor: pointer;
80
+          display: flex;
81
+          .icon-item {
82
+            display: flex;
83
+            max-width: 100%;
84
+            height: 100%;
85
+            padding: 0 5px;
86
+            &:hover {
87
+              background: #ececec;
88
+              border-radius: 5px;
89
+            }
90
+            .icon {
91
+              flex-shrink: 0;
92
+            }
93
+            span {
94
+              display: inline-block;
95
+              vertical-align: -0.15em;
96
+              fill: currentColor;
97
+              padding-left: 2px;
98
+              overflow: hidden;
99
+              text-overflow: ellipsis;
100
+              white-space: nowrap;
101
+            }
102
+          }
103
+          .icon-item.active {
104
+            background: #ececec;
105
+            border-radius: 5px;
106
+          }
107
+        }
108
+      }
109
+    }
110
+  }
111
+</style>

+ 8 - 0
src/components/IconSelect/requireIcons.js

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

+ 92 - 0
src/components/ImagePreview/index.vue

@@ -0,0 +1,92 @@
1
+<template>
2
+  <el-image
3
+    :src="`${realSrc}`"
4
+    fit="cover"
5
+    :style="`width:${realWidth};height:${realHeight};`"
6
+    :preview-src-list="realSrcList"
7
+    preview-teleported
8
+  >
9
+    <template #error>
10
+      <div class="image-slot">
11
+        <el-icon><picture-filled /></el-icon>
12
+      </div>
13
+    </template>
14
+  </el-image>
15
+</template>
16
+
17
+<script setup>
18
+import { isExternal } from "@/utils/validate"
19
+
20
+const props = defineProps({
21
+  src: {
22
+    type: String,
23
+    default: ""
24
+  },
25
+  width: {
26
+    type: [Number, String],
27
+    default: ""
28
+  },
29
+  height: {
30
+    type: [Number, String],
31
+    default: ""
32
+  }
33
+})
34
+
35
+const realSrc = computed(() => {
36
+  if (!props.src) {
37
+    return
38
+  }
39
+  let real_src = props.src.split(",")[0]
40
+  if (isExternal(real_src)) {
41
+    return real_src
42
+  }
43
+  return import.meta.env.VITE_APP_BASE_API + real_src
44
+})
45
+
46
+const realSrcList = computed(() => {
47
+  if (!props.src) {
48
+    return
49
+  }
50
+  let real_src_list = props.src.split(",")
51
+  let srcList = []
52
+  real_src_list.forEach(item => {
53
+    if (isExternal(item)) {
54
+      return srcList.push(item)
55
+    }
56
+    return srcList.push(import.meta.env.VITE_APP_BASE_API + item)
57
+  })
58
+  return srcList
59
+})
60
+
61
+const realWidth = computed(() =>
62
+  typeof props.width == "string" ? props.width : `${props.width}px`
63
+)
64
+
65
+const realHeight = computed(() =>
66
+  typeof props.height == "string" ? props.height : `${props.height}px`
67
+)
68
+</script>
69
+
70
+<style lang="scss" scoped>
71
+.el-image {
72
+  border-radius: 5px;
73
+  background-color: #ebeef5;
74
+  box-shadow: 0 0 5px 1px #ccc;
75
+  :deep(.el-image__inner) {
76
+    transition: all 0.3s;
77
+    cursor: pointer;
78
+    &:hover {
79
+      transform: scale(1.2);
80
+    }
81
+  }
82
+  :deep(.image-slot) {
83
+    display: flex;
84
+    justify-content: center;
85
+    align-items: center;
86
+    width: 100%;
87
+    height: 100%;
88
+    color: #909399;
89
+    font-size: 30px;
90
+  }
91
+}
92
+</style>

+ 274 - 0
src/components/ImageUpload/index.vue

@@ -0,0 +1,274 @@
1
+<template>
2
+  <div class="component-upload-image">
3
+    <el-upload
4
+      multiple
5
+      :disabled="disabled"
6
+      :action="uploadImgUrl"
7
+      list-type="picture-card"
8
+      :on-success="handleUploadSuccess"
9
+      :before-upload="handleBeforeUpload"
10
+      :data="data"
11
+      :limit="limit"
12
+      :on-error="handleUploadError"
13
+      :on-exceed="handleExceed"
14
+      ref="imageUpload"
15
+      :before-remove="handleDelete"
16
+      :show-file-list="true"
17
+      :headers="headers"
18
+      :file-list="fileList"
19
+      :on-preview="handlePictureCardPreview"
20
+      :class="{ hide: fileList.length >= limit }"
21
+    >
22
+      <el-icon class="avatar-uploader-icon"><plus /></el-icon>
23
+    </el-upload>
24
+    <!-- 上传提示 -->
25
+    <div class="el-upload__tip" v-if="showTip && !disabled">
26
+      请上传
27
+      <template v-if="fileSize">
28
+        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
29
+      </template>
30
+      <template v-if="fileType">
31
+        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
32
+      </template>
33
+      的文件
34
+    </div>
35
+
36
+    <el-dialog
37
+      v-model="dialogVisible"
38
+      title="预览"
39
+      width="800px"
40
+      append-to-body
41
+    >
42
+      <img
43
+        :src="dialogImageUrl"
44
+        style="display: block; max-width: 100%; margin: 0 auto"
45
+      />
46
+    </el-dialog>
47
+  </div>
48
+</template>
49
+
50
+<script setup>
51
+import { getToken } from "@/utils/auth"
52
+import { isExternal } from "@/utils/validate"
53
+import Sortable from 'sortablejs'
54
+
55
+const props = defineProps({
56
+  modelValue: [String, Object, Array],
57
+  // 上传接口地址
58
+  action: {
59
+    type: String,
60
+    default: "/common/upload"
61
+  },
62
+  // 上传携带的参数
63
+  data: {
64
+    type: Object
65
+  },
66
+  // 图片数量限制
67
+  limit: {
68
+    type: Number,
69
+    default: 5
70
+  },
71
+  // 大小限制(MB)
72
+  fileSize: {
73
+    type: Number,
74
+    default: 5
75
+  },
76
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
77
+  fileType: {
78
+    type: Array,
79
+    default: () => ["png", "jpg", "jpeg"]
80
+  },
81
+  // 是否显示提示
82
+  isShowTip: {
83
+    type: Boolean,
84
+    default: true
85
+  },
86
+  // 禁用组件(仅查看图片)
87
+  disabled: {
88
+    type: Boolean,
89
+    default: false
90
+  },
91
+  // 拖动排序
92
+  drag: {
93
+    type: Boolean,
94
+    default: true
95
+  },
96
+  resultType: { // 之前的逻辑不敢动 加一个控制返回值格式
97
+    type: String,
98
+    default: 'string'
99
+  }
100
+})
101
+
102
+const { proxy } = getCurrentInstance()
103
+const emit = defineEmits()
104
+const number = ref(0)
105
+const uploadList = ref([])
106
+const dialogImageUrl = ref("")
107
+const dialogVisible = ref(false)
108
+const baseUrl = import.meta.env.VITE_APP_BASE_API
109
+const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action) // 上传的图片服务器地址
110
+const headers = ref({ Authorization: "Bearer " + getToken() })
111
+const fileList = ref([])
112
+const showTip = computed(
113
+  () => props.isShowTip && (props.fileType || props.fileSize)
114
+)
115
+
116
+watch(() => props.modelValue, val => {
117
+  if (val) {
118
+    // 首先将值转为数组
119
+    const list = Array.isArray(val) ? val : props.modelValue.split(",")
120
+    // 然后将数组转为对象数组
121
+    fileList.value = list.map(item => {
122
+      if (typeof item === "string") {
123
+        if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
124
+          item = { name: baseUrl + item, url: baseUrl + item }
125
+        } else {
126
+          item = { name: item, url: item }
127
+        }
128
+      }
129
+      return item
130
+    })
131
+  } else {
132
+    fileList.value = []
133
+    return []
134
+  }
135
+},{ deep: true, immediate: true })
136
+
137
+// 上传前loading加载
138
+function handleBeforeUpload(file) {
139
+  let isImg = false
140
+  if (props.fileType.length) {
141
+    let fileExtension = ""
142
+    if (file.name.lastIndexOf(".") > -1) {
143
+      fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1)
144
+    }
145
+    isImg = props.fileType.some(type => {
146
+      if (file.type.indexOf(type) > -1) return true
147
+      if (fileExtension && fileExtension.indexOf(type) > -1) return true
148
+      return false
149
+    })
150
+  } else {
151
+    isImg = file.type.indexOf("image") > -1
152
+  }
153
+  if (!isImg) {
154
+    proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`)
155
+    return false
156
+  }
157
+  if (file.name.includes(',')) {
158
+    proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
159
+    return false
160
+  }
161
+  if (props.fileSize) {
162
+    const isLt = file.size / 1024 / 1024 < props.fileSize
163
+    if (!isLt) {
164
+      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`)
165
+      return false
166
+    }
167
+  }
168
+  proxy.$modal.loading("正在上传图片,请稍候...")
169
+  number.value++
170
+}
171
+
172
+// 文件个数超出
173
+function handleExceed() {
174
+  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`)
175
+}
176
+
177
+// 上传成功回调
178
+function handleUploadSuccess(res, file) {
179
+  if (res.code === 200) {
180
+    uploadList.value.push({ name: res.originalFilename, url: res.url })
181
+    uploadedSuccessfully()
182
+  } else {
183
+    number.value--
184
+    proxy.$modal.closeLoading()
185
+    proxy.$modal.msgError(res.msg)
186
+    proxy.$refs.imageUpload.handleRemove(file)
187
+    uploadedSuccessfully()
188
+  }
189
+}
190
+
191
+// 删除图片
192
+function handleDelete(file) {
193
+  const findex = fileList.value.map(f => f.name).indexOf(file.name)
194
+  if (findex > -1 && uploadList.value.length === number.value) {
195
+    fileList.value.splice(findex, 1)
196
+    if (props.resultType === 'object') {
197
+      emit("update:modelValue", fileList.value)
198
+    } else {
199
+      emit("update:modelValue", listToString(fileList.value))
200
+    }
201
+    return false
202
+  }
203
+}
204
+
205
+// 上传结束处理
206
+function uploadedSuccessfully() {
207
+  if (number.value > 0 && uploadList.value.length === number.value) {
208
+    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
209
+    uploadList.value = []
210
+    number.value = 0
211
+    if (props.resultType === 'object') {
212
+      emit("update:modelValue", fileList.value)
213
+    } else {
214
+      emit("update:modelValue", listToString(fileList.value))
215
+    }
216
+    proxy.$modal.closeLoading()
217
+  }
218
+}
219
+
220
+// 上传失败
221
+function handleUploadError() {
222
+  proxy.$modal.msgError("上传图片失败")
223
+  proxy.$modal.closeLoading()
224
+}
225
+
226
+// 预览
227
+function handlePictureCardPreview(file) {
228
+  dialogImageUrl.value = file.url
229
+  dialogVisible.value = true
230
+}
231
+
232
+// 对象转成指定字符串分隔
233
+function listToString(list, separator) {
234
+  let strs = ""
235
+  separator = separator || ","
236
+  for (let i in list) {
237
+    if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
238
+      strs += list[i].url.replace(baseUrl, "") + separator
239
+    }
240
+  }
241
+  return strs != "" ? strs.substr(0, strs.length - 1) : ""
242
+}
243
+
244
+// 初始化拖拽排序
245
+onMounted(() => {
246
+  if (props.drag && !props.disabled) {
247
+    nextTick(() => {
248
+      const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
249
+      Sortable.create(element, {
250
+        onEnd: (evt) => {
251
+          const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
252
+          fileList.value.splice(evt.newIndex, 0, movedItem) 
253
+          if (props.resultType === 'object') {
254
+            emit("update:modelValue", fileList.value)
255
+          } else {
256
+            emit("update:modelValue", listToString(fileList.value))
257
+          }
258
+        }
259
+      })
260
+    })
261
+  }
262
+})
263
+</script>
264
+
265
+<style scoped lang="scss">
266
+// .el-upload--picture-card 控制加号部分
267
+:deep(.hide .el-upload--picture-card) {
268
+    display: none;
269
+}
270
+
271
+:deep(.el-upload.el-upload--picture-card.is-disabled) {
272
+  display: none !important;
273
+} 
274
+</style>

+ 105 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,105 @@
1
+<template>
2
+  <div :class="{ 'hidden': hidden }" class="pagination-container">
3
+    <el-pagination
4
+      :background="background"
5
+      v-model:current-page="currentPage"
6
+      v-model:page-size="pageSize"
7
+      :layout="layout"
8
+      :page-sizes="pageSizes"
9
+      :pager-count="pagerCount"
10
+      :total="total"
11
+      @size-change="handleSizeChange"
12
+      @current-change="handleCurrentChange"
13
+    />
14
+  </div>
15
+</template>
16
+
17
+<script setup>
18
+import { scrollTo } from '@/utils/scroll-to'
19
+
20
+const props = defineProps({
21
+  total: {
22
+    required: true,
23
+    type: Number
24
+  },
25
+  page: {
26
+    type: Number,
27
+    default: 1
28
+  },
29
+  limit: {
30
+    type: Number,
31
+    default: 20
32
+  },
33
+  pageSizes: {
34
+    type: Array,
35
+    default() {
36
+      return [10, 20, 30, 50]
37
+    }
38
+  },
39
+  // 移动端页码按钮的数量端默认值5
40
+  pagerCount: {
41
+    type: Number,
42
+    default: document.body.clientWidth < 992 ? 5 : 7
43
+  },
44
+  layout: {
45
+    type: String,
46
+    default: 'total, sizes, prev, pager, next, jumper'
47
+  },
48
+  background: {
49
+    type: Boolean,
50
+    default: true
51
+  },
52
+  autoScroll: {
53
+    type: Boolean,
54
+    default: true
55
+  },
56
+  hidden: {
57
+    type: Boolean,
58
+    default: false
59
+  }
60
+})
61
+
62
+const emit = defineEmits()
63
+const currentPage = computed({
64
+  get() {
65
+    return props.page
66
+  },
67
+  set(val) {
68
+    emit('update:page', val)
69
+  }
70
+})
71
+const pageSize = computed({
72
+  get() {
73
+    return props.limit
74
+  },
75
+  set(val){
76
+    emit('update:limit', val)
77
+  }
78
+})
79
+
80
+function handleSizeChange(val) {
81
+  if (currentPage.value * val > props.total) {
82
+    currentPage.value = 1
83
+  }
84
+  emit('pagination', { page: currentPage.value, limit: val })
85
+  if (props.autoScroll) {
86
+    scrollTo(0, 800)
87
+  }
88
+}
89
+
90
+function handleCurrentChange(val) {
91
+  emit('pagination', { page: val, limit: pageSize.value })
92
+  if (props.autoScroll) {
93
+    scrollTo(0, 800)
94
+  }
95
+}
96
+</script>
97
+
98
+<style scoped>
99
+.pagination-container {
100
+  background: #fff;
101
+}
102
+.pagination-container.hidden {
103
+  display: none;
104
+}
105
+</style>

+ 3 - 0
src/components/ParentView/index.vue

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

+ 157 - 0
src/components/RightToolbar/index.vue

@@ -0,0 +1,157 @@
1
+<template>
2
+  <div class="top-right-btn" :style="style">
3
+    <el-row>
4
+      <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
5
+        <el-button circle icon="Search" @click="toggleSearch()" />
6
+      </el-tooltip>
7
+      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
8
+        <el-button circle icon="Refresh" @click="refresh()" />
9
+      </el-tooltip>
10
+      <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
11
+        <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
12
+        <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
13
+          <el-button circle icon="Menu" />
14
+          <template #dropdown>
15
+            <el-dropdown-menu>
16
+              <!-- 全选/反选 按钮 -->
17
+              <el-dropdown-item>
18
+                <el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
19
+              </el-dropdown-item>
20
+              <div class="check-line"></div>
21
+              <template v-for="item in columns" :key="item.key">
22
+                <el-dropdown-item>
23
+                  <el-checkbox v-model="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
24
+                </el-dropdown-item>
25
+              </template>
26
+            </el-dropdown-menu>
27
+          </template>
28
+        </el-dropdown>
29
+      </el-tooltip>
30
+    </el-row>
31
+    <el-dialog :title="title" v-model="open" append-to-body>
32
+      <el-transfer
33
+        :titles="['显示', '隐藏']"
34
+        v-model="value"
35
+        :data="columns"
36
+        @change="dataChange"
37
+      ></el-transfer>
38
+    </el-dialog>
39
+  </div>
40
+</template>
41
+
42
+<script setup>
43
+const props = defineProps({
44
+  /* 是否显示检索条件 */
45
+  showSearch: {
46
+    type: Boolean,
47
+    default: true
48
+  },
49
+  /* 显隐列信息 */
50
+  columns: {
51
+    type: Array
52
+  },
53
+  /* 是否显示检索图标 */
54
+  search: {
55
+    type: Boolean,
56
+    default: true
57
+  },
58
+  /* 显隐列类型(transfer穿梭框、checkbox复选框) */
59
+  showColumnsType: {
60
+    type: String,
61
+    default: "checkbox"
62
+  },
63
+  /* 右外边距 */
64
+  gutter: {
65
+    type: Number,
66
+    default: 10
67
+  },
68
+})
69
+
70
+const emits = defineEmits(['update:showSearch', 'queryTable'])
71
+
72
+// 显隐数据
73
+const value = ref([])
74
+// 弹出层标题
75
+const title = ref("显示/隐藏")
76
+// 是否显示弹出层
77
+const open = ref(false)
78
+
79
+const style = computed(() => {
80
+  const ret = {}
81
+  if (props.gutter) {
82
+    ret.marginRight = `${props.gutter / 2}px`
83
+  }
84
+  return ret
85
+})
86
+
87
+// 是否全选/半选 状态
88
+const isChecked = computed({
89
+  get: () => props.columns.every(col => col.visible),
90
+  set: () => {}
91
+})
92
+const isIndeterminate = computed(() => props.columns.some((col) => col.visible) && !isChecked.value)
93
+
94
+// 搜索
95
+function toggleSearch() {
96
+  emits("update:showSearch", !props.showSearch)
97
+}
98
+
99
+// 刷新
100
+function refresh() {
101
+  emits("queryTable")
102
+}
103
+
104
+// 右侧列表元素变化
105
+function dataChange(data) {
106
+  for (let item in props.columns) {
107
+    const key = props.columns[item].key
108
+    props.columns[item].visible = !data.includes(key)
109
+  }
110
+}
111
+
112
+// 打开显隐列dialog
113
+function showColumn() {
114
+  open.value = true
115
+}
116
+
117
+if (props.showColumnsType == 'transfer') {
118
+  // 显隐列初始默认隐藏列
119
+  for (let item in props.columns) {
120
+    if (props.columns[item].visible === false) {
121
+      value.value.push(parseInt(item))
122
+    }
123
+  }
124
+}
125
+
126
+// 单勾选
127
+function checkboxChange(event, label) {
128
+  props.columns.filter(item => item.label == label)[0].visible = event
129
+}
130
+
131
+// 切换全选/反选
132
+function toggleCheckAll() {
133
+  const newValue = !isChecked.value
134
+  props.columns.forEach((col) => (col.visible = newValue))
135
+}
136
+</script>
137
+
138
+<style lang='scss' scoped>
139
+:deep(.el-transfer__button) {
140
+  border-radius: 50%;
141
+  display: block;
142
+  margin-left: 0px;
143
+}
144
+:deep(.el-transfer__button:first-child) {
145
+  margin-bottom: 10px;
146
+}
147
+:deep(.el-dropdown-menu__item) {
148
+  line-height: 30px;
149
+  padding: 0 17px;
150
+}
151
+.check-line {
152
+  width: 90%;
153
+  height: 1px;
154
+  background-color: #ccc;
155
+  margin: 3px auto;
156
+}
157
+</style>

+ 13 - 0
src/components/RuoYi/Doc/index.vue

@@ -0,0 +1,13 @@
1
+<template>
2
+  <div>
3
+    <svg-icon icon-class="question" @click="goto" />
4
+  </div>
5
+</template>
6
+
7
+<script setup>
8
+const url = ref('http://doc.ruoyi.vip/ruoyi-vue')
9
+
10
+function goto() {
11
+  window.open(url.value)
12
+}
13
+</script>

+ 13 - 0
src/components/RuoYi/Git/index.vue

@@ -0,0 +1,13 @@
1
+<template>
2
+  <div>
3
+    <svg-icon icon-class="github" @click="goto" />
4
+  </div>
5
+</template>
6
+
7
+<script setup>
8
+const url = ref('https://gitee.com/y_project/RuoYi-Vue')
9
+
10
+function goto() {
11
+  window.open(url.value)
12
+}
13
+</script>

+ 22 - 0
src/components/Screenfull/index.vue

@@ -0,0 +1,22 @@
1
+<template>
2
+  <div>
3
+    <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
4
+  </div>
5
+</template>
6
+
7
+<script setup>
8
+import { useFullscreen } from '@vueuse/core'
9
+
10
+const { isFullscreen, enter, exit, toggle } = useFullscreen()
11
+</script>
12
+
13
+<style lang='scss' scoped>
14
+.screenfull-svg {
15
+  display: inline-block;
16
+  cursor: pointer;
17
+  fill: #5a5e66;
18
+  width: 20px;
19
+  height: 20px;
20
+  vertical-align: 10px;
21
+}
22
+</style>

+ 45 - 0
src/components/SizeSelect/index.vue

@@ -0,0 +1,45 @@
1
+<template>
2
+  <div>
3
+    <el-dropdown trigger="click" @command="handleSetSize">
4
+      <div class="size-icon--style">
5
+        <svg-icon class-name="size-icon" icon-class="size" />
6
+      </div>
7
+      <template #dropdown>
8
+        <el-dropdown-menu>
9
+          <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
10
+            {{ item.label }}
11
+          </el-dropdown-item>
12
+        </el-dropdown-menu>
13
+      </template>
14
+    </el-dropdown>
15
+  </div>
16
+</template>
17
+
18
+<script setup>
19
+import useAppStore from "@/store/modules/app"
20
+
21
+const appStore = useAppStore()
22
+const size = computed(() => appStore.size)
23
+const route = useRoute()
24
+const router = useRouter()
25
+const { proxy } = getCurrentInstance()
26
+const sizeOptions = ref([
27
+  { label: "较大", value: "large" },
28
+  { label: "默认", value: "default" },
29
+  { label: "稍小", value: "small" },
30
+])
31
+
32
+function handleSetSize(size) {
33
+  proxy.$modal.loading("正在设置布局大小,请稍候...")
34
+  appStore.setSize(size)
35
+  setTimeout("window.location.reload()", 1000)
36
+}
37
+</script>
38
+
39
+<style lang='scss' scoped>
40
+.size-icon--style {
41
+  font-size: 18px;
42
+  line-height: 50px;
43
+  padding-right: 7px;
44
+}
45
+</style>

+ 53 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,53 @@
1
+<template>
2
+  <svg :class="svgClass" aria-hidden="true">
3
+    <use :xlink:href="iconName" :fill="color" />
4
+  </svg>
5
+</template>
6
+
7
+<script>
8
+export default defineComponent({
9
+  props: {
10
+    iconClass: {
11
+      type: String,
12
+      required: true
13
+    },
14
+    className: {
15
+      type: String,
16
+      default: ''
17
+    },
18
+    color: {
19
+      type: String,
20
+      default: ''
21
+    },
22
+  },
23
+  setup(props) {
24
+    return {
25
+      iconName: computed(() => `#icon-${props.iconClass}`),
26
+      svgClass: computed(() => {
27
+        if (props.className) {
28
+          return `svg-icon ${props.className}`
29
+        }
30
+        return 'svg-icon'
31
+      })
32
+    }
33
+  }
34
+})
35
+</script>
36
+
37
+<style scope lang="scss">
38
+.sub-el-icon,
39
+.nav-icon {
40
+  display: inline-block;
41
+  font-size: 15px;
42
+  margin-right: 12px;
43
+  position: relative;
44
+}
45
+
46
+.svg-icon {
47
+  width: 1em;
48
+  height: 1em;
49
+  position: relative;
50
+  fill: currentColor;
51
+  vertical-align: -2px;
52
+}
53
+</style>

+ 10 - 0
src/components/SvgIcon/svgicon.js

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

+ 217 - 0
src/components/TopNav/index.vue

@@ -0,0 +1,217 @@
1
+<template>
2
+  <el-menu
3
+    :default-active="activeMenu"
4
+    mode="horizontal"
5
+    @select="handleSelect"
6
+    :ellipsis="false"
7
+  >
8
+    <template v-for="(item, index) in topMenus">
9
+      <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
10
+        <svg-icon
11
+        v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
12
+        :icon-class="item.meta.icon"/>
13
+        {{ item.meta.title }}
14
+      </el-menu-item>
15
+    </template>
16
+
17
+    <!-- 顶部菜单超出数量折叠 -->
18
+    <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
19
+      <template #title>更多菜单</template>
20
+      <template v-for="(item, index) in topMenus">
21
+        <el-menu-item
22
+          :index="item.path"
23
+          :key="index"
24
+          v-if="index >= visibleNumber">
25
+        <svg-icon
26
+          v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
27
+          :icon-class="item.meta.icon"/>
28
+        {{ item.meta.title }}
29
+        </el-menu-item>
30
+      </template>
31
+    </el-sub-menu>
32
+  </el-menu>
33
+</template>
34
+
35
+<script setup>
36
+import { constantRoutes } from "@/router"
37
+import { isHttp } from '@/utils/validate'
38
+import useAppStore from '@/store/modules/app'
39
+import useSettingsStore from '@/store/modules/settings'
40
+import usePermissionStore from '@/store/modules/permission'
41
+
42
+// 顶部栏初始数
43
+const visibleNumber = ref(null)
44
+// 当前激活菜单的 index
45
+const currentIndex = ref(null)
46
+// 隐藏侧边栏路由
47
+const hideList = ['/index', '/user/profile']
48
+
49
+const appStore = useAppStore()
50
+const settingsStore = useSettingsStore()
51
+const permissionStore = usePermissionStore()
52
+const route = useRoute()
53
+const router = useRouter()
54
+
55
+// 主题颜色
56
+const theme = computed(() => settingsStore.theme)
57
+// 所有的路由信息
58
+const routers = computed(() => permissionStore.topbarRouters)
59
+
60
+// 顶部显示菜单
61
+const topMenus = computed(() => {
62
+  let topMenus = []
63
+  routers.value.map((menu) => {
64
+    if (menu.hidden !== true) {
65
+      // 兼容顶部栏一级菜单内部跳转
66
+      if (menu.path === '/' && menu.children) {
67
+          topMenus.push(menu.children[0])
68
+      } else {
69
+          topMenus.push(menu)
70
+      }
71
+    }
72
+  })
73
+  return topMenus
74
+})
75
+
76
+// 设置子路由
77
+const childrenMenus = computed(() => {
78
+  let childrenMenus = []
79
+  routers.value.map((router) => {
80
+    for (let item in router.children) {
81
+      if (router.children[item].parentPath === undefined) {
82
+        if(router.path === "/") {
83
+          router.children[item].path = "/" + router.children[item].path
84
+        } else {
85
+          if(!isHttp(router.children[item].path)) {
86
+            router.children[item].path = router.path + "/" + router.children[item].path
87
+          }
88
+        }
89
+        router.children[item].parentPath = router.path
90
+      }
91
+      childrenMenus.push(router.children[item])
92
+    }
93
+  })
94
+  return constantRoutes.concat(childrenMenus)
95
+})
96
+
97
+// 默认激活的菜单
98
+const activeMenu = computed(() => {
99
+  const path = route.path
100
+  let activePath = path
101
+  if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
102
+    const tmpPath = path.substring(1, path.length)
103
+    if (!route.meta.link) {
104
+      activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
105
+      appStore.toggleSideBarHide(false)
106
+    }
107
+  } else if(!route.children) {
108
+    activePath = path
109
+    appStore.toggleSideBarHide(true)
110
+  }
111
+  activeRoutes(activePath)
112
+  return activePath
113
+})
114
+
115
+function setVisibleNumber() {
116
+  const width = document.body.getBoundingClientRect().width / 3
117
+  visibleNumber.value = parseInt(width / 85)
118
+}
119
+
120
+function handleSelect(key, keyPath) {
121
+  currentIndex.value = key
122
+  const route = routers.value.find(item => item.path === key)
123
+  if (isHttp(key)) {
124
+    // http(s):// 路径新窗口打开
125
+    window.open(key, "_blank")
126
+  } else if (!route || !route.children) {
127
+    // 没有子路由路径内部打开
128
+    const routeMenu = childrenMenus.value.find(item => item.path === key)
129
+    if (routeMenu && routeMenu.query) {
130
+      let query = JSON.parse(routeMenu.query)
131
+      router.push({ path: key, query: query })
132
+    } else {
133
+      router.push({ path: key })
134
+    }
135
+    appStore.toggleSideBarHide(true)
136
+  } else {
137
+    // 显示左侧联动菜单
138
+    activeRoutes(key)
139
+    appStore.toggleSideBarHide(false)
140
+  }
141
+}
142
+
143
+function activeRoutes(key) {
144
+  let routes = []
145
+  if (childrenMenus.value && childrenMenus.value.length > 0) {
146
+    childrenMenus.value.map((item) => {
147
+      if (key == item.parentPath || (key == "index" && "" == item.path)) {
148
+        routes.push(item)
149
+      }
150
+    })
151
+  }
152
+  if(routes.length > 0) {
153
+    permissionStore.setSidebarRouters(routes)
154
+  } else {
155
+    appStore.toggleSideBarHide(true)
156
+  }
157
+  return routes
158
+}
159
+
160
+onMounted(() => {
161
+  window.addEventListener('resize', setVisibleNumber)
162
+})
163
+
164
+onBeforeUnmount(() => {
165
+  window.removeEventListener('resize', setVisibleNumber)
166
+})
167
+
168
+onMounted(() => {
169
+  setVisibleNumber()
170
+})
171
+</script>
172
+
173
+<style lang="scss">
174
+.topmenu-container.el-menu--horizontal > .el-menu-item {
175
+  float: left;
176
+  height: 50px !important;
177
+  line-height: 50px !important;
178
+  color: #999093 !important;
179
+  padding: 0 5px !important;
180
+  margin: 0 10px !important;
181
+}
182
+
183
+.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
184
+  border-bottom: 2px solid #{'var(--theme)'} !important;
185
+  color: #303133;
186
+}
187
+
188
+/* sub-menu item */
189
+.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
190
+  float: left;
191
+  height: 50px !important;
192
+  line-height: 50px !important;
193
+  color: #999093 !important;
194
+  padding: 0 5px !important;
195
+  margin: 0 10px !important;
196
+}
197
+
198
+/* 背景色隐藏 */
199
+.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 {
200
+  background-color: #ffffff;
201
+}
202
+
203
+/* 图标右间距 */
204
+.topmenu-container .svg-icon {
205
+  margin-right: 4px;
206
+}
207
+
208
+/* topmenu more arrow */
209
+.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
210
+  position: static;
211
+  vertical-align: middle;
212
+  margin-left: 8px;
213
+  margin-top: 0px;
214
+}
215
+
216
+
217
+</style>

+ 87 - 0
src/components/UserSelect/index.vue

@@ -0,0 +1,87 @@
1
+<template>
2
+  <div class="flex flex-wrap">
3
+    <el-select
4
+      v-model="selectedValue"
5
+      filterable
6
+      remote
7
+      reserve-keyword
8
+      placeholder="请输入用户名"
9
+      :remote-method="debouncedRemoteMethod"
10
+      :loading="loading"
11
+      :multiple="multiple"
12
+      :disabled="disabled"
13
+      :style="{ width: width }"
14
+      clearable
15
+    >
16
+      <el-option
17
+        v-for="item in options"
18
+        :key="item.value"
19
+        :label="item.label"
20
+        :value="item.value"
21
+      />
22
+    </el-select>
23
+  </div>
24
+</template>
25
+
26
+<script lang="ts" setup>
27
+import { onMounted, ref, watch, defineProps, defineEmits, onUnmounted } from 'vue'
28
+import { debounce } from 'lodash-es'
29
+import { listUser } from "@/api/system/user"
30
+
31
+interface ListItem {
32
+  value: string
33
+  label: string
34
+}
35
+
36
+// 定义组件属性
37
+const props = defineProps<{
38
+  modelValue: string | string[]
39
+  multiple?: boolean
40
+  disabled?: boolean
41
+  width?: string
42
+}>()
43
+
44
+// 定义组件事件
45
+const emits = defineEmits(['update:modelValue'])
46
+
47
+const options = ref<ListItem[]>([])
48
+const loading = ref(false)
49
+const selectedValue = ref<string | string[]>(props.modelValue)
50
+
51
+// 远程搜索方法
52
+const remoteMethod = async (query: string) => {
53
+  if (query) {
54
+    loading.value = true
55
+    try {
56
+      // 调用远程接口获取数据
57
+      const response = await listUser({ nickName: query }) 
58
+      // 假设接口返回的数据结构为 { data: [...] },根据实际情况调整
59
+      options.value = response.rows.map((item) => ({ 
60
+        value: item.userId, // 根据实际接口返回字段调整
61
+        label: item.nickName // 根据实际接口返回字段调整
62
+      }))
63
+    } catch (error) {
64
+      console.error('远程搜索出错:', error)
65
+    } finally {
66
+      loading.value = false
67
+    }
68
+  } else {
69
+    options.value = []
70
+  }
71
+}
72
+
73
+// 防抖处理远程搜索
74
+const debouncedRemoteMethod = debounce(remoteMethod, 300)
75
+
76
+// 监听选中值变化并触发更新事件
77
+watch(selectedValue, (newValue) => {
78
+  emits('update:modelValue', newValue)
79
+})
80
+
81
+// 组件销毁时清空数据
82
+onUnmounted(() => {
83
+  options.value = []
84
+  selectedValue.value = props.multiple ? [] : ''
85
+  emits('update:modelValue', selectedValue.value)
86
+})
87
+</script>

+ 5 - 0
src/components/VueCountTo/index.js

@@ -0,0 +1,5 @@
1
+import CountTo from './vue-countTo.vue';
2
+export default CountTo;
3
+if (typeof window !== 'undefined' && window.Vue) {
4
+  window.Vue.component('count-to', CountTo);
5
+}

+ 46 - 0
src/components/VueCountTo/requestAnimationFrame.js

@@ -0,0 +1,46 @@
1
+let lastTime = 0
2
+const prefixes = 'webkit moz ms o'.split(' ') // 各浏览器前缀
3
+
4
+let requestAnimationFrame
5
+let cancelAnimationFrame
6
+
7
+const isServer = typeof window === 'undefined'
8
+if (isServer) {
9
+  requestAnimationFrame = function() {
10
+    return
11
+  }
12
+  cancelAnimationFrame = function() {
13
+    return
14
+  }
15
+} else {
16
+  requestAnimationFrame = window.requestAnimationFrame
17
+  cancelAnimationFrame = window.cancelAnimationFrame
18
+  let prefix
19
+    // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
20
+  for (let i = 0; i < prefixes.length; i++) {
21
+    if (requestAnimationFrame && cancelAnimationFrame) { break }
22
+    prefix = prefixes[i]
23
+    requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
24
+    cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
25
+  }
26
+
27
+  // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
28
+  if (!requestAnimationFrame || !cancelAnimationFrame) {
29
+    requestAnimationFrame = function(callback) {
30
+      const currTime = new Date().getTime()
31
+      // 为了使setTimteout的尽可能的接近每秒60帧的效果
32
+      const timeToCall = Math.max(0, 16 - (currTime - lastTime))
33
+      const id = window.setTimeout(() => {
34
+        callback(currTime + timeToCall)
35
+      }, timeToCall)
36
+      lastTime = currTime + timeToCall
37
+      return id
38
+    }
39
+
40
+    cancelAnimationFrame = function(id) {
41
+      window.clearTimeout(id)
42
+    }
43
+  }
44
+}
45
+
46
+export { requestAnimationFrame, cancelAnimationFrame }

+ 191 - 0
src/components/VueCountTo/vue-countTo.vue

@@ -0,0 +1,191 @@
1
+<template>
2
+    <span>
3
+      {{displayValue}}
4
+    </span>
5
+</template>
6
+<script>
7
+import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'
8
+export default {
9
+  props: {
10
+    startVal: {
11
+      type: Number,
12
+      required: false,
13
+      default: 0
14
+    },
15
+    endVal: {
16
+      type: Number,
17
+      required: false,
18
+      default: 2017
19
+    },
20
+    duration: {
21
+      type: Number,
22
+      required: false,
23
+      default: 3000
24
+    },
25
+    autoplay: {
26
+      type: Boolean,
27
+      required: false,
28
+      default: true
29
+    },
30
+    decimals: {
31
+      type: Number,
32
+      required: false,
33
+      default: 0,
34
+      validator(value) {
35
+        return value >= 0
36
+      }
37
+    },
38
+    decimal: {
39
+      type: String,
40
+      required: false,
41
+      default: '.'
42
+    },
43
+    separator: {
44
+      type: String,
45
+      required: false,
46
+      default: ','
47
+    },
48
+    prefix: {
49
+      type: String,
50
+      required: false,
51
+      default: ''
52
+    },
53
+    suffix: {
54
+      type: String,
55
+      required: false,
56
+      default: ''
57
+    },
58
+    useEasing: {
59
+      type: Boolean,
60
+      required: false,
61
+      default: true
62
+    },
63
+    easingFn: {
64
+      type: Function,
65
+      default(t, b, c, d) {
66
+        return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
67
+      }
68
+    }
69
+  },
70
+  data() {
71
+    return {
72
+      localStartVal: this.startVal,
73
+      displayValue: this.formatNumber(this.startVal),
74
+      printVal: null,
75
+      paused: false,
76
+      localDuration: this.duration,
77
+      startTime: null,
78
+      timestamp: null,
79
+      remaining: null,
80
+      rAF: null
81
+    };
82
+  },
83
+  computed: {
84
+    countDown() {
85
+      return this.startVal > this.endVal
86
+    }
87
+  },
88
+  watch: {
89
+    startVal() {
90
+      if (this.autoplay) {
91
+        this.start();
92
+      }
93
+    },
94
+    endVal() {
95
+      if (this.autoplay) {
96
+        this.start();
97
+      }
98
+    }
99
+  },
100
+  mounted() {
101
+    if (this.autoplay) {
102
+      this.start();
103
+    }
104
+    this.$emit('mountedCallback')
105
+  },
106
+  methods: {
107
+    start() {
108
+      this.localStartVal = this.startVal;
109
+      this.startTime = null;
110
+      this.localDuration = this.duration;
111
+      this.paused = false;
112
+      this.rAF = requestAnimationFrame(this.count);
113
+    },
114
+    pauseResume() {
115
+      if (this.paused) {
116
+        this.resume();
117
+        this.paused = false;
118
+      } else {
119
+        this.pause();
120
+        this.paused = true;
121
+      }
122
+    },
123
+    pause() {
124
+      cancelAnimationFrame(this.rAF);
125
+    },
126
+    resume() {
127
+      this.startTime = null;
128
+      this.localDuration = +this.remaining;
129
+      this.localStartVal = +this.printVal;
130
+      requestAnimationFrame(this.count);
131
+    },
132
+    reset() {
133
+      this.startTime = null;
134
+      cancelAnimationFrame(this.rAF);
135
+      this.displayValue = this.formatNumber(this.startVal);
136
+    },
137
+    count(timestamp) {
138
+      if (!this.startTime) this.startTime = timestamp;
139
+      this.timestamp = timestamp;
140
+      const progress = timestamp - this.startTime;
141
+      this.remaining = this.localDuration - progress;
142
+
143
+      if (this.useEasing) {
144
+        if (this.countDown) {
145
+          this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration)
146
+        } else {
147
+          this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
148
+        }
149
+      } else {
150
+        if (this.countDown) {
151
+          this.printVal = this.localStartVal - ((this.localStartVal - this.endVal) * (progress / this.localDuration));
152
+        } else {
153
+          this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
154
+        }
155
+      }
156
+      if (this.countDown) {
157
+        this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
158
+      } else {
159
+        this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
160
+      }
161
+
162
+      this.displayValue = this.formatNumber(this.printVal)
163
+      if (progress < this.localDuration) {
164
+        this.rAF = requestAnimationFrame(this.count);
165
+      } else {
166
+        this.$emit('callback');
167
+      }
168
+    },
169
+    isNumber(val) {
170
+      return !isNaN(parseFloat(val))
171
+    },
172
+    formatNumber(num) {
173
+      num = num.toFixed(this.decimals);
174
+      num += '';
175
+      const x = num.split('.');
176
+      let x1 = x[0];
177
+      const x2 = x.length > 1 ? this.decimal + x[1] : '';
178
+      const rgx = /(\d+)(\d{3})/;
179
+      if (this.separator && !this.isNumber(this.separator)) {
180
+        while (rgx.test(x1)) {
181
+          x1 = x1.replace(rgx, '$1' + this.separator + '$2');
182
+        }
183
+      }
184
+      return this.prefix + x1 + x2 + this.suffix;
185
+    }
186
+  },
187
+  destroyed() {
188
+    cancelAnimationFrame(this.rAF)
189
+  }
190
+};
191
+</script>

+ 31 - 0
src/components/iFrame/index.vue

@@ -0,0 +1,31 @@
1
+<template>
2
+  <div v-loading="loading" :style="'height:' + height">
3
+    <iframe 
4
+      :src="url" 
5
+      frameborder="no" 
6
+      style="width: 100%; height: 100%" 
7
+      scrolling="auto" />
8
+  </div>
9
+</template>
10
+
11
+<script setup>
12
+const props = defineProps({
13
+  src: {
14
+    type: String,
15
+    required: true
16
+  }
17
+})
18
+
19
+const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
20
+const loading = ref(true)
21
+const url = computed(() => props.src)
22
+
23
+onMounted(() => {
24
+  setTimeout(() => {
25
+    loading.value = false
26
+  }, 300)
27
+  window.onresize = function temp() {
28
+    height.value = document.documentElement.clientHeight - 94.5 + "px;"
29
+  }
30
+})
31
+</script>

+ 165 - 0
src/components/sqlEdit/index.vue

@@ -0,0 +1,165 @@
1
+<template>
2
+  <div class="sql-editor-wrapper">
3
+    <div class="sql-editor-container">
4
+      <Codemirror
5
+        v-model:value="internalValue"
6
+        :options="cmOptions"
7
+        :width="width"
8
+        :height="height"
9
+        :readonly="readonly"
10
+        @ready="onReady"
11
+        @blur="onBlur"
12
+        @change="onChange"
13
+      />
14
+    </div>
15
+  </div>
16
+</template>
17
+
18
+<script setup>
19
+import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
20
+import Codemirror from 'codemirror-editor-vue3'
21
+
22
+// 引入必要的样式和插件
23
+import 'codemirror/lib/codemirror.css'
24
+import 'codemirror/theme/idea.css'
25
+import 'codemirror/mode/sql/sql.js'
26
+import 'codemirror/addon/hint/show-hint.css'
27
+import 'codemirror/addon/hint/show-hint'
28
+import 'codemirror/addon/hint/sql-hint'
29
+import 'codemirror/addon/display/placeholder.js'
30
+
31
+const props = defineProps({
32
+  modelValue: String,
33
+  readonly: Boolean,
34
+  width: {
35
+    type: String,
36
+    default: '100%'
37
+  },
38
+  height: {
39
+    type: String,
40
+    default: '300px'
41
+  },
42
+  placeholder: {
43
+    type: String,
44
+    default: '请输入 SQL 语句...'
45
+  },
46
+  tables: {
47
+    type: Object,
48
+    default: () => ({})
49
+  }
50
+})
51
+
52
+const emit = defineEmits(['update:modelValue', 'change', 'blur'])
53
+
54
+const internalValue = ref(props.modelValue || '')
55
+const editorInstance = ref(null)
56
+const isEditorReady = ref(false)
57
+
58
+// 强制刷新编辑器内容
59
+const refreshEditor = () => {
60
+  if (editorInstance.value) {
61
+    nextTick(() => {
62
+      editorInstance.value.refresh()
63
+      editorInstance.value.setValue(internalValue.value)
64
+    })
65
+  }
66
+}
67
+
68
+// 监听外部值变化
69
+watch(() => props.modelValue, (newVal) => {
70
+  if (newVal !== internalValue.value) {
71
+    internalValue.value = newVal || ''
72
+    if (isEditorReady.value) {
73
+      editorInstance.value.setValue(newVal || '')
74
+      refreshEditor()
75
+    }
76
+  }
77
+}, { immediate: true })
78
+
79
+const cmOptions = computed(() => ({
80
+  mode: 'text/x-sql',
81
+  theme: 'idea',
82
+  lineNumbers: true,
83
+  lineWrapping: true,
84
+  tabSize: 2,
85
+  readOnly: props.readonly ? 'nocursor' : false,
86
+  placeholder: props.placeholder,
87
+  extraKeys: { 'Ctrl-Space': 'autocomplete' },
88
+  hintOptions: {
89
+    completeSingle: false,
90
+    tables: props.tables
91
+  }
92
+}))
93
+
94
+const onReady = (editor) => {
95
+  editorInstance.value = editor
96
+  isEditorReady.value = true
97
+  
98
+  // 初始设置值
99
+  editor.setValue(internalValue.value)
100
+  refreshEditor()
101
+  
102
+  // 设置代码提示
103
+  editor.on('inputRead', (cm, event) => {
104
+    if (event && event.text && /[a-zA-Z]/.test(event.text[0])) {
105
+      setTimeout(() => cm.showHint(), 0)
106
+    }
107
+  })
108
+}
109
+
110
+const onBlur = () => {
111
+  emit('blur', internalValue.value)
112
+}
113
+
114
+const onChange = (value) => {
115
+  internalValue.value = value
116
+  emit('update:modelValue', value)
117
+  emit('change', value)
118
+}
119
+
120
+onBeforeUnmount(() => {
121
+  if (editorInstance.value) {
122
+    editorInstance.value.toTextArea()
123
+    editorInstance.value = null
124
+  }
125
+  isEditorReady.value = false
126
+})
127
+</script>
128
+
129
+<style>
130
+.sql-editor-wrapper {
131
+  width: 100%;
132
+  display: flex;
133
+}
134
+
135
+.sql-editor-container {
136
+  flex: 1;
137
+  min-width: 0;
138
+  border: 1px solid #dcdfe6;
139
+  border-radius: 4px;
140
+  transition: border-color 0.2s;
141
+  overflow: hidden; /* 确保内容不会溢出 */
142
+}
143
+
144
+.sql-editor-container:hover {
145
+  border-color: #c0c4cc;
146
+}
147
+
148
+/* 确保CodeMirror容器正确继承宽度 */
149
+.CodeMirror {
150
+  width: 100% !important;
151
+  height: 100%;
152
+  font-family: Consolas, Monaco, 'Andale Mono', monospace;
153
+  font-size: 14px;
154
+}
155
+
156
+/* 修复行号区域的宽度 */
157
+.CodeMirror-gutters {
158
+  height: 100% !important;
159
+}
160
+
161
+.CodeMirror-hints {
162
+  z-index: 9999 !important;
163
+  font-family: Consolas, Monaco, 'Andale Mono', monospace;
164
+}
165
+</style>