first
This commit is contained in:
85
src/components/baInput/components/array.vue
Normal file
85
src/components/baInput/components/array.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10" class="ba-array-key">{{ state.keyTitle }}</el-col>
|
||||
<el-col :span="10" class="ba-array-value">{{ state.valueTitle }}</el-col>
|
||||
</el-row>
|
||||
<el-row class="ba-array-item" v-for="(item, idx) in state.value" :gutter="10" :key="idx">
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.key"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.value"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button @click="onDelArrayItem(idx)" size="small" icon="el-icon-Delete" circle />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10" :offset="10">
|
||||
<el-button v-blur class="ba-add-array-item" @click="onAddArrayItem" icon="el-icon-Plus">{{ t('Add') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
type baInputArray = { key: string; value: string }
|
||||
interface Props {
|
||||
modelValue: baInputArray[]
|
||||
keyTitle?: string
|
||||
valueTitle?: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: () => [],
|
||||
keyTitle: '',
|
||||
valueTitle: '',
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
value: props.modelValue,
|
||||
keyTitle: props.keyTitle ? props.keyTitle : t('utils.ArrayKey'),
|
||||
valueTitle: props.valueTitle ? props.valueTitle : t('utils.ArrayValue'),
|
||||
})
|
||||
|
||||
const onAddArrayItem = () => {
|
||||
state.value.push({
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
|
||||
const onDelArrayItem = (idx: number) => {
|
||||
state.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
state.value = newVal
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-array-key,
|
||||
.ba-array-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.ba-array-item {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.ba-add-array-item {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
39
src/components/baInput/components/editor.vue
Normal file
39
src/components/baInput/components/editor.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- 多编辑器共存支持 -->
|
||||
<!-- 所有编辑器的代码位于 @/components/mixins/editor 文件夹,一个文件为一种编辑器,文件名则为编辑器名称 -->
|
||||
<!-- 向本组件传递 editorType(文件名/编辑器名称)自动加载对应的编辑器进行渲染 -->
|
||||
<template>
|
||||
<div>
|
||||
<component v-bind="$attrs" :is="mixins[state.editorType]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
interface Props {
|
||||
editorType?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
editorType: 'default',
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
editorType: props.editorType,
|
||||
})
|
||||
|
||||
const mixins: Record<string, Component> = {}
|
||||
const mixinComponents: Record<string, any> = import.meta.glob('../../mixins/editor/**.vue', { eager: true })
|
||||
for (const key in mixinComponents) {
|
||||
const fileName = key.replace('../../mixins/editor/', '').replace('.vue', '')
|
||||
mixins[fileName] = mixinComponents[key].default
|
||||
|
||||
// 未安装富文本编辑器时,值为 default,安装之后,则值为最后一个编辑器的名称
|
||||
if (props.editorType == 'default' && fileName != 'default') {
|
||||
state.editorType = fileName
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
310
src/components/baInput/components/iconSelector.vue
Normal file
310
src/components/baInput/components/iconSelector.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<el-popover
|
||||
:placement='placement'
|
||||
trigger='focus'
|
||||
:hide-after='0'
|
||||
:width='state.selectorWidth'
|
||||
:visible='state.popoverVisible'
|
||||
>
|
||||
<div
|
||||
@mouseover.stop='state.iconSelectorMouseover = true'
|
||||
@mouseout.stop='state.iconSelectorMouseover = false'
|
||||
class='icon-selector'
|
||||
>
|
||||
<transition name='el-zoom-in-center'>
|
||||
<div class='icon-selector-box'>
|
||||
<div class='selector-header'>
|
||||
<div class='selector-title'>{{ title ? title : '请选择图标' }}</div>
|
||||
<div class='selector-tab'>
|
||||
<span
|
||||
:title="'Element Puls ' + 'utils.Icon'"
|
||||
@click="onChangeTab('ele')"
|
||||
:class="state.iconType == 'ele' ? 'active' : ''"
|
||||
>ele</span>
|
||||
<span
|
||||
:title="'Font Awesome ' + 'utils.Icon'"
|
||||
@click="onChangeTab('awe')"
|
||||
:class="state.iconType == 'awe' ? 'active' : ''"
|
||||
>awe</span>
|
||||
<!-- <span :title="'utils.Ali iconcont Icon'" @click="onChangeTab('ali')"-->
|
||||
<!-- :class="state.iconType == 'ali' ? 'active' : ''"-->
|
||||
<!-- >ali</span>-->
|
||||
<!-- <span-->
|
||||
<!-- :title="'utils.Local icon title'"-->
|
||||
<!-- @click="onChangeTab('local')"-->
|
||||
<!-- :class="state.iconType == 'local' ? 'active' : ''"-->
|
||||
<!-- >local</span-->
|
||||
<!-- >-->
|
||||
</div>
|
||||
</div>
|
||||
<div class='selector-body'>
|
||||
<el-scrollbar ref='selectorScrollbarRef'>
|
||||
<div v-if='renderFontIconNames.length > 0'>
|
||||
<div
|
||||
class='icon-selector-item'
|
||||
:title='item'
|
||||
@click='onIcon(item)'
|
||||
v-for='(item, key) in renderFontIconNames'
|
||||
:key='key'
|
||||
>
|
||||
<Icon :name='item' />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model='state.inputValue'
|
||||
:size='size'
|
||||
:disabled='disabled'
|
||||
placeholder='搜索图标'
|
||||
ref='selectorInput'
|
||||
@focus='onInputFocus'
|
||||
@blur='onInputBlur'
|
||||
:class="'size-' + size"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class='icon-prepend'>
|
||||
<Icon
|
||||
:key="'icon' + state.iconKey"
|
||||
:name='state.prependIcon ? state.prependIcon : state.defaultModelValue'
|
||||
/>
|
||||
<div v-if='showIconName' class='name'>
|
||||
{{ state.prependIcon ? state.prependIcon : state.defaultModelValue }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<Icon @click='onInputRefresh' name='el-icon-RefreshRight' />
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { reactive, ref, onMounted, nextTick, watch, computed } from 'vue'
|
||||
import { getElementPlusIconfontNames,getAwesomeIconfontNames } from '@/utils/iconfont'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import type { Placement } from 'element-plus'
|
||||
|
||||
type IconType = 'ele' | 'awe' | 'ali' | 'local'
|
||||
|
||||
interface Props {
|
||||
size?: 'default' | 'small' | 'large'
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
type?: IconType
|
||||
placement?: Placement
|
||||
modelValue?: string
|
||||
showIconName?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'default',
|
||||
disabled: false,
|
||||
title: '',
|
||||
type: 'ele',
|
||||
placement: 'bottom',
|
||||
modelValue: '',
|
||||
showIconName: false
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'change', value: string): void
|
||||
}>()
|
||||
|
||||
const selectorInput = ref()
|
||||
const selectorScrollbarRef = ref()
|
||||
const state: {
|
||||
iconType: IconType
|
||||
selectorWidth: number
|
||||
popoverVisible: boolean
|
||||
inputFocus: boolean
|
||||
iconSelectorMouseover: boolean
|
||||
fontIconNames: string[]
|
||||
inputValue: string
|
||||
prependIcon: string
|
||||
defaultModelValue: string
|
||||
iconKey: number
|
||||
} = reactive({
|
||||
iconType: props.type,
|
||||
selectorWidth: 0,
|
||||
popoverVisible: false,
|
||||
inputFocus: false,
|
||||
iconSelectorMouseover: false,
|
||||
fontIconNames: [],
|
||||
inputValue: '',
|
||||
prependIcon: props.modelValue,
|
||||
defaultModelValue: props.modelValue || 'fa fa-circle-o',
|
||||
iconKey: 0 // 给icon标签准备个key,以随时使用 h 函数重新生成元素
|
||||
})
|
||||
|
||||
const onInputFocus = () => {
|
||||
state.inputFocus = state.popoverVisible = true
|
||||
}
|
||||
const onInputBlur = () => {
|
||||
state.inputFocus = false
|
||||
state.popoverVisible = state.iconSelectorMouseover
|
||||
}
|
||||
const onInputRefresh = () => {
|
||||
state.iconKey++
|
||||
state.prependIcon = state.defaultModelValue
|
||||
state.inputValue = ''
|
||||
emits('update:modelValue', state.defaultModelValue)
|
||||
emits('change', state.defaultModelValue)
|
||||
}
|
||||
const onChangeTab = (name: IconType) => {
|
||||
state.iconType = name
|
||||
state.fontIconNames = []
|
||||
if (name == 'ele') {
|
||||
getElementPlusIconfontNames().then((res) => {
|
||||
state.fontIconNames = res
|
||||
})
|
||||
} else if (name == 'awe') {
|
||||
getAwesomeIconfontNames().then((res) => {
|
||||
state.fontIconNames = res.map((name) => `fa ${name}`)
|
||||
})
|
||||
}
|
||||
// else if (name == 'ali') {
|
||||
// getIconfontNames().then((res) => {
|
||||
// state.fontIconNames = res.map((name) => `iconfont ${name}`)
|
||||
// })
|
||||
// } else if (name == 'local') {
|
||||
// getLocalIconfontNames().then((res) => {
|
||||
// state.fontIconNames = res
|
||||
// })
|
||||
// }
|
||||
}
|
||||
const onIcon = (icon: string) => {
|
||||
state.iconSelectorMouseover = state.popoverVisible = false
|
||||
state.iconKey++
|
||||
state.prependIcon = icon
|
||||
state.inputValue = ''
|
||||
emits('update:modelValue', icon)
|
||||
emits('change', icon)
|
||||
nextTick(() => {
|
||||
selectorInput.value.blur()
|
||||
})
|
||||
}
|
||||
|
||||
const renderFontIconNames = computed(() => {
|
||||
if (!state.inputValue) return state.fontIconNames
|
||||
|
||||
let inputValue = state.inputValue.trim().toLowerCase()
|
||||
return state.fontIconNames.filter((icon: string) => {
|
||||
if (icon.toLowerCase().indexOf(inputValue) !== -1) {
|
||||
return icon
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 获取 input 的宽度
|
||||
const getInputWidth = () => {
|
||||
nextTick(() => {
|
||||
state.selectorWidth = selectorInput.value.$el.offsetWidth < 260 ? 260 : selectorInput.value.$el.offsetWidth
|
||||
})
|
||||
}
|
||||
|
||||
const popoverVisible = () => {
|
||||
state.popoverVisible = state.inputFocus || state.iconSelectorMouseover ? true : false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
state.iconKey++
|
||||
if (props.modelValue != state.prependIcon) state.defaultModelValue = props.modelValue
|
||||
if (props.modelValue == '') state.defaultModelValue = 'fa fa-circle-o'
|
||||
state.prependIcon = props.modelValue
|
||||
}
|
||||
)
|
||||
onMounted(() => {
|
||||
getInputWidth()
|
||||
useEventListener(document, 'click', popoverVisible)
|
||||
getElementPlusIconfontNames().then((res) => {
|
||||
state.fontIconNames = res
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang='scss'>
|
||||
.size-small {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.size-large {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.size-default {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.icon-prepend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.name {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.selector-tab {
|
||||
margin-left: auto;
|
||||
|
||||
span {
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selector-body {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.icon-selector-item {
|
||||
display: inline-block;
|
||||
padding: 10px 10px 6px 10px;
|
||||
margin: 3px;
|
||||
border: 1px solid var(--ba-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
|
||||
.icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input-group__prepend) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
||||
310
src/components/baInput/components/remoteSelect.vue
Normal file
310
src/components/baInput/components/remoteSelect.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="w100">
|
||||
<!-- el-select 的远程下拉只在有搜索词时,才会加载数据(显示出 option 列表) -->
|
||||
<!-- 使用 el-popover 在无数据/无搜索词时,显示一个无数据的提醒 -->
|
||||
<el-popover
|
||||
width="100%"
|
||||
placement="bottom"
|
||||
popper-class="remote-select-popper"
|
||||
:visible="state.focusStatus && !state.loading && !state.keyword && !state.options.length"
|
||||
:teleported="false"
|
||||
:content="$t('utils.No data')"
|
||||
>
|
||||
<template #reference>
|
||||
<el-select
|
||||
ref="selectRef"
|
||||
class="w100"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
:loading="state.loading || state.accidentBlur"
|
||||
:filterable="true"
|
||||
:remote="true"
|
||||
clearable
|
||||
remote-show-suffix
|
||||
:remote-method="onLogKeyword"
|
||||
v-model="state.value"
|
||||
@change="onChangeSelect"
|
||||
:multiple="multiple"
|
||||
:key="state.selectKey"
|
||||
@clear="onClear"
|
||||
@visible-change="onVisibleChange"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<el-option
|
||||
class="remote-select-option"
|
||||
v-for="item in state.options"
|
||||
:label="item[field]"
|
||||
:value="item[state.primaryKey].toString()"
|
||||
:key="item[state.primaryKey]"
|
||||
>
|
||||
<el-tooltip placement="right" effect="light" v-if="!isEmpty(tooltipParams)">
|
||||
<template #content>
|
||||
<p v-for="(tooltipParam, key) in tooltipParams" :key="key">{{ key }}: {{ item[tooltipParam] }}</p>
|
||||
</template>
|
||||
<div>{{ item[field] }}</div>
|
||||
</el-tooltip>
|
||||
</el-option>
|
||||
<el-pagination
|
||||
v-if="state.total"
|
||||
:currentPage="state.currentPage"
|
||||
:page-size="state.pageSize"
|
||||
class="select-pagination"
|
||||
layout="->, prev, next"
|
||||
:total="state.total"
|
||||
@current-change="onSelectCurrentPageChange"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, onMounted, onUnmounted, ref, nextTick, getCurrentInstance, toRaw } from 'vue'
|
||||
import { getSelectData } from '@/api/common'
|
||||
import { uuid } from '@/utils/random'
|
||||
import type { ElSelect } from 'element-plus'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { getArrayKey } from '@/utils/common'
|
||||
|
||||
const selectRef = ref<InstanceType<typeof ElSelect> | undefined>()
|
||||
type ElSelectProps = Partial<InstanceType<typeof ElSelect>['$props']>
|
||||
type valType = string | number | string[] | number[]
|
||||
|
||||
interface Props extends /* @vue-ignore */ ElSelectProps {
|
||||
pk?: string
|
||||
field?: string
|
||||
params?: anyObj
|
||||
multiple?: boolean
|
||||
remoteUrl: string
|
||||
modelValue: valType
|
||||
labelFormatter?: (optionData: anyObj, optionKey: string) => string
|
||||
tooltipParams?: anyObj
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
pk: 'id',
|
||||
field: 'name',
|
||||
params: () => {
|
||||
return {}
|
||||
},
|
||||
remoteUrl: '',
|
||||
modelValue: '',
|
||||
multiple: false,
|
||||
tooltipParams: () => {
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
const state: {
|
||||
// 主表字段名(不带表别名)
|
||||
primaryKey: string
|
||||
options: anyObj[]
|
||||
loading: boolean
|
||||
total: number
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
params: anyObj
|
||||
keyword: string
|
||||
value: valType
|
||||
selectKey: string
|
||||
initializeData: boolean
|
||||
accidentBlur: boolean
|
||||
focusStatus: boolean
|
||||
} = reactive({
|
||||
primaryKey: props.pk,
|
||||
options: [],
|
||||
loading: false,
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
params: props.params,
|
||||
keyword: '',
|
||||
value: props.modelValue ? props.modelValue : '',
|
||||
selectKey: uuid(),
|
||||
initializeData: false,
|
||||
accidentBlur: false,
|
||||
focusStatus: false,
|
||||
})
|
||||
|
||||
let io: null | IntersectionObserver = null
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:modelValue', value: valType): void
|
||||
(e: 'row', value: any): void
|
||||
}>()
|
||||
|
||||
const onChangeSelect = (val: valType) => {
|
||||
emits('update:modelValue', val)
|
||||
if (typeof instance?.vnode.props?.onRow == 'function') {
|
||||
let pkArr = props.pk.split('.')
|
||||
let pk = pkArr[pkArr.length - 1]
|
||||
if (typeof val == 'number' || typeof val == 'string') {
|
||||
const dataKey = getArrayKey(state.options, pk, val.toString())
|
||||
emits('row', dataKey ? toRaw(state.options[dataKey]) : {})
|
||||
} else {
|
||||
const valueArr = []
|
||||
for (const key in val) {
|
||||
let dataKey = getArrayKey(state.options, pk, val[key].toString())
|
||||
if (dataKey) valueArr.push(toRaw(state.options[dataKey]))
|
||||
}
|
||||
emits('row', valueArr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onVisibleChange = (val: boolean) => {
|
||||
// 保持面板状态和焦点状态一致
|
||||
if (!val) {
|
||||
nextTick(() => {
|
||||
selectRef.value?.blur()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
state.focusStatus = true
|
||||
if (selectRef.value?.query != state.keyword) {
|
||||
state.keyword = ''
|
||||
state.initializeData = false
|
||||
// el-select 自动清理搜索词会产生意外的脱焦
|
||||
state.accidentBlur = true
|
||||
}
|
||||
if (!state.initializeData) {
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
state.focusStatus = false
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
state.keyword = ''
|
||||
state.initializeData = false
|
||||
}
|
||||
|
||||
const onLogKeyword = (q: string) => {
|
||||
if (state.keyword != q) {
|
||||
state.keyword = q
|
||||
getData()
|
||||
}
|
||||
}
|
||||
|
||||
const getData = (initValue: valType = '') => {
|
||||
state.loading = true
|
||||
state.params.page = state.currentPage
|
||||
state.params.initKey = props.pk
|
||||
state.params.initValue = initValue
|
||||
getSelectData(props.remoteUrl, state.keyword, state.params)
|
||||
.then((res) => {
|
||||
let initializeData = true
|
||||
let opts = res.data.options ? res.data.options : res.data.list
|
||||
if (typeof props.labelFormatter == 'function') {
|
||||
for (const key in opts) {
|
||||
opts[key][props.field] = props.labelFormatter(opts[key], key)
|
||||
}
|
||||
}
|
||||
state.options = opts
|
||||
state.total = res.data.total ?? 0
|
||||
if (initValue) {
|
||||
// 重新渲染组件,确保在赋值前,opts已加载到-兼容 modelValue 更新
|
||||
state.selectKey = uuid()
|
||||
initializeData = false
|
||||
}
|
||||
state.loading = false
|
||||
state.initializeData = initializeData
|
||||
if (state.accidentBlur) {
|
||||
nextTick(() => {
|
||||
const inputEl = selectRef.value?.$el.querySelector('.el-select__tags .el-select__input')
|
||||
inputEl && inputEl.focus()
|
||||
state.accidentBlur = false
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
state.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
const onSelectCurrentPageChange = (val: number) => {
|
||||
state.currentPage = val
|
||||
getData()
|
||||
}
|
||||
|
||||
const initDefaultValue = () => {
|
||||
if (state.value) {
|
||||
// number[]转string[]确保默认值能够选中
|
||||
if (typeof state.value === 'object') {
|
||||
for (const key in state.value as string[]) {
|
||||
state.value[key] = state.value[key].toString()
|
||||
}
|
||||
} else if (typeof state.value === 'number') {
|
||||
state.value = state.value.toString()
|
||||
}
|
||||
getData(state.value)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.pk.indexOf('.') > 0) {
|
||||
let pk = props.pk.split('.')
|
||||
state.primaryKey = pk[1] ? pk[1] : pk[0]
|
||||
}
|
||||
initDefaultValue()
|
||||
|
||||
setTimeout(() => {
|
||||
if (window?.IntersectionObserver) {
|
||||
io = new IntersectionObserver((entries) => {
|
||||
for (const key in entries) {
|
||||
if (!entries[key].isIntersecting) selectRef.value?.blur()
|
||||
}
|
||||
})
|
||||
if (selectRef.value?.$el instanceof Element) {
|
||||
io.observe(selectRef.value.$el)
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
io?.disconnect()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (String(state.value) != String(newVal)) {
|
||||
state.value = newVal ? newVal : ''
|
||||
initDefaultValue()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const getSelectRef = () => {
|
||||
return selectRef.value
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
selectRef.value?.focus()
|
||||
}
|
||||
|
||||
const blur = () => {
|
||||
selectRef.value?.blur()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
blur,
|
||||
focus,
|
||||
getSelectRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.remote-select-popper) {
|
||||
text-align: center;
|
||||
}
|
||||
.remote-select-option {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
200
src/components/baInput/helper.ts
Normal file
200
src/components/baInput/helper.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import type { FieldData } from './index'
|
||||
|
||||
export const npuaFalse = () => {
|
||||
return {
|
||||
null: false,
|
||||
primaryKey: false,
|
||||
unsigned: false,
|
||||
autoIncrement: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有 Input 支持的类型对应的数据字段类型等数据(默认/示例设计)
|
||||
*/
|
||||
export const fieldData: FieldData = {
|
||||
string: {
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
password: {
|
||||
type: 'varchar',
|
||||
length: 32,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
number: {
|
||||
type: 'int',
|
||||
length: 10,
|
||||
precision: 0,
|
||||
default: '0',
|
||||
...npuaFalse(),
|
||||
},
|
||||
radio: {
|
||||
type: 'enum',
|
||||
length: 0,
|
||||
precision: 0,
|
||||
default: '',
|
||||
...npuaFalse(),
|
||||
},
|
||||
checkbox: {
|
||||
type: 'set',
|
||||
length: 0,
|
||||
precision: 0,
|
||||
default: '',
|
||||
...npuaFalse(),
|
||||
},
|
||||
switch: {
|
||||
type: 'tinyint',
|
||||
length: 1,
|
||||
precision: 0,
|
||||
default: '1',
|
||||
...npuaFalse(),
|
||||
unsigned: true,
|
||||
},
|
||||
textarea: {
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
array: {
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
datetime: {
|
||||
type: 'bigint',
|
||||
length: 16,
|
||||
precision: 0,
|
||||
default: 'null',
|
||||
...npuaFalse(),
|
||||
null: true,
|
||||
unsigned: true,
|
||||
},
|
||||
year: {
|
||||
type: 'year',
|
||||
length: 4,
|
||||
precision: 0,
|
||||
default: 'null',
|
||||
...npuaFalse(),
|
||||
null: true,
|
||||
},
|
||||
date: {
|
||||
type: 'date',
|
||||
length: 0,
|
||||
precision: 0,
|
||||
default: 'null',
|
||||
...npuaFalse(),
|
||||
null: true,
|
||||
},
|
||||
time: {
|
||||
type: 'time',
|
||||
length: 0,
|
||||
precision: 0,
|
||||
default: 'null',
|
||||
...npuaFalse(),
|
||||
null: true,
|
||||
},
|
||||
select: {
|
||||
type: 'enum',
|
||||
length: 0,
|
||||
precision: 0,
|
||||
default: '',
|
||||
...npuaFalse(),
|
||||
},
|
||||
selects: {
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
remoteSelect: {
|
||||
type: 'int',
|
||||
length: 10,
|
||||
precision: 0,
|
||||
default: '0',
|
||||
...npuaFalse(),
|
||||
unsigned: true,
|
||||
},
|
||||
remoteSelects: {
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
editor: {
|
||||
type: 'text',
|
||||
length: 0,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
null: true,
|
||||
},
|
||||
city: {
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
image: {
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
images: {
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
file: {
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
files: {
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
icon: {
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
color: {
|
||||
type: 'varchar',
|
||||
length: 30,
|
||||
precision: 0,
|
||||
default: 'empty string',
|
||||
...npuaFalse(),
|
||||
},
|
||||
}
|
||||
|
||||
export const stringToArray = (val: string | string[]) => {
|
||||
if (typeof val === 'string') {
|
||||
return val == '' ? [] : val.split(',')
|
||||
} else {
|
||||
return val as string[]
|
||||
}
|
||||
}
|
||||
204
src/components/baInput/index.ts
Normal file
204
src/components/baInput/index.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { Component, CSSProperties } from 'vue'
|
||||
|
||||
/**
|
||||
* 支持的输入框类型
|
||||
* 若您正在设计数据表,可以找到 ./helper.ts 文件来参考对应类型的:数据字段设计示例
|
||||
*/
|
||||
export const inputTypes = [
|
||||
'string',
|
||||
'password',
|
||||
'number',
|
||||
'radio',
|
||||
'checkbox',
|
||||
'switch',
|
||||
'textarea',
|
||||
'array',
|
||||
'datetime',
|
||||
'year',
|
||||
'date',
|
||||
'time',
|
||||
'select',
|
||||
'selects',
|
||||
'remoteSelect',
|
||||
'remoteSelects',
|
||||
'editor',
|
||||
'city',
|
||||
'image',
|
||||
'images',
|
||||
'file',
|
||||
'files',
|
||||
'icon',
|
||||
'color',
|
||||
]
|
||||
export type modelValueTypes = string | number | boolean | object
|
||||
|
||||
export interface InputData {
|
||||
// 标题
|
||||
title?: string
|
||||
// 内容,比如radio的选项列表数据 content: { a: '选项1', b: '选项2' }
|
||||
content?: any
|
||||
// 提示信息
|
||||
tip?: string
|
||||
// 需要生成子级元素时,子级元素属性(比如radio)
|
||||
childrenAttr?: anyObj
|
||||
// 城市选择器等级,1=省,2=市,3=区
|
||||
level?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* input可用属性,用于代码提示,渲染不同输入组件时,需要的属性是不一样的
|
||||
* https://element-plus.org/zh-CN/component/input.html#input-属性
|
||||
*/
|
||||
export interface InputAttr {
|
||||
id?: string
|
||||
name?: string
|
||||
type?: string
|
||||
placeholder?: string
|
||||
maxlength?: string | number
|
||||
minlength?: string | number
|
||||
showWordLimit?: boolean
|
||||
clearable?: boolean
|
||||
showPassword?: boolean
|
||||
disabled?: boolean
|
||||
size?: 'large' | 'default' | 'small'
|
||||
prefixIcon?: string | Component
|
||||
suffixIcon?: string | Component
|
||||
rows?: number
|
||||
border?: boolean
|
||||
autosize?: boolean | anyObj
|
||||
autocomplete?: string
|
||||
readonly?: boolean
|
||||
max?: string | number
|
||||
min?: string | number
|
||||
step?: string | number
|
||||
resize?: 'none' | 'both' | 'horizontal' | 'vertical'
|
||||
autofocus?: boolean
|
||||
form?: string
|
||||
label?: string
|
||||
tabindex?: string | number
|
||||
validateEvent?: boolean
|
||||
inputStyle?: anyObj
|
||||
// DateTimePicker属性
|
||||
editable?: boolean
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
timeArrowControl?: boolean
|
||||
format?: string
|
||||
popperClass?: string
|
||||
rangeSeparator?: string
|
||||
defaultValue?: Date
|
||||
defaultTime?: Date | Date[]
|
||||
valueFormat?: string
|
||||
unlinkPanels?: boolean
|
||||
clearIcon?: string | Component
|
||||
shortcuts?: { text: string; value: Date | Function }[]
|
||||
disabledDate?: Function
|
||||
cellClassName?: Function
|
||||
teleported?: boolean
|
||||
// select属性
|
||||
multiple?: boolean
|
||||
valueKey?: string
|
||||
collapseTags?: string
|
||||
collapseTagsTooltip?: boolean
|
||||
multipleLimit?: number
|
||||
effect?: 'dark' | 'light'
|
||||
filterable?: boolean
|
||||
allowCreate?: boolean
|
||||
filterMethod?: Function
|
||||
remote?: false // 禁止使用远程搜索,如需使用请使用单独封装好的 remoteSelect 组件
|
||||
remoteMethod?: false
|
||||
labelFormatter?: (optionData: anyObj, optionKey: string) => string
|
||||
noMatchText?: string
|
||||
noDataText?: string
|
||||
reserveKeyword?: boolean
|
||||
defaultFirstOption?: boolean
|
||||
popperAppendToBody?: boolean
|
||||
persistent?: boolean
|
||||
automaticDropdown?: boolean
|
||||
fitInputWidth?: boolean
|
||||
tagType?: 'success' | 'info' | 'warning' | 'danger'
|
||||
params?: anyObj
|
||||
// 远程select属性
|
||||
pk?: string
|
||||
field?: string
|
||||
remoteUrl?: string
|
||||
tooltipParams?: anyObj
|
||||
// 图标选择器属性
|
||||
showIconName?: boolean
|
||||
placement?: string
|
||||
title?: string
|
||||
// 颜色选择器
|
||||
showAlpha?: boolean
|
||||
colorFormat?: string
|
||||
predefine?: string[]
|
||||
// 图片文件上传属性
|
||||
action?: string
|
||||
headers?: anyObj
|
||||
method?: string
|
||||
data?: anyObj
|
||||
withCredentials?: boolean
|
||||
showFileList?: boolean
|
||||
drag?: boolean
|
||||
accept?: string
|
||||
listType?: string
|
||||
autoUpload?: boolean
|
||||
limit?: number
|
||||
hideSelectFile?: boolean
|
||||
returnFullUrl?: boolean
|
||||
forceLocal?: boolean
|
||||
// editor属性
|
||||
height?: string
|
||||
mode?: string
|
||||
editorStyle?: CSSProperties
|
||||
style?: CSSProperties
|
||||
toolbarConfig?: anyObj
|
||||
editorConfig?: anyObj
|
||||
editorType?: string
|
||||
preview?: boolean
|
||||
language?: string
|
||||
theme?: 'light' | 'dark'
|
||||
toolbarsExclude?: string[]
|
||||
fileForceLocal?: boolean
|
||||
// array组件属性
|
||||
keyTitle?: string
|
||||
valueTitle?: string
|
||||
// 返回数据类型
|
||||
dataType?: string
|
||||
// 事件
|
||||
onPreview?: Function
|
||||
onRemove?: Function
|
||||
onSuccess?: Function
|
||||
onError?: Function
|
||||
onProgress?: Function
|
||||
onExceed?: Function
|
||||
onBeforeUpload?: Function
|
||||
onBeforeRemove?: Function
|
||||
onChange?: Function
|
||||
onInput?: Function
|
||||
onVisibleChange?: Function
|
||||
onRemoveTag?: Function
|
||||
onClear?: Function
|
||||
onBlur?: Function
|
||||
onFocus?: Function
|
||||
onCalendarChange?: Function
|
||||
onPanelChange?: Function
|
||||
onActiveChange?: Function
|
||||
onRow?: Function
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Input 支持的类型对应的数据字段设计数据
|
||||
*/
|
||||
export interface FieldData {
|
||||
[key: string]: {
|
||||
type: string // 数据类型
|
||||
length: number // 长度
|
||||
precision: number // 小数点
|
||||
default: string // 默认值
|
||||
null: boolean // 允许 null
|
||||
primaryKey: boolean // 主键
|
||||
unsigned: boolean // 无符号
|
||||
autoIncrement: boolean // 自动递增
|
||||
}
|
||||
}
|
||||
414
src/components/baInput/index.vue
Normal file
414
src/components/baInput/index.vue
Normal file
@@ -0,0 +1,414 @@
|
||||
<script lang="ts">
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import type { modelValueTypes, InputAttr, InputData } from '@/components/baInput'
|
||||
import { createVNode, resolveComponent, defineComponent, computed, reactive } from 'vue'
|
||||
import { inputTypes } from '@/components/baInput'
|
||||
import Array from '@/components/baInput/components/array.vue'
|
||||
import RemoteSelect from '@/components/baInput/components/remoteSelect.vue'
|
||||
import IconSelector from '@/components/baInput/components/iconSelector.vue'
|
||||
import Editor from '@/components/baInput/components/editor.vue'
|
||||
import { getArea } from '@/api/common'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'baInput',
|
||||
props: {
|
||||
// 输入框类型,支持的输入框见 inputTypes
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value: string) => {
|
||||
return inputTypes.includes(value)
|
||||
},
|
||||
},
|
||||
// 双向绑定值
|
||||
modelValue: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
// 输入框的附加属性
|
||||
attr: {
|
||||
type: Object as PropType<InputAttr>,
|
||||
default: () => {},
|
||||
},
|
||||
// 额外数据,radio、checkbox的选项等数据
|
||||
data: {
|
||||
type: Object as PropType<InputData>,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const onValueUpdate = (value: modelValueTypes) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
// 子级元素属性
|
||||
let childrenAttr = props.data && props.data.childrenAttr ? props.data.childrenAttr : {}
|
||||
|
||||
// string number textarea password
|
||||
const sntp = () => {
|
||||
return () =>
|
||||
createVNode(resolveComponent('el-input'), {
|
||||
type: props.type == 'string' ? 'text' : props.type,
|
||||
...props.attr,
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
})
|
||||
}
|
||||
// radio checkbox
|
||||
const rc = () => {
|
||||
if (!props.data || !props.data.content) {
|
||||
console.warn('请传递 ' + props.type + '的 content')
|
||||
}
|
||||
let vNode: VNode[] = []
|
||||
for (const key in props.data.content) {
|
||||
vNode.push(
|
||||
createVNode(
|
||||
resolveComponent('el-' + props.type),
|
||||
{
|
||||
label: key,
|
||||
...childrenAttr,
|
||||
},
|
||||
() => props.data.content[key]
|
||||
)
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
const valueComputed = computed(() => {
|
||||
if (props.type == 'radio') {
|
||||
if (props.modelValue == undefined) return ''
|
||||
return '' + props.modelValue
|
||||
} else {
|
||||
let modelValueArr: anyObj = []
|
||||
for (const key in props.modelValue) {
|
||||
modelValueArr[key] = '' + props.modelValue[key]
|
||||
}
|
||||
return modelValueArr
|
||||
}
|
||||
})
|
||||
return createVNode(
|
||||
resolveComponent('el-' + props.type + '-group'),
|
||||
{
|
||||
...props.attr,
|
||||
modelValue: valueComputed.value,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
},
|
||||
() => vNode
|
||||
)
|
||||
}
|
||||
}
|
||||
// select selects
|
||||
const select = () => {
|
||||
let vNode: VNode[] = []
|
||||
if (!props.data || !props.data.content) {
|
||||
console.warn('请传递 ' + props.type + '的 content')
|
||||
}
|
||||
for (const key in props.data.content) {
|
||||
vNode.push(
|
||||
createVNode(resolveComponent('el-option'), {
|
||||
key: key,
|
||||
label: props.data.content[key],
|
||||
value: key,
|
||||
...childrenAttr,
|
||||
})
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
const valueComputed = computed(() => {
|
||||
if (props.type == 'select') {
|
||||
if (props.modelValue == undefined) return ''
|
||||
return '' + props.modelValue
|
||||
} else {
|
||||
let modelValueArr: anyObj = []
|
||||
for (const key in props.modelValue) {
|
||||
modelValueArr[key] = '' + props.modelValue[key]
|
||||
}
|
||||
return modelValueArr
|
||||
}
|
||||
})
|
||||
return createVNode(
|
||||
resolveComponent('el-select'),
|
||||
{
|
||||
class: 'w100',
|
||||
multiple: props.type == 'select' ? false : true,
|
||||
clearable: true,
|
||||
...props.attr,
|
||||
modelValue: valueComputed.value,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
},
|
||||
() => vNode
|
||||
)
|
||||
}
|
||||
}
|
||||
// datetime
|
||||
const datetime = () => {
|
||||
let valueFormat = 'YYYY-MM-DD HH:mm:ss'
|
||||
switch (props.type) {
|
||||
case 'date':
|
||||
valueFormat = 'YYYY-MM-DD'
|
||||
break
|
||||
case 'year':
|
||||
valueFormat = 'YYYY'
|
||||
break
|
||||
}
|
||||
return () =>
|
||||
createVNode(resolveComponent('el-date-picker'), {
|
||||
class: 'w100',
|
||||
type: props.type,
|
||||
'value-format': valueFormat,
|
||||
...props.attr,
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
})
|
||||
}
|
||||
// remoteSelect remoteSelects
|
||||
const remoteSelect = () => {
|
||||
return () =>
|
||||
createVNode(RemoteSelect, {
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
multiple: props.type == 'remoteSelect' ? false : true,
|
||||
...props.attr,
|
||||
})
|
||||
}
|
||||
|
||||
const buildFun = new Map([
|
||||
['string', sntp],
|
||||
['number', sntp],
|
||||
['textarea', sntp],
|
||||
['password', sntp],
|
||||
['radio', rc],
|
||||
['checkbox', rc],
|
||||
[
|
||||
'switch',
|
||||
() => {
|
||||
const valueType = computed(() => typeof props.modelValue)
|
||||
const valueComputed = computed(() => {
|
||||
if (valueType.value === 'boolean') {
|
||||
return props.modelValue
|
||||
} else {
|
||||
let valueTmp = parseInt(props.modelValue as string)
|
||||
return isNaN(valueTmp) || valueTmp <= 0 ? false : true
|
||||
}
|
||||
})
|
||||
return () =>
|
||||
createVNode(resolveComponent('el-switch'), {
|
||||
...props.attr,
|
||||
modelValue: valueComputed.value,
|
||||
'onUpdate:modelValue': (value: boolean) => {
|
||||
let newValue: boolean | string | number = value
|
||||
switch (valueType.value) {
|
||||
case 'string':
|
||||
newValue = value ? '1' : '0'
|
||||
break
|
||||
case 'number':
|
||||
newValue = value ? 1 : 0
|
||||
}
|
||||
emit('update:modelValue', newValue)
|
||||
},
|
||||
})
|
||||
},
|
||||
],
|
||||
['datetime', datetime],
|
||||
[
|
||||
'year',
|
||||
() => {
|
||||
return () => {
|
||||
const valueComputed = computed(() => (!props.modelValue ? null : '' + props.modelValue))
|
||||
return createVNode(resolveComponent('el-date-picker'), {
|
||||
class: 'w100',
|
||||
type: props.type,
|
||||
'value-format': 'YYYY',
|
||||
...props.attr,
|
||||
modelValue: valueComputed.value,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
['date', datetime],
|
||||
[
|
||||
'time',
|
||||
() => {
|
||||
const valueComputed = computed(() => {
|
||||
if (props.modelValue instanceof Date) {
|
||||
return props.modelValue
|
||||
} else if (!props.modelValue) {
|
||||
return ''
|
||||
} else {
|
||||
let date = new Date()
|
||||
return new Date(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + props.modelValue)
|
||||
}
|
||||
})
|
||||
return () =>
|
||||
createVNode(resolveComponent('el-time-picker'), {
|
||||
class: 'w100',
|
||||
clearable: true,
|
||||
format: 'HH:mm:ss',
|
||||
...props.attr,
|
||||
modelValue: valueComputed.value,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
})
|
||||
},
|
||||
],
|
||||
['select', select],
|
||||
['selects', select],
|
||||
[
|
||||
'array',
|
||||
() => {
|
||||
return () =>
|
||||
createVNode(Array, {
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
...props.attr,
|
||||
})
|
||||
},
|
||||
],
|
||||
['remoteSelect', remoteSelect],
|
||||
['remoteSelects', remoteSelect],
|
||||
[
|
||||
'city',
|
||||
() => {
|
||||
type Node = { value?: number; label?: string; leaf?: boolean }
|
||||
let maxLevel = props.data && props.data.level ? props.data.level - 1 : 2
|
||||
const lastLazyValue: {
|
||||
value: string | number[] | unknown
|
||||
nodes: Node[]
|
||||
key: string
|
||||
currentRequest: any
|
||||
} = reactive({
|
||||
value: 'ready',
|
||||
nodes: [],
|
||||
key: '',
|
||||
currentRequest: null,
|
||||
})
|
||||
|
||||
// 请求到的node备份-s
|
||||
let nodeEbak: anyObj = {}
|
||||
const getNodes = (level: number, key: string) => {
|
||||
if (nodeEbak[level] && nodeEbak[level][key]) {
|
||||
return nodeEbak[level][key]
|
||||
}
|
||||
return false
|
||||
}
|
||||
const setNodes = (level: number, key: string, nodes: Node[] = []) => {
|
||||
if (!nodeEbak[level]) {
|
||||
nodeEbak[level] = {}
|
||||
}
|
||||
nodeEbak[level][key] = nodes
|
||||
}
|
||||
// 请求到的node备份-e
|
||||
|
||||
return () =>
|
||||
createVNode(resolveComponent('el-cascader'), {
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
class: 'w100',
|
||||
clearable: true,
|
||||
props: {
|
||||
lazy: true,
|
||||
lazyLoad(node: any, resolve: any) {
|
||||
// lazyLoad会频繁触发,在本地存储请求结果,供重复触发时直接读取
|
||||
const { level, pathValues } = node
|
||||
let key = pathValues.join(',')
|
||||
key = key ? key : 'init'
|
||||
|
||||
let locaNode = getNodes(level, key)
|
||||
if (locaNode) {
|
||||
return resolve(locaNode)
|
||||
}
|
||||
|
||||
if (lastLazyValue.key == key && lastLazyValue.value == props.modelValue) {
|
||||
if (lastLazyValue.currentRequest) {
|
||||
return lastLazyValue.currentRequest
|
||||
}
|
||||
return resolve(lastLazyValue.nodes)
|
||||
}
|
||||
|
||||
let nodes: Node[] = []
|
||||
lastLazyValue.key = key
|
||||
lastLazyValue.value = props.modelValue
|
||||
lastLazyValue.currentRequest = getArea(pathValues).then((res) => {
|
||||
let toStr = false
|
||||
if (props.modelValue && typeof (props.modelValue as anyObj)[0] === 'string') {
|
||||
toStr = true
|
||||
}
|
||||
for (const key in res.data) {
|
||||
if (toStr) {
|
||||
res.data[key].value = res.data[key].value.toString()
|
||||
}
|
||||
res.data[key].leaf = level >= maxLevel
|
||||
nodes.push(res.data[key])
|
||||
}
|
||||
lastLazyValue.nodes = nodes
|
||||
lastLazyValue.currentRequest = null
|
||||
setNodes(level, key, nodes)
|
||||
resolve(nodes)
|
||||
})
|
||||
},
|
||||
},
|
||||
...props.attr,
|
||||
})
|
||||
},
|
||||
],
|
||||
['image', upload],
|
||||
['images', upload],
|
||||
['file', upload],
|
||||
['files', upload],
|
||||
[
|
||||
'icon',
|
||||
() => {
|
||||
return () =>
|
||||
createVNode(IconSelector, {
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
...props.attr,
|
||||
})
|
||||
},
|
||||
],
|
||||
[
|
||||
'color',
|
||||
() => {
|
||||
return () =>
|
||||
createVNode(resolveComponent('el-color-picker'), {
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
...props.attr,
|
||||
})
|
||||
},
|
||||
],
|
||||
[
|
||||
'editor',
|
||||
() => {
|
||||
return () =>
|
||||
createVNode(Editor, {
|
||||
modelValue: props.modelValue,
|
||||
'onUpdate:modelValue': onValueUpdate,
|
||||
...props.attr,
|
||||
})
|
||||
},
|
||||
],
|
||||
[
|
||||
'default',
|
||||
() => {
|
||||
console.warn('暂不支持' + props.type + '的输入框类型,你可以自行在 BaInput 组件内添加逻辑')
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
let action = buildFun.get(props.type) || buildFun.get('default')
|
||||
return action!.call(this)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-upload-image :deep(.el-upload--picture-card) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ba-upload-file :deep(.el-upload-list) {
|
||||
margin-left: -10px;
|
||||
}
|
||||
</style>
|
||||
108
src/components/contextmenu/index.vue
Normal file
108
src/components/contextmenu/index.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<transition name="el-zoom-in-center">
|
||||
<div
|
||||
class="el-popper is-pure is-light el-dropdown__popper ba-contextmenu"
|
||||
:style="`top: ${state.axis.y + 5}px;left: ${state.axis.x - 14}px;width:${props.width}px`"
|
||||
:key="Math.random()"
|
||||
v-show="state.show"
|
||||
aria-hidden="false"
|
||||
data-popper-placement="bottom"
|
||||
>
|
||||
<ul class="el-dropdown-menu">
|
||||
<template v-for="(item, idx) in props.items" :key="idx">
|
||||
<li class="el-dropdown-menu__item" :class="item.disabled ? 'is-disabled' : ''" tabindex="-1" @click="onContextmenuItem(item)">
|
||||
<Icon size="12" :name="item.icon" />
|
||||
<span>{{ item.label }}</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<span class="el-popper__arrow" :style="{ left: `${state.arrowAxis}px` }"></span>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, toRaw } from 'vue'
|
||||
import type { Axis, ContextmenuItemClickEmitArg, Props } from './interface'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: 150,
|
||||
items: () => [],
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'contextmenuItemClick', item: ContextmenuItemClickEmitArg): void
|
||||
}>()
|
||||
|
||||
const state: {
|
||||
show: boolean
|
||||
axis: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
menu: RouteLocationNormalized | undefined
|
||||
arrowAxis: number
|
||||
} = reactive({
|
||||
show: false,
|
||||
axis: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
menu: undefined,
|
||||
arrowAxis: 10,
|
||||
})
|
||||
|
||||
const onShowContextmenu = (menu: RouteLocationNormalized, axis: Axis) => {
|
||||
state.menu = menu
|
||||
state.axis = axis
|
||||
state.show = true
|
||||
}
|
||||
|
||||
const onContextmenuItem = (item: ContextmenuItemClickEmitArg) => {
|
||||
if (item.disabled) return
|
||||
item.menu = toRaw(state.menu)
|
||||
emits('contextmenuItemClick', item)
|
||||
}
|
||||
|
||||
const onHideContextmenu = () => {
|
||||
state.show = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onShowContextmenu,
|
||||
onHideContextmenu,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
useEventListener(document, 'click', onHideContextmenu)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-contextmenu {
|
||||
z-index: 9999;
|
||||
}
|
||||
.el-popper,
|
||||
.el-popper.is-light .el-popper__arrow::before {
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
}
|
||||
.el-dropdown-menu__item {
|
||||
padding: 8px 20px;
|
||||
user-select: none;
|
||||
}
|
||||
.el-dropdown-menu__item .icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.el-dropdown-menu__item:not(.is-disabled) {
|
||||
&:hover {
|
||||
background-color: var(--el-dropdown-menuItem-hover-fill);
|
||||
color: var(--el-dropdown-menuItem-hover-color);
|
||||
.fa {
|
||||
color: var(--el-dropdown-menuItem-hover-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/components/contextmenu/interface.ts
Normal file
22
src/components/contextmenu/interface.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
export interface Axis {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
name: string
|
||||
label: string
|
||||
icon?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface ContextmenuItemClickEmitArg extends ContextMenuItem {
|
||||
menu?: RouteLocationNormalized
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
width?: number
|
||||
items: ContextMenuItem[]
|
||||
}
|
||||
208
src/components/echarts/MyEchart.vue
Normal file
208
src/components/echarts/MyEchart.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="my-chart" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, defineExpose, watch } from 'vue'
|
||||
// import echarts from './echarts'
|
||||
import * as echarts from 'echarts' // 全引入
|
||||
|
||||
import 'echarts/lib/component/dataZoom'
|
||||
|
||||
const chartRef = ref<HTMLDivElement>()
|
||||
|
||||
const props = defineProps(['options'])
|
||||
let chart: echarts.ECharts | any = null
|
||||
const resizeHandler = () => {
|
||||
chart.getZr().painter.getViewportRoot().style.display = 'none'
|
||||
requestAnimationFrame(() => {
|
||||
chart.resize()
|
||||
chart.getZr().painter.getViewportRoot().style.display = ''
|
||||
})
|
||||
}
|
||||
const initChart = () => {
|
||||
chart?.dispose()
|
||||
|
||||
chart = echarts.init(chartRef.value as HTMLDivElement)
|
||||
chart.setOption({
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
color: '#000',
|
||||
fontSize: 18
|
||||
},
|
||||
...(props.options.title || null)
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'normal',
|
||||
opacity: 0.35,
|
||||
fontSize: 14
|
||||
},
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
borderWidth: 0,
|
||||
...(props.options.tooltip || null)
|
||||
},
|
||||
legend: {
|
||||
right: 20,
|
||||
top: 0,
|
||||
itemGap: 10,
|
||||
itemStyle: {},
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
padding: [2, 0, 0, 0] //[上、右、下、左]
|
||||
},
|
||||
itemWidth: 15,
|
||||
itemHeight: 10,
|
||||
...(props.options.legend || null)
|
||||
},
|
||||
|
||||
grid: {
|
||||
top: '50px',
|
||||
left: '10px',
|
||||
right: '60px',
|
||||
bottom: '40px',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: handlerXAxis(),
|
||||
yAxis: handlerYAxis(),
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
height: 13,
|
||||
start: 0,
|
||||
bottom: '20px',
|
||||
end: 100,
|
||||
...(props.options.dataZoom || null)
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
height: 13,
|
||||
bottom: '20px',
|
||||
end: 100,
|
||||
...(props.options.dataZoom || null)
|
||||
}
|
||||
],
|
||||
color: [
|
||||
...(props.options.color || ''),
|
||||
'#07CCCA ',
|
||||
'#00BFF5',
|
||||
'#FFBF00',
|
||||
'#77DA63',
|
||||
'#D5FF6B',
|
||||
'#Ff6600',
|
||||
'#FF9100',
|
||||
'#5B6E96',
|
||||
'#66FFCC',
|
||||
'#B3B3B3'
|
||||
],
|
||||
...props.options.options
|
||||
})
|
||||
}
|
||||
const handlerYAxis = () => {
|
||||
let temp = {
|
||||
type: 'value',
|
||||
|
||||
nameTextStyle: {
|
||||
color: '#000'
|
||||
},
|
||||
minInterval: 1,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#000'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#000',
|
||||
fontSize: 14
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
// 使用深浅的间隔色
|
||||
color: ['#000'],
|
||||
type: 'dashed',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
// props.options.xAxis 是数组还是对象
|
||||
if (Array.isArray(props.options.yAxis)) {
|
||||
return props.options.yAxis.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
...temp
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
...temp,
|
||||
...props.options.yAxis
|
||||
}
|
||||
}
|
||||
}
|
||||
const handlerXAxis = () => {
|
||||
let temp = {
|
||||
type: 'category',
|
||||
axisTick: { show: false },
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#000'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
fontFamily: 'dinproRegular',
|
||||
color: '#000',
|
||||
fontSize: '12'
|
||||
}
|
||||
}
|
||||
}
|
||||
// props.options.xAxis 是数组还是对象
|
||||
if (Array.isArray(props.options.xAxis)) {
|
||||
return props.options.xAxis.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
...temp
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return {
|
||||
...temp,
|
||||
...props.options.xAxis
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
})
|
||||
defineExpose({ initChart })
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
chart?.dispose()
|
||||
})
|
||||
watch(
|
||||
() => props.options,
|
||||
(newVal, oldVal) => {
|
||||
initChart()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.my-chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
217
src/components/echarts/MyEchartMap.vue
Normal file
217
src/components/echarts/MyEchartMap.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<!-- 地图组件 -->
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<div class="bars_w" ref="chartMap" id="chartMap"></div>
|
||||
<span @click="circle" v-show="showCircle" class="iconfont icon-back" style="color: #003078"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, watch, onMounted, defineEmits } from 'vue'
|
||||
import * as echarts from 'echarts4'
|
||||
import { useDictData } from '@/stores/dictData'
|
||||
const dictData = useDictData()
|
||||
const props = defineProps(['options'])
|
||||
const myCharts = ref()
|
||||
const showCircle = ref(false)
|
||||
|
||||
const fetchConfig = async (name: string) => {
|
||||
const res = await import(`../../assets/map/${name}.json`)
|
||||
return res.default
|
||||
// GetEchar(res.default)
|
||||
}
|
||||
// fetchConfig()
|
||||
|
||||
const emit = defineEmits(['getRegionByRegion', 'eliminate'])
|
||||
onMounted(() => {})
|
||||
|
||||
const GetEchar = async (name: string) => {
|
||||
let chartDom = document.getElementById('chartMap')
|
||||
myCharts.value?.dispose()
|
||||
myCharts.value = echarts.init(chartDom)
|
||||
name == '中国' ? (showCircle.value = false) : (showCircle.value = true)
|
||||
|
||||
echarts.registerMap(name, await fetchConfig(name)) //注册可用的地图
|
||||
let option = {
|
||||
title: {
|
||||
left: 'center',
|
||||
top: '3%',
|
||||
...props.options.title
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
label: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
}
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'normal',
|
||||
opacity: 0.35,
|
||||
fontSize: 14
|
||||
},
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
...(props.options.tooltip || null)
|
||||
},
|
||||
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 26,
|
||||
bottom: 40,
|
||||
itemWidth: 16,
|
||||
itemHeight: 16,
|
||||
|
||||
...(props.options.legend || null)
|
||||
},
|
||||
|
||||
color: [
|
||||
...(props.options.color || ''),
|
||||
'#07CCCA ',
|
||||
'#00BFF5',
|
||||
'#FFBF00',
|
||||
'#77DA63',
|
||||
'#D5FF6B',
|
||||
'#Ff6600',
|
||||
'#FF9100',
|
||||
'#5B6E96',
|
||||
'#66FFCC',
|
||||
'#B3B3B3'
|
||||
],
|
||||
|
||||
geo: {
|
||||
map: name,
|
||||
zoom: 1.2,
|
||||
// top: 0,
|
||||
// bottom: 0,
|
||||
roam: true,
|
||||
label: {
|
||||
normal: {
|
||||
show: true,
|
||||
fontSize: '14',
|
||||
color: 'rgba(0,0,0,0.7)'
|
||||
}
|
||||
},
|
||||
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgba(51, 69, 129, .8)', //地图背景色
|
||||
borderColor: '#999999',
|
||||
borderWidth: 1,
|
||||
areaColor: {
|
||||
type: 'radial',
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
r: 0.8,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(147, 235, 248, 0)' // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(147, 135, 148, .2)' // 100% 处的颜色
|
||||
}
|
||||
],
|
||||
globalCoord: false // 缺省为 false
|
||||
},
|
||||
shadowColor: 'rgba(128, 217, 248, 1)',
|
||||
// shadowColor: 'rgba(255, 255, 255, 1)',
|
||||
shadowOffsetX: -2,
|
||||
shadowOffsetY: 2,
|
||||
shadowBlur: 10
|
||||
},
|
||||
emphasis: {
|
||||
areaColor: '#ccc',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0,
|
||||
borderWidth: 0
|
||||
}
|
||||
}
|
||||
// regions: [
|
||||
// {
|
||||
// name: '南海诸岛',
|
||||
// itemStyle: {
|
||||
// // 隐藏地图
|
||||
// normal: {
|
||||
// opacity: 0 // 为 0 时不绘制该图形
|
||||
// }
|
||||
// },
|
||||
// label: {
|
||||
// show: false // 隐藏文字
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
},
|
||||
...props.options.options
|
||||
}
|
||||
if (props.options.visualMap) {
|
||||
option.visualMap = props.options.visualMap
|
||||
}
|
||||
myCharts.value.setOption(option)
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
|
||||
// 点击事件
|
||||
myCharts.value.off('click')
|
||||
myCharts.value.on('click', (e: any) => {
|
||||
if (name == '中国' && e.componentIndex == 0) {
|
||||
MapReturn(e.name)
|
||||
|
||||
// console.log('🚀 ~ file: MyEchartMap.vue:156 ~ myCharts.value.on ~ MapReturn(e.name):', MapReturn(e.name))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const MapReturn = (name: string) => {
|
||||
let area = dictData.state.area?.[0]?.children ?? []
|
||||
let list = {}
|
||||
let flag = true
|
||||
for (let i = 0; i < area.length; i++) {
|
||||
if (area[i].name == name) {
|
||||
list = area[i]
|
||||
flag = false
|
||||
emit('getRegionByRegion', list)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
emit('eliminate', name)
|
||||
}
|
||||
}
|
||||
// 返回
|
||||
const circle = () => {
|
||||
emit('getRegionByRegion', dictData.state.area[0])
|
||||
showCircle.value = false
|
||||
}
|
||||
const resizeHandler = () => {
|
||||
myCharts.value?.resize()
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
myCharts.value?.dispose()
|
||||
})
|
||||
defineExpose({ GetEchar })
|
||||
watch(
|
||||
() => props.options,
|
||||
(newVal, oldVal) => {
|
||||
// GetEchar('中国')
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bars_w {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.iconfont {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
84
src/components/echarts/echarts.ts
Normal file
84
src/components/echarts/echarts.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import * as echarts from 'echarts/core'
|
||||
|
||||
//引入需要的图表,需要什么就加什么
|
||||
import {
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
ScatterChart,
|
||||
EffectScatterChart,
|
||||
RadarChart,
|
||||
GaugeChart
|
||||
} from 'echarts/charts'
|
||||
|
||||
// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
// 数据集组件
|
||||
DatasetComponent,
|
||||
// 内置数据转换器组件 (filter, sort)
|
||||
TransformComponent
|
||||
} from 'echarts/components'
|
||||
|
||||
// 标签自动布局,全局过渡动画等特性
|
||||
import { LabelLayout, UniversalTransition } from 'echarts/features'
|
||||
|
||||
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
|
||||
import type {
|
||||
// 系列类型的定义后缀都为 SeriesOption
|
||||
BarSeriesOption,
|
||||
LineSeriesOption
|
||||
} from 'echarts/charts'
|
||||
|
||||
import type {
|
||||
// 组件类型的定义后缀都为 ComponentOption
|
||||
TitleComponentOption,
|
||||
TooltipComponentOption,
|
||||
GridComponentOption,
|
||||
LegendComponentOption,
|
||||
ToolboxComponentOption,
|
||||
DatasetComponentOption
|
||||
} from 'echarts/components'
|
||||
import type { ComposeOption } from 'echarts/core'
|
||||
|
||||
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
|
||||
type ECOption = ComposeOption<
|
||||
| BarSeriesOption
|
||||
| LineSeriesOption
|
||||
| TitleComponentOption
|
||||
| TooltipComponentOption
|
||||
| GridComponentOption
|
||||
| DatasetComponentOption
|
||||
| LegendComponentOption
|
||||
| ToolboxComponentOption
|
||||
>
|
||||
|
||||
// 注册必须的组件,上面引入的都需要在此注册
|
||||
echarts.use([
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DatasetComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
TransformComponent,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
ScatterChart,
|
||||
LabelLayout,
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
EffectScatterChart,
|
||||
RadarChart,
|
||||
GaugeChart
|
||||
])
|
||||
|
||||
// 导出
|
||||
export default echarts
|
||||
1544
src/components/echarts/rmsboxi.vue
Normal file
1544
src/components/echarts/rmsboxi.vue
Normal file
File diff suppressed because it is too large
Load Diff
1153
src/components/echarts/shushiboxi.vue
Normal file
1153
src/components/echarts/shushiboxi.vue
Normal file
File diff suppressed because it is too large
Load Diff
42
src/components/form/area/index.vue
Normal file
42
src/components/form/area/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<el-cascader ref="cascader" v-bind="$attrs" :options="options" :props="cascaderProps" @change="change" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, ref, watch } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Area'
|
||||
})
|
||||
import { useDictData } from '@/stores/dictData'
|
||||
|
||||
const cascaderProps = {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
checkStrictly: true,
|
||||
emitPath: false
|
||||
}
|
||||
const cascader = ref()
|
||||
const dictData = useDictData()
|
||||
const options = dictData.state.area
|
||||
const areaName = ref(dictData.state.area[0].name)
|
||||
const change = (e: any) => {
|
||||
if (cascader.value.getCheckedNodes()[0].pathLabels.length == 1) {
|
||||
areaName.value = cascader.value.getCheckedNodes()[0].pathLabels[0]
|
||||
} else if (cascader.value.getCheckedNodes()[0].pathLabels.length >= 2) {
|
||||
areaName.value = cascader.value.getCheckedNodes()[0].pathLabels[1]
|
||||
}
|
||||
}
|
||||
// watch(
|
||||
// () => $attrs,
|
||||
// (newVal, oldVal) => {
|
||||
// console.log(123)
|
||||
|
||||
// // GetEchar('中国')
|
||||
// }
|
||||
// )
|
||||
|
||||
defineExpose({ areaName, change })
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
478
src/components/form/datePicker/index.vue
Normal file
478
src/components/form/datePicker/index.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<el-select v-model="interval" style="width: 90px; margin-right: 10px" @change="timeChange">
|
||||
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="timeValue"
|
||||
type="daterange"
|
||||
:disabled="disabledPicker"
|
||||
style="width: 230px; margin-right: 10px"
|
||||
unlink-panels
|
||||
:clearable="false"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:shortcuts="shortcuts"
|
||||
/>
|
||||
<el-button :disabled="backDisabled" type="primary" :icon="DArrowLeft" @click="preClick"></el-button>
|
||||
<el-button type="primary" :icon="VideoPause" @click="nowTime">当前</el-button>
|
||||
<el-button :disabled="preDisabled" type="primary" :icon="DArrowRight" @click="next"></el-button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DArrowLeft, VideoPause, DArrowRight } from '@element-plus/icons-vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const interval = ref(3)
|
||||
const timeFlag = ref(1)
|
||||
const count = ref(0)
|
||||
const disabledPicker = ref(true)
|
||||
const timeValue = ref()
|
||||
const backDisabled = ref(false)
|
||||
const preDisabled = ref(false)
|
||||
const timeOptions = [
|
||||
{ label: '年份', value: 1 },
|
||||
{ label: '季度', value: 2 },
|
||||
{ label: '月份', value: 3 },
|
||||
{ label: '周', value: 4 },
|
||||
{ label: '自定义', value: 5 }
|
||||
]
|
||||
const shortcuts = [
|
||||
{
|
||||
text: '最近一周',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近一个月',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
||||
return [start, end]
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近3个月',
|
||||
value: () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
|
||||
return [start, end]
|
||||
}
|
||||
}
|
||||
]
|
||||
onMounted(() => {
|
||||
timeChange(3)
|
||||
})
|
||||
// 选择时间范围
|
||||
const timeChange = (e: number) => {
|
||||
backDisabled.value = false
|
||||
preDisabled.value = false
|
||||
count.value = 0
|
||||
if (e == 1) {
|
||||
disabledPicker.value = true
|
||||
|
||||
timeValue.value = [setTime(1), setTime()]
|
||||
} else if (e == 2) {
|
||||
disabledPicker.value = true
|
||||
timeValue.value = [setTime(2), setTime()]
|
||||
} else if (e == 3) {
|
||||
disabledPicker.value = true
|
||||
timeValue.value = [setTime(3), setTime()]
|
||||
} else if (e == 4) {
|
||||
disabledPicker.value = true
|
||||
timeValue.value = [setTime(0, 7), setTime()]
|
||||
} else if (e == 5) {
|
||||
disabledPicker.value = false
|
||||
backDisabled.value = true
|
||||
preDisabled.value = true
|
||||
timeValue.value = [setTime(), setTime()]
|
||||
}
|
||||
if (e == 1 || e == 2) {
|
||||
timeFlag.value = 0
|
||||
} else {
|
||||
timeFlag.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 当前
|
||||
const nowTime = () => {
|
||||
timeChange(interval.value)
|
||||
}
|
||||
// 上一个
|
||||
const preClick = () => {
|
||||
let startTime = timeValue.value[0]
|
||||
let endTime = timeValue.value[1]
|
||||
let year = parseInt(startTime.substring(0, 4))
|
||||
let month = parseInt(startTime.substring(5, 7))
|
||||
let date = parseInt(startTime.substring(8, 10))
|
||||
//按月
|
||||
if (interval.value == 3) {
|
||||
// 换年份
|
||||
if (month == 1) {
|
||||
year = year - 1
|
||||
startTime = year + '-12-01'
|
||||
endTime = year + '-12-31'
|
||||
} else if (month <= 10) {
|
||||
month = month - 1
|
||||
startTime = year + '-0' + month + '-01'
|
||||
let day = getDays(year, month)
|
||||
endTime = year + '-0' + month + '-' + day
|
||||
} else {
|
||||
month = month - 1
|
||||
startTime = year + '-' + month + '-01'
|
||||
let day = getDays(year, month)
|
||||
endTime = year + '-' + month + '-' + day
|
||||
}
|
||||
//按周
|
||||
} else if (interval.value == 4) {
|
||||
//根据开始时间推
|
||||
let start = new Date(year, month - 1, date)
|
||||
start.setDate(start.getDate() - 7)
|
||||
startTime = formatTime(start)
|
||||
var end = new Date(start)
|
||||
end.setDate(start.getDate() + 6)
|
||||
endTime = formatTime(end)
|
||||
//按季度
|
||||
} else if (interval.value == 2) {
|
||||
// 换年份
|
||||
if (month == 1) {
|
||||
year = year - 1
|
||||
startTime = year + '-10-01'
|
||||
endTime = year + '-12-31'
|
||||
} else {
|
||||
// 还是本年
|
||||
month = month - 3
|
||||
startTime = year + '-0' + month + '-01'
|
||||
month = month + 2
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-0' + month + '-' + day
|
||||
}
|
||||
//自定义
|
||||
} else if (interval.value == 1) {
|
||||
year = year - 1
|
||||
startTime = year + '-01-01'
|
||||
endTime = year + '-12-31'
|
||||
}
|
||||
timeValue.value = [startTime, endTime]
|
||||
|
||||
// 判断向后键的状态
|
||||
// var temp = NowgetEndTime()
|
||||
// timeStatus(temp, endTime)
|
||||
}
|
||||
//下一个
|
||||
const next = () => {
|
||||
//向后
|
||||
|
||||
let startTime = timeValue.value[0]
|
||||
let endTime = timeValue.value[1]
|
||||
let year = parseInt(startTime.substring(0, 4))
|
||||
let month = parseInt(startTime.substring(5, 7))
|
||||
let date = parseInt(startTime.substring(8, 10))
|
||||
var now = new Date()
|
||||
// 获取当前年份
|
||||
var presentY = now.getFullYear()
|
||||
// 获取当前月份
|
||||
var presentM = now.getMonth() + 1
|
||||
// 获取当前日期
|
||||
var presentD = now.getDate()
|
||||
if (interval.value == 3) {
|
||||
if (month == 12) {
|
||||
year = year + 1
|
||||
// 年份进位后,大于当前的年份,是不科学的
|
||||
if (presentY < year) {
|
||||
startTime = presentY + '-12-01'
|
||||
if (presentD < 10) {
|
||||
endTime = presentY + '-12' + '-0' + presentD
|
||||
} else {
|
||||
endTime = presentY + '-12' + '-' + presentD
|
||||
}
|
||||
// 年份进位后,等于当前的年份
|
||||
} else if (presentY == year) {
|
||||
startTime = year + '-01-01'
|
||||
if (presentM > 1) {
|
||||
endTime = year + '-01-31'
|
||||
} else {
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-01' + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-01' + '-' + presentD
|
||||
}
|
||||
}
|
||||
// 年份进位后,依旧小于当前的年份
|
||||
} else {
|
||||
startTime = year + '-01-01'
|
||||
endTime = year + '-01-31'
|
||||
}
|
||||
} else {
|
||||
month = month + 1
|
||||
// 年份等于当前年份
|
||||
if (presentY == year) {
|
||||
// 月份超过当前月份,是不科学的
|
||||
if (month >= presentM) {
|
||||
if (presentM < 10) {
|
||||
startTime = year + '-0' + presentM + '-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-0' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-0' + presentM + '-' + presentD
|
||||
}
|
||||
} else {
|
||||
startTime = year + '-' + presentM + '-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-' + presentM + '-' + presentD
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (month < 10) {
|
||||
startTime = year + '-0' + month + '-01'
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-0' + month + '-' + day
|
||||
} else {
|
||||
startTime = year + '-' + month + '-01'
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-' + month + '-' + day
|
||||
}
|
||||
}
|
||||
// 年份小于当前的年份
|
||||
} else {
|
||||
if (month < 10) {
|
||||
startTime = year + '-0' + month + '-01'
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-0' + month + '-' + day
|
||||
} else {
|
||||
startTime = year + '-' + month + '-01'
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-' + month + '-' + day
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (interval.value == 2) {
|
||||
// 前进需要年份进位
|
||||
if (month == 10) {
|
||||
year = year + 1
|
||||
// 年份进位后大于当前年份是不科学的
|
||||
if (year > presentY) {
|
||||
startTime = presentY + '-10-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-' + presentM + '-' + presentD
|
||||
}
|
||||
} else if (year == presentY) {
|
||||
startTime = year + '-01-01'
|
||||
// 当前月份大约3月份
|
||||
if (presentM > 3) {
|
||||
endTime = year + '-03-31'
|
||||
} else {
|
||||
// 当前月份也在第一季度里
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-0' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-0' + presentM + '-' + presentD
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTime = year + '-01-01'
|
||||
endTime = year + '-03-31'
|
||||
}
|
||||
} else {
|
||||
month = month + 3
|
||||
// 季度进位后,超过当前月份是不科学的
|
||||
if (year == presentY) {
|
||||
if (month >= presentM) {
|
||||
// 当季度进位后大于当前月,以当前月的时间显示季度
|
||||
if (presentM > 0 && presentM < 4) {
|
||||
// 第一季度
|
||||
startTime = year + '-01-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-0' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-0' + presentM + '-' + presentD
|
||||
}
|
||||
} else if (presentM > 3 && presentM < 7) {
|
||||
// 第二季度
|
||||
startTime = year + '-04-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-0' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-0' + presentM + '-' + presentD
|
||||
}
|
||||
} else if (presentM > 6 && presentM < 10) {
|
||||
// 第三季度
|
||||
startTime = year + '-07-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-0' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-0' + presentM + '-' + presentD
|
||||
}
|
||||
} else {
|
||||
// 第四季度
|
||||
startTime = year + '-10-01'
|
||||
if (presentD < 10) {
|
||||
endTime = year + '-' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = year + '-' + presentM + '-' + presentD
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (month == 10) {
|
||||
startTime = year + '-' + month + '-01'
|
||||
} else {
|
||||
startTime = year + '-0' + month + '-01'
|
||||
}
|
||||
month = month + 2
|
||||
if (month >= presentM) {
|
||||
endTime = NowgetEndTime()
|
||||
} else {
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-' + month + '-' + day
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (month == 10) {
|
||||
startTime = year + '-' + month + '-01'
|
||||
month = month + 2
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-' + month + '-' + day
|
||||
} else {
|
||||
startTime = year + '-0' + month + '-01'
|
||||
month = month + 2
|
||||
var day = getDays(year, month)
|
||||
endTime = year + '-0' + month + '-' + day
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (interval.value == 5) {
|
||||
} else if (interval.value == 4) {
|
||||
//根据开始时间推
|
||||
var start = new Date(year, month - 1, date)
|
||||
start.setDate(start.getDate() + 7)
|
||||
startTime = formatTime(start)
|
||||
var end = new Date(start)
|
||||
end.setDate(start.getDate() + 6)
|
||||
endTime = formatTime(end)
|
||||
} else {
|
||||
year = year + 1
|
||||
// 年份进位后大于当前年份,是不科学的
|
||||
if (year >= presentY) {
|
||||
startTime = presentY + '-01-01'
|
||||
if (presentM < 10) {
|
||||
if (presentD < 10) {
|
||||
endTime = presentY + '-0' + presentM + '-0' + presentD
|
||||
} else {
|
||||
endTime = presentY + '-0' + presentM + '-' + presentD
|
||||
}
|
||||
} else {
|
||||
endTime = presentY + '-' + presentM + '-' + presentD
|
||||
}
|
||||
} else {
|
||||
startTime = year + '-01-01'
|
||||
endTime = year + '-12-31'
|
||||
}
|
||||
}
|
||||
|
||||
timeValue.value = [startTime, endTime]
|
||||
}
|
||||
|
||||
const setTime = (flag = 0, e = 0) => {
|
||||
let dd = window.XEUtils.toDateString(new Date().getTime() - e * 3600 * 1000 * 24, 'dd')
|
||||
let data = ''
|
||||
if (dd < 4) {
|
||||
data = window.XEUtils.toDateString(new Date().getTime() - (e + dd) * 3600 * 1000 * 24, 'yyyy-MM-dd')
|
||||
} else {
|
||||
data = window.XEUtils.toDateString(new Date().getTime() - e * 3600 * 1000 * 24, 'yyyy-MM-dd')
|
||||
}
|
||||
|
||||
if (flag == 1) {
|
||||
data = data.slice(0, 5) + '01-01'
|
||||
} else if (flag == 2) {
|
||||
let quarter = parseInt(data.slice(5, 7))
|
||||
if (0 < quarter && quarter <= 3) {
|
||||
data = data.slice(0, 5) + '01-01'
|
||||
} else if (3 < quarter && quarter <= 6) {
|
||||
data = data.slice(0, 5) + '04-01'
|
||||
} else if (6 < quarter && quarter <= 9) {
|
||||
data = data.slice(0, 5) + '07-01'
|
||||
} else {
|
||||
data = data.slice(0, 5) + '10-01'
|
||||
}
|
||||
}
|
||||
if (flag == 3) {
|
||||
data = data.slice(0, 8) + '01'
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// 获取月份的天数
|
||||
const getDays = (year: any, month: any) => {
|
||||
let max = new Date(year, month, 0).getDate()
|
||||
return max
|
||||
}
|
||||
// 时间格式化
|
||||
const formatTime = (time: any) => {
|
||||
return (
|
||||
time.getFullYear() +
|
||||
'-' +
|
||||
(time.getMonth() + 1 < 10 ? '0' : '') +
|
||||
(time.getMonth() + 1) +
|
||||
'-' +
|
||||
(time.getDate() < 10 ? '0' : '') +
|
||||
time.getDate()
|
||||
)
|
||||
}
|
||||
const NowgetEndTime = () => {
|
||||
let now = new Date()
|
||||
let sep = '-'
|
||||
let year = now.getFullYear()
|
||||
let month: any = now.getMonth() + 1
|
||||
if (month < 10) {
|
||||
month = '0' + month
|
||||
}
|
||||
let date: any = now.getDate()
|
||||
if (date < 10) {
|
||||
date = '0' + date
|
||||
}
|
||||
|
||||
// 拼接当前的日期
|
||||
let endTime = year + sep + month + sep + date
|
||||
return endTime
|
||||
}
|
||||
defineExpose({ timeValue, interval, timeFlag })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-date-picker {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.demo-date-picker .block {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
border-right: solid 1px var(--el-border-color);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo-date-picker .block:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.demo-date-picker .demonstration {
|
||||
display: block;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
43
src/components/icon/index.vue
Normal file
43
src/components/icon/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { createVNode, resolveComponent, defineComponent, computed, type CSSProperties } from 'vue'
|
||||
import svg from '@/components/icon/svg/index.vue'
|
||||
import { isExternal } from '@/utils/common'
|
||||
export default defineComponent({
|
||||
name: 'Icon',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '18px'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const iconStyle = computed((): CSSProperties => {
|
||||
const { size, color } = props
|
||||
let s = `${size.replace('px', '')}px`
|
||||
return {
|
||||
fontSize: s,
|
||||
color: color
|
||||
}
|
||||
})
|
||||
|
||||
if (props.name.indexOf('el-icon-') === 0) {
|
||||
return () =>
|
||||
createVNode('el-icon', { class: 'icon el-icon', style: iconStyle.value }, [
|
||||
createVNode(resolveComponent(props.name))
|
||||
])
|
||||
} else if (props.name.indexOf('local-') === 0 || isExternal(props.name)) {
|
||||
return () => createVNode(svg, { name: props.name, size: props.size, color: props.color })
|
||||
} else {
|
||||
return () => createVNode('i', { class: [props.name, 'icon'], style: iconStyle.value })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
71
src/components/icon/svg/index.ts
Normal file
71
src/components/icon/svg/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { readFileSync, readdirSync } from 'fs'
|
||||
|
||||
let idPerfix = ''
|
||||
const iconNames: string[] = []
|
||||
const svgTitle = /<svg([^>+].*?)>/
|
||||
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
|
||||
const hasViewBox = /(viewBox="[^>+].*?")/g
|
||||
const clearReturn = /(\r)|(\n)/g
|
||||
// 清理 svg 的 fill
|
||||
const clearFill = /(fill="[^>+].*?")/g
|
||||
|
||||
function findSvgFile(dir: string): string[] {
|
||||
const svgRes = []
|
||||
const dirents = readdirSync(dir, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
for (const dirent of dirents) {
|
||||
iconNames.push(`${idPerfix}-${dirent.name.replace('.svg', '')}`)
|
||||
if (dirent.isDirectory()) {
|
||||
svgRes.push(...findSvgFile(dir + dirent.name + '/'))
|
||||
} else {
|
||||
const svg = readFileSync(dir + dirent.name)
|
||||
.toString()
|
||||
.replace(clearReturn, '')
|
||||
.replace(clearFill, 'fill=""')
|
||||
.replace(svgTitle, ($1, $2) => {
|
||||
let width = 0
|
||||
let height = 0
|
||||
let content = $2.replace(clearHeightWidth, (s1: string, s2: string, s3: number) => {
|
||||
if (s2 === 'width') {
|
||||
width = s3
|
||||
} else if (s2 === 'height') {
|
||||
height = s3
|
||||
}
|
||||
return ''
|
||||
})
|
||||
if (!hasViewBox.test($2)) {
|
||||
content += `viewBox="0 0 ${width} ${height}"`
|
||||
}
|
||||
return `<symbol id="${idPerfix}-${dirent.name.replace('.svg', '')}" ${content}>`
|
||||
})
|
||||
.replace('</svg>', '</symbol>')
|
||||
svgRes.push(svg)
|
||||
}
|
||||
}
|
||||
return svgRes
|
||||
}
|
||||
|
||||
export const svgBuilder = (path: string, perfix = 'local') => {
|
||||
if (path === '') return
|
||||
idPerfix = perfix
|
||||
const res = findSvgFile(path)
|
||||
return {
|
||||
name: 'svg-transform',
|
||||
transformIndexHtml(html: string) {
|
||||
/* eslint-disable */
|
||||
return html.replace(
|
||||
'<body>',
|
||||
`
|
||||
<body>
|
||||
<svg id="local-icon" data-icon-name="${iconNames.join(
|
||||
','
|
||||
)}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">
|
||||
${res.join('')}
|
||||
</svg>
|
||||
`
|
||||
)
|
||||
/* eslint-enable */
|
||||
},
|
||||
}
|
||||
}
|
||||
49
src/components/icon/svg/index.vue
Normal file
49
src/components/icon/svg/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div v-if="isUrl" :style="urlIconStyle" class="url-svg svg-icon icon" />
|
||||
<svg v-else class="svg-icon icon" :style="iconStyle">
|
||||
<use :href="iconName" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type CSSProperties } from 'vue'
|
||||
import { isExternal } from '@/utils/common'
|
||||
interface Props {
|
||||
name: string
|
||||
size: string
|
||||
color: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
name: '',
|
||||
size: '18px',
|
||||
color: '#000000',
|
||||
})
|
||||
|
||||
const s = `${props.size.replace('px', '')}px`
|
||||
const iconName = computed(() => `#${props.name}`)
|
||||
const iconStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
color: props.color,
|
||||
fontSize: s,
|
||||
}
|
||||
})
|
||||
const isUrl = computed(() => isExternal(props.name))
|
||||
const urlIconStyle = computed(() => {
|
||||
return {
|
||||
width: s,
|
||||
height: s,
|
||||
mask: `url(${props.name}) no-repeat 50% 50%`,
|
||||
'-webkit-mask': `url(${props.name}) no-repeat 50% 50%`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
23
src/components/table/column/index.vue
Normal file
23
src/components/table/column/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang='ts'>
|
||||
import { defineComponent, createVNode, reactive } from 'vue'
|
||||
import { Column } from 'vxe-table'
|
||||
import { uuid } from '@/utils/random'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Column',
|
||||
props: {
|
||||
attr: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const attr = reactive(props.attr)
|
||||
attr['align'] = attr['align'] ? attr['align'] : 'center'
|
||||
attr['column-key'] = attr['column-key'] ? attr['column-key'] : attr.prop || uuid()
|
||||
return () => {
|
||||
return createVNode(Column, attr, slots.default)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
17
src/components/table/defaultAttribute.ts
Normal file
17
src/components/table/defaultAttribute.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { VxeTableProps } from 'vxe-table'
|
||||
|
||||
export const defaultAttribute: VxeTableProps = {
|
||||
align: 'center',
|
||||
headerCellClassName: 'table-header-cell',
|
||||
border: true,
|
||||
stripe: true,
|
||||
size: 'small',
|
||||
columnConfig: { resizable: true },
|
||||
rowConfig: { isCurrent: true, isHover: true,keyField: 'id' },
|
||||
scrollX: { scrollToLeftOnChange: true },
|
||||
scrollY: { scrollToTopOnChange: true, enabled: true },
|
||||
treeConfig: {
|
||||
reserve: true
|
||||
},
|
||||
showOverflow:true
|
||||
}
|
||||
277
src/components/table/fieldRender/index.vue
Normal file
277
src/components/table/fieldRender/index.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<!-- Icon -->
|
||||
<Icon class="ba-icon-dark" v-if="field.render == 'icon'" :name="fieldValue ? fieldValue : field.default ?? ''" />
|
||||
|
||||
<!-- switch -->
|
||||
<el-switch
|
||||
v-if="field.render == 'switch'"
|
||||
@change="onChangeField"
|
||||
:model-value="fieldValue.toString()"
|
||||
:loading="row.loading"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
/>
|
||||
|
||||
<!-- image -->
|
||||
<div v-if="field.render == 'image' && fieldValue" class="ba-render-image">
|
||||
<el-image
|
||||
:hide-on-click-modal="true"
|
||||
:preview-teleported="true"
|
||||
:preview-src-list="[fullUrl(fieldValue)]"
|
||||
:src="fullUrl(fieldValue)"
|
||||
></el-image>
|
||||
</div>
|
||||
|
||||
<!-- tag -->
|
||||
<div v-if="field.render == 'tag' && fieldValue !== ''">
|
||||
<el-tag :type="getTagType(fieldValue, field.custom)" size="small">
|
||||
{{ field.replaceValue ? field.replaceValue[fieldValue] : fieldValue }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- datetime -->
|
||||
<div v-if="field.render == 'datetime'">
|
||||
{{ !fieldValue ? '/' : timeFormat(fieldValue, field.timeFormat ?? undefined) }}
|
||||
</div>
|
||||
|
||||
<!-- color -->
|
||||
<div v-if="field.render == 'color'">
|
||||
<div :style="{ background: fieldValue }" class="ba-render-color"></div>
|
||||
</div>
|
||||
|
||||
<!-- customTemplate 自定义模板 -->
|
||||
<div
|
||||
v-if="field.render == 'customTemplate'"
|
||||
v-html="field.customTemplate ? field.customTemplate(row, field, fieldValue, column, index) : ''"
|
||||
></div>
|
||||
|
||||
<!-- 自定义组件/函数渲染 -->
|
||||
<component
|
||||
v-if="field.render == 'customRender'"
|
||||
:is="field.customRender"
|
||||
:renderRow="row"
|
||||
:renderField="field"
|
||||
:renderValue="fieldValue"
|
||||
:renderColumn="column"
|
||||
:renderIndex="index"
|
||||
/>
|
||||
|
||||
<!-- 按钮组 -->
|
||||
<div v-if="field.render == 'buttons' && buttons" class="cn-render-buttons">
|
||||
<template v-for="(btn, idx) in buttons" :key="idx">
|
||||
<!-- 常规按钮 -->
|
||||
<el-button
|
||||
link
|
||||
v-if="btn.render == 'basicButton'"
|
||||
@click="onButtonClick(btn)"
|
||||
:class="btn.class"
|
||||
class="table-operate"
|
||||
:type="btn.type"
|
||||
:loading="props.row.loading || false"
|
||||
v-bind="btn.attr"
|
||||
>
|
||||
<div v-if="btn.text || btn.title" class="table-operate-text">{{ btn.text || btn.title }}</div>
|
||||
</el-button>
|
||||
|
||||
<!-- 带提示信息的按钮 -->
|
||||
<el-tooltip
|
||||
v-if="btn.render == 'tipButton'"
|
||||
:disabled="btn.title && !btn.disabledTip ? false : true"
|
||||
:content="btn.title"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
@click="onButtonClick(btn)"
|
||||
:class="btn.class"
|
||||
class="table-operate"
|
||||
:type="btn.type"
|
||||
v-bind="btn.attr"
|
||||
>
|
||||
<div v-if="btn.text || btn.title" class="table-operate-text">
|
||||
{{ btn.text || btn.title }}
|
||||
</div>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 带确认框的按钮 -->
|
||||
<el-popconfirm
|
||||
v-if="btn.render == 'confirmButton'"
|
||||
:disabled="btn.disabled && btn.disabled(row, field)"
|
||||
v-bind="btn.popconfirm"
|
||||
@confirm="onButtonClick(btn)"
|
||||
>
|
||||
<template #reference>
|
||||
<div style="display: inline-block">
|
||||
<el-button link :class="btn.class" class="table-operate" :type="btn.type" v-bind="btn.attr">
|
||||
<div v-if="btn.text || btn.title" class="table-operate-text">
|
||||
{{ btn.text || btn.title }}
|
||||
</div>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
<el-dropdown v-if="btn.render == 'dropdown'" trigger="click" @command="handlerCommand">
|
||||
<el-button link type="primary" class="table-operate">
|
||||
<div class="table-operate-text">更多</div>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in btn.buttons"
|
||||
:key="item.text"
|
||||
:command="item"
|
||||
:style="{
|
||||
color: item.type === 'primary' ? 'var(--el-color-primary)' : 'var(--el-color-danger'
|
||||
}"
|
||||
>
|
||||
{{ item.text }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from 'vue'
|
||||
import { ElMessageBox, type TagProps } from 'element-plus'
|
||||
import type TableStoreClass from '@/utils/tableStore'
|
||||
import { fullUrl, timeFormat } from '@/utils/common'
|
||||
import type { VxeColumnProps } from 'vxe-table'
|
||||
|
||||
const TableStore = inject('tableStore') as TableStoreClass
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: VxeColumnProps
|
||||
index: number
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 字段值(单元格值)
|
||||
const fieldName = ref(props.field.field)
|
||||
const fieldValue = ref(fieldName.value ? props.row[fieldName.value] : '')
|
||||
if (fieldName.value && fieldName.value.indexOf('.') > -1) {
|
||||
let fieldNameArr = fieldName.value.split('.')
|
||||
let val: any = ref(props.row[fieldNameArr[0]])
|
||||
for (let index = 1; index < fieldNameArr.length; index++) {
|
||||
val.value = val.value ? val.value[fieldNameArr[index]] ?? '' : ''
|
||||
}
|
||||
fieldValue.value = val.value
|
||||
}
|
||||
|
||||
if (props.field.renderFormatter && typeof props.field.renderFormatter == 'function') {
|
||||
fieldValue.value = props.field.renderFormatter(props.row, props.field, fieldValue.value, props.column, props.index)
|
||||
}
|
||||
|
||||
const onChangeField = (value: any) => {
|
||||
TableStore.onTableAction('field-change', { value: value, ...props })
|
||||
}
|
||||
|
||||
const onButtonClick = (btn: OptButton) => {
|
||||
btn.click && btn.click(props.row, props.field)
|
||||
}
|
||||
|
||||
const getTagType = (value: string, custom: any): TagProps['type'] => {
|
||||
return custom && custom[value] ? custom[value] : ''
|
||||
}
|
||||
|
||||
// 按钮组处理 最多显示三个按钮 多余的显示为下拉
|
||||
const buttonsFilter = props.field.buttons?.filter(btn => !(btn.disabled && btn.disabled(props.row, props.field))) || []
|
||||
const buttons = ref<any[]>([])
|
||||
if (buttonsFilter.length > 3) {
|
||||
buttonsFilter?.forEach((btn, index) => {
|
||||
btn.text = btn.text || btn.title
|
||||
if (index < 2) {
|
||||
buttons.value.push(btn)
|
||||
} else {
|
||||
if (buttons.value.length > 2) {
|
||||
buttons.value[buttons.value.length - 1].buttons.push(btn)
|
||||
} else {
|
||||
buttons.value.push({
|
||||
render: 'dropdown',
|
||||
buttons: [btn]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
buttons.value = buttonsFilter
|
||||
}
|
||||
const handlerCommand = (item: OptButton) => {
|
||||
switch (item.render) {
|
||||
case 'basicButton':
|
||||
onButtonClick(item)
|
||||
break
|
||||
case 'confirmButton':
|
||||
ElMessageBox.confirm(item.popconfirm?.title || '提示', {
|
||||
confirmButtonText: item.popconfirm?.confirmButtonText || '确认',
|
||||
cancelButtonText: item.popconfirm?.cancelButtonText || '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
onButtonClick(item)
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.m-10 {
|
||||
margin: 4px;
|
||||
}
|
||||
.ba-render-image {
|
||||
text-align: center;
|
||||
}
|
||||
.images-item {
|
||||
width: 50px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.el-image {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
.table-operate-text {
|
||||
padding-left: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.table-operate {
|
||||
padding: 4px 5px;
|
||||
height: auto;
|
||||
}
|
||||
.cn-render-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.icon {
|
||||
font-size: 12px !important;
|
||||
// color: var(--ba-bg-color-overlay) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.move-button {
|
||||
cursor: move;
|
||||
}
|
||||
.ml-6 {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.ml-6 + .el-button {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.ba-render-color {
|
||||
height: 25px;
|
||||
width: 100%;
|
||||
}
|
||||
.cn-render-buttons {
|
||||
:deep(.el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
282
src/components/table/header/index.vue
Normal file
282
src/components/table/header/index.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div ref="tableHeader" class="cn-table-header">
|
||||
<div class="table-header ba-scroll-style">
|
||||
<el-form
|
||||
style="flex: 1; height: 32px; overflow: hidden; margin-right: 20px"
|
||||
id="header-form"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="onComSearch"
|
||||
label-position="left"
|
||||
:inline="true"
|
||||
>
|
||||
<el-form-item label="区域" v-if="area">
|
||||
<Area ref="areaRef" v-model="tableStore.table.params.deptIndex" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日期" v-if="datePicker" style="grid-column: span 2; max-width: unset">
|
||||
<DatePicker ref="datePickerRef"></DatePicker>
|
||||
</el-form-item>
|
||||
<slot name="select"></slot>
|
||||
</el-form>
|
||||
<template v-if="$slots.select || datePicker">
|
||||
<el-button type="primary" @click="showSelectChange" v-if="showUnfoldButton">
|
||||
<Icon size="14" name="el-icon-ArrowUp" style="color: #fff" v-if="showSelect" />
|
||||
<Icon size="14" name="el-icon-ArrowDown" style="color: #fff" v-else />
|
||||
</el-button>
|
||||
<el-button @click="onComSearch" type="primary" :icon="Search">查询</el-button>
|
||||
<el-button @click="onResetForm" :icon="RefreshLeft">重置</el-button>
|
||||
</template>
|
||||
<slot name="operation"></slot>
|
||||
</div>
|
||||
<el-form
|
||||
:style="showSelect && showUnfoldButton ? headerFormSecondStyleOpen : headerFormSecondStyleClose"
|
||||
id="header-form-second"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="onComSearch"
|
||||
label-position="left"
|
||||
:inline="true"
|
||||
>
|
||||
<el-form-item label="区域" v-if="area">
|
||||
<Area ref="areaRef" v-model="tableStore.table.params.deptIndex" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日期" v-if="datePicker" style="grid-column: span 2; max-width: unset">
|
||||
<DatePicker ref="datePickerRef"></DatePicker>
|
||||
</el-form-item>
|
||||
<slot name="select"></slot>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref, onMounted, nextTick, onUnmounted } from 'vue'
|
||||
import type TableStore from '@/utils/tableStore'
|
||||
import DatePicker from '@/components/form/datePicker/index.vue'
|
||||
import Area from '@/components/form/area/index.vue'
|
||||
import { mainHeight } from '@/utils/layout'
|
||||
import { useDictData } from '@/stores/dictData'
|
||||
import { Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
|
||||
const tableStore = inject('tableStore') as TableStore
|
||||
const tableHeader = ref()
|
||||
const datePickerRef = ref()
|
||||
const dictData = useDictData()
|
||||
const areaRef = ref()
|
||||
|
||||
interface Props {
|
||||
datePicker?: boolean
|
||||
area?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
datePicker: false,
|
||||
area: false
|
||||
})
|
||||
// 动态计算table高度
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
handlerHeight()
|
||||
computedSearchRow()
|
||||
}
|
||||
})
|
||||
const showUnfoldButton = ref(false)
|
||||
const headerFormSecondStyleOpen = {
|
||||
opacity: 1,
|
||||
height: 'auto',
|
||||
padding: '0 15px 13px 15px'
|
||||
}
|
||||
const headerFormSecondStyleClose = {
|
||||
opacity: 0,
|
||||
height: '0',
|
||||
padding: '0'
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.datePicker) {
|
||||
tableStore.table.params.searchBeginTime = datePickerRef.value.timeValue[0]
|
||||
tableStore.table.params.searchEndTime = datePickerRef.value.timeValue[1]
|
||||
tableStore.table.params.startTime = datePickerRef.value.timeValue[0]
|
||||
tableStore.table.params.endTime = datePickerRef.value.timeValue[1]
|
||||
tableStore.table.params.timeFlag = datePickerRef.value.timeFlag
|
||||
}
|
||||
if (props.area) {
|
||||
tableStore.table.params.deptIndex = dictData.state.area[0].id
|
||||
}
|
||||
nextTick(() => {
|
||||
resizeObserver.observe(tableHeader.value)
|
||||
computedSearchRow()
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
resizeObserver.disconnect()
|
||||
})
|
||||
|
||||
const handlerHeight = () => {
|
||||
tableStore.table.height = mainHeight(
|
||||
tableStore.table.publicHeight + tableHeader.value.offsetHeight + (tableStore.showPage ? 58 : 0) + 20
|
||||
).height as string
|
||||
}
|
||||
const computedSearchRow = () => {
|
||||
const headerForm = document.getElementById('header-form') as HTMLElement
|
||||
const headerFormSecond = document.getElementById('header-form-second') as HTMLElement
|
||||
if (!headerForm) return
|
||||
// 判断是否需要折叠
|
||||
if (headerForm.scrollHeight > 50) {
|
||||
showUnfoldButton.value = true
|
||||
} else {
|
||||
showUnfoldButton.value = false
|
||||
}
|
||||
|
||||
// 清空headerFormSecond下的元素
|
||||
while (headerFormSecond.firstChild) {
|
||||
headerFormSecond.removeChild(headerFormSecond.firstChild)
|
||||
}
|
||||
|
||||
// 获取第一行放了几个表单
|
||||
const elFormItem = document.querySelectorAll('#header-form .el-form-item') as NodeListOf<HTMLElement>
|
||||
|
||||
// 把第一行放不下的复制一份放到headerFormSecond
|
||||
let width = 0
|
||||
for (let i = 0; i < elFormItem.length; i++) {
|
||||
width += elFormItem[i].offsetWidth + 32
|
||||
if (width > headerForm.offsetWidth) {
|
||||
const clonedForm = elFormItem[i].cloneNode(true)
|
||||
headerFormSecond.appendChild(clonedForm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showSelect = ref(false)
|
||||
const showSelectChange = () => {
|
||||
showSelect.value = !showSelect.value
|
||||
}
|
||||
const onComSearch = async () => {
|
||||
if (props.datePicker) {
|
||||
tableStore.table.params.searchBeginTime = datePickerRef.value.timeValue[0]
|
||||
tableStore.table.params.searchEndTime = datePickerRef.value.timeValue[1]
|
||||
tableStore.table.params.startTime = datePickerRef.value.timeValue[0]
|
||||
tableStore.table.params.endTime = datePickerRef.value.timeValue[1]
|
||||
tableStore.table.params.timeFlag = datePickerRef.value.timeFlag
|
||||
}
|
||||
|
||||
await tableStore.onTableAction('search', {})
|
||||
}
|
||||
const onResetForm = () => {
|
||||
tableStore.onTableAction('reset', {})
|
||||
}
|
||||
defineExpose({ onComSearch, areaRef })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-header {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-bottom: none;
|
||||
padding: 13px 15px;
|
||||
font-size: 14px;
|
||||
|
||||
.table-header-operate-text {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-com-search {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border: 1px solid var(--ba-border-color);
|
||||
border-bottom: none;
|
||||
padding: 13px 15px 20px 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#header-form-second,
|
||||
#header-form {
|
||||
// display: flex;
|
||||
// flex-wrap: wrap;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.mlr-12 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.mlr-12 + .el-button {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.table-search {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
|
||||
.quick-search {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table-search-button-group {
|
||||
display: flex;
|
||||
margin-left: 12px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
overflow: hidden;
|
||||
|
||||
button:focus,
|
||||
button:active {
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--el-color-info-light-7);
|
||||
}
|
||||
|
||||
.table-search-button-item {
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.el-button + .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-border {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
.table-search-button-group {
|
||||
button:focus,
|
||||
button:active {
|
||||
background-color: var(--el-color-info-dark-2);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--el-color-info-light-7);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
|
||||
el-icon {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#header-form,
|
||||
#header-form-second {
|
||||
:deep(.el-select) {
|
||||
--el-select-width: 220px;
|
||||
}
|
||||
:deep(.el-input) {
|
||||
--el-input-width: 220px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
179
src/components/table/index.vue
Normal file
179
src/components/table/index.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div :style="{ height: tableStore.table.height }">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
height="auto"
|
||||
:data="tableStore.table.data"
|
||||
v-loading="tableStore.table.loading"
|
||||
v-bind="Object.assign({}, defaultAttribute, $attrs)"
|
||||
@checkbox-all="selectChangeEvent"
|
||||
@checkbox-change="selectChangeEvent"
|
||||
>
|
||||
<!-- Column 组件内部是 el-table-column -->
|
||||
<template v-if="isGroup">
|
||||
<vxe-table-colgroup
|
||||
v-if="isGroup"
|
||||
v-for="(item, index) in tableStore.table.column"
|
||||
:field="item.field"
|
||||
:title="item.title"
|
||||
:key="index"
|
||||
:min-width="item.width"
|
||||
show-overflow
|
||||
align="center"
|
||||
>
|
||||
<Column
|
||||
:attr="child"
|
||||
:key="key + '-column'"
|
||||
v-for="(child, key) in item.children"
|
||||
:tree-node="child.treeNode"
|
||||
>
|
||||
<!-- tableStore 预设的列 render 方案 -->
|
||||
<template v-if="child.render" #default="scope">
|
||||
<FieldRender
|
||||
:field="child"
|
||||
:row="scope.row"
|
||||
:column="scope.column"
|
||||
:index="scope.rowIndex"
|
||||
:key="
|
||||
key +
|
||||
'-' +
|
||||
child.render +
|
||||
'-' +
|
||||
(child.field ? '-' + child.field + '-' + scope.row[child.field] : '')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</vxe-table-colgroup>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Column
|
||||
:attr="item"
|
||||
:key="key + '-column'"
|
||||
v-for="(item, key) in tableStore.table.column"
|
||||
:tree-node="item.treeNode"
|
||||
>
|
||||
<!-- tableStore 预设的列 render 方案 -->
|
||||
<template v-if="item.render" #default="scope">
|
||||
<FieldRender
|
||||
:field="item"
|
||||
:row="scope.row"
|
||||
:column="scope.column"
|
||||
:index="scope.rowIndex"
|
||||
:key="
|
||||
key +
|
||||
'-' +
|
||||
item.render +
|
||||
'-' +
|
||||
(item.field ? '-' + item.field + '-' + scope.row[item.field] : '')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</template>
|
||||
<slot name="columns"></slot>
|
||||
</vxe-table>
|
||||
</div>
|
||||
|
||||
<div v-if="tableStore.showPage" class="table-pagination">
|
||||
<el-pagination
|
||||
:currentPage="tableStore.table.params!.pageNum"
|
||||
:page-size="tableStore.table.params!.pageSize"
|
||||
:page-sizes="pageSizes"
|
||||
background
|
||||
:layout="config.layout.shrink ? 'prev, next, jumper' : 'sizes,total, ->, prev, pager, next, jumper'"
|
||||
:total="tableStore.table.total"
|
||||
@size-change="onTableSizeChange"
|
||||
@current-change="onTableCurrentChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, inject, computed, onMounted } from 'vue'
|
||||
import type { ElTable } from 'element-plus'
|
||||
import { VxeTableEvents, VxeTableInstance } from 'vxe-table'
|
||||
import FieldRender from '@/components/table/fieldRender/index.vue'
|
||||
import Column from '@/components/table/column/index.vue'
|
||||
import { useConfig } from '@/stores/config'
|
||||
import type TableStoreClass from '@/utils/tableStore'
|
||||
import { defaultAttribute } from '@/components/table/defaultAttribute'
|
||||
|
||||
const config = useConfig()
|
||||
const tableRef = ref<VxeTableInstance>()
|
||||
const tableStore = inject('tableStore') as TableStoreClass
|
||||
|
||||
interface Props extends /* @vue-ignore */ Partial<InstanceType<typeof ElTable>> {
|
||||
isGroup?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isGroup: false
|
||||
})
|
||||
onMounted(() => {
|
||||
tableStore.table.ref = tableRef.value as VxeTableInstance
|
||||
})
|
||||
console.log(props)
|
||||
const onTableSizeChange = (val: number) => {
|
||||
tableStore.onTableAction('page-size-change', { size: val })
|
||||
}
|
||||
|
||||
const onTableCurrentChange = (val: number) => {
|
||||
tableStore.onTableAction('current-page-change', { page: val })
|
||||
}
|
||||
|
||||
const pageSizes = computed(() => {
|
||||
let defaultSizes = [10, 20, 50, 100]
|
||||
if (tableStore.table.params!.pageSize) {
|
||||
if (!defaultSizes.includes(tableStore.table.params!.pageSize)) {
|
||||
defaultSizes.push(tableStore.table.params!.pageSize)
|
||||
}
|
||||
}
|
||||
return defaultSizes
|
||||
})
|
||||
|
||||
/*
|
||||
* 记录选择的项
|
||||
*/
|
||||
const selectChangeEvent: VxeTableEvents.CheckboxChange<any> = ({ checked }) => {
|
||||
const $table = tableRef.value
|
||||
if ($table) {
|
||||
const records = $table.getCheckboxRecords()
|
||||
tableStore.onTableAction('selection-change', records)
|
||||
}
|
||||
}
|
||||
|
||||
const getRef = () => {
|
||||
return tableRef.value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getRef
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-data-table :deep(.el-button + .el-button) {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.ba-data-table :deep(.table-header-cell) .cell {
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
height: 58px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
padding: 13px 15px;
|
||||
border-left: 1px solid #e4e7e9;
|
||||
border-right: 1px solid #e4e7e9;
|
||||
border-bottom: 1px solid #e4e7e9;
|
||||
}
|
||||
</style>
|
||||
73
src/components/tree/govern/deviceTree.vue
Normal file
73
src/components/tree/govern/deviceTree.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<Tree
|
||||
ref="treRef"
|
||||
@check-change="handleCheckChange"
|
||||
:default-checked-keys="defaultCheckedKeys"
|
||||
:show-checkbox="props.showCheckbox"
|
||||
:data="tree"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import Tree from '../index.vue'
|
||||
import { getDeviceTree } from '@/api/cs-device-boot/csLedger'
|
||||
import { useConfig } from '@/stores/config'
|
||||
defineOptions({
|
||||
name: 'govern/deviceTree'
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showCheckbox?: boolean
|
||||
defaultCheckedKeys?: any
|
||||
}>(),
|
||||
{
|
||||
showCheckbox: false,
|
||||
defaultCheckedKeys: []
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits(['init', 'checkChange'])
|
||||
const config = useConfig()
|
||||
const tree = ref()
|
||||
const treRef = ref()
|
||||
getDeviceTree().then(res => {
|
||||
let arr: any[] = []
|
||||
res.data.forEach((item: any) => {
|
||||
item.icon = 'el-icon-HomeFilled'
|
||||
item.color = config.getColorVal('elementUiPrimary')
|
||||
item.children.forEach((item2: any) => {
|
||||
item2.icon = 'el-icon-List'
|
||||
item2.color = config.getColorVal('elementUiPrimary')
|
||||
item2.children.forEach((item3: any) => {
|
||||
item3.icon = 'el-icon-Platform'
|
||||
item3.color = config.getColorVal('elementUiPrimary')
|
||||
if (item3.comFlag === 1) {
|
||||
item3.color = '#e26257 !important'
|
||||
}
|
||||
arr.push(item3)
|
||||
})
|
||||
})
|
||||
})
|
||||
tree.value = res.data
|
||||
nextTick(() => {
|
||||
if (arr.length) {
|
||||
treRef.value.treeRef.setCurrentKey(arr[0].id)
|
||||
// 注册父组件事件
|
||||
emit('init', {
|
||||
level: 2,
|
||||
...arr[0]
|
||||
})
|
||||
} else {
|
||||
emit('init')
|
||||
}
|
||||
})
|
||||
})
|
||||
const handleCheckChange = (data: any, checked: any, indeterminate: any) => {
|
||||
emit('checkChange', {
|
||||
data,
|
||||
checked,
|
||||
indeterminate
|
||||
})
|
||||
}
|
||||
</script>
|
||||
29
src/components/tree/govern/getMarketList.vue
Normal file
29
src/components/tree/govern/getMarketList.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<Tree ref="treRef" :data="tree" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { getMarketList } from '@/api/user-boot/user'
|
||||
import Tree from '../index.vue'
|
||||
import { useConfig } from '@/stores/config'
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
const config = useConfig()
|
||||
const tree = ref()
|
||||
const treRef = ref()
|
||||
const emit = defineEmits(['selectUser'])
|
||||
getMarketList().then((res: any) => {
|
||||
if (res.code === 'A0000') {
|
||||
tree.value = res.data.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
icon: 'el-icon-User',
|
||||
color: 'royalblue'
|
||||
}
|
||||
})
|
||||
emit('selectUser', tree.value[0])
|
||||
nextTick(() => {
|
||||
treRef.value.treeRef.setCurrentKey(tree.value[0].id)
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
52
src/components/tree/govern/pointTree.vue
Normal file
52
src/components/tree/govern/pointTree.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<Tree ref="treRef" :data="tree" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
import Tree from '../index.vue'
|
||||
import { getLineTree } from '@/api/cs-device-boot/csLedger'
|
||||
import { useConfig } from '@/stores/config'
|
||||
defineOptions({
|
||||
name: 'govern/deviceTree'
|
||||
})
|
||||
const emit = defineEmits(['init'])
|
||||
const config = useConfig()
|
||||
const tree = ref()
|
||||
const treRef = ref()
|
||||
getLineTree().then(res => {
|
||||
let arr: any[] = []
|
||||
res.data.forEach((item: any) => {
|
||||
item.icon = 'el-icon-HomeFilled'
|
||||
item.color = config.getColorVal('elementUiPrimary')
|
||||
item.children.forEach((item2: any) => {
|
||||
item2.icon = 'el-icon-List'
|
||||
item.color = config.getColorVal('elementUiPrimary')
|
||||
item2.children.forEach((item3: any) => {
|
||||
item3.icon = 'el-icon-Platform'
|
||||
item3.color = config.getColorVal('elementUiPrimary')
|
||||
if (item3.comFlag === 1) {
|
||||
item3.color = '#e26257 !important'
|
||||
}
|
||||
item3.children.forEach((item4: any) => {
|
||||
item4.icon = 'el-icon-LocationFilled'
|
||||
arr.push(item4)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
tree.value = res.data
|
||||
nextTick(() => {
|
||||
if (arr.length) {
|
||||
treRef.value.treeRef.setCurrentKey(arr[0].id)
|
||||
// 注册父组件事件
|
||||
emit('init', {
|
||||
level: 2,
|
||||
...arr[0]
|
||||
})
|
||||
} else {
|
||||
emit('init')
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
114
src/components/tree/index.vue
Normal file
114
src/components/tree/index.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div :style="{ width: menuCollapse ? '40px' : props.width }" style="transition: all 0.3s; overflow: hidden">
|
||||
<Icon
|
||||
v-show="menuCollapse"
|
||||
@click="onMenuCollapse"
|
||||
:name="menuCollapse ? 'el-icon-Expand' : 'el-icon-Fold'"
|
||||
:class="menuCollapse ? 'unfold' : ''"
|
||||
size="18"
|
||||
class="fold ml10 mt20 menu-collapse"
|
||||
style="cursor: pointer"
|
||||
/>
|
||||
<div class="cn-tree" :style="{ opacity: menuCollapse ? 0 : 1 }">
|
||||
<div style="display: flex; align-items: center" class="mb10">
|
||||
<el-input v-model="filterText" placeholder="请输入内容" clearable>
|
||||
<template #prefix>
|
||||
<Icon name="el-icon-Search" style="font-size: 16px" />
|
||||
</template>
|
||||
</el-input>
|
||||
<Icon
|
||||
@click="onMenuCollapse"
|
||||
:name="menuCollapse ? 'el-icon-Expand' : 'el-icon-Fold'"
|
||||
:class="menuCollapse ? 'unfold' : ''"
|
||||
size="18"
|
||||
class="fold ml10 menu-collapse"
|
||||
style="cursor: pointer"
|
||||
/>
|
||||
</div>
|
||||
<el-tree
|
||||
style="flex: 1; overflow: auto"
|
||||
ref="treeRef"
|
||||
:props="defaultProps"
|
||||
v-bind="$attrs"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
:filter-node-method="filterNode"
|
||||
node-key="id"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="custom-tree-node">
|
||||
<Icon
|
||||
:name="data.icon"
|
||||
style="font-size: 16px"
|
||||
:style="{ color: data.color }"
|
||||
v-if="data.icon"
|
||||
/>
|
||||
<span style="margin-left: 4px">{{ node.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useCurrentInstance from '@/utils/useCurrentInstance'
|
||||
import { ElTree } from 'element-plus'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'govern/tree'
|
||||
})
|
||||
interface Props {
|
||||
width?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: '280px'
|
||||
})
|
||||
const { proxy } = useCurrentInstance()
|
||||
const menuCollapse = ref(false)
|
||||
const filterText = ref('')
|
||||
const defaultProps = {
|
||||
label: 'name',
|
||||
value: 'id'
|
||||
}
|
||||
watch(filterText, val => {
|
||||
treeRef.value!.filter(val)
|
||||
})
|
||||
const onMenuCollapse = () => {
|
||||
menuCollapse.value = !menuCollapse.value
|
||||
proxy.eventBus.emit('cnTreeCollapse', menuCollapse)
|
||||
}
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true
|
||||
return data.name.includes(value)
|
||||
}
|
||||
const treeRef = ref<InstanceType<typeof ElTree>>()
|
||||
defineExpose({ treeRef })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cn-tree {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
:deep(.el-tree) {
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
|
||||
background-color: var(--el-color-primary-light-7);
|
||||
}
|
||||
.menu-collapse {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.custom-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
74
src/components/wangEditor/index.vue
Normal file
74
src/components/wangEditor/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div style='border: 1px solid #e4e4e4;height: 100%'>
|
||||
<Toolbar
|
||||
style='border-bottom: 1px solid #e4e4e4;border-top: 1px solid #e4e4e4'
|
||||
:editor='editorRef'
|
||||
:defaultConfig='toolbarConfig'
|
||||
mode='default'
|
||||
/>
|
||||
<Editor
|
||||
v-bind='$attrs'
|
||||
:defaultConfig='editorConfig'
|
||||
mode='default'
|
||||
@onCreated='handleCreated'
|
||||
style='height: calc(100% - 42px); '
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang='ts' setup>
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
|
||||
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
// 编辑器实例,必须用 shallowRef
|
||||
const editorRef = shallowRef()
|
||||
|
||||
// 内容 HTML
|
||||
const valueHtml = ref('<p>hello</p>')
|
||||
|
||||
// 模拟 ajax 异步获取内容
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
valueHtml.value = '<p>模拟 Ajax 异步设置内容</p>'
|
||||
}, 1500)
|
||||
})
|
||||
|
||||
const toolbarConfig = {
|
||||
excludeKeys: ['fullScreen', 'emotion']
|
||||
}
|
||||
let sever = '/cs-harmonic-boot/csconfiguration/uploadImage'
|
||||
// 本地加api
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
sever = '/api' + sever
|
||||
}
|
||||
const editorConfig = {
|
||||
placeholder: '请输入内容...',
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
server: sever,
|
||||
fieldName: 'file',
|
||||
compress: true,
|
||||
uploadFileName: 'file',
|
||||
withCredentials: true,
|
||||
headers: {},
|
||||
timeout: 0,
|
||||
customInsert: (insertImg, result, editor) => {
|
||||
const url = result.data.url
|
||||
insertImg(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件销毁时,也及时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor // 记录 editor 实例,重要!
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user