This commit is contained in:
仲么了
2024-02-19 13:44:32 +08:00
commit 361cbb713d
238 changed files with 202544 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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[]
}
}

View 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 // 自动递增
}
}

View 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>

View 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>

View 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[]
}

View 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>

View 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>

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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 */
},
}
}

View 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>

View 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>

View 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
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>