ui库和web端产品库合并版本(还需修复细节)
This commit is contained in:
		
							
								
								
									
										331
									
								
								ui/packages/basic/AiArticle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								ui/packages/basic/AiArticle.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,331 @@ | ||||
| <template> | ||||
|   <div class="ai-article"> | ||||
|     <div v-html="value"></div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: 'AiArticle', | ||||
|  | ||||
|     props: { | ||||
|       value: { | ||||
|         type: String | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| </script>  | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   .ai-article { | ||||
|     width: 100%; | ||||
|     line-height: 1.75; | ||||
|     font-weight: 400; | ||||
|     color: #333; | ||||
|     font-size: 14px; | ||||
|     text-align: justify; | ||||
|     overflow-x: auto; | ||||
|     word-break: break-word; | ||||
|      | ||||
|     ::v-deep h1 { | ||||
|       margin: 1.3rem 0; | ||||
|       line-height: 1.2 | ||||
|     } | ||||
|      | ||||
|     ::v-deep p { | ||||
|       line-height: 2.27rem | ||||
|     } | ||||
|      | ||||
|     ::v-deep hr { | ||||
|       border: none; | ||||
|       border-top: 1px solid #ddd; | ||||
|       margin-top: 2.7rem; | ||||
|       margin-bottom: 2.7rem | ||||
|     } | ||||
|      | ||||
|     ::v-deep img:not(.equation), ::v-deep iframe, ::v-deep embed, ::v-deep video { | ||||
|       display: block; | ||||
|       margin: 18px auto; | ||||
|       max-width: 100% !important; | ||||
|     } | ||||
|      | ||||
|     ::v-deep img.equation { | ||||
|       margin: 0 .1em; | ||||
|       max-width: 100% !important; | ||||
|       vertical-align: middle | ||||
|     } | ||||
|  | ||||
|     ::v-deep figure { | ||||
|       margin: 2.7rem auto; | ||||
|       text-align: center | ||||
|     } | ||||
|  | ||||
|     ::v-deep img:not(.equation) { | ||||
|       cursor: zoom-in | ||||
|     } | ||||
|  | ||||
|     ::v-deep figure figcaption { | ||||
|       text-align: center; | ||||
|       font-size: 1rem; | ||||
|       line-height: 2.7rem; | ||||
|       color: #909090 | ||||
|     } | ||||
|  | ||||
|     ::v-deep pre { | ||||
|       line-height: 1.93rem; | ||||
|       overflow: auto | ||||
|     } | ||||
|  | ||||
|     ::v-deep code, | ||||
|     ::v-deep pre { | ||||
|       font-family: Menlo, Monaco, Consolas, Courier New, monospace | ||||
|     } | ||||
|  | ||||
|     ::v-deep code { | ||||
|       font-size: 1rem; | ||||
|       padding: .26rem .53em; | ||||
|       word-break: break-word; | ||||
|       color: #4e5980; | ||||
|       background-color: #f8f8f8; | ||||
|       border-radius: 2px; | ||||
|       overflow-x: auto | ||||
|     } | ||||
|      | ||||
|     ::v-deep pre>code { | ||||
|       font-size: 1rem; | ||||
|       padding: .67rem 1.3rem; | ||||
|       margin: 0; | ||||
|       word-break: normal; | ||||
|       display: block | ||||
|     } | ||||
|      | ||||
|     ::v-deep a { | ||||
|       color: #259 | ||||
|     } | ||||
|  | ||||
|     ::v-deep a:active, | ||||
|     ::v-deep a:hover { | ||||
|       color: #275b8c | ||||
|     } | ||||
|  | ||||
|     ::v-deep table { | ||||
|       width: 100%; | ||||
|       margin-top: 18px; | ||||
|       margin-bottom: 18px; | ||||
|       overflow: auto; | ||||
|       font-size: 1rem; | ||||
|       text-align: center; | ||||
|       border-top: 1px solid #ccc; | ||||
|       border-left: 1px solid #ccc; | ||||
|     } | ||||
|  | ||||
|     ::v-deep thead { | ||||
|       background: #f6f6f6; | ||||
|       color: #000; | ||||
|       text-align: left | ||||
|     } | ||||
|  | ||||
|     ::v-deep td, | ||||
|     ::v-deep th { | ||||
|       padding: 3px 5px; | ||||
|       border-bottom: 1px solid #ccc; | ||||
|       border-right: 1px solid #ccc; | ||||
|     } | ||||
|      | ||||
|     ::v-deep td { | ||||
|       min-width: 10rem | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote { | ||||
|       margin: 1em 0; | ||||
|       border-left: 4px solid #ddd; | ||||
|       padding: 0 1.3rem | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote>p { | ||||
|       margin: .6rem 0 | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol, | ||||
|     ::v-deep ul { | ||||
|       padding-left: 2.7rem | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol li, | ||||
|     ::v-deep ul li { | ||||
|       margin-bottom: .6rem | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol ol, | ||||
|     ::v-deep ol ul, | ||||
|     ::v-deep ul ol, | ||||
|     ::v-deep ul ul { | ||||
|       margin-top: .27rem | ||||
|     } | ||||
|  | ||||
|     ::v-deep pre>code { | ||||
|       overflow-x: auto; | ||||
|       -webkit-overflow-scrolling: touch; | ||||
|       color: #333; | ||||
|       background: #f8f8f8 | ||||
|     } | ||||
|  | ||||
|     ::v-deep p { | ||||
|       line-height: inherit; | ||||
|       margin-top: 18px; | ||||
|       margin-bottom: 18px | ||||
|     } | ||||
|  | ||||
|     ::v-deep img { | ||||
|       max-height: none | ||||
|     } | ||||
|  | ||||
|     ::v-deep a { | ||||
|       color: #0269c8; | ||||
|       border-bottom: 1px solid #d1e9ff | ||||
|     } | ||||
|  | ||||
|     ::v-deep code { | ||||
|       background-color: #fff5f5; | ||||
|       color: #ff502c; | ||||
|       font-size: .87em; | ||||
|       padding: .065em .4em | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote { | ||||
|       color: #666; | ||||
|       padding: 1px 23px; | ||||
|       margin: 18px 0; | ||||
|       border-left: 4px solid #cbcbcb; | ||||
|       background-color: #f8f8f8 | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote:after { | ||||
|       display: block; | ||||
|       content: "" | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote>p { | ||||
|       margin: 10px 0 | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote.warning { | ||||
|       position: relative; | ||||
|       border-left-color: #f75151; | ||||
|       margin-left: 8px | ||||
|     } | ||||
|  | ||||
|     ::v-deep blockquote.warning:before { | ||||
|       position: absolute; | ||||
|       top: 14px; | ||||
|       left: -12px; | ||||
|       background: #f75151; | ||||
|       border-radius: 50%; | ||||
|       content: "!"; | ||||
|       width: 20px; | ||||
|       height: 20px; | ||||
|       color: #fff; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: center | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol, | ||||
|     ::v-deep ul { | ||||
|       padding-left: 28px | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol li, | ||||
|     ::v-deep ul li { | ||||
|       margin-bottom: 0; | ||||
|       list-style: inherit | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol li.task-list-item, | ||||
|     ::v-deep ul li.task-list-item { | ||||
|       list-style: none | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol li.task-list-item ol, | ||||
|     ::v-deep ol li.task-list-item ul, | ||||
|     ::v-deep ul li.task-list-item ol, | ||||
|     ::v-deep ul li.task-list-item ul { | ||||
|       margin-top: 0 | ||||
|     } | ||||
|  | ||||
|     ::v-deep ol li { | ||||
|       padding-left: 6px | ||||
|     } | ||||
|  | ||||
|     ::v-deep pre { | ||||
|       position: relative; | ||||
|       line-height: 1.75 | ||||
|     } | ||||
|  | ||||
|     ::v-deep pre>code { | ||||
|       padding: 15px 12px | ||||
|     } | ||||
|  | ||||
|     ::v-deep pre>code.hljs[lang] { | ||||
|       padding: 18px 15px 12px | ||||
|     } | ||||
|  | ||||
|     ::v-deep h1, | ||||
|     ::v-deep h2, | ||||
|     ::v-deep h3, | ||||
|     ::v-deep h4, | ||||
|     ::v-deep h5, | ||||
|     ::v-deep h6 { | ||||
|       color: #333; | ||||
|       line-height: 1.5; | ||||
|       margin-top: 35px; | ||||
|       margin-bottom: 10px; | ||||
|       padding-bottom: 5px; | ||||
|       font-weight: 500; | ||||
|     } | ||||
|  | ||||
|     ::v-deep h1 { | ||||
|       font-size: 30px; | ||||
|       margin-bottom: 5px | ||||
|     } | ||||
|  | ||||
|     ::v-deep h2 { | ||||
|       padding-bottom: 12px; | ||||
|       font-size: 24px; | ||||
|       border-bottom: 1px solid #ececec | ||||
|     } | ||||
|  | ||||
|     ::v-deep h3 { | ||||
|       font-size: 18px; | ||||
|       padding-bottom: 0 | ||||
|     } | ||||
|  | ||||
|     ::v-deep h4 { | ||||
|       font-size: 16px | ||||
|     } | ||||
|  | ||||
|     ::v-deep h5 { | ||||
|       font-size: 15px | ||||
|     } | ||||
|  | ||||
|     ::v-deep h6 { | ||||
|       margin-top: 5px | ||||
|     } | ||||
|  | ||||
|     ::v-deep h1.heading+h2.heading { | ||||
|       margin-top: 20px | ||||
|     } | ||||
|  | ||||
|     ::v-deep h1.heading+h3.heading { | ||||
|       margin-top: 15px | ||||
|     } | ||||
|  | ||||
|     ::v-deep .heading+.heading { | ||||
|       margin-top: 0 | ||||
|     } | ||||
|  | ||||
|     ::v-deep h1+:not(.heading) { | ||||
|       margin-top: 25px | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										238
									
								
								ui/packages/basic/AiAudio.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								ui/packages/basic/AiAudio.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,238 @@ | ||||
| <template> | ||||
|   <section class="ai-audio"> | ||||
|     <div class="controller" :class="skinClassName"> | ||||
|       <el-row :gutter="10" type="flex"> | ||||
|         <el-col> | ||||
|           <i class="play-icon" :class="playIcon" @click="play"></i> | ||||
|         </el-col> | ||||
|         <el-col class="play-progress"> | ||||
|           <el-slider | ||||
|               :format-tooltip="getDateFormat" | ||||
|               v-model="currentTime" | ||||
|               :max="totalDuration" | ||||
|               @change="audio.currentTime = currentTime"> | ||||
|           </el-slider> | ||||
|           <span class="total" v-if="skin === 'flat'">{{ getDateFormat(currentTime) }}</span> | ||||
|           <span class="total" v-else>{{ [getDateFormat(currentTime), getDateFormat(totalDuration)].join('/') }}</span> | ||||
|         </el-col> | ||||
|       </el-row> | ||||
|       <audio | ||||
|           ref="audio" | ||||
|           style="display: none;" | ||||
|           :src="src" | ||||
|           preload="metadata" | ||||
|           @pause="playState = false" | ||||
|           @playing="playing" | ||||
|           @loadedmetadata="loadedmetadata" | ||||
|           @timeupdate="timeupdate" | ||||
|       ></audio> | ||||
|     </div> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import moment from 'dayjs' | ||||
|  | ||||
| export default { | ||||
|   name: 'AiAudio', | ||||
|   props: { | ||||
|     /** | ||||
|      * 播放资源url | ||||
|      */ | ||||
|     src: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|     /** | ||||
|      * 同时只允许一个播放 | ||||
|      */ | ||||
|     singlePlay: {type: Boolean, default: false}, | ||||
|     /** | ||||
|      * 播放器风格 | ||||
|      * default 旧版样式,flat 新版样式 | ||||
|      * @values default,flat | ||||
|      */ | ||||
|     skin: { | ||||
|       type: String, | ||||
|       default: 'default' | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       audio: null, | ||||
|       playState: false, | ||||
|       currentTime: 0, | ||||
|       totalTime: 0 | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     playIcon() { | ||||
|       if (this.skin === 'flat') { | ||||
|         return this.playState ? 'iconfont iconMediaPlayer_Stop' : 'iconfont iconMediaPlayer_Play' | ||||
|       } | ||||
|  | ||||
|       return this.playState ? 'el-icon-video-pause' : 'el-icon-video-play' | ||||
|     }, | ||||
|  | ||||
|     totalDuration() { | ||||
|       return Math.round(this.totalTime) | ||||
|     }, | ||||
|  | ||||
|     skinClassName() { | ||||
|       return this.skin === 'default' ? '' : this.skin | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   mounted() { | ||||
|     this.audio = this.$refs.audio | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     play() { | ||||
|       if (this.audio) { | ||||
|         if (this.audio.paused) { | ||||
|           if (this.singlePlay) this.stopAllAudio() | ||||
|           this.audio.play() | ||||
|         } else { | ||||
|           this.audio.pause() | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     stopAllAudio() { | ||||
|       let audios = document.getElementsByTagName('audio') | ||||
|       for (let i = 0; i < audios.length; i++) { | ||||
|         if (audios[i]) audios[i].pause() | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     playing() { | ||||
|       this.playState = true | ||||
|     }, | ||||
|  | ||||
|     loadedmetadata() { | ||||
|       this.totalTime = this.$refs.audio.duration || 0 | ||||
|     }, | ||||
|  | ||||
|     getDateFormat(val) { | ||||
|       const time = moment.duration(val, 'seconds') | ||||
|       return [ | ||||
|         this.prefixNum(time.minutes(), 2), | ||||
|         this.prefixNum(time.seconds(), 2), | ||||
|       ].join(':') | ||||
|     }, | ||||
|  | ||||
|     prefixNum(val, num) { | ||||
|       return (Array(num).join('0') + val).slice(-num) | ||||
|     }, | ||||
|  | ||||
|     timeupdate() { | ||||
|       this.currentTime = parseInt(this.audio.currentTime) | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   destroyed() { | ||||
|     this.audio.pause() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .ai-audio { | ||||
|   display: flex; | ||||
|   flex: 1; | ||||
|  | ||||
|   .controller { | ||||
|     width: 205px; | ||||
|     height: 40px; | ||||
|     padding: 0 5px; | ||||
|     box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); | ||||
|     border-radius: 99px; | ||||
|  | ||||
|     .el-col { | ||||
|       width: auto; | ||||
|     } | ||||
|  | ||||
|     .play-icon { | ||||
|       color: #4c84ff; | ||||
|       cursor: pointer; | ||||
|       line-height: 40px; | ||||
|       font-size: 32px; | ||||
|       transition: color 0.3s; | ||||
|  | ||||
|       &:hover { | ||||
|         color: #b3d8ff; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .play-progress { | ||||
|       display: flex; | ||||
|       flex: 1; | ||||
|  | ||||
|       .el-slider { | ||||
|         width: 60px; | ||||
|       } | ||||
|  | ||||
|       .total { | ||||
|         margin-left: 15px; | ||||
|         font-size: 12px; | ||||
|         line-height: 40px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .flat { | ||||
|     height: 32px; | ||||
|     background: rgba(239, 246, 255, 1); | ||||
|     border: 1px solid rgba(132, 181, 255, 1); | ||||
|     border-radius: 5px; | ||||
|     box-shadow: none; | ||||
|  | ||||
|     .play-icon { | ||||
|       position: relative; | ||||
|       padding-left: 3px; | ||||
|       color: #4c84ff; | ||||
|       cursor: pointer; | ||||
|       line-height: 32px; | ||||
|       font-size: 20px; | ||||
|       transition: color 0.3s; | ||||
|  | ||||
|       &:hover { | ||||
|         color: #b3d8ff; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     ::v-deep.el-slider__button { | ||||
|       width: 8px; | ||||
|       height: 8px; | ||||
|       background: #1365DD; | ||||
|     } | ||||
|  | ||||
|     ::v-deep.el-slider__bar { | ||||
|       height: 2px; | ||||
|     } | ||||
|  | ||||
|     ::v-deep.el-slider__button-wrapper { | ||||
|       height: 32px; | ||||
|     } | ||||
|  | ||||
|     ::v-deep.el-slider__runway { | ||||
|       height: 2px; | ||||
|       margin: 15px 0; | ||||
|     } | ||||
|  | ||||
|     .play-progress { | ||||
|       display: flex; | ||||
|  | ||||
|       .el-slider { | ||||
|         width: 84px; | ||||
|       } | ||||
|  | ||||
|       .total { | ||||
|         margin-left: 8px; | ||||
|         font-size: 12px; | ||||
|         line-height: 32px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										30
									
								
								ui/packages/basic/AiBadge.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								ui/packages/basic/AiBadge.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <template> | ||||
|   <section class="AiBadge"> | ||||
|     <slot></slot> | ||||
|     <sup v-if="isShow" class="badge"> | ||||
|       <span v-if="badge">{{badge}}</span> | ||||
|       <slot v-else name="badge"></slot> | ||||
|     </sup> | ||||
|   </section> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: "AiBadge", | ||||
|     props: { | ||||
|       badge: String, | ||||
|       isShow: {type: Boolean, default: true} | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   .AiBadge { | ||||
|     .badge { | ||||
|       position: absolute; | ||||
|       margin-left: -10px; | ||||
|       margin-top: -10px; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										117
									
								
								ui/packages/basic/AiBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								ui/packages/basic/AiBar.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| <template> | ||||
|   <div class="aibar" :style="{ marginBottom: marginBottom }" :class="[titlePosition === 'center' ? 'aibar-center' : '']"> | ||||
|     <div v-if="titlePosition === 'center'"></div> | ||||
|     <div class="aibar-left" :class="[titlePosition === 'center' ? 'aibar-left__center' : '']"> | ||||
|       <template v-if="!isHasTitleSlot">{{ title }}</template> | ||||
|       <slot name="title" v-else></slot> | ||||
|     </div> | ||||
|     <div class="aibar-right"> | ||||
|       <slot name="right"></slot> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: 'AiBar', | ||||
|  | ||||
|     props: { | ||||
|       title: { | ||||
|         type: String | ||||
|       }, | ||||
|  | ||||
|       customCliker: { | ||||
|         type: Boolean, | ||||
|         default: false | ||||
|       }, | ||||
|  | ||||
|       marginBottom: { | ||||
|         type: String, | ||||
|         default: '16px' | ||||
|       }, | ||||
|  | ||||
|       titlePosition: { | ||||
|         type: String, | ||||
|         default: 'left' | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|       isHasTitleSlot () { | ||||
|         return this.$slots.title | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     data () { | ||||
|       return { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   .aibar { | ||||
|     display: flex; | ||||
|     position: relative; | ||||
|     align-items: center; | ||||
|     justify-content: space-between; | ||||
|     height: 56px; | ||||
|     padding: 0 16px; | ||||
|     box-sizing: border-box; | ||||
|     border-bottom: 1px solid #EEEEEE; | ||||
|  | ||||
|     .aibar-left { | ||||
|       color: #222; | ||||
|       font-size: 16px; | ||||
|       font-weight: 700; | ||||
|     } | ||||
|  | ||||
|     .aibar-left__center { | ||||
|       position: relative; | ||||
|       width: 556px; | ||||
|       text-align: center; | ||||
|       word-break: break-all; | ||||
|       line-height: 24px; | ||||
|     } | ||||
|  | ||||
|     .aibar-right { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       color: #5088FF; | ||||
|       font-size: 12px; | ||||
|  | ||||
|       i { | ||||
|         line-height: 1; | ||||
|         color: #5088FF; | ||||
|       } | ||||
|  | ||||
|       span { | ||||
|         font-size: 12px; | ||||
|       } | ||||
|  | ||||
|       & > div, & > a { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         margin-right: 20px; | ||||
|  | ||||
|         &:last-child { | ||||
|           margin-right: 0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .aibar-center { | ||||
|     height: auto; | ||||
|     padding: 10px 0; | ||||
|  | ||||
|     h2 { | ||||
|       margin: 0 0 10px 0; | ||||
|     } | ||||
|  | ||||
|     p { | ||||
|       color: #888; | ||||
|       font-size: 14px; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										151
									
								
								ui/packages/basic/AiBigTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								ui/packages/basic/AiBigTable.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| <template> | ||||
|   <section class="AiBigTable"/> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import AiEmpty from "./AiEmpty"; | ||||
|  | ||||
| export default { | ||||
|   name: "AiBigTable", | ||||
|   components: {AiEmpty}, | ||||
|   model: { | ||||
|     prop: "data", | ||||
|     event: "input" | ||||
|   }, | ||||
|   props: { | ||||
|     /** | ||||
|      * 表数据 | ||||
|      */ | ||||
|     data: {default: () => []}, | ||||
|     /** | ||||
|      * 表格配置 | ||||
|      */ | ||||
|     colConfigs: {default: () => []}, | ||||
|     /** | ||||
|      * 是否显示边框 | ||||
|      */ | ||||
|     border: Boolean, | ||||
|     /** | ||||
|      * 是否启用dom对象 | ||||
|      */ | ||||
|     isDom: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       html2canvas: null | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     renderTable() { | ||||
|       const table = document.createElement("div") | ||||
|       table.style.display = 'flex' | ||||
|       table.style.flexDirection = 'column' | ||||
|       table.appendChild(this.renderHead()) | ||||
|       if (this.data.length > 0) { | ||||
|         this.data.map(e => table.appendChild(this.renderRow(e))) | ||||
|       } else table.appendChild(this.renderEmpty()) | ||||
|       if (this.isDom) { | ||||
|         this.$el.appendChild(table) | ||||
|       } else { | ||||
|         this.$el.appendChild(table) | ||||
|         this.$load(this.html2canvas).then(() => this.html2canvas(table, { | ||||
|           allowTaint: true, | ||||
|           useCORS: true, | ||||
|           height: this.$el.offsetHeight, | ||||
|           width: this.$el.offsetWidth | ||||
|         })).then(ctx => { | ||||
|           this.$el.removeChild(table) | ||||
|           this.$el.appendChild(ctx) | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     renderHead() { | ||||
|       const head = document.createElement("div") | ||||
|       head.style.display = 'flex' | ||||
|       head.style.alignItems = 'center' | ||||
|       head.style.fontWeight = 'bold' | ||||
|       head.style.background = 'rgba(243, 246, 249, 1)' | ||||
|       if (this.border) { | ||||
|         head.style.borderLeft = '1px solid #eee' | ||||
|         head.style.borderTop = '1px solid #eee' | ||||
|       } else { | ||||
|         head.style.borderBottom = '1px solid #eee' | ||||
|       } | ||||
|       this.colConfigs.map(e => { | ||||
|         const cell = this.renderCell(e) | ||||
|         head.appendChild(cell) | ||||
|       }) | ||||
|       return head | ||||
|     }, | ||||
|     renderRow(item) { | ||||
|       const row = document.createElement("div") | ||||
|       row.style.display = 'flex' | ||||
|       row.style.alignItems = 'center' | ||||
|       if (this.border) { | ||||
|         row.style.borderLeft = '1px solid #eee' | ||||
|       } else { | ||||
|         row.style.borderBottom = '1px solid #eee' | ||||
|       } | ||||
|       this.colConfigs.map(e => { | ||||
|         const cell = this.renderCell(e, item) | ||||
|         row.appendChild(cell) | ||||
|       }) | ||||
|       return row | ||||
|     }, | ||||
|     renderCell(config, row) { | ||||
|       const cell = document.createElement("div") | ||||
|       cell.style.display = 'flex' | ||||
|       cell.style.alignItems = 'center' | ||||
|       cell.style.minheight = '32px' | ||||
|       cell.style.padding = '0 8px' | ||||
|       if (this.border) { | ||||
|         cell.style.borderBottom = '1px solid #eee' | ||||
|         cell.style.borderRight = '1px solid #eee' | ||||
|       } | ||||
|       if (config.align) { | ||||
|         cell.style.justifyContent = config.align | ||||
|       } | ||||
|       if (config.width) { | ||||
|         cell.style.width = config.width.toString().replace(/(\d+)/g, '$1px') | ||||
|         cell.style.flexShrink = 0 | ||||
|       } else { | ||||
|         cell.style.flex = 1 | ||||
|         cell.style.minWidth = 0 | ||||
|       } | ||||
|       cell.innerHTML = row?.[config.prop] || config.label | ||||
|       return cell | ||||
|     }, | ||||
|     renderEmpty() { | ||||
|       const empty = document.createElement("div") | ||||
|       empty.style.background = 'url("https://cdn.cunwuyun.cn/dvcp/empty.svg") no-repeat' | ||||
|       empty.style.width = '100%' | ||||
|       empty.style.height = '140px' | ||||
|       empty.style.backgroundPosition = 'center 0' | ||||
|       empty.style.backgroundSize = '120px' | ||||
|       empty.style.borderBottom = '1px solid #eee' | ||||
|       empty.style.borderLeft = '1px solid #eee' | ||||
|       empty.style.borderRight = '1px solid #eee' | ||||
|       empty.style.textAlign = 'center' | ||||
|       empty.style.color = '#999' | ||||
|       empty.style.paddingTop = '110px' | ||||
|       empty.innerHTML = "暂无数据" | ||||
|       return empty | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$injectLib("https://cdn.cunwuyun.cn/html2canvas.min.js", () => { | ||||
|       this.html2canvas = window?.html2canvas | ||||
|       this.$nextTick(this.renderTable) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .AiBigTable { | ||||
|   flex: 1; | ||||
|   min-width: 0; | ||||
|   min-height: 0; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										522
									
								
								ui/packages/basic/AiCron.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										522
									
								
								ui/packages/basic/AiCron.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,522 @@ | ||||
| <template> | ||||
|   <section class="AiCron"> | ||||
|     <div id="changeContab"> | ||||
|       <el-tabs type="border-card"> | ||||
|         <el-tab-pane> | ||||
|           <span slot="label"><i class="el-icon-date"></i>秒</span> | ||||
|           <div class="tabBody"> | ||||
|             <el-row> | ||||
|               <el-radio v-model="second.cronEvery" label="1">每一秒中</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="second.cronEvery" label="2">每隔 | ||||
|                 <el-input-number size="small" v-model="second.incrementIncrement" :min="1" :max="60"></el-input-number> | ||||
|                 秒执行 从 | ||||
|                 <el-input-number size="small" v-model="second.incrementStart" :min="0" :max="59"></el-input-number> | ||||
|                 秒开始 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio class="long" v-model="second.cronEvery" label="3">具体秒数(可多选) | ||||
|                 <el-select size="small" multiple v-model="second.specificSpecific"> | ||||
|                   <el-option v-for="(val,i) in 60" :key="i" :value="val-1">{{val-1}}</el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="second.cronEvery" label="4">周期从 | ||||
|                 <el-input-number size="small" v-model="second.rangeStart" :min="1" :max="60"></el-input-number> | ||||
|                 到 | ||||
|                 <el-input-number size="small" v-model="second.rangeEnd" :min="0" :max="59"></el-input-number> | ||||
|                 秒 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane> | ||||
|           <span slot="label"><i class="el-icon-date"></i>分</span> | ||||
|           <div class="tabBody"> | ||||
|             <el-row> | ||||
|               <el-radio v-model="minute.cronEvery" label="1">每一分钟</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="minute.cronEvery" label="2">每隔 | ||||
|                 <el-input-number size="small" v-model="minute.incrementIncrement" :min="1" :max="60"></el-input-number> | ||||
|                 分执行 从 | ||||
|                 <el-input-number size="small" v-model="minute.incrementStart" :min="0" :max="59"></el-input-number> | ||||
|                 分开始 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio class="long" v-model="minute.cronEvery" label="3">具体分钟数(可多选) | ||||
|                 <el-select size="small" multiple v-model="minute.specificSpecific"> | ||||
|                   <el-option v-for="(val,i) in 60" :key="i" :value="val-1">{{val-1}}</el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="minute.cronEvery" label="4">周期从 | ||||
|                 <el-input-number size="small" v-model="minute.rangeStart" :min="1" :max="60"></el-input-number> | ||||
|                 到 | ||||
|                 <el-input-number size="small" v-model="minute.rangeEnd" :min="0" :max="59"></el-input-number> | ||||
|                 分 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane> | ||||
|           <span slot="label"><i class="el-icon-date"></i>时</span> | ||||
|           <div class="tabBody"> | ||||
|             <el-row> | ||||
|               <el-radio v-model="hour.cronEvery" label="1">每一小时</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="hour.cronEvery" label="2">每隔 | ||||
|                 <el-input-number size="small" v-model="hour.incrementIncrement" :min="0" :max="23"></el-input-number> | ||||
|                 小时执行 从 | ||||
|                 <el-input-number size="small" v-model="hour.incrementStart" :min="0" :max="23"></el-input-number> | ||||
|                 小时开始 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio class="long" v-model="hour.cronEvery" label="3">具体小时数(可多选) | ||||
|                 <el-select size="small" multiple v-model="hour.specificSpecific"> | ||||
|                   <el-option v-for="(val,i) in 24" :key="i" :value="val-1">{{val-1}}</el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="hour.cronEvery" label="4">周期从 | ||||
|                 <el-input-number size="small" v-model="hour.rangeStart" :min="0" :max="23"></el-input-number> | ||||
|                 到 | ||||
|                 <el-input-number size="small" v-model="hour.rangeEnd" :min="0" :max="23"></el-input-number> | ||||
|                 小时 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane> | ||||
|           <span slot="label"><i class="el-icon-date"></i>天</span> | ||||
|           <div class="tabBody"> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="1">每一天</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="2">每隔 | ||||
|                 <el-input-number size="small" v-model="week.incrementIncrement" :min="1" :max="7"></el-input-number> | ||||
|                 周执行 从 | ||||
|                 <el-select size="small" v-model="week.incrementStart"> | ||||
|                   <el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option> | ||||
|                 </el-select> | ||||
|                 开始 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="3">每隔 | ||||
|                 <el-input-number size="small" v-model="day.incrementIncrement" :min="1" :max="31"></el-input-number> | ||||
|                 天执行 从 | ||||
|                 <el-input-number size="small" v-model="day.incrementStart" :min="1" :max="31"></el-input-number> | ||||
|                 天开始 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio class="long" v-model="day.cronEvery" label="4">具体星期几(可多选) | ||||
|                 <el-select size="small" multiple v-model="week.specificSpecific"> | ||||
|                   <el-option v-for="(val,i) in 7" | ||||
|                              :key="i" | ||||
|                              :label="weeks[val-1]" | ||||
|                              :value="['SUN','MON','TUE','WED','THU','FRI','SAT'][val-1]" | ||||
|                   ></el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio class="long" v-model="day.cronEvery" label="5">具体天数(可多选) | ||||
|                 <el-select size="small" multiple v-model="day.specificSpecific"> | ||||
|                   <el-option v-for="(val,i) in 31" :key="i" :value="val">{{val}}</el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="6">在这个月的最后一天</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="7">在这个月的最后一个工作日</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="8">在这个月的最后一个 | ||||
|                 <el-select size="small" v-model="day.cronLastSpecificDomDay"> | ||||
|                   <el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="9"> | ||||
|                 <el-input-number size="small" v-model="day.cronDaysBeforeEomMinus" :min="1" :max="31"></el-input-number> | ||||
|                 在本月底前 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="10">最近的工作日(周一至周五) 至本月 | ||||
|                 <el-input-number size="small" v-model="day.cronDaysNearestWeekday" :min="1" :max="31"></el-input-number> | ||||
|                 日 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="day.cronEvery" label="11">在这个月的第 | ||||
|                 <el-input-number size="small" v-model="week.cronNthDayNth" :min="1" :max="5"></el-input-number> | ||||
|                 个 | ||||
|                 <el-select size="small" v-model="week.cronNthDayDay"> | ||||
|                   <el-option v-for="(val,i) in 7" :key="i" :label="weeks[val-1]" :value="val"></el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane> | ||||
|           <span slot="label"><i class="el-icon-date"></i> 月</span> | ||||
|           <div class="tabBody"> | ||||
|             <el-row> | ||||
|               <el-radio v-model="month.cronEvery" label="1">每个月</el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="month.cronEvery" label="2">每隔 | ||||
|                 <el-input-number size="small" v-model="month.incrementIncrement" :min="0" :max="12"></el-input-number> | ||||
|                 月执行 从 | ||||
|                 <el-input-number size="small" v-model="month.incrementStart" :min="0" :max="12"></el-input-number> | ||||
|                 月开始 | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio class="long" v-model="month.cronEvery" label="3">具体月数(可多选) | ||||
|                 <el-select size="small" multiple v-model="month.specificSpecific"> | ||||
|                   <el-option v-for="(val,i) in 12" :key="i" :label="val" :value="val"></el-option> | ||||
|                 </el-select> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|             <el-row> | ||||
|               <el-radio v-model="month.cronEvery" label="4">从 | ||||
|                 <el-input-number size="small" v-model="month.rangeStart" :min="1" :max="12"></el-input-number> | ||||
|                 到 | ||||
|                 <el-input-number size="small" v-model="month.rangeEnd" :min="1" :max="12"></el-input-number> | ||||
|               </el-radio> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </el-tab-pane> | ||||
|       </el-tabs> | ||||
|       <div class="bottom"> | ||||
|         <span class="value">{{cron}}</span> | ||||
|         <el-button type="primary" @click="change">保存</el-button> | ||||
|         <el-button type="primary" @click="close">取消</el-button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: "AiCron", | ||||
|     model: { | ||||
|       prop: 'value', | ||||
|       event: "change" | ||||
|     }, | ||||
|     props: { | ||||
|       value: String | ||||
|     }, | ||||
|     data() { | ||||
|       return { | ||||
|         weeks: ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"], | ||||
|         second: { | ||||
|           cronEvery: '', | ||||
|           incrementStart: '3', | ||||
|           incrementIncrement: '5', | ||||
|           rangeStart: '', | ||||
|           rangeEnd: '', | ||||
|           specificSpecific: [], | ||||
|         }, | ||||
|         minute: { | ||||
|           cronEvery: '', | ||||
|           incrementStart: '3', | ||||
|           incrementIncrement: '5', | ||||
|           rangeStart: '', | ||||
|           rangeEnd: '', | ||||
|           specificSpecific: [], | ||||
|         }, | ||||
|         hour: { | ||||
|           cronEvery: '', | ||||
|           incrementStart: '3', | ||||
|           incrementIncrement: '5', | ||||
|           rangeStart: '', | ||||
|           rangeEnd: '', | ||||
|           specificSpecific: [], | ||||
|         }, | ||||
|         day: { | ||||
|           cronEvery: '', | ||||
|           incrementStart: '1', | ||||
|           incrementIncrement: '1', | ||||
|           rangeStart: '', | ||||
|           rangeEnd: '', | ||||
|           specificSpecific: [], | ||||
|           cronLastSpecificDomDay: 1, | ||||
|           cronDaysBeforeEomMinus: '', | ||||
|           cronDaysNearestWeekday: '', | ||||
|         }, | ||||
|         week: { | ||||
|           cronEvery: '', | ||||
|           incrementStart: '1', | ||||
|           incrementIncrement: '1', | ||||
|           specificSpecific: [], | ||||
|           cronNthDayDay: 1, | ||||
|           cronNthDayNth: '1', | ||||
|         }, | ||||
|         month: { | ||||
|           cronEvery: '', | ||||
|           incrementStart: '3', | ||||
|           incrementIncrement: '5', | ||||
|           rangeStart: '', | ||||
|           rangeEnd: '', | ||||
|           specificSpecific: [], | ||||
|         }, | ||||
|         output: { | ||||
|           second: '', | ||||
|           minute: '', | ||||
|           hour: '', | ||||
|           day: '', | ||||
|           month: '', | ||||
|           Week: '', | ||||
|         }, | ||||
|         visible: false, | ||||
|         cronArr:['*','*','*','*','*','?'] | ||||
|       } | ||||
|     }, | ||||
|     watch: { | ||||
|       cron(val) { | ||||
|         this.value = val | ||||
|       } | ||||
|     }, | ||||
|     computed: { | ||||
|       secondsText() { | ||||
|         let seconds = this.cronArr[0]; | ||||
|         let cronEvery = this.second.cronEvery; | ||||
|         switch (cronEvery.toString()) { | ||||
|           case '1': | ||||
|             seconds = '*'; | ||||
|             break; | ||||
|           case '2': | ||||
|             seconds = this.second.incrementStart + '/' + this.second.incrementIncrement; | ||||
|             break; | ||||
|           case '3': | ||||
|             this.second.specificSpecific.map(val => { | ||||
|               seconds += val + ',' | ||||
|             }); | ||||
|             seconds = seconds.slice(0, -1); | ||||
|             break; | ||||
|           case '4': | ||||
|             seconds = this.second.rangeStart + '-' + this.second.rangeEnd; | ||||
|             break; | ||||
|         } | ||||
|         return seconds; | ||||
|       }, | ||||
|       minutesText() { | ||||
|         let minutes = this.cronArr[2];; | ||||
|         let cronEvery = this.minute.cronEvery; | ||||
|         switch (cronEvery.toString()) { | ||||
|           case '1': | ||||
|             minutes = '*'; | ||||
|             break; | ||||
|           case '2': | ||||
|             minutes = this.minute.incrementStart + '/' + this.minute.incrementIncrement; | ||||
|             break; | ||||
|           case '3': | ||||
|             this.minute.specificSpecific.map(val => { | ||||
|               minutes += val + ',' | ||||
|             }); | ||||
|             minutes = minutes.slice(0, -1); | ||||
|             break; | ||||
|           case '4': | ||||
|             minutes = this.minute.rangeStart + '-' + this.minute.rangeEnd; | ||||
|             break; | ||||
|         } | ||||
|         return minutes; | ||||
|       }, | ||||
|       hoursText() { | ||||
|         let hours = this.cronArr[2];; | ||||
|         let cronEvery = this.hour.cronEvery; | ||||
|         switch (cronEvery.toString()) { | ||||
|           case '1': | ||||
|             hours = '*'; | ||||
|             break; | ||||
|           case '2': | ||||
|             hours = this.hour.incrementStart + '/' + this.hour.incrementIncrement; | ||||
|             break; | ||||
|           case '3': | ||||
|             this.hour.specificSpecific.map(val => { | ||||
|               hours += val + ',' | ||||
|             }); | ||||
|             hours = hours.slice(0, -1); | ||||
|             break; | ||||
|           case '4': | ||||
|             hours = this.hour.rangeStart + '-' + this.hour.rangeEnd; | ||||
|             break; | ||||
|         } | ||||
|         return hours; | ||||
|       }, | ||||
|       daysText() { | ||||
|         let days = this.cronArr[3];; | ||||
|         let cronEvery = this.day.cronEvery; | ||||
|         switch (cronEvery.toString()) { | ||||
|           case '1': | ||||
|             break; | ||||
|           case '2': | ||||
|           case '4': | ||||
|           case '11': | ||||
|             days = '?'; | ||||
|             break; | ||||
|           case '3': | ||||
|             days = this.day.incrementStart + '/' + this.day.incrementIncrement; | ||||
|             break; | ||||
|           case '5': | ||||
|             this.day.specificSpecific.map(val => { | ||||
|               days += val + ',' | ||||
|             }); | ||||
|             days = days.slice(0, -1); | ||||
|             break; | ||||
|           case '6': | ||||
|             days = "L"; | ||||
|             break; | ||||
|           case '7': | ||||
|             days = "LW"; | ||||
|             break; | ||||
|           case '8': | ||||
|             days = this.day.cronLastSpecificDomDay + 'L'; | ||||
|             break; | ||||
|           case '9': | ||||
|             days = 'L-' + this.day.cronDaysBeforeEomMinus; | ||||
|             break; | ||||
|           case '10': | ||||
|             days = this.day.cronDaysNearestWeekday + "W"; | ||||
|             break | ||||
|         } | ||||
|         return days; | ||||
|       }, | ||||
|       weeksText() { | ||||
|         let weeks = this.cronArr[5];; | ||||
|         let cronEvery = this.day.cronEvery; | ||||
|         switch (cronEvery.toString()) { | ||||
|           case '1': | ||||
|           case '3': | ||||
|           case '5': | ||||
|             weeks = '?'; | ||||
|             break; | ||||
|           case '2': | ||||
|             weeks = this.week.incrementStart + '/' + this.week.incrementIncrement; | ||||
|             break; | ||||
|           case '4': | ||||
|             this.week.specificSpecific.map(val => { | ||||
|               weeks += val + ',' | ||||
|             }); | ||||
|             weeks = weeks.slice(0, -1); | ||||
|             break; | ||||
|           case '6': | ||||
|           case '7': | ||||
|           case '8': | ||||
|           case '9': | ||||
|           case '10': | ||||
|             weeks = "?"; | ||||
|             break; | ||||
|           case '11': | ||||
|             weeks = this.week.cronNthDayDay + "#" + this.week.cronNthDayNth; | ||||
|             break; | ||||
|         } | ||||
|         return weeks; | ||||
|       }, | ||||
|       monthsText() { | ||||
|         let months = this.cronArr[4]; | ||||
|         let cronEvery = this.month.cronEvery; | ||||
|         switch (cronEvery.toString()) { | ||||
|           case '1': | ||||
|             months = '*'; | ||||
|             break; | ||||
|           case '2': | ||||
|             months = this.month.incrementStart + '/' + this.month.incrementIncrement; | ||||
|             break; | ||||
|           case '3': | ||||
|             this.month.specificSpecific.map(val => { | ||||
|               months += val + ',' | ||||
|             }); | ||||
|             months = months.slice(0, -1); | ||||
|             break; | ||||
|           case '4': | ||||
|             months = this.month.rangeStart + '-' + this.month.rangeEnd; | ||||
|             break; | ||||
|         } | ||||
|         return months; | ||||
|       }, | ||||
|       cron() { | ||||
|         return `${this.secondsText || '*'} ${this.minutesText || '*'} ${this.hoursText || '*'} ${this.daysText || '*'} ${this.monthsText || '*'} ${this.weeksText || '?'} ` | ||||
|       }, | ||||
|     }, | ||||
|     methods: { | ||||
|       getValue() { | ||||
|         return this.cron; | ||||
|       }, | ||||
|       change() { | ||||
|         this.$emit('change', this.cron); | ||||
|         this.close(); | ||||
|       }, | ||||
|       close() { | ||||
|         this.$emit('close') | ||||
|       }, | ||||
|     }, | ||||
|     mounted() { | ||||
|       if(this.value) this.cronArr = this.value.trim().split(" ") | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   .AiCron { | ||||
|  | ||||
|     #changeContab { | ||||
|       .language { | ||||
|         position: absolute; | ||||
|         right: 25px; | ||||
|         z-index: 1; | ||||
|       } | ||||
|  | ||||
|       .el-tabs { | ||||
|         box-shadow: none; | ||||
|       } | ||||
|  | ||||
|       .tabBody { | ||||
|         .el-row { | ||||
|           margin: 10px 0; | ||||
|  | ||||
|           .long { | ||||
|             .el-select { | ||||
|               width: 350px; | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           .el-input-number { | ||||
|             width: 110px; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .bottom { | ||||
|         width: 100%; | ||||
|         text-align: center; | ||||
|         margin-top: 5px; | ||||
|         position: relative; | ||||
|  | ||||
|         .value { | ||||
|           font-size: 18px; | ||||
|           vertical-align: middle; | ||||
|           margin-right: 10px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										163
									
								
								ui/packages/basic/AiDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								ui/packages/basic/AiDialog.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| <template> | ||||
|   <section class="ai-dialog__wrapper"> | ||||
| 		<el-dialog | ||||
| 			custom-class="ai-dialog" | ||||
|       v-on="$listeners" | ||||
| 			v-if="isEmpty" | ||||
| 			:close-on-click-modal="closeOnClickModal" | ||||
|       v-bind="$attrs" | ||||
| 			:destroy-on-close="destroyOnClose" | ||||
| 			:visible.sync="dialogVisible"> | ||||
| 			<div class="ai-dialog__header" slot="title"> | ||||
| 				<h2>{{ title }}</h2> | ||||
| 			</div> | ||||
| 			<div class="ai-dialog__content" :style="{'max-height': isScrool ? '500px' : 'auto'}"> | ||||
| 				<div class="ai-dialog__content--wrapper" :style="{'padding-right': isScrool ? '8px' : '0'}"> | ||||
| 					<slot></slot> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<template v-if="customFooter" slot="footer"> | ||||
| 				<slot name="footer"></slot> | ||||
| 			</template> | ||||
| 			<div v-else class="dialog-footer" slot="footer"> | ||||
| 				<el-button @click="onCancel">取消</el-button> | ||||
| 				<el-button @click="onConfirm" type="primary" style="width: 92px;">确认</el-button> | ||||
| 			</div> | ||||
| 		</el-dialog> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| 	export default { | ||||
| 		name: 'AiDialog', | ||||
| 		props: { | ||||
| 			visible: { | ||||
| 				type: Boolean, | ||||
| 				default: false | ||||
| 			}, | ||||
| 			title: { | ||||
| 				type: String, | ||||
| 				default: '' | ||||
| 			}, | ||||
| 			customFooter: { | ||||
| 				type: Boolean, | ||||
| 				default: false | ||||
| 			}, | ||||
| 			'close-on-click-modal': { | ||||
| 				type: Boolean, | ||||
| 				default: false | ||||
| 			}, | ||||
| 			destroyOnClose: { | ||||
| 				type: Boolean, | ||||
| 				default: true | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		data () { | ||||
| 			return { | ||||
| 				dialogVisible: false, | ||||
| 				isScrool: true, | ||||
| 				isEmpty: true | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		watch: { | ||||
| 			visible: { | ||||
| 				handler (val) { | ||||
| 					this.dialogVisible = val | ||||
|  | ||||
| 					// if (val) { | ||||
| 					// 	this.$nextTick(() => { | ||||
| 					// 		setTimeout(() => { | ||||
| 					// 			this.isScrool = document.querySelector('.ai-dialog__content') && document.querySelector('.ai-dialog__content').clientHeight >= 500 | ||||
| 					// 		}, 100) | ||||
| 					// 	}) | ||||
| 					// } | ||||
|  | ||||
| 					if (this.destroyOnClose && !val) { | ||||
| 						setTimeout(() => { | ||||
| 							this.isEmpty = false | ||||
|  | ||||
| 							setTimeout(() => { | ||||
| 								this.isEmpty = true | ||||
| 							}, 50) | ||||
| 						}, 500) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
|  | ||||
| 		mounted () { | ||||
| 		}, | ||||
|  | ||||
| 		methods: { | ||||
| 			onCancel () { | ||||
| 				this.$emit('update:visible', false) | ||||
| 				this.$emit('onCancel') | ||||
| 			}, | ||||
|  | ||||
| 			onConfirm () { | ||||
| 				this.$emit('onConfirm') | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
| <style lang="scss"> | ||||
| 	.ai-dialog { | ||||
| 		margin: 0!important; | ||||
| 		top: 50%; | ||||
| 		left: 50%; | ||||
| 		transform: translate(-50%, -50%); | ||||
|  | ||||
| 		.el-dialog__body { | ||||
| 			padding: 20px 40px 20px; | ||||
| 		} | ||||
|  | ||||
| 		.ai-dialog__content { | ||||
| 			overflow-y: auto; | ||||
| 			padding-bottom: 4px; | ||||
|  | ||||
| 			.ai-dialog__content--wrapper { | ||||
| 				height: 100%; | ||||
| 				overflow-x: hidden; | ||||
| 				overflow-y: overlay; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		.ai-dialog__header { | ||||
| 			height: 48px; | ||||
| 			line-height: 48px; | ||||
| 			padding: 0 16px; | ||||
| 			border-bottom: 1px solid #eee; | ||||
|  | ||||
| 			h2 { | ||||
| 				font-size: 16px; | ||||
| 				font-weight: 700; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		.el-dialog__footer { | ||||
| 			padding: 16px 20px; | ||||
| 			box-sizing: border-box; | ||||
| 			background: #F3F6F9; | ||||
| 			text-align: center; | ||||
|  | ||||
| 			& + .el-button { | ||||
| 				margin-left: 8px; | ||||
| 			} | ||||
|  | ||||
| 			.el-button { | ||||
| 				width: 92px!important; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		.el-dialog__header { | ||||
| 			padding: 0; | ||||
| 		} | ||||
|  | ||||
| 		.el-dialog__headerbtn { | ||||
| 			top: 24px; | ||||
| 			transform: translateY(-50%); | ||||
| 		} | ||||
| 	} | ||||
| </style> | ||||
							
								
								
									
										139
									
								
								ui/packages/basic/AiDrawer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								ui/packages/basic/AiDrawer.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| <template> | ||||
|   <section class="AiDrawer" :style="{width:`${width}px`,height:`${height}px`}"> | ||||
|     <slot name="tools"/> | ||||
|     <div class="resetSignature" @click.stop="handleClean"> | ||||
|       <ai-icon icon="iconResetting"/> | ||||
|       <span>重写</span> | ||||
|     </div> | ||||
|     <div v-if="drawPlaceholder" class="placeholder">{{ placeholder }}</div> | ||||
|     <canvas id="drawer" :width="width" :height="height" @mousedown.stop="handleDrawStart($event)" | ||||
|             @mouseup.stop="handleDrawEnd" @mouseleave="handleDrawEnd" | ||||
|             @mousemove.stop="handleDrawing($event)" | ||||
|             @touchstart.stop="handleDrawStart" @touchend.stop="handleDrawEnd" @touchmove="handleDrawing"/> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: "AiDrawer", | ||||
|   props: { | ||||
|     seal: String, | ||||
|     placeholder: {type: String, default: "请在此处清晰书写你的签名"}, | ||||
|     width: {type: Number, default: 640}, | ||||
|     height: {type: Number, default: 480} | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       drawing: false,//判断是否处于绘画中 | ||||
|       drawer: {}, | ||||
|       drawPlaceholder: true | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     drawing() { | ||||
|       this.drawPlaceholder = false | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.initDrawer() | ||||
|   }, | ||||
|   methods: { | ||||
|     initDrawer() { | ||||
|       this.$nextTick(() => { | ||||
|         let canvas = document.querySelector("#drawer") | ||||
|         if (canvas) { | ||||
|           this.drawer = canvas?.getContext("2d") | ||||
|           this.drawer.lineWidth = 3 | ||||
|           this.drawer.shadowColor = '#333' | ||||
|           this.drawer.strokeStyle = '#333' | ||||
|           this.drawer.shadowBlur = 2 | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     handleDrawStart(e) { | ||||
|       this.drawing = true | ||||
|       const canvasX = e.offsetX | ||||
|       const canvasY = e.offsetY | ||||
|       this.drawer?.beginPath() | ||||
|       this.drawer?.moveTo(canvasX, canvasY) | ||||
|     }, | ||||
|     handleDrawEnd() { | ||||
|       this.drawing = false | ||||
|       this.$emit("update:seal", this.drawer.canvas.toDataURL("image/png", 1)) | ||||
|     }, | ||||
|     handleDrawing(e) { | ||||
|       if (this.drawing) { | ||||
|         const t = e.target?.getBoundingClientRect() | ||||
|         let canvasX | ||||
|         let canvasY | ||||
|         // 根据触发事件类型来进行定位 | ||||
|         if (e.type == "touchmove") { | ||||
|           canvasX = e.changedTouches[0].clientX - t.x | ||||
|           canvasY = e.changedTouches[0].clientY - t.y | ||||
|         } else { | ||||
|           canvasX = e.offsetX | ||||
|           canvasY = e.offsetY | ||||
|         } | ||||
|         // 连接到移动的位置并上色 | ||||
|         this.drawer?.lineTo(canvasX, canvasY) | ||||
|         this.drawer?.stroke() | ||||
|       } | ||||
|     }, | ||||
|     handleClean() { | ||||
|       this.drawer.clearRect(0, 0, this.drawer.canvas.width, this.drawer.canvas.height) | ||||
|       this.drawPlaceholder = true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .AiDrawer { | ||||
|   margin: 0 40px; | ||||
|   background: #F7F7F7; | ||||
|   border: 1px solid #CCCCCC; | ||||
|   position: relative; | ||||
|  | ||||
|   #drawer { | ||||
|     display: block; | ||||
|     cursor: crosshair; | ||||
|   } | ||||
|  | ||||
|   .resetSignature { | ||||
|     position: absolute; | ||||
|     width: 76px; | ||||
|     height: 32px; | ||||
|     background: rgba(#000, .5); | ||||
|     border-radius: 16px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 8px; | ||||
|     color: rgba(#fff, .6); | ||||
|     cursor: pointer; | ||||
|     right: 16px; | ||||
|     top: 16px; | ||||
|  | ||||
|     .AiIcon { | ||||
|       width: auto; | ||||
|       height: auto; | ||||
|     } | ||||
|  | ||||
|     &:hover { | ||||
|       color: #fff; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .placeholder { | ||||
|     pointer-events: none; | ||||
|     position: absolute; | ||||
|     left: 50%; | ||||
|     top: 50%; | ||||
|     transform: translate(-50%, -50%); | ||||
|     max-width: 407px; | ||||
|     text-align: center; | ||||
|     font-size: 64px; | ||||
|     color: #DDDDDD; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										129
									
								
								ui/packages/basic/AiEditor/AiEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								ui/packages/basic/AiEditor/AiEditor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| <template> | ||||
|   <section class="AiEditor" :class="{invalid:valid&&!validateState}"> | ||||
|     <ck-editor v-if="sourceEditor" :editor="sourceEditor" v-model="content" :config="editorConfig" @ready="handleReady" @input="$emit('change',repairContent)"/> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| /** | ||||
|  * 原组件ckeditor封装 | ||||
|  * 修改者:Kubbo | ||||
|  */ | ||||
| import CKEditor from '@ckeditor/ckeditor5-vue2' | ||||
| import {UploadAdapter} from "./models"; | ||||
|  | ||||
| export default { | ||||
|   name: "AiEditor", | ||||
|   components: {ckEditor: CKEditor.component}, | ||||
|   inject: { | ||||
|     elFormItem: {default: ""}, | ||||
|     elForm: {default: ''}, | ||||
|   }, | ||||
|   model: { | ||||
|     prop: "value", | ||||
|     event: "change" | ||||
|   }, | ||||
|   props: { | ||||
|     value: {type: String, required: true, default: ""}, | ||||
|     placeholder: {type: String, default: '请输入正文'}, | ||||
|     conf: Object, | ||||
|     instance: {type: Function, required: true}, | ||||
|     valid: {type: Boolean, default: true}, | ||||
|     text: {default: ""}, | ||||
|     action: {default: "/admin/file/add"}, | ||||
|     params: {default: () => ({})}, | ||||
|     mode: {default: "default"} | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       sourceEditor: null, | ||||
|       editor: null, | ||||
|       isPasteStyle: true,//粘贴是否携带格式 | ||||
|       content: "" | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     validateState() { | ||||
|       return ['', 'success'].includes(this.elFormItem?.validateState) | ||||
|     }, | ||||
|     editorConfig() { | ||||
|       return { | ||||
|         mediaEmbed: { | ||||
|           previewsInData: true | ||||
|         }, | ||||
|         htmlSupport: { | ||||
|           allow: [{name: /[\s\S]+/, styles: true, classes: true, attributes: true}] | ||||
|         }, | ||||
|         ...this.conf | ||||
|       } | ||||
|     }, | ||||
|     repairContent: v => `<section class="ck-content">${v.content?.replace(/\s0w"/g, '"')?.replace(/srcset/g, 'src')}</section>` | ||||
|   }, | ||||
|   watch: { | ||||
|     content(v) { | ||||
|       v && this.dispatch('ElFormItem', 'el.form.change', [v]) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleReady(editor) { | ||||
|       this.editor = editor | ||||
|       const {instance, action, params} = this | ||||
|       editor.plugins.get('FileRepository').createUploadAdapter = loader => new UploadAdapter(loader, instance, action, params) | ||||
|     }, | ||||
|     /** | ||||
|      * 表单验证 | ||||
|      * @param componentName | ||||
|      * @param eventName | ||||
|      * @param params | ||||
|      */ | ||||
|     dispatch(componentName, eventName, params) { | ||||
|       let parent = this.$parent || this.$root; | ||||
|       let name = parent.$options.componentName; | ||||
|  | ||||
|       while (parent && (!name || name !== componentName)) { | ||||
|         parent = parent.$parent; | ||||
|  | ||||
|         if (parent) { | ||||
|           name = parent.$options.componentName; | ||||
|         } | ||||
|       } | ||||
|       if (parent) { | ||||
|         parent.$emit.apply(parent, [eventName].concat(params)); | ||||
|       } | ||||
|     }, | ||||
|     initValue() { | ||||
|       let unwatch = this.$watch('value', (v) => { | ||||
|         const init = v.replace(/<section class="ck-content">(.*)<\/section>/, '$1') | ||||
|         if (!!this.content) unwatch && unwatch() | ||||
|         else if (!!init) { | ||||
|           this.content = init | ||||
|           unwatch && unwatch() | ||||
|         } | ||||
|       }, {immediate: true}) | ||||
|     }, | ||||
|     loadEditor(count = 0) { | ||||
|       if (!!window?.ClassicEditor) { | ||||
|         this.sourceEditor = ClassicEditor | ||||
|         this.initValue() | ||||
|       } else if (count < 10) { | ||||
|         setTimeout(() => this.loadEditor(++count), 50) | ||||
|       } else { | ||||
|         console.error("无法加载编辑器资源,请联系管理员") | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.$injectLib("https://cdn.cunwuyun.cn/ckeditor.js", () => { | ||||
|       this.loadEditor() | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .AiEditor { | ||||
|   ::v-deep.ck-content { | ||||
|     min-height: 300px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										24
									
								
								ui/packages/basic/AiEditor/models.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								ui/packages/basic/AiEditor/models.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import {Loading} from "element-ui" | ||||
|  | ||||
| export class UploadAdapter { | ||||
|   constructor(loader, instance, action, params) { | ||||
|     this.instance = instance | ||||
|     this.action = action | ||||
|     this.loader = loader | ||||
|     this.params = params | ||||
|   } | ||||
|  | ||||
|   async upload() { | ||||
|     const formData = new FormData() | ||||
|     formData.append('file', await this.loader.file) | ||||
|     const loading = Loading.service({}) | ||||
|     return this.instance.post(this.action, formData, {...this.params, returnError: true}).then(res => { | ||||
|       if (res?.data) { | ||||
|         return res.data.map(m => m.split(";")?.[0]) | ||||
|       } else return this.loader.status = "aborted" && Promise.reject() | ||||
|     }).finally(() => loading.close()) | ||||
|   } | ||||
|  | ||||
|   abort() { | ||||
|   } | ||||
| } | ||||
							
								
								
									
										34
									
								
								ui/packages/basic/AiEmpty.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								ui/packages/basic/AiEmpty.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <template> | ||||
|   <div class="ai-empty"> | ||||
|     <div class="ai-empty__bg"></div> | ||||
|     <template v-if="!isHasTitleSlot">暂无数据</template> | ||||
|     <slot v-else></slot> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: 'AiEmpty', | ||||
|  | ||||
|     computed: { | ||||
|       isHasTitleSlot () { | ||||
|         return this.$slots.default | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss"> | ||||
|   .ai-empty { | ||||
|     margin-bottom: 10px; | ||||
|     text-align: center; | ||||
|     color: #acaaad; | ||||
|  | ||||
|     .ai-empty__bg { | ||||
|       background: url("https://cdn.cunwuyun.cn/dvcp/empty.svg") no-repeat center; | ||||
|       background-size: 120px 120px; | ||||
|       height: 120px; | ||||
|       margin: 48px auto 0; | ||||
|     } | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										118
									
								
								ui/packages/basic/AiFileList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								ui/packages/basic/AiFileList.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| <template> | ||||
|   <div class="ai-filelist"> | ||||
|     <div class="ai-flie__item" v-for="(item, index) in fileList" :key="index" @click="downFile(item)" | ||||
|          :title="item.name"> | ||||
|       <div class="ai-flie__item--left flex1"> | ||||
|         <svg aria-hidden="true"> | ||||
|           <use xlink:href="#iconAppendix_UNdownload"></use> | ||||
|         </svg> | ||||
|         <span>{{ item[fileProps.name] }}</span> | ||||
|       </div> | ||||
|       <div class="ai-file__item--right"> | ||||
|         <span>{{ item[fileProps.size] }}</span> | ||||
|         <i class="iconfont iconDownload"></i> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-if="!fileList.length" style="width: 120px" class="no-data"></div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AiFileList', | ||||
|  | ||||
|   props: { | ||||
|     fileList: { | ||||
|       type: Array, | ||||
|       default: () => [] | ||||
|     }, | ||||
|  | ||||
|     fileOps: { | ||||
|       type: Object | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     fileProps() { | ||||
|       const props = { | ||||
|         name: 'name', | ||||
|         size: 'size' | ||||
|       } | ||||
|  | ||||
|       return this.fileOps || props | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     downFile(item) { | ||||
|       window.open(`${item.url}`) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ai-filelist { | ||||
|   .ai-flie__item { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: space-between; | ||||
|     height: 40px; | ||||
|     line-height: 40px; | ||||
|     margin-bottom: 16px; | ||||
|     padding: 0 8px; | ||||
|     font-size: 14px; | ||||
|     color: #333; | ||||
|     background: #fff; | ||||
|     border-radius: 4px; | ||||
|     border: 1px solid #d0d4dc; | ||||
|     cursor: pointer; | ||||
|  | ||||
|     &:hover { | ||||
|       background-color: #f3f6f9; | ||||
|       border-color: transparent; | ||||
|     } | ||||
|  | ||||
|     &:last-child { | ||||
|       margin-bottom: 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ai-flie__item--left { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     margin-right: 20px; | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|  | ||||
|     svg { | ||||
|       width: 24px; | ||||
|       height: 24px; | ||||
|       flex-shrink: 1; | ||||
|     } | ||||
|  | ||||
|     span { | ||||
|       flex: 1; | ||||
|       white-space: nowrap; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ai-file__item--right { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     flex-shrink: 1; | ||||
|  | ||||
|     span { | ||||
|       padding-right: 5px; | ||||
|       color: #999; | ||||
|     } | ||||
|  | ||||
|     i { | ||||
|       color: #5088FF; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										38
									
								
								ui/packages/basic/AiIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								ui/packages/basic/AiIcon.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| <template> | ||||
|   <div class="AiIcon"> | ||||
|     <div v-if="type=='icon'" class="iconfont" :class="icon"/> | ||||
|     <div v-if="type=='logo'" class="logofont" :class="icon"/> | ||||
|     <svg v-if="type=='svg'" class="icon" aria-hidden="true"> | ||||
|       <use :xlink:href="`#${icon}`"></use> | ||||
|     </svg> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import "../../meta/styles/iconfont/iconfont"; | ||||
| import "../../meta/styles/iconfont/iconfont.css"; | ||||
| import "../../meta/styles/iconfont/logofont.css"; | ||||
|  | ||||
| export default { | ||||
|   name: "AiIcon", | ||||
|   props: { | ||||
|     type: {type: String, default: "icon"}, | ||||
|     icon: {type: String, required: true}, | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .AiIcon { | ||||
|   box-sizing: border-box; | ||||
|   width: 28px; | ||||
|   height: 28px; | ||||
|   font-size: 16px; | ||||
|  | ||||
|   .iconfont, .logofont, .icon { | ||||
|     font-size: inherit; | ||||
|     width: inherit; | ||||
|     height: inherit; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										321
									
								
								ui/packages/basic/AiImport.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								ui/packages/basic/AiImport.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | ||||
| <template> | ||||
|   <section class="ai-import"> | ||||
|     <a v-if="$slots.default" class="custom-clicker" @click="dialog = true"> | ||||
|       <slot/> | ||||
|     </a> | ||||
|     <el-button v-else size="small" @click="dialog=true" icon="iconfont iconImport">导入</el-button> | ||||
|     <ai-dialog | ||||
|       :title="dialogTitle" | ||||
|       :visible.sync="dialog" | ||||
|       :destroy-on-close="true" | ||||
|       width="800px" | ||||
|       customFooter | ||||
|       :close-on-click-modal="false" | ||||
|       @closed="onClose"> | ||||
|       <el-form size="small" ref="importForm" label-width="0" class="import-form" :model="fileForm" | ||||
|                :rules="fileRules"> | ||||
|         <el-form-item class="ai-import__tips"> | ||||
|           <h2>导入说明:</h2> | ||||
|           <p class="ai-import__content"> | ||||
|             1、您正在进行【{{ name }}】批量导入操作,请先 | ||||
|             <span @click="downloadFile" title="下载导入模板" class="ai-link" v-text="'【点击下载导入模板】'"/> | ||||
|             并规范填写;</p> | ||||
|           <p class="ai-import__content">2、填写完成后,上传您编辑完成的模板文件:</p> | ||||
|           <div class="ai-import__text"> | ||||
|             <template v-if="$slots.tips"> | ||||
|               <div>请注意:</div> | ||||
|               <slot name="tips"/> | ||||
|             </template> | ||||
|             <el-row type="flex" v-else-if="!!dict"> | ||||
|               <div v-text="'请注意:'"/> | ||||
|               <span v-text="dict.getLabel('importTips',type)"/> | ||||
|             </el-row> | ||||
|           </div> | ||||
|         </el-form-item> | ||||
|         <el-form-item prop="file" style="width: 100%"> | ||||
|           <ai-uploader isImport :instance="instance" v-model="fileForm.file" fileType="file" :limit="1" | ||||
|                        acceptType=".xls,.xlsx" @change="onChange" :clearable="false"> | ||||
|             <template #trigger> | ||||
|               <el-button icon="iconfont iconfangda">选择文件</el-button> | ||||
|             </template> | ||||
|             <template #tips>最多上传1个文件,单个文件最大10MB,仅支持Excel格式</template> | ||||
|           </ai-uploader> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|       <div slot="footer" style="text-align: center;"> | ||||
|         <el-button @click="dialog=false">取消</el-button> | ||||
|         <el-button type="primary" @click="onClick">立即导入</el-button> | ||||
|       </div> | ||||
|     </ai-dialog> | ||||
|     <div class="ai-import__loading" v-if="isHasLoadingSlot && isLoading"> | ||||
|       <slot name="loading"/> | ||||
|     </div> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AiImport', | ||||
|   props: { | ||||
|     title: String, | ||||
|     name: { | ||||
|       type: String, | ||||
|       required: true | ||||
|     }, | ||||
|     instance: { | ||||
|       type: Function | ||||
|     }, | ||||
|     importUrl: { | ||||
|       type: String, | ||||
|     }, | ||||
|     importParams: { | ||||
|       type: Object | ||||
|     }, | ||||
|     suffixName: { | ||||
|       type: String, | ||||
|       default: 'xls' | ||||
|     }, | ||||
|     tplParams: Object, | ||||
|     url: { | ||||
|       type: String, | ||||
|     }, | ||||
|     timeout: { | ||||
|       type: Number, | ||||
|       default: 10 * 60 * 1000 | ||||
|     }, | ||||
|     customError: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     type: String, | ||||
|     dict: Object | ||||
|   }, | ||||
|   computed: { | ||||
|     isHasLoadingSlot() { | ||||
|       return this.$slots.loading | ||||
|     }, | ||||
|     actions() { | ||||
|       return { | ||||
|         url: this.importUrl || `/app/${this.type}/import`,//导入接口 | ||||
|         tpl: this.url || `/app/${this.type}/downloadTemplate`,//下载模板接口 | ||||
|       } | ||||
|     }, | ||||
|     dialogTitle() { | ||||
|       return this.title || `${this.name}数据导入` | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       dialog: false, | ||||
|       fileForm: { | ||||
|         file: [] | ||||
|       }, | ||||
|       loading: null, | ||||
|       isLoading: false, | ||||
|       fileRules: { | ||||
|         file: [{required: true, message: '请上传相关文件', trigger: 'change'}] | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     onChange(e) { | ||||
|       if (e.length) { | ||||
|         this.$refs.importForm.clearValidate() | ||||
|       } else { | ||||
|         this.$refs.importForm.validate() | ||||
|       } | ||||
|     }, | ||||
|     onClick() { | ||||
|       this.$refs.importForm.validate(v => { | ||||
|         if (v) { | ||||
|           const data = new FormData() | ||||
|           data.append('file', this.fileForm.file[0].raw) | ||||
|           if (!this.isHasLoadingSlot) { | ||||
|             this.loading = this.$loading({ | ||||
|               lock: true, | ||||
|               text: '导入中', | ||||
|               background: 'rgba(0, 0, 0, 0.5)' | ||||
|             }) | ||||
|           } else { | ||||
|             this.isLoading = true | ||||
|           } | ||||
|           this.instance.post(this.actions.url, data, { | ||||
|             params: this.importParams, | ||||
|             timeout: this.timeout | ||||
|           }).then(res => { | ||||
|             if (!this.isHasLoadingSlot) { | ||||
|               this.loading?.close() | ||||
|             } else { | ||||
|               this.isLoading = false | ||||
|             } | ||||
|             if (res?.data?.importStatus == 1) { | ||||
|               this.dialog = false | ||||
|               const h = this.$createElement | ||||
|               this.$emit('onSuccess', res) | ||||
|               this.$emit('success', res) | ||||
|               if (this.customError) { | ||||
|                 this.$emit('error') | ||||
|               } else this.$confirm('导入失败数据具体原因请', { | ||||
|                 type: 'success', | ||||
|                 title: '数据导入完成', | ||||
|                 closeOnClickModal: false, | ||||
|                 customClass: 'message-wrapper', | ||||
|                 showConfirmButton: false, | ||||
|                 cancelButtonText: '关闭', | ||||
|                 message: h('div', { | ||||
|                   class: 'importResult' | ||||
|                 }, [ | ||||
|                   h('span', null, '成功新增'), | ||||
|                   h('a', { | ||||
|                     style: 'color: #2EA222;' | ||||
|                   }, `${res.data.addCount}`), | ||||
|                   h('span', null, '条数据,更新'), | ||||
|                   h('a', { | ||||
|                     style: 'color: #26f;' | ||||
|                   }, `${res.data.updateCount}`), | ||||
|                   h('span', null, '条数据,导入失败'), | ||||
|                   h('a', { | ||||
|                     style: 'color: #f46;' | ||||
|                   }, `${res.data.failCount}`), | ||||
|                   h('span', null, '条数据。'), | ||||
|                   h('div', {class: 'gap'}), | ||||
|                   h('div', {style: `display:${res.data.errorFileURL ? 'block' : 'none'}`}, [ | ||||
|                     h('span', null, '点此'), | ||||
|                     h('a', { | ||||
|                       class: 'tips-link', | ||||
|                       attrs: { | ||||
|                         href: res.data.errorFileURL | ||||
|                       } | ||||
|                     }, '下载异常数据') | ||||
|                   ]) | ||||
|                 ]) | ||||
|               }) | ||||
|             } else if (res?.data?.importStatus == 0) { | ||||
|               this.$message.error(res?.data?.errorMsg) | ||||
|             } | ||||
|           }).catch(() => { | ||||
|             if (!this.isHasLoadingSlot) { | ||||
|               this.loading?.close() | ||||
|             } else { | ||||
|               this.isLoading = false | ||||
|             } | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     onClose() { | ||||
|       this.loading?.close() | ||||
|       this.fileForm.file = [] | ||||
|     }, | ||||
|     downloadFile() { | ||||
|       this.instance.post(this.actions.tpl, null, { | ||||
|         responseType: 'blob', | ||||
|         params: this.tplParams | ||||
|       }).then((res) => { | ||||
|         const link = document.createElement('a') | ||||
|         let blob = new Blob([res], {type: 'application/vnd.ms-excel'}) | ||||
|         link.style.display = 'none' | ||||
|         link.href = URL.createObjectURL(blob) | ||||
|         link.setAttribute('download', this.name + '模板.' + this.suffixName) | ||||
|         document.body.appendChild(link) | ||||
|         link.click() | ||||
|         document.body.removeChild(link) | ||||
|       }) | ||||
|     }, | ||||
|     hide() { | ||||
|       this.dialog = false | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.dict?.load("importTips") | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .ai-import { | ||||
|   .empty-input { | ||||
|     opacity: 0; | ||||
|     position: absolute; | ||||
|     z-index: -1; | ||||
|     visibility: hidden; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   .custom-clicker { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|   } | ||||
|  | ||||
|   .ai-import__loading { | ||||
|     position: fixed; | ||||
|     left: 0; | ||||
|     top: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     background: rgba(0, 0, 0, 0.6); | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-message-box { | ||||
|     width: 720px !important; | ||||
|   } | ||||
|  | ||||
|   .ai-import__content { | ||||
|     line-height: 22px; | ||||
|   } | ||||
|  | ||||
|   .ai-import__tips { | ||||
|     line-height: 1; | ||||
|  | ||||
|     ::v-deep.el-form-item__content { | ||||
|       line-height: 1; | ||||
|     } | ||||
|  | ||||
|     h2 { | ||||
|       margin-top: 4px; | ||||
|       color: #333333; | ||||
|       font-size: 16px; | ||||
|       font-weight: 700; | ||||
|     } | ||||
|  | ||||
|     p { | ||||
|       margin-top: 8px; | ||||
|       color: #424242; | ||||
|       font-size: 14px; | ||||
|     } | ||||
|  | ||||
|     .ai-link { | ||||
|       cursor: pointer; | ||||
|       color: $primaryColor; | ||||
|     } | ||||
|  | ||||
|     .ai-import__text { | ||||
|       font-size: 12px; | ||||
|       margin-top: 8px; | ||||
|       line-height: 16px; | ||||
|       color: #999999; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| <style lang="scss"> | ||||
| .message-wrapper { | ||||
|   width: 560px !important; | ||||
|  | ||||
|   .importResult { | ||||
|     color: #222222; | ||||
|     font-size: 16px; | ||||
|     line-height: 24px; | ||||
|     font-weight: bold; | ||||
|  | ||||
|     .gap { | ||||
|       width: 100%; | ||||
|       height: 8px; | ||||
|     } | ||||
|  | ||||
|     .tips-link { | ||||
|       color: $primaryColor; | ||||
|       text-decoration: unset; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										91
									
								
								ui/packages/basic/AiInfoItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								ui/packages/basic/AiInfoItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| <template> | ||||
|   <div class="ai-info-item" :style="contentStyle"> | ||||
|     <label class="ai-info-item__left" :style="contentLabelStyle"> | ||||
|       <template v-if="$slots.label"> | ||||
|         <slot name="label"/> | ||||
|       </template> | ||||
|       <template v-else>{{ label }}</template> | ||||
|     </label> | ||||
|     <div class="ai-info-item__right"> | ||||
|       <slot v-if="$scopedSlots.default"/> | ||||
|       <template v-else-if="!!openType"> | ||||
|         <ai-open-data :type="openType" :openid="value"/> | ||||
|       </template> | ||||
|       <template v-else>{{ value || '-' }}</template> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AiInfoItem', | ||||
|  | ||||
|   inject: ['AiWrapper'], | ||||
|  | ||||
|   props: { | ||||
|     label: { | ||||
|       type: String | ||||
|     }, | ||||
|  | ||||
|     value: { | ||||
|       type: [String, Number] | ||||
|     }, | ||||
|  | ||||
|     'label-width': { | ||||
|       type: String | ||||
|     }, | ||||
|  | ||||
|     isLine: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     openType: {default: ""} | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     contentStyle() { | ||||
|       let width = this.AiWrapper.autoWidth | ||||
|  | ||||
|       if (this.isLine) { | ||||
|         width = '100%' | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         width | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     contentLabelStyle() { | ||||
|       return { | ||||
|         width: this.labelWidth || this.AiWrapper.autoLableWidth | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   methods: {} | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ai-info-item { | ||||
|   display: flex; | ||||
|   line-height: 1.4; | ||||
|   margin-bottom: 16px; | ||||
|  | ||||
|   label { | ||||
|     flex-shrink: 0; | ||||
|     width: 96px; | ||||
|     margin-right: 40px; | ||||
|     text-align: right; | ||||
|     color: #888; | ||||
|     font-size: 14px; | ||||
|   } | ||||
|  | ||||
|   .ai-info-item__right { | ||||
|     flex: 1; | ||||
|     color: #222; | ||||
|     font-size: 14px; | ||||
|     word-break: break-all; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										58
									
								
								ui/packages/basic/AiNumber.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								ui/packages/basic/AiNumber.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| <template> | ||||
|   <section class="AiNumber"> | ||||
|     <el-input v-model="num" :size="size" @input.native="validate" @focus="$emit('focus')" @blur="format" clearable | ||||
|              @change="$emit('change')" :validate-event="isRule"></el-input> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|   export default { | ||||
|     name: "AiNumber", | ||||
|     model: { | ||||
|       prop: "value", | ||||
|       event: "change" | ||||
|     }, | ||||
|     props: { | ||||
|       value: String, | ||||
|       size: String, | ||||
|       decimal: {type: [String, Number], default: 2}, | ||||
|       isRule: {type: Boolean, default: true} | ||||
|     }, | ||||
|     computed: { | ||||
|       validRegex() { | ||||
|         let dec = Number(this.decimal || 0), | ||||
|             regex = `^(\\d*\\.?\\d{0,${dec}}).*` | ||||
|         return new RegExp(regex) | ||||
|       } | ||||
|  | ||||
|     }, | ||||
|     data() { | ||||
|       return { | ||||
|         num: "" | ||||
|       } | ||||
|     }, | ||||
|     methods: { | ||||
|       validate() { | ||||
|         let num = JSON.parse(JSON.stringify(this.num)) | ||||
|         this.num = num.replace(this.validRegex, '$1') | ||||
|       }, | ||||
|       format() { | ||||
|         this.num = Number(this.num||0).toString() | ||||
|         this.$emit("blur") | ||||
|       } | ||||
|     }, | ||||
|     watch: { | ||||
|       num(v) { | ||||
|         this.$emit("change", v) | ||||
|       } | ||||
|     }, | ||||
|     mounted() { | ||||
|       this.num = this.value | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   .AiNumber { | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										98
									
								
								ui/packages/basic/AiOperate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								ui/packages/basic/AiOperate.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| <template> | ||||
|   <div class="ai-operate"> | ||||
|     <template v-if="filterBtns.length <= 3"> | ||||
|       <el-button | ||||
|           v-for="(item, index) in filterBtns" | ||||
|           :key="index" | ||||
|           type="text" | ||||
|           :title="item.text" | ||||
|           @click="onClick(index)"> | ||||
|         {{ item.text }} | ||||
|       </el-button> | ||||
|     </template> | ||||
|     <template v-else> | ||||
|       <el-button | ||||
|           v-for="(item, index) in list" | ||||
|           :key="index" | ||||
|           type="text" | ||||
|           :title="item.text" | ||||
|           @click="onClick(index)"> | ||||
|         {{ item.text }} | ||||
|       </el-button> | ||||
|       <el-dropdown | ||||
|           trigger="click" | ||||
|           @command="onDropClick"> | ||||
|         <el-button | ||||
|             style="margin-left: 10px;" | ||||
|             title="更多" | ||||
|             type="text"> | ||||
|           更多 | ||||
|         </el-button> | ||||
|         <el-dropdown-menu | ||||
|             slot="dropdown"> | ||||
|           <el-dropdown-item | ||||
|               v-for="(item, index) in moreList" | ||||
|               :key="index" | ||||
|               :command="index"> | ||||
|             {{ item.text }} | ||||
|           </el-dropdown-item> | ||||
|         </el-dropdown-menu> | ||||
|       </el-dropdown> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AiOperate', | ||||
|  | ||||
|   props: { | ||||
|     permissions: { | ||||
|       type: Function, | ||||
|       required: true | ||||
|     }, | ||||
|  | ||||
|     btns: { | ||||
|       type: Array, | ||||
|       required: true, | ||||
|       default: () => [] | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     filterBtns() { | ||||
|       return this.btns.filter(e => this.permissions(e.permissions)) | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   data() { | ||||
|     return { | ||||
|       list: [], | ||||
|       moreList: [] | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   mounted() { | ||||
|     this.filterBtns.forEach((item, index) => { | ||||
|       if (index <= 1) { | ||||
|         this.list.push(item) | ||||
|       } else { | ||||
|         this.moreList.push(item) | ||||
|       } | ||||
|     }) | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     onClick(index) { | ||||
|       this.$emit('on-click', index) | ||||
|     }, | ||||
|  | ||||
|     onDropClick(e) { | ||||
|       this.onClick(e + 2) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
							
								
								
									
										76
									
								
								ui/packages/basic/AiPullDown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								ui/packages/basic/AiPullDown.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| <template> | ||||
|   <section class="AiPullDown"> | ||||
|     <div class="line"/> | ||||
|     <div class="down-content" @click="handleExpand"> | ||||
|       <i :class="expandIcon"/> | ||||
|       <span>{{ btnText }}</span> | ||||
|     </div> | ||||
|     <div class="line"/> | ||||
|   </section> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: "AiPullDown", | ||||
|   props: { | ||||
|     target: String, | ||||
|     height: {default: 4}, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       expand: false | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleExpand() { | ||||
|       this.expand = !this.expand | ||||
|       if (this.target) { | ||||
|  | ||||
|       } else this.$emit('change', this.expandStyle) | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     btnText() { | ||||
|       return this.expand ? '收起高级搜索' : '展开高级搜索' | ||||
|     }, | ||||
|     expandStyle() { | ||||
|       let initStyle = {overflow: 'hidden', height: `${this.height}px`} | ||||
|       this.expand && (initStyle.height = "auto") | ||||
|       return initStyle | ||||
|     }, | ||||
|     expandIcon() { | ||||
|       return this.expand ? 'iconfont iconDouble_Up' : 'iconfont iconDouble_Down' | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$emit("change", this.expandStyle) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| <style lang="scss" scoped> | ||||
| .AiPullDown { | ||||
|   display: flex; | ||||
|  | ||||
|   .line { | ||||
|     flex: 1; | ||||
|     min-width: 0; | ||||
|     border-top: 1px solid #eee; | ||||
|   } | ||||
|  | ||||
|   .down-content { | ||||
|     cursor: pointer; | ||||
|     padding: 0 8px; | ||||
|     height: 24px; | ||||
|     border-radius: 0 0 8px 8px; | ||||
|     border: 1px solid #eee; | ||||
|     border-top: 0; | ||||
|     box-sizing: border-box; | ||||
|     color: #333; | ||||
|     font-size: 12px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										101
									
								
								ui/packages/basic/AiRange.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								ui/packages/basic/AiRange.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| <template> | ||||
|   <section class="AiRange" :style="{width}"> | ||||
|     <el-input type="number" v-model.number="valueStart" size="small" :placeholder="startPlaceholder" @change="saveNum"/> | ||||
|     <span class="separator">-</span> | ||||
|     <el-input type="number" v-model.number="valueEnd" size="small" :placeholder="endPlaceholder" @change="saveNum"/> | ||||
|     <span class="el-icon-circle-close" v-if="isUse" @click="shutoff"/> | ||||
|     <div v-else/> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: "AiRange", | ||||
|   model: { | ||||
|     prop: 'value', | ||||
|     event: 'change' | ||||
|   }, | ||||
|   props: { | ||||
|     width: {type: String, default: '200px'}, | ||||
|     startPlaceholder: String, | ||||
|     endPlaceholder: String, | ||||
|     value: {default: () => []}, | ||||
|   }, | ||||
|   computed: { | ||||
|     isUse() { | ||||
|       return !!this.valueStart || !!this.valueEnd | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       valueStart: null, | ||||
|       valueEnd: null | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     shutoff() { | ||||
|       this.valueStart = null | ||||
|       this.valueEnd = null | ||||
|       this.saveNum() | ||||
|     }, | ||||
|     saveNum() { | ||||
|       this.$emit('change', [this.valueStart, this.valueEnd]) | ||||
|     }, | ||||
|     initValue() { | ||||
|       let unwatch = this.$watch('value', (v) => { | ||||
|         if (this.isUse) unwatch && unwatch() | ||||
|         else if (!!v) { | ||||
|           this.valueStart = v?.[0] || "" | ||||
|           this.valueEnd = v?.[1] || "" | ||||
|           unwatch && unwatch() | ||||
|         } | ||||
|       }, {immediate: true}) | ||||
|     }, | ||||
|   }, | ||||
|   created() { | ||||
|     this.initValue() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .AiRange { | ||||
|   display: flex; | ||||
|   border: 1px solid #D0D4DC; | ||||
|   border-radius: 2px; | ||||
|   align-items: center; | ||||
|   height: 32px; | ||||
|   box-sizing: border-box; | ||||
|  | ||||
|   ::v-deep.el-input { | ||||
|     min-width: 80px; | ||||
|     flex: 1; | ||||
|  | ||||
|     .el-input__inner { | ||||
|       border: none; | ||||
|       padding: 0; | ||||
|       text-align: center; | ||||
|       height: 100%; | ||||
|       line-height: normal; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .el-icon-circle-close { | ||||
|     cursor: pointer; | ||||
|     opacity: 0; | ||||
|     margin-right: 4px; | ||||
|  | ||||
|     &:hover { | ||||
|       color: #5088FF; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &:hover { | ||||
|     border-color: #5088FF; | ||||
|  | ||||
|     .el-icon-circle-close { | ||||
|       opacity: 1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										106
									
								
								ui/packages/basic/AiSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								ui/packages/basic/AiSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| <template> | ||||
|   <div class="ai-select"> | ||||
|     <el-select | ||||
|         style="width: 100%;" | ||||
|         clearable | ||||
|         :value="value" | ||||
|         :size="$attrs.size || 'small'" | ||||
|         :filterable="isAction" | ||||
|         v-bind="$attrs" | ||||
|         v-on="$listeners"> | ||||
|       <template v-if="isAction"> | ||||
|         <el-option v-for="op in actionOps" :key="op.id" | ||||
|                    :label="op[actionProp.label]" :value="op[actionProp.value]"/> | ||||
|       </template> | ||||
|       <template v-else> | ||||
|         <el-option | ||||
|             v-for="(item, index) in selectList" | ||||
|             :key="index" | ||||
|             :label="item.dictName" | ||||
|             :value="item.dictValue"/> | ||||
|       </template> | ||||
|     </el-select> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AiSelect', | ||||
|  | ||||
|   model: { | ||||
|     prop: 'value', | ||||
|     event: 'change' | ||||
|   }, | ||||
|   watch: { | ||||
|     instance: { | ||||
|       deep: true, | ||||
|       handler(v) { | ||||
|         v && this.isAction && !this.options.toString() && this.getOptions() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   props: { | ||||
|     value: {default: null}, | ||||
|     selectList: { | ||||
|       type: Array | ||||
|     }, | ||||
|  | ||||
|     width: { | ||||
|       type: String, | ||||
|       default: '216' | ||||
|     }, | ||||
|     instance: Function, | ||||
|     action: {default: ""}, | ||||
|     prop: { | ||||
|       default: () => ({}) | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       options: [], | ||||
|       filter: "" | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     selectWidth() { | ||||
|       if (this.width.indexOf('px') > -1) { | ||||
|         return this.width | ||||
|       } | ||||
|       return `${this.width}px` | ||||
|     }, | ||||
|     isAction() { | ||||
|       return !!this.action | ||||
|     }, | ||||
|     actionOps() { | ||||
|       return this.options.filter(e => !this.filter || e[this.actionProp.label].indexOf(this.filter) > -1) | ||||
|     }, | ||||
|     actionProp() { | ||||
|       return { | ||||
|         label: 'label', | ||||
|         value: 'id', | ||||
|         ...this.prop | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getOptions() { | ||||
|       this.instance?.post(this.action, null, { | ||||
|         params: {size: 999} | ||||
|       }).then(res => { | ||||
|         if (res?.data) { | ||||
|           this.options = res.data.records || res.data | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.getOptions() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| ::v-deep .ai-select .el-select { | ||||
|   width: 100%; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										446
									
								
								ui/packages/basic/AiTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										446
									
								
								ui/packages/basic/AiTable.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,446 @@ | ||||
| <template> | ||||
|   <div class="ai-table" :class="[isShowBorder ? 'ai-table__border' : 'ai-table__noborder']"> | ||||
|     <el-table | ||||
|         :data="tableData" | ||||
|         header-cell-class-name="ai-table__header" | ||||
|         cell-class-name="ai-table__cell" | ||||
|         row-class-name="ai-table__row" | ||||
|         :class="{'ai-header__border': isShowBorder}" | ||||
|         :ref="refName" | ||||
|         :size="tableSize" | ||||
|         :stripe="stripe" | ||||
|         :tooltip-effect="tooltipEffect" | ||||
|         @selection-change="handleSelectionChange" | ||||
|         v-on="$listeners" | ||||
|         v-bind="$attrs" | ||||
|         v-loading="loading"> | ||||
|       <template v-for="colConfig in colConfigs.filter(e=>!e.hide)"> | ||||
|         <slot v-if="colConfig.slot && colConfig.slot !== 'options'" :name="colConfig.slot"/> | ||||
|         <component | ||||
|             :key="colConfig.id" | ||||
|             v-else-if="colConfig.component" | ||||
|             :is="colConfig.component" | ||||
|             :col-config="colConfig"> | ||||
|         </component> | ||||
|         <el-table-column | ||||
|             v-else-if="colConfig.dict" | ||||
|             :key="colConfig.id" | ||||
|             v-bind="colConfig"> | ||||
|           <span slot-scope="{row}" :style="{color:colConfig.color||dict.getColor(colConfig.dict, row[colConfig.prop])}"> | ||||
|             {{ dict.getLabel(colConfig.dict, row[colConfig.prop]) }} | ||||
|           </span> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|             v-else-if="colConfig.openType" | ||||
|             :key="colConfig.id" | ||||
|             v-bind="colConfig"> | ||||
|           <template v-slot="{row}"> | ||||
|             <ai-open-data :type="colConfig.openType" :openid="row[colConfig.prop]"/> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|             v-else-if="colConfig.type" | ||||
|             :key="colConfig.id" | ||||
|             v-bind="colConfig" | ||||
|             :width="colConfig.width || 100"/> | ||||
|         <el-table-column v-else v-bind="colConfig" :key="colConfig.id" | ||||
|                          :show-overflow-tooltip="colConfig['show-overflow-tooltip'] != false"> | ||||
|           <template slot-scope="scope"> | ||||
|             <render-slot v-if="colConfig.render" :render="colConfig.render" :row="scope.row" :index="scope.$index" | ||||
|                          :column="colConfig"/> | ||||
|             <span v-else>{{ getValue(colConfig, scope.row) }}</span> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </template> | ||||
|       <slot class="table-options" name="options"></slot> | ||||
|       <template #empty> | ||||
|         <slot v-if="$scopedSlots.empty" name="empty"/> | ||||
|         <div v-else class="no-data" style="height:160px;"/> | ||||
|       </template> | ||||
|     </el-table> | ||||
|     <div class="pagination newPagination" v-if="isShowPagination"> | ||||
|       <el-pagination | ||||
|           background | ||||
|           :current-page.sync="page.current" | ||||
|           :total="page.total" | ||||
|           :page-size="page.size" | ||||
|           v-bind="$attrs" | ||||
|           :page-sizes="pageSizes" | ||||
|           :layout="layout" | ||||
|           :pager-count="page.pagerCount" | ||||
|           @size-change="handleSizeChange" | ||||
|           @current-change="handleChange"> | ||||
|         <div class="paginationPre"> | ||||
|           <el-checkbox v-if="isHasPaginationBtnsSlot" :disabled="!tableData.length" :indeterminate="isIndeterminate" | ||||
|                        :value="checkAll" | ||||
|                        @click.native="toggleAllSelection">全选 | ||||
|           </el-checkbox> | ||||
|           <slot name="pagination"/> | ||||
|           <div class="pagination-btns"> | ||||
|             <slot name="paginationBtns"></slot> | ||||
|           </div> | ||||
|           <div class="paginationPre-total" :style="{marginLeft: isHasPaginationBtnsSlot ? '24px' : 0}">共<label class="color-primary">{{ page.total }}</label>条记录 | ||||
|           </div> | ||||
|         </div> | ||||
|       </el-pagination> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import moment from 'dayjs' | ||||
| import dict from "../../meta/js/dict" | ||||
|  | ||||
| let renderSlot = { | ||||
|   functional: true, | ||||
|   props: { | ||||
|     row: Object, | ||||
|     render: Function, | ||||
|     index: Number, | ||||
|     column: {type: Object, default: null}, | ||||
|   }, | ||||
|   render: (h, data) => { | ||||
|     const params = { | ||||
|       row: data.props.row, | ||||
|       index: data.props.index | ||||
|     } | ||||
|  | ||||
|     if (data.props.column) { | ||||
|       params.column = data.props.column | ||||
|     } | ||||
|  | ||||
|     return data.props.render(h, params) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default { | ||||
|   name: 'AiTable', | ||||
|   props: { | ||||
|     colConfigs: Array, | ||||
|     tableData: Array, | ||||
|     current: { | ||||
|       type: Number, | ||||
|       default: 1 | ||||
|     }, | ||||
|     size: { | ||||
|       type: Number, | ||||
|       default: 10 | ||||
|     }, | ||||
|     isShowPagination: { | ||||
|       type: Boolean, | ||||
|       default: true | ||||
|     }, | ||||
|     total: { | ||||
|       type: Number | ||||
|     }, | ||||
|     layout: { | ||||
|       type: String, | ||||
|       default: 'slot,->, prev, pager, next, sizes, jumper' | ||||
|     }, | ||||
|     stripe: { | ||||
|       type: Boolean, | ||||
|       default: true | ||||
|     }, | ||||
|     loading: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     tooltipEffect: { | ||||
|       type: String | ||||
|     }, | ||||
|     tableSize: { | ||||
|       type: String | ||||
|     }, | ||||
|     tableRef: String, | ||||
|     dict: {default: () => dict}, | ||||
|     pagerCount: {default: 5}, | ||||
|     pageSizes: {default: () => [10, 20, 50, 100]} | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       name: '', | ||||
|       chooseList: [] | ||||
|     } | ||||
|   }, | ||||
|   components: {renderSlot}, | ||||
|   computed: { | ||||
|     refName() { | ||||
|       return this.tableRef || `aiTable${new Date().getTime()}` | ||||
|     }, | ||||
|     isShowBorder() { | ||||
|       return !!this.$attrs.border || this.$attrs.border === '' | ||||
|     }, | ||||
|  | ||||
|     isHasPaginationBtnsSlot() { | ||||
|       return this.$slots.paginationBtns | ||||
|     }, | ||||
|  | ||||
|     page() { | ||||
|       return { | ||||
|         current: this.current, | ||||
|         size: this.size, | ||||
|         total: this.total, | ||||
|         pagerCount: this.pagerCount | ||||
|       } | ||||
|     }, | ||||
|     isIndeterminate() { | ||||
|       return this.chooseList.length > 0 && this.chooseList.length < this.tableData.length | ||||
|     }, | ||||
|     checkAll() { | ||||
|       return this.chooseList.length == this.tableData.length && this.tableData !== 0 | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleChange(e) { | ||||
|       this.$emit('update:current', e) | ||||
|  | ||||
|       this.$nextTick(() => { | ||||
|         this.$emit('getList') | ||||
|       }) | ||||
|     }, | ||||
|     handleSizeChange(e) { | ||||
|       this.$emit('update:size', e) | ||||
|       this.$nextTick(() => { | ||||
|         this.$emit('getList') | ||||
|       }) | ||||
|     }, | ||||
|     handleSelectionChange(e) { | ||||
|       this.chooseList = e | ||||
|       this.$emit('handleSelectionChange', e) | ||||
|     }, | ||||
|     getValue(colConfig, row) { | ||||
|       if (this.isFunction(colConfig.formart)) { | ||||
|         return colConfig.formart.call(this, row[colConfig.prop]) | ||||
|       } | ||||
|  | ||||
|       if (colConfig.dateFormat) { | ||||
|         return moment(row[colConfig.prop]).format(colConfig.dateFormat) | ||||
|       } | ||||
|  | ||||
|       return this.isInvalidValue(row[colConfig.prop]) | ||||
|     }, | ||||
|     isInvalidValue(value) { | ||||
|       if (value === null || value === undefined || value === '') { | ||||
|         return '-' | ||||
|       } | ||||
|  | ||||
|       return value | ||||
|     }, | ||||
|     isFunction(fun) { | ||||
|       return typeof fun === 'function' | ||||
|     }, | ||||
|     /** | ||||
|      * 表格方法代理 | ||||
|      */ | ||||
|     clearSelection() { | ||||
|       this.$refs[this.refName].clearSelection() | ||||
|     }, | ||||
|     toggleRowSelection() { | ||||
|       this.$refs[this.refName].toggleRowSelection(...arguments) | ||||
|     }, | ||||
|     toggleAllSelection() { | ||||
|       this.$refs[this.refName].toggleAllSelection() | ||||
|     }, | ||||
|     toggleRowExpansion() { | ||||
|       this.$refs[this.refName].toggleRowExpansion(...arguments) | ||||
|     }, | ||||
|     setCurrentRow() { | ||||
|       this.$refs[this.refName].setCurrentRow(...arguments) | ||||
|     }, | ||||
|     clearSort() { | ||||
|       this.$refs[this.refName].clearSort() | ||||
|     }, | ||||
|     clearFilter() { | ||||
|       this.$refs[this.refName].clearFilter(...arguments) | ||||
|     }, | ||||
|     doLayout() { | ||||
|       this.$refs[this.refName].doLayout() | ||||
|     }, | ||||
|     sort() { | ||||
|       this.$refs[this.refName].sort(...arguments) | ||||
|     }, | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ai-table { | ||||
|   .color-primary { | ||||
|     color: $primaryColor; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .ai-header__border .ai-table__header { | ||||
|     border-bottom: 1px solid $borderColor !important; | ||||
|     border-right: 1px solid $borderColor !important; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table--border { | ||||
|     border: 1px solid $borderColor; | ||||
|     border-right: none; | ||||
|     border-bottom: none; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table { | ||||
|     color: #222; | ||||
|  | ||||
|     .caret-wrapper { | ||||
|       height: 24px; | ||||
|  | ||||
|       .ascending { | ||||
|         top: 1px; | ||||
|       } | ||||
|  | ||||
|       .descending { | ||||
|         bottom: 1px; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     thead { | ||||
|       color: #555 | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ::v-deep .cell { | ||||
|     line-height: 24px; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table__header { | ||||
|     th { | ||||
|       padding: 8px 0; | ||||
|     } | ||||
|  | ||||
|     tr { | ||||
|       .cell { | ||||
|         font-weight: 700; | ||||
|       } | ||||
|  | ||||
|       th:first-child { | ||||
|         .cell { | ||||
|           padding-left: 40px !important; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table__body { | ||||
|     tr td:first-child .cell { | ||||
|       padding-left: 40px !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table__fixed-right { | ||||
|     .el-table__body { | ||||
|       tr td:first-child .cell { | ||||
|         padding-left: 0 !important; | ||||
|         padding-right: 0; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ::v-deep .ai-table__header { | ||||
|     border-bottom: none; | ||||
|     background: #F3F4F5; | ||||
|   } | ||||
|  | ||||
|   ::v-deep.el-pager { | ||||
|     li.active + li { | ||||
|       border-left: 1px solid $borderColor; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .newPagination { | ||||
|     width: 100%; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     height: 64px; | ||||
|     padding: 0 40px !important; | ||||
|  | ||||
|     .el-pagination { | ||||
|       width: 100%; | ||||
|       padding: 0; | ||||
|     } | ||||
|  | ||||
|     .paginationPre { | ||||
|       display: flex; | ||||
|       height: 28px; | ||||
|       line-height: 1; | ||||
|       font-size: 14px; | ||||
|       font-weight: normal; | ||||
|       align-items: center; | ||||
|  | ||||
|       .pagination-btns { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 8px; | ||||
|         color: $primaryColor !important; | ||||
|  | ||||
|         ::v-deep span, ::v-deep div { | ||||
|           font-size: 12px; | ||||
|           cursor: pointer; | ||||
|           color: $primaryColor !important; | ||||
|  | ||||
|           &:hover { | ||||
|             opacity: 0.8; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .paginationPre-total { | ||||
|         font-size: 12px; | ||||
|         color: #555; | ||||
|  | ||||
|         label { | ||||
|           padding: 0 2px; | ||||
|           font-weight: 700; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       & > * + * { | ||||
|         margin-left: 24px; | ||||
|       } | ||||
|  | ||||
|       ::v-deep .el-pagination button, .el-pagination span:not([class*=suffix]) { | ||||
|         line-height: 1 !important; | ||||
|       } | ||||
|  | ||||
|       ::v-deep.el-checkbox { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|  | ||||
|         .el-checkbox__input, .el-checkbox__inner { | ||||
|           width: 14px; | ||||
|           height: 14px; | ||||
|           min-width: 0 !important; | ||||
|           line-height: 1 !important; | ||||
|         } | ||||
|  | ||||
|         .el-checkbox__label { | ||||
|           font-size: 12px; | ||||
|           color: #222222; | ||||
|           height: auto !important; | ||||
|           line-height: 1 !important; | ||||
|           padding-left: 3px !important; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .ai-table__noborder { | ||||
|   ::v-deep .el-table td, ::v-deep .el-table th.is-center { | ||||
|     border: none; | ||||
|   } | ||||
|  | ||||
|   .el-table::before { | ||||
|     display: none; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td { | ||||
|     background: #F5F6F9; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-table__fixed-right::before, ::v-deep .el-table__fixed::before { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										242
									
								
								ui/packages/basic/AiTableSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								ui/packages/basic/AiTableSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,242 @@ | ||||
| <template> | ||||
|   <section class="AiTableSelect"> | ||||
|     <el-row type="flex"> | ||||
|       <ai-table v-if="isShowPagination" ref="PendingTable" :tableData="tableData" :total="page.total" :current.sync="page.current" | ||||
|                 :size.sync="page.size" class="fill" border height="330px" @getList="getTableData" tableSize="mini" | ||||
|                 :col-configs="[{slot: 'resident'}]" layout="slot,->, prev, pager, next, jumper" :pagerCount="5"> | ||||
|         <el-table-column slot="resident"> | ||||
|           <template #header> | ||||
|             <b v-text="tableTitle"/> | ||||
|             <el-input class="fill searchbar" v-model="search[searchKey]" size="small" placeholder="搜索" clearable | ||||
|                       @change="page.current=1,getTableData()"/> | ||||
|           </template> | ||||
|           <template slot-scope="{row}"> | ||||
|             <slot name="pending" v-if="$scopedSlots.pending" :row="row"/> | ||||
|             <el-row v-else type="flex" justify="space-between" @click.native="handleSelect(row)" class="toggle" | ||||
|                     :class="{selected:findSelected(row)>-1}"> | ||||
|               <span v-text="row[nodeName]"/> | ||||
|               <slot name="extra" v-if="$scopedSlots.extra" :row="row"/> | ||||
|               <span v-else v-text="getExtra(row)"/> | ||||
|             </el-row> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </ai-table> | ||||
|       <ai-table v-else ref="PendingTable" :tableData="tableData" class="fill" border height="330px" @getList="getTableData" tableSize="mini" | ||||
|                 :col-configs="[{slot: 'resident'}]" :isShowPagination="false"> | ||||
|         <el-table-column slot="resident"> | ||||
|           <template #header> | ||||
|             <b v-text="tableTitle"/> | ||||
|             <el-input class="fill searchbar" v-model="search[searchKey]" size="small" placeholder="搜索" clearable | ||||
|                       @change="page.current=1,getTableData()"/> | ||||
|           </template> | ||||
|           <template slot-scope="{row}"> | ||||
|             <slot name="pending" v-if="$scopedSlots.pending" :row="row"/> | ||||
|             <el-row v-else type="flex" justify="space-between" @click.native="handleSelect(row)" class="toggle" | ||||
|                     :class="{selected:findSelected(row)>-1}"> | ||||
|               <span v-text="row[nodeName]"/> | ||||
|               <slot name="extra" v-if="$scopedSlots.extra" :row="row"/> | ||||
|               <span v-else v-text="getExtra(row)"/> | ||||
|             </el-row> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </ai-table> | ||||
|       <ai-table :tableData="selected" :col-configs="[{slot:'resident'}]" :isShowPagination="false" border | ||||
|                 height="330px" tableSize="mini" class="el-table--scrollable-y"> | ||||
|         <el-table-column slot="resident"> | ||||
|           <template #header> | ||||
|             <b v-text="`已选择`"/> | ||||
|             <el-button type="text" @click="selected=[]">清空</el-button> | ||||
|           </template> | ||||
|           <template slot-scope="{row,$index}"> | ||||
|             <slot name="selected" v-if="$scopedSlots.selected" :row="row" :index="$index"/> | ||||
|             <el-row v-else type="flex" align="middle" justify="space-between"> | ||||
|               <div v-text="[row[nodeName], getExtra(row)].join(' ')"/> | ||||
|               <el-button type="text" @click="selected.splice($index,1)">删除</el-button> | ||||
|             </el-row> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|       </ai-table> | ||||
|     </el-row> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| /** | ||||
|  * 智能列表选择器 | ||||
|  * @displayName AiTableSelect | ||||
|  */ | ||||
| export default { | ||||
|   name: "AiTableSelect", | ||||
|   model: { | ||||
|     event: "change", | ||||
|     prop: "value" | ||||
|   }, | ||||
|   props: { | ||||
|     /** | ||||
|      * 接口方法类:必填 | ||||
|      */ | ||||
|     instance: {type: Function, required: true}, | ||||
|     /** | ||||
|      * 接口方法地址:必填 | ||||
|      */ | ||||
|     action: {default: "", required: true}, | ||||
|     /** | ||||
|      * 选择表格标题 | ||||
|      */ | ||||
|     tableTitle: {default: "选择列表"}, | ||||
|     /** | ||||
|      * 选择项 | ||||
|      * @model | ||||
|      */ | ||||
|     value: {default: ""}, | ||||
|     /** | ||||
|      * 选择项绑定key | ||||
|      */ | ||||
|     nodeKey: {default: "id"}, | ||||
|     /** | ||||
|      * 选择项绑定展示值:必填 | ||||
|      */ | ||||
|     nodeName: {default: "name"}, | ||||
|     /** | ||||
|      * 是否多选 | ||||
|      */ | ||||
|     multiple: Boolean, | ||||
|     /** | ||||
|      * 返回值为选择的对象而不是id | ||||
|      */ | ||||
|     valueObj: Boolean, | ||||
|     /** | ||||
|      * 扩展字段,用于右侧展示 | ||||
|      */ | ||||
|     extra: {default: ""}, | ||||
|     /** | ||||
|      * 搜索字段,用于搜索框 | ||||
|      */ | ||||
|     searchKey: {default: "con"}, | ||||
|     /** | ||||
|      * 是否分页 | ||||
|      */ | ||||
|     isShowPagination: {default: true} | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       page: {total: 0, current: 1, size: 10}, | ||||
|       search: {}, | ||||
|       tableData: [], | ||||
|       selected: [], | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     action(v) { | ||||
|       v && (this.page.current = 1, this.getTableData()) | ||||
|     }, | ||||
|     selected: { | ||||
|       deep: true, handler(v) { | ||||
|         let ids = v.map(e => e[this.nodeKey] || e).filter(e => !!e) | ||||
|         this.$emit("change", this.valueObj ? v : this.multiple ? ids : ids?.toString()) | ||||
|         this.$emit("select", v) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getExtra(row) { | ||||
|       let {extra} = this | ||||
|       return extra ? row[extra] : this.idCardNoUtil.hideId(row.idNumber) | ||||
|     }, | ||||
|     initValue() { | ||||
|       let unwatch = this.$watch('value', (v) => { | ||||
|         if (this.selected.length > 0) unwatch && unwatch() | ||||
|         else if (!!v) { | ||||
|           this.selected = this.multiple ? [v].flat().filter(e => !!e) || [] : (v?.split(",") || []) | ||||
|           unwatch && unwatch() | ||||
|         } | ||||
|       }, {immediate: true}) | ||||
|     }, | ||||
|     getTableData() { | ||||
|       let {page, search, action} = this | ||||
|       this.instance?.post(action, null, { | ||||
|         params: {...page, ...search} | ||||
|       }).then(res => { | ||||
|         if (res?.data) { | ||||
|           this.tableData = res.data.records || res.data || [] | ||||
|           this.isShowPagination && (this.page.total = res.data.total) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     handleSelect(row) { | ||||
|       let index = this.findSelected(row) | ||||
|       if (index > -1) { | ||||
|         this.selected.splice(index, 1) | ||||
|       } else { | ||||
|         if (this.multiple) { | ||||
|           this.selected.push(row) | ||||
|         } else { | ||||
|           this.selected = [row] | ||||
|         } | ||||
|       } | ||||
|       this.$forceUpdate() | ||||
|     }, | ||||
|     findSelected(item) { | ||||
|       let {nodeKey} = this | ||||
|       return this.selected?.findIndex(e => e[nodeKey] == item[nodeKey]) | ||||
|     }, | ||||
|   }, | ||||
|   created() { | ||||
|     this.$set(this.search, this.searchKey, "") | ||||
|     this.initValue() | ||||
|     this.getTableData() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| ::v-deep.AiTableSelect { | ||||
|   .el-row { | ||||
|     width: 100%; | ||||
|  | ||||
|     .ai-table + .ai-table { | ||||
|       margin-left: 16px; | ||||
|       width: 400px; | ||||
|  | ||||
|       .ai-table__header > .cell { | ||||
|         line-height: 40px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .toggle { | ||||
|     cursor: pointer; | ||||
|  | ||||
|     &.selected { | ||||
|       color: $primaryColor; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ai-table__header { | ||||
|     padding: 0 !important; | ||||
|  | ||||
|     & > .cell { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .searchbar { | ||||
|     padding-right: 0; | ||||
|   } | ||||
|  | ||||
|   .newPagination { | ||||
|     height: 32px; | ||||
|     padding: 0 !important; | ||||
|     margin-top: 8px; | ||||
|   } | ||||
|  | ||||
|   .ai-table__cell { | ||||
|     .el-button--text { | ||||
|       padding: 0 8px; | ||||
|       height: 24px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										163
									
								
								ui/packages/basic/AiTitle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								ui/packages/basic/AiTitle.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| <template> | ||||
|   <section class="AiTitle" :class="{ 'bottomBorder': isShowBottomBorder, AiTitleSub: isHasSub}"> | ||||
|     <i class="iconfont iconBack_Large" v-if="isShowBack" @click="onBackBtnClick"/> | ||||
|     <div class="fill"> | ||||
|       <div class="ailist-title"> | ||||
|         <div class="ailist-title__left"> | ||||
|           <h2>{{ title }}</h2> | ||||
|           <div v-if="isShowIM" class="openIM iconfont iconGroup_IM" @click="openIM"></div> | ||||
|         </div> | ||||
|         <div class="ailist-title__right"> | ||||
|           <ai-area | ||||
|               v-if="isShowArea" | ||||
|               :instance="instance" | ||||
|               v-bind="$attrs" | ||||
|               :value="value" | ||||
|               v-on="$listeners" | ||||
|               :areaLevel="areaLevel" | ||||
|               :hideLevel="hideLevel" | ||||
|               :valueLevel="valueLevel" | ||||
|               :disabled="disabled"/> | ||||
|           <div class="aititle-right__btns"> | ||||
|             <slot name="rightBtn"></slot> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="subtitle" v-if="$scopedSlots.sub"> | ||||
|         <slot name="sub"/> | ||||
|       </div> | ||||
|     </div> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AiTitle', | ||||
|   model: { | ||||
|     prop: 'value', | ||||
|     event: 'change' | ||||
|   }, | ||||
|   props: { | ||||
|     title: { | ||||
|       type: String, | ||||
|       required: true | ||||
|     }, | ||||
|     instance: { | ||||
|       type: Function | ||||
|     }, | ||||
|     areaLevel: [String, Number], | ||||
|     hideLevel: {default: 2}, | ||||
|     valueLevel: [String, Number], | ||||
|     disabled: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     value: { | ||||
|       type: String | ||||
|     }, | ||||
|     isShowIM: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     isShowArea: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     openIM: { | ||||
|       type: Function | ||||
|     }, | ||||
|     isShowBack: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     isShowBottomBorder: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     classic: Boolean | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     isHasSub() { | ||||
|       return this.$slots.sub | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     onBackBtnClick() { | ||||
|       this.$emit('onBackClick') | ||||
|       this.$emit('back') | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .AiTitle { | ||||
|   display: flex; | ||||
|  | ||||
|   .ailist-title { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: space-between; | ||||
|     height: 48px; | ||||
|  | ||||
|     .ailist-title__left { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|  | ||||
|       & > i { | ||||
|         width: auto; | ||||
|         height: auto; | ||||
|         margin-right: 8px; | ||||
|         font-size: 16px; | ||||
|       } | ||||
|  | ||||
|       h2 { | ||||
|         color: #222; | ||||
|         font-size: 16px; | ||||
|         font-weight: 600; | ||||
|       } | ||||
|  | ||||
|       .openIM { | ||||
|         margin-left: 8px; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .ailist-title__right { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|     } | ||||
|  | ||||
|     ::v-deep.el-button { | ||||
|       margin-left: 8px !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.AiTitleSub { | ||||
|     height: auto; | ||||
|     padding: 16px 0; | ||||
|  | ||||
|     .ailist-title { | ||||
|       height: auto; | ||||
|       margin-bottom: 3px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.bottomBorder { | ||||
|     border-bottom: 1px solid #D8DCE3; | ||||
|   } | ||||
|  | ||||
|   .subtitle { | ||||
|     width: 100%; | ||||
|     color: #888888; | ||||
|     font-size: 12px; | ||||
|     margin-top: 4px; | ||||
|   } | ||||
|  | ||||
|   .iconBack_Large { | ||||
|     line-height: 48px; | ||||
|     margin-right: 8px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										44
									
								
								ui/packages/basic/AiTransSpeech.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								ui/packages/basic/AiTransSpeech.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <template> | ||||
|   <el-row type="flex" align="middle"> | ||||
|     <el-button type="text" size="small" :loading="loading" icon="iconfont iconClock" title="语音播报" | ||||
|                @click="getSpeechByContent"/> | ||||
|     <ai-audio v-if="speech" :src="speech" skin="flat"/> | ||||
|   </el-row> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: "AiTransSpeech", | ||||
|   props: { | ||||
|     instance: {type: Function, required: true}, | ||||
|     content: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       speech: "", | ||||
|       loading: false | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getSpeechByContent() { | ||||
|       this.loading = true | ||||
|       this.instance.post("/app/msc/transToSpeech", null, { | ||||
|         params: { | ||||
|           fileName: "demo", | ||||
|           words: this.content | ||||
|         } | ||||
|       }).then(res => { | ||||
|         this.loading = false | ||||
|         if (res && res.data) { | ||||
|           let url = res.data.join("") | ||||
|           this.speech = url.substring(0, url.indexOf(";")) | ||||
|         } | ||||
|       }).catch(() => this.loading = false) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|  | ||||
| </style> | ||||
							
								
								
									
										514
									
								
								ui/packages/basic/AiUploader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								ui/packages/basic/AiUploader.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,514 @@ | ||||
| <template> | ||||
|   <section class="uploader"> | ||||
|     <el-upload | ||||
|         action | ||||
|         multiple | ||||
|         ref="upload" | ||||
|         :class="{validError:!validateState}" | ||||
|         :http-request="submitUpload" | ||||
|         :on-remove="handleRemove" | ||||
|         :on-change="handleChange" | ||||
|         :before-upload="onBeforeUpload" | ||||
|         :file-list="fileList" | ||||
|         :limit="limit" | ||||
|         :disabled="disabled" | ||||
|         :list-type="isImg ? 'picture-card' : 'text'" | ||||
|         :accept="accept" | ||||
|         :show-file-list="!isSingle" | ||||
|         :on-preview="handlePictureCardPreview" | ||||
|         :auto-upload="isAutoUpload" | ||||
|         :on-exceed="handleExceed"> | ||||
|       <template v-if="!disabled"> | ||||
|         <template v-if="hasUploaded&&isSingle"> | ||||
|           <div class="fileItem"> | ||||
|             <div class="uploadFile" @click.stop> | ||||
|               <ai-icon type="svg" :icon="uploadFile.icon"/> | ||||
|               <div class="info"> | ||||
|                 <span v-text="uploadFile.name"/> | ||||
|                 <span class="size" v-text="uploadFile.size"/> | ||||
|               </div> | ||||
|             </div> | ||||
|             <el-button>重新选择</el-button> | ||||
|             <el-button v-if="clearable" plain type="danger" @click.stop="handleClear">删除</el-button> | ||||
|           </div> | ||||
|         </template> | ||||
|         <template v-else-if="limit > fileList.length"> | ||||
|           <slot v-if="hasTriggerSlot" name="trigger"/> | ||||
|           <div v-else class="uploaderBox"> | ||||
|             <span class="iconfont" :class="isImg ? 'iconPhoto' : 'iconAdd'"/> | ||||
|             <p>上传{{ isImg ? '图片' : '附件' }}</p> | ||||
|           </div> | ||||
|         </template> | ||||
|         <div slot="tip" class="el-upload__tip" v-if="showTips"> | ||||
|           <p v-if="fileType === 'img' && !acceptType && !hasTipsSlot">最多上传{{ | ||||
|               limit | ||||
|             }}张图片,单个文件最大10MB,支持jpg、jpeg、png格式</p> | ||||
|           <p v-if="fileType === 'file' && !acceptType && !hasTipsSlot">最多上传{{ limit }}个附件,单个文件最大10MB</p> | ||||
|           <p v-if="fileType === 'file' && !acceptType && !hasTipsSlot"> | ||||
|             支持.zip、.rar、.doc、.docx、.xls、.xlsx、.ppt、.pptx、.pdf、.txt、.jpg、.png格式</p> | ||||
|           <p> | ||||
|             <slot name="tips" v-if="hasTipsSlot"></slot> | ||||
|           </p> | ||||
|         </div> | ||||
|       </template> | ||||
|     </el-upload> | ||||
|     <el-dialog :visible.sync="dialog" title="图片预览编辑器" :modal="false" :show-close="false" append-to-body> | ||||
|       <vue-cropper | ||||
|           ref="cropper" | ||||
|           style="height: 400px;" | ||||
|           :img="fileList.length ? fileList[0].url : ''" v-bind="crop"/> | ||||
|       <div style="text-align: center;margin-top: 10px;"> | ||||
|         <el-radio-group v-if="crop.fixed" size="small" v-model="currFixedIndex" @change="onFixedChange" | ||||
|                         style="margin-right: 8px;"> | ||||
|           <el-radio-button | ||||
|               :label="0" | ||||
|               size="small"> | ||||
|             1.6:1 | ||||
|           </el-radio-button> | ||||
|           <el-radio-button | ||||
|               :label="1" | ||||
|               size="small"> | ||||
|             4:3 | ||||
|           </el-radio-button> | ||||
|         </el-radio-group> | ||||
|         <el-button size="small" circle icon="el-icon-refresh-right" @click="$refs.cropper.rotateRight()"></el-button> | ||||
|         <el-button size="small" circle icon="el-icon-refresh-left" @click="$refs.cropper.rotateLeft()"></el-button> | ||||
|       </div> | ||||
|       <div slot="footer"> | ||||
|         <el-popconfirm title="是否关闭图片预览器,并上传图片?" @confirm="previewCrop"> | ||||
|           <el-button slot="reference" type="primary">保存</el-button> | ||||
|         </el-popconfirm> | ||||
|         <el-button @click="onClose">关闭</el-button> | ||||
|       </div> | ||||
|     </el-dialog> | ||||
|     <div class="images" v-viewer="{movable: true}" v-show="false"> | ||||
|       <img v-for="(item, index) in imgList" :src="item" :key="index" alt=""> | ||||
|     </div> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import {VueCropper} from 'vue-cropper' | ||||
| import 'viewerjs/dist/viewer.css' | ||||
| import Viewer from 'v-viewer' | ||||
| import Vue from "vue"; | ||||
|  | ||||
| Viewer.setDefaults({ | ||||
|   zIndex: 20170 | ||||
| }) | ||||
| Vue.use(Viewer) | ||||
| export default { | ||||
|   name: 'AiUploader', | ||||
|   components: {VueCropper}, | ||||
|   inject: { | ||||
|     elFormItem: {default: ""}, | ||||
|     elForm: {default: ''}, | ||||
|   }, | ||||
|   model: { | ||||
|     prop: 'value', | ||||
|     event: 'change' | ||||
|   }, | ||||
|   props: { | ||||
|     value: {default: () => []}, | ||||
|     url: { | ||||
|       type: String, | ||||
|       default: '/admin/file/add' | ||||
|     }, | ||||
|     isShowTip: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     isWechat: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     maxSize: { | ||||
|       type: Number, | ||||
|       default: 10 | ||||
|     }, | ||||
|     instance: Function, | ||||
|     acceptType: {type: String}, | ||||
|     fileType: {type: String, default: 'img'}, | ||||
|     limit: {type: Number, default: 9}, | ||||
|     disabled: {type: Boolean, default: false}, | ||||
|     isCrop: {type: Boolean, default: false}, | ||||
|     cropOps: Object, | ||||
|     isImport: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     clearable: {default: true}, | ||||
|     valueIsUrl: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       fileList: [], | ||||
|       dialog: false, | ||||
|       currFixedIndex: 0, | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     value: { | ||||
|       handler(v) { | ||||
|         this.dispatch('ElFormItem', 'el.form.change', [v]); | ||||
|         if (v?.length > 0) { | ||||
|           this.fileList = this.valueIsUrl ? v?.split(",")?.map(url => ({url})) : [...v] | ||||
|         } | ||||
|       }, | ||||
|       immediate: true, | ||||
|       deep: true | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     isImg() { | ||||
|       return this.fileType === 'img' | ||||
|     }, | ||||
|     validateState() { | ||||
|       return ['', 'success'].includes(this.elFormItem?.validateState) | ||||
|     }, | ||||
|     isAutoUpload() { | ||||
|       return !(this.isCrop || this.isImport); | ||||
|     }, | ||||
|     hasTipsSlot() { | ||||
|       return this.$slots.tips | ||||
|     }, | ||||
|     hasTriggerSlot() { | ||||
|       return this.$slots.trigger | ||||
|     }, | ||||
|     accept() { | ||||
|       if (this.acceptType) { | ||||
|         return this.acceptType | ||||
|       } | ||||
|  | ||||
|       return this.isImg ? '.jpg,.png,.jpeg' : '.zip,.rar,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.jpg,.png,.mp4' | ||||
|     }, | ||||
|     crop() { | ||||
|       return { | ||||
|         autoCrop: true, | ||||
|         outputType: 'png', | ||||
|         fixedBox: false, | ||||
|         fixed: true, | ||||
|         fixedNumber: [1.6, 1], | ||||
|         width: 0, | ||||
|         height: 0, | ||||
|         ...this.cropOps | ||||
|       } | ||||
|     }, | ||||
|     imgList() { | ||||
|       return this.fileList.map(v => v.url) | ||||
|     }, | ||||
|     isSingle() { | ||||
|       return this.limit == 1 && this.isImport | ||||
|     }, | ||||
|     showTips() { | ||||
|       return this.isShowTip || this.$slots.tips | ||||
|     }, | ||||
|     hasUploaded() { | ||||
|       return this.fileList?.length > 0 | ||||
|     }, | ||||
|     uploadFile() { | ||||
|       let file = this.fileList?.[0], | ||||
|           size = Number(file.size), | ||||
|           icon = "iconTxt" | ||||
|       //显示大小 | ||||
|       if (size > Math.pow(1024, 2)) { | ||||
|         size = (size / Math.pow(1024, 2)).toFixed(1) + 'MB' | ||||
|       } else { | ||||
|         size = (size / 1024).toFixed(1) + 'KB' | ||||
|       } | ||||
|       //显示图标 | ||||
|       if (/\.(xls|xlsx)$/.test(file.name)) { | ||||
|         icon = "iconExcel" | ||||
|       } else if (/\.(zip)$/.test(file.name)) { | ||||
|         icon = "iconZip" | ||||
|       } else if (/\.(rar)$/.test(file.name)) { | ||||
|         icon = "iconRar" | ||||
|       } else if (/\.(png)$/.test(file.name)) { | ||||
|         icon = "iconPng" | ||||
|       } else if (/\.(pptx|ppt)$/.test(file.name)) { | ||||
|         icon = "iconPPT" | ||||
|       } else if (/\.(doc|docx)$/.test(file.name)) { | ||||
|         icon = "iconWord" | ||||
|       } | ||||
|  | ||||
|       return {...file, size, icon} | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     onFixedChange(e) { | ||||
|       this.fixedNumber = e === 0 ? [1.6, 1] : [4, 3] | ||||
|  | ||||
|       this.$nextTick(() => { | ||||
|         this.$refs.cropper.goAutoCrop() | ||||
|       }) | ||||
|     }, | ||||
|     handleChange(file, fileList) { | ||||
|       if (this.isImport) { | ||||
|         if (!this.onOverSize(file)) { | ||||
|           this.fileList = [] | ||||
|           return false | ||||
|         } | ||||
|         this.fileList = fileList | ||||
|         this.emitChange(fileList) | ||||
|         return false | ||||
|       } | ||||
|       if (this.isCrop) { | ||||
|         if (file.raw.type === 'image/gif') { | ||||
|           this.$message.error(`不支持gif格式的图片`) | ||||
|           this.fileList = [] | ||||
|  | ||||
|           return false | ||||
|         } | ||||
|  | ||||
|         if (!this.onOverSize(file)) { | ||||
|           this.fileList = [] | ||||
|  | ||||
|           return false | ||||
|         } | ||||
|         this.dialog = true | ||||
|         this.fileList = fileList | ||||
|       } else { | ||||
|         this.fileList = fileList | ||||
|       } | ||||
|     }, | ||||
|     handleExceed(files) { | ||||
|       if (this.isSingle && files[0]) { | ||||
|         this.$refs.upload?.clearFiles() | ||||
|         this.$refs.upload?.handleStart(files[0]) | ||||
|       } else this.$message.warning(`最多上传${this.limit}个${this.isImg ? '图片' : '文件'}`) | ||||
|     }, | ||||
|     handlePictureCardPreview(file) { | ||||
|       if (this.fileType !== 'img') return | ||||
|  | ||||
|       const index = this.imgList.indexOf(file.url) | ||||
|       const viewer = this.$el.querySelector('.images').$viewer | ||||
|       viewer.view(index) | ||||
|     }, | ||||
|  | ||||
|     handleRemove(file, fileList) { | ||||
|       this.fileList = fileList | ||||
|       this.emitChange(fileList) | ||||
|     }, | ||||
|     emitChange(files) { | ||||
|       this.$emit('change', this.valueIsUrl ? files?.map(e => e.url)?.toString() : files) | ||||
|     }, | ||||
|     handleClear() { | ||||
|       this.fileList = [] | ||||
|     }, | ||||
|     getExtension(name) { | ||||
|       return name.substring(name.lastIndexOf('.')) | ||||
|     }, | ||||
|  | ||||
|     onOverSize(e) { | ||||
|       const isLt10M = e.size / 1024 / 1024 < this.maxSize | ||||
|       const suffixName = this.getExtension(e.name) | ||||
|       const suffixNameList = this.accept.split(',') | ||||
|  | ||||
|       if (suffixNameList.indexOf(`${suffixName.toLowerCase()}`) === -1) { | ||||
|         this.$message.error(`不支持该格式`) | ||||
|         return false | ||||
|       } | ||||
|  | ||||
|       if (!isLt10M) { | ||||
|         this.$message.error(`${this.isImg ? '图片' : '文件'}大小不超过${this.maxSize}MB!`) | ||||
|         return false | ||||
|       } | ||||
|  | ||||
|       return true | ||||
|     }, | ||||
|  | ||||
|     onBeforeUpload(event) { | ||||
|       return this.onOverSize(event) | ||||
|     }, | ||||
|  | ||||
|     onClose() { | ||||
|       this.fileList = [] | ||||
|       this.dialog = false | ||||
|     }, | ||||
|  | ||||
|     submitUpload(file) { | ||||
|       let formData = new FormData() | ||||
|       formData.append('file', file.file) | ||||
|       this.instance.post(this.url, formData, { | ||||
|         withCredentials: false | ||||
|       }).then(res => { | ||||
|         if (res?.code == 0) { | ||||
|           if (this.isWechat) { | ||||
|             this.emitChange([{ | ||||
|               ...res.data.file, | ||||
|               media: res.data.media | ||||
|             }]) | ||||
|             this.fileList.forEach(item => { | ||||
|               if (item.uid === file.file.uid) { | ||||
|                 item.id = res.data.file.id | ||||
|                 item.path = res.data.file.url | ||||
|                 item.url = res.data.file.url, | ||||
|                     item.media = res.data.media | ||||
|               } | ||||
|             }) | ||||
|             this.emitChange(this.fileList) | ||||
|             this.$message.success('上传成功') | ||||
|             return false | ||||
|           } | ||||
|           let data = res.data[0].split(';') | ||||
|           this.fileList.forEach(item => { | ||||
|             if (item.uid === file.file.uid) { | ||||
|               item.id = data[1] | ||||
|               item.path = data[0] | ||||
|               item.url = data[0] | ||||
|             } | ||||
|           }) | ||||
|           this.emitChange(this.fileList) | ||||
|           this.$message.success('上传成功') | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     previewCrop() { | ||||
|       this.$refs.cropper.getCropBlob(data => { | ||||
|         data.name = this.fileList[0].name; | ||||
|         this.fileList[0].file = new window.File([data], data.name, {type: data.type}) | ||||
|         this.fileList[0].file.uid = this.fileList[0].uid | ||||
|         this.submitUpload(this.fileList[0]) | ||||
|         this.dialog = false | ||||
|       }) | ||||
|     }, | ||||
|     /** | ||||
|      * 表单验证 | ||||
|      * @param componentName | ||||
|      * @param eventName | ||||
|      * @param params | ||||
|      */ | ||||
|     dispatch(componentName, eventName, params) { | ||||
|       let parent = this.$parent || this.$root; | ||||
|       let name = parent.$options.componentName; | ||||
|  | ||||
|       while (parent && (!name || name !== componentName)) { | ||||
|         parent = parent.$parent; | ||||
|  | ||||
|         if (parent) { | ||||
|           name = parent.$options.componentName; | ||||
|         } | ||||
|       } | ||||
|       if (parent) { | ||||
|         parent.$emit.apply(parent, [eventName].concat(params)); | ||||
|       } | ||||
|     }, | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .uploader { | ||||
|   line-height: 1; | ||||
|  | ||||
|   ::v-deep.el-upload { | ||||
|     width: 100%; | ||||
|     text-align: start; | ||||
|   } | ||||
|  | ||||
|   ::v-deep.validError { | ||||
|     .el-button { | ||||
|       border-color: #f46; | ||||
|       color: #f46; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ::v-deep.el-upload--picture-card { | ||||
|     border: none; | ||||
|   } | ||||
|  | ||||
|   ::v-deep .el-list-leave-active, ::v-deep .el-upload-list__item { | ||||
|     transition: all 0s !important; | ||||
|   } | ||||
|  | ||||
|   ::v-deep.el-upload-list--picture-card .el-upload-list__item { | ||||
|     width: 120px; | ||||
|     height: 120px; | ||||
|     border-radius: 4px; | ||||
|   } | ||||
|  | ||||
|   ::v-deep.el-upload--picture-card { | ||||
|     width: auto; | ||||
|     height: auto; | ||||
|   } | ||||
|  | ||||
|   .el-upload__tip p { | ||||
|     color: #999; | ||||
|     line-height: 16px; | ||||
|   } | ||||
|  | ||||
|   ::v-deep.fileItem { | ||||
|     width: 100%; | ||||
|     height: 60px; | ||||
|     background: #FFFFFF; | ||||
|     border-radius: 2px; | ||||
|     border: 1px solid #D0D4DC; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     padding: 0 16px; | ||||
|     cursor: default; | ||||
|  | ||||
|     .uploadFile { | ||||
|       text-align: start; | ||||
|       flex: 1; | ||||
|       min-width: 0; | ||||
|       display: flex; | ||||
|       color: #222; | ||||
|  | ||||
|       .AiIcon { | ||||
|         width: 40px; | ||||
|         height: 40px; | ||||
|       } | ||||
|  | ||||
|       .info { | ||||
|         flex: 1; | ||||
|         min-width: 0; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         margin-left: 8px; | ||||
|         margin-right: 40px; | ||||
|         line-height: 22px; | ||||
|       } | ||||
|  | ||||
|       .size { | ||||
|         color: #888; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .uploaderBox { | ||||
|     width: 120px; | ||||
|     height: 120px; | ||||
|     line-height: 1; | ||||
|     background: #F3F4F7; | ||||
|     border-radius: 2px; | ||||
|     border: 1px solid #D0D4DC; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|  | ||||
|     &:hover { | ||||
|       opacity: 0.6; | ||||
|     } | ||||
|  | ||||
|     span { | ||||
|       font-size: 32px; | ||||
|       color: #8899bb; | ||||
|  | ||||
|       &:hover { | ||||
|         color: #8899bb; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     p { | ||||
|       margin: 0; | ||||
|       padding-top: 4px; | ||||
|       color: #555; | ||||
|       font-size: 12px; | ||||
|       text-align: center; | ||||
|       line-height: 1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										86
									
								
								ui/packages/basic/AiWrapper.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								ui/packages/basic/AiWrapper.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| <template> | ||||
|   <div class="ai-wrapper"> | ||||
|     <ai-title class="w100" v-if="title" :title="title"/> | ||||
|     <div class="ai-wrapper-content" :class="{border}"> | ||||
|       <slot/> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import AiTitle from "./AiTitle"; | ||||
|  | ||||
| export default { | ||||
|   name: 'AiWrapper', | ||||
|   components: {AiTitle}, | ||||
|   componentName: 'AiWrapper', | ||||
|  | ||||
|   provide() { | ||||
|     return { | ||||
|       AiWrapper: this | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   props: { | ||||
|     'label-width': { | ||||
|       type: String | ||||
|     }, | ||||
|  | ||||
|     columnsNumber: { | ||||
|       type: Number, | ||||
|       default: 2 | ||||
|     }, | ||||
|     border: Boolean, | ||||
|     title: String | ||||
|   }, | ||||
|  | ||||
|   computed: { | ||||
|     autoWidth() { | ||||
|       return ((1 / this.columnsNumber) * 100).toFixed(2) + '%' | ||||
|     }, | ||||
|  | ||||
|     autoLableWidth() { | ||||
|       return this.labelWidth | ||||
|     } | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .ai-wrapper { | ||||
|   .w100 { | ||||
|     width: 100%; | ||||
|   } | ||||
|  | ||||
|   .ai-wrapper-content { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     justify-content: space-between; | ||||
|  | ||||
|     &.border { | ||||
|       border-left: 1px solid $borderColor; | ||||
|       border-top: 1px solid $borderColor; | ||||
|  | ||||
|       .ai-info-item { | ||||
|         border-bottom: 1px solid $borderColor; | ||||
|         border-right: 1px solid $borderColor; | ||||
|         margin-bottom: 0; | ||||
|         line-height: 32px; | ||||
|  | ||||
|         .ai-info-item__left { | ||||
|           background: rgba(0, 0, 0, .03); | ||||
|           padding-right: 16px; | ||||
|           border-right: 1px solid $borderColor; | ||||
|           margin-right: 16px; | ||||
|         } | ||||
|  | ||||
|         .el-textarea__inner { | ||||
|           border: none; | ||||
|           padding-left: 0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| </style> | ||||
		Reference in New Issue
	
	Block a user