增加业务菜单

This commit is contained in:
hanqi 2025-02-21 11:25:09 +08:00
parent c5073ebd6c
commit 53bd29677b
39 changed files with 6789 additions and 0 deletions

View File

@ -0,0 +1,243 @@
<template>
<TreeSelector
v-model="innerValue"
:data="agencyData"
:tree-props="treeProps"
:node-key="'agencyId'"
:filter-method="filterAgencyNode"
:placeholder="placeholder"
:query-place-holder="'请输入机构名称或代码搜索'"
:multiple="multiple"
:disabled="disabled"
:lazy-tree="lazyLoad"
:load-node="loadAgencyNode"
:show-close="true"
:selection-filter="selectionFilter"
:default-expanded-keys="defaultExpandedKeys"
/>
</template>
<script setup>
// API
import { ref, computed, watch, onMounted } from 'vue';
import TreeSelector from './TreeSelector.vue';
import agencies from '@/api/lawenforcement/Agency';
import { useUserStore } from '@/stores/modules/user';
// props
const props = defineProps({
//
multiple: {
type: Boolean,
default: false
},
//
disabled: {
type: Boolean,
default: false
},
//
placeholder: {
type: String,
default: '请选择'
},
//
lazyLoad: {
type: Boolean,
default: false
},
// ID
rootId: {
type: [String, Number],
default: () => {
// store
const userStore = useUserStore();
// 使 agencyId id
if (userStore.deptInfo) {
const agency = userStore.deptInfo;
//
return agency.agencyId || '0';
}
// 使 '0'
return '0';
}
},
// v-model
modelValue: {
type: [Array, Object, String, Number]
},
//
leafOnly: {
type: Boolean,
default: false
}
});
// emit
const emit = defineEmits(['update:modelValue', 'node-change']);
//
const agencyData = ref([]);
const loading = ref(false);
const defaultExpandedKeys = ref([]);
//
const selfRequestCache = globalThis.__agencySelfCache || (globalThis.__agencySelfCache = new Map()); // id -> Promise
const childrenRequestCache = globalThis.__agencyChildrenCache || (globalThis.__agencyChildrenCache = new Map()); // parentId -> Promise
//
const innerValue = computed({
get() {
// modelValue
//
if (props.multiple && !props.modelValue) {
return [];
}
return props.modelValue;
},
set(val) {
//
let formattedValue = val;
//
if (!props.multiple && Array.isArray(formattedValue) && formattedValue.length > 0) {
formattedValue = formattedValue[0];
}
//
if (props.multiple && !Array.isArray(formattedValue)) {
formattedValue = formattedValue ? [formattedValue] : [];
}
emit('update:modelValue', formattedValue);
emit('node-change', formattedValue);
}
});
//
const treeProps = {
label: (data) => `${data.agencyName || ''}`,
children: 'children',
isLeaf: 'leaf'
};
//
//
const filterAgencyNode = (value, data) => {
if (!value) return true;
//
return data.agencyName?.includes(value) || data.agencyCode?.includes(value) || data.agencySimpleCode?.includes(value);
};
//
const loadAgencyNode = async (node, resolve) => {
if (node.level === 0) {
//
const data = await fetchAgencySelf(props.rootId);
resolve(data);
} else {
//
const data = await fetchAgencyChildren(node.data.agencyId);
resolve(data);
}
};
//
const selectionFilter = (data, node, tree) => {
// - TreeSelector
if (Array.isArray(data)) {
return data.every(item => {
//
if (props.leafOnly) {
const nodeData = tree?.getNode(item[props.nodeKey || 'agencyId']);
return nodeData ? nodeData.isLeaf : true;
}
return true;
});
}
//
if (props.leafOnly && node && !node.isLeaf) {
return false;
}
return true;
};
const fetchAgencySelf = async (id = props.rootId) => {
const key = String(id)
if (selfRequestCache.has(key)) {
return selfRequestCache.get(key);
}
loading.value = true;
const p = agencies.findOne(key)
.then((res) => {
const node = res.data || null;
const data = node ? [{ ...node }] : [];
return data;
})
.catch((error) => {
console.error('获取机构自身信息失败:', error);
return [];
})
.finally(() => {
loading.value = false;
});
selfRequestCache.set(key, p);
return p;
};
const fetchAgencyChildren = async (parentId = props.rootId) => {
const key = String(parentId)
if (childrenRequestCache.has(key)) {
return childrenRequestCache.get(key);
}
loading.value = true;
const p = agencies.querylist({ parentId: key })
.then((res) => {
const data = res.data || []
return data
})
.catch((error) => {
console.error('获取机构子级失败:', error);
return [];
})
.finally(() => {
loading.value = false;
});
childrenRequestCache.set(key, p);
return p;
};
const initAgencyData = async () => {
const rootNodes = await fetchAgencySelf(props.rootId);
if (!props.lazyLoad && rootNodes.length > 0) {
const children = await fetchAgencyChildren(props.rootId);
rootNodes[0].children = children;
}
agencyData.value = rootNodes;
defaultExpandedKeys.value = rootNodes.length > 0 ? [rootNodes[0].agencyId] : [];
};
// modelValue
watch(() => props.modelValue, (newVal) => {
// modelValuenode-change
if (JSON.stringify(newVal) !== JSON.stringify(innerValue.value)) {
emit('node-change', newVal);
}
}, { deep: true });
//
onMounted(() => {
if (props.lazyLoad) {
// el-tree load
agencyData.value = [];
defaultExpandedKeys.value = props.rootId ? [props.rootId] : [];
} else {
initAgencyData();
}
});
</script>
<style lang="scss" scoped>
// AgencySelector
</style>

View File

@ -0,0 +1,152 @@
<template>
<el-select
v-model="cvalue"
class="base-selector"
popper-class="base-selector-popper"
ref="elselect"
clearable
:filterable="config.filterable"
:filter-method="config.filterMethod ? config.filterMethod : filterMethod"
:multiple="config.multiple"
:remote-method="config.remote ? remoteFilter : null"
:remote="config.remote"
:placeholder="computePlaceholder"
:loading="loading"
v-bind="$attrs"
@change="changeSelect"
>
<template #prefix v-if="config.filterable">
<font-awesome-icon :icon="['fas', 'search']" />
</template>
<el-checkbox v-if="config.multiple && config.useSelectAll" v-model="checked" @change="selectAll">全选</el-checkbox>
<el-option
v-for="item in optionData"
:disabled="doDisabledOptionsBy(item)"
v-show="!doDisabledOptionsBy(item)"
:key="item[optionProps.key]"
:label="item[optionProps.label]"
:value="item[optionProps.value]"
/>
<slot name="loadMore"></slot>
</el-select>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
const props = defineProps({
config: {
type: Object,
default: () => ({
remote: false,
filterable: false,
multiple: false,
useSelectAll: true,
filterMethod: null,
}),
},
optionProps: {
type: Object,
default: () => ({ key: 'value', label: 'display', value: 'value' }),
},
modelValue: {
type: [Array, String, Number],
default: () => (config.multiple ? [] : ''),
},
data: {
type: Array,
default: () => [],
},
disabledOptions: {
type: Array,
default: () => [],
},
disabledOptionsBy: Function,
placeholder: String,
})
const emit = defineEmits(['update:modelValue', 'remote-filter', 'selected-clear', 'select-load-option', 'change'])
const loading = ref(false)
const checked = ref(false)
const elselect = ref(null)
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const computePlaceholder = computed(() => props.placeholder || (props.config.filterable ? '请输入查找内容' : '请选择'))
const optionData = ref(props.data)
//
const doDisabledOptionsBy = (item) =>
props.disabledOptionsBy ? props.disabledOptionsBy(item) : props.disabledOptions.includes(item[props.optionProps.value])
const remoteFilter = (query) => {
loading.value = true
setTimeout(() => {
loading.value = false
emit('remote-filter', query)
}, 200)
}
const filterMethod = (query) => {
if (!query) optionData.value = props.data
optionData.value = props.data.filter((option) => {
const label = option[props.optionProps.label]
const value = option[props.optionProps.value]
return label.toLowerCase().includes(query.toLowerCase()) || value.toString().toLowerCase().includes(query.toLowerCase())
})
}
const selectAll = () => {
cvalue.value = checked.value ? optionData.value.map((obj) => obj[props.optionProps.value]) : []
}
const changeSelect = (val) => {
checked.value = Array.isArray(val) ? val.length === optionData.value.length : false
emit('change', val)
}
watch(
() => props.data,
(val) => {
optionData.value = val
}
)
//
onMounted(() => {
emit('select-load-option')
})
</script>
<style lang="scss">
.base-selector {
.filter-prefix {
text-align: center;
width: 25px;
}
}
.base-selector-popper {
.left-info-bar {
font-size: 12px;
padding: 0 20px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: 34px;
line-height: 34px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
cursor: pointer;
color: var(--el-color-primary);
font-weight: 700;
font-weight: 700;
}
}
</style>

View File

@ -0,0 +1,466 @@
<template>
<el-container class="browser content-bg" direction="vertical" v-loading="componentLoading">
<el-row>
<el-col :span="24">
<slot name="toolbar" :selection="selection" :query-params="queryParams" :permissions="permissions">
<Toolbar
ref="toolbar"
@do-add="mergeActions.add"
@batch-delete="mergeActions.remove"
@do-query="mergeActions.startQuery"
@do-importFile="mergeActions.importData"
@do-exportFile="mergeActions.exportData"
@do-exportSelectFile="mergeActions.exportSelected"
@do-downloadTemp="mergeActions.downloadTemplate"
@help="mergeActions.showHelp"
@do-reset="mergeActions.reset"
@do-senior="mergeActions.senior"
:selection="selection"
:permissions="permissions"
>
<template #bottomPanel>
<slot name="queryPanel" :queryParams="queryParams" :permissions="permissions" />
</template>
<template #extraButton>
<slot name="extraButton"/>
</template>
</Toolbar>
</slot>
</el-col>
</el-row>
<el-row class="browser-content">
<slot name="table" :permissions="permissions" :data="tableData" :selection="selection" :mergeActions="mergeActions" :queryParams="queryParams">
<BusinessTable
ref="busiTable"
v-loading="tableConfig.tableLoading"
:data-res="tableData"
:config="tableConfig"
:query-params="queryParams"
:permissions="permissions"
@do-query="mergeActions.query"
@show-modify="mergeActions.modify"
@show-detail="mergeActions.detail"
@do-remove="mergeActions.remove"
@table-selection="mergeActions.handleSelectionChange"
@do-customize="mergeActions.doCustomize"
@query-params="mergeActions.updateQueryParams"
>
<slot></slot>
<template #otherControlButtons="data">
<slot name="tableControlColumn" :data="data"></slot>
</template>
</BusinessTable>
</slot>
</el-row>
<slot name="dialog" :dialogConfig="mergeDialogConfig" :dialogActions="mergeDialogActions">
<ElDialog
:open="mergeDialogConfig.show"
:title="mergeDialogConfig.title"
v-if="mergeDialogConfig.show"
:type="mergeDialogConfig.type"
:ok-label="mergeDialogConfig.okLabel"
:show-footer="mergeDialogConfig.mode !== 'detail' || mergeDialogConfig.showFooter"
class="business-dialog"
@ok="mergeDialogActions.handleDialogOk(dialogFormRef)"
@update:open="mergeDialogConfig.show = $event"
>
<template #default>
<el-form
ref="dialogFormRef"
:label-width="mergeDialogConfig.formLabelWidth"
class="businesss-table-dialog-form"
:model="mergeDialogConfig.data"
:rules="mergeDialogConfig.rules"
>
<slot name="dialogContent" :dialog-config="mergeDialogConfig" :dialog-actions="mergeDialogActions"></slot>
</el-form>
</template>
<template #footerOther>
<slot name="footOtherButtons" :form="dialogFormRef"></slot>
</template>
</ElDialog>
</slot>
</el-container>
</template>
<script setup>
import { ref, defineProps, defineEmits, reactive, nextTick, watch, onMounted } from 'vue'
import BusinessTable from '@/components/BusinessTable.vue'
import Toolbar from '@/components/Toolbar.vue'
import ElDialog from '@/components/ElDialog/index.vue'
import { merge } from 'lodash'
import {ElButton, ElLoading, ElMessage} from 'element-plus'
const props = defineProps({
componentLoading: {
type: Boolean,
default: false,
},
customQuery: {
type: Boolean,
default: false,
},
customReset: {
type: Boolean,
default: false,
},
permissions: {
type: Object,
default: () => ({}),
},
tableConfig: {
type: Object,
default: () => ({}),
},
apiConfig: {
type: Object,
},
actions: {
type: Object,
},
dialogConfig: {
type: Object,
default: () => ({
actions: {},
}),
},
defaultQueryParams: {
type: Object,
default: () => ({}),
},
})
const busiTable = ref(null)
const dialogFormRef = ref(null)
const emit = defineEmits(['show-help', 'downloadTemplate', 'import', 'export', 'update:dialog-config', 'update:actions', 'update:query-params', 'do-senior'])
//
const handleAdd = async () => {
mergeDialogConfig.mode = 'add'
mergeDialogConfig.data = {}
mergeDialogConfig.title = `新增${mergeDialogConfig.baseTitle}`
mergeDialogConfig.show = true
mergeDialogConfig.loading = false
emit('update:dialog-config', mergeDialogConfig)
}
//
const startQuery = () => {
busiTable.value.resetQuery()
mergeActions.query()
}
//
const importData = async (file) => {
const formData = new FormData()
formData.append('file', file)
formData.append('modelId', props.apiConfig.modelId)
await emit('import', formData)
}
//
const exportData = async () => {
await emit('export')
}
//
const exportSelected = async () => {}
const downloadTemplate = async () => {
await emit('downloadTemplate')
}
const showHelp = async () => await emit('show-help')
const handleSelectionChange = (selectedData) => {
if (props.tableConfig.isMultipleGlobal) {
selection.value.push(...selectedData)
} else {
selection.value = selectedData
}
emit('update:selection', selection.value)
}
const handleReset = () => {
//
if(props.customReset){
emit('custom-reset', queryParams.value)
}else{
queryParams.value = {}
busiTable.value.resetQuery()
emit('update:query-params', queryParams.value)
mergeActions.query()
}
}
const handleSenior = (v) => {
emit('do-senior', v)
}
//
const handleBatchDelete = async (row) => {
mergeDialogConfig.loading = true
try {
let res
if (row && row instanceof Object) {
res = await props.apiConfig.api.remove(row[props.apiConfig.modelId])
if (res.success) {
ElMessage.success('删除成功')
} else {
ElMessage.error('删除失败')
}
} else if (selection.value && selection.value.length > 0) {
res = await props.apiConfig.api.removeAll(selection.value.map((item) => item[props.apiConfig.modelId]))
if (res.success) {
ElMessage.success(`成功删除${selection.value.length}条记录`)
} else {
ElMessage.error('删除失败')
}
}
if (res.success) handleQuery()
} finally {
mergeDialogConfig.loading = false
}
}
const handleQuery = async () => {
//customQuery
if(props.customQuery){
await emit('custom-query',queryParams.value)
}else{
props.tableConfig.tableLoading = true
try {
const res = await props.apiConfig.api.query(queryParams.value)
if (res.success) {
tableData.value = res
}
} finally {
props.tableConfig.tableLoading = false
}
}
}
//
const handleModify = async (row) => {
mergeDialogConfig.data = {}
mergeDialogConfig.title = `修改${mergeDialogConfig.baseTitle}`
mergeDialogConfig.show = true
mergeDialogConfig.mode = 'modify'
mergeDialogConfig.loading = true
nextTick(() => {
mergeDialogConfig.loading = true
})
try {
const res = await props.apiConfig.api.findOne(row[props.apiConfig.modelId])
if (!res?.success) throw new Error('接口返回数据异常')
mergeDialogConfig.data = res.data || {}
} catch (error) {
console.error('详情加载失败:', error)
ElMessage.error(error.message)
} finally {
mergeDialogConfig.loading = false
emit('update:dialog-config', mergeDialogConfig)
}
}
const handleDelete = async (row) => {
mergeDialogConfig.loading = true
try {
const res = await props.apiConfig.api.delete(row[props.apiConfig.modelId])
if (res.success) {
emit('reload')
ElMessage.success('删除成功')
} else {
throw new Error('删除操作失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error(error.message)
} finally {
mergeDialogConfig.loading = false
}
}
const handleDetail = async (row) => {
mergeDialogConfig.data = {}
mergeDialogConfig.title = `详情${mergeDialogConfig.baseTitle}`
mergeDialogConfig.show = true
mergeDialogConfig.mode = 'detail'
mergeDialogConfig.showFooter = false
nextTick(() => {
mergeDialogConfig.loading = true
})
mergeDialogConfig.loading = true
try {
const res = await props.apiConfig.api.findOne(row[props.apiConfig.modelId])
if (!res?.success) throw new Error('接口返回数据异常')
mergeDialogConfig.data = res.data || {}
} catch (error) {
console.error('详情加载失败:', error)
ElMessage.error(error.message)
} finally {
mergeDialogConfig.loading = false
emit('update:dialog-config', mergeDialogConfig)
}
}
const updateQueryParams = (val) => {
// 使 Object.assign
Object.assign(queryParams.value, val)
//
if (val && typeof val === 'object' && Object.keys(val).length > 0) {
emit('update:query-params', queryParams.value)
}
}
const handleDialogOk = async (formRef) => {
dialogFormRef.value.validate(async (valid) => {
if (valid) {
mergeDialogConfig.loading = true
nextTick(() => {
mergeDialogConfig.loading = true
})
let res
if (mergeDialogConfig.mode === 'add') {
res = await props.apiConfig.api.add(mergeDialogConfig.data)
} else if (mergeDialogConfig.mode === 'modify') {
res = await props.apiConfig.api.modify(mergeDialogConfig.data[props.apiConfig.modelId], mergeDialogConfig.data)
}
if (res.success) {
ElMessage.success('操作成功')
mergeDialogConfig.loading = false
mergeDialogConfig.show = false
handleQuery()
} else {
mergeDialogConfig.loading = false
throw new Error('操作失败')
}
}
})
}
//
const defaultDialogConfig = {
type: 'max',
baseTitle: '对话框',
footer: true,
mode: 'detail',
loading: true,
show: false,
formLabelWidth: '130',
}
const mergeDialogConfig = reactive(merge({}, defaultDialogConfig, props.dialogConfig))
mergeDialogConfig.formRef = dialogFormRef
const defaultDialogActions = {
handleDialogOk,
}
const mergeDialogActions = reactive(merge({}, defaultDialogActions, mergeDialogConfig.actions))
mergeDialogConfig.actions = mergeDialogActions
emit('update:dialog-config', mergeDialogConfig)
//
const selection = ref([])
const queryParams = ref({})
Object.assign(queryParams.value, props.defaultQueryParams)
const tableData = ref({ success: false, data: [], total: 0 })
const defaultActions = {
add: handleAdd,
modify: handleModify,
remove: handleBatchDelete,
detail: handleDetail,
startQuery,
query: handleQuery,
updateQueryParams,
handleSelectionChange,
reset: handleReset,
senior: handleSenior
}
const mergeActions = reactive(merge({}, defaultActions, props.actions))
emit('update:actions', mergeActions)
const dialogLoading = ref()
//
watch(
() => mergeDialogConfig.loading,
(newVal) => {
if (newVal) {
dialogLoading.value = ElLoading.service({
customClass: 'business-dialog-loading',
})
} else {
if (dialogLoading.value) dialogLoading.value.close()
}
}
)
watch(props.dialogConfig, (newVal) => {
Object.assign(mergeDialogConfig, newVal)
emit('update:dialog-config', mergeDialogConfig)
})
watch(
() => props.tableConfig.tableData,
(newVal) => {
tableData.value = newVal
},
{ immediate: true, deep: true }
)
watch(
() => props.defaultQueryParams,
(newVal) => {
if (newVal) {
// emit
Object.assign(queryParams.value, newVal)
}
},
)
onMounted(() => {
mergeActions.startQuery()
})
defineExpose({busiTable})
</script>
<style lang="scss">
.business-dialog-loading {
z-index: 9999 !important;
}
</style>
<style lang="scss">
.browser {
// browser
border-radius: 8px;
overflow: hidden;
background-color: transparent !important; // content-bg
.businesss-table-dialog-form {
padding: 10px 20px 10px 20px;
}
.query-condition {
padding: 20px 20px 0 20px;
border-top: 1px solid var(--el-border-color);
border-bottom: 1px solid var(--el-border-color);
.el-col {
height: 50px;
}
}
.toolbar .top-panel {
padding: 16px 20px 16px 20px;
}
.toolbar {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
}
.business-table {
margin: 20px 0 0 0;
background-color: #fff;
box-sizing: border-box;
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<el-dialog :center="center" :title="title" :visible="show" @close="$emit('close-dialog')" v-loading.fullscreen.lock="loading"
:width="width"
:close-on-click-modal="false"
:append-to-body="true"
v-bind="$attrs"
class="business-edit-dialog">
<el-form ref="form" :label-width="labelWidth" :size="size" v-bind:id="bdid">
<slot>
<el-row :gutter="10">
<template></template>
</el-row>
</slot>
</el-form>
<slot name="footer">
<span slot="footer" class="dialog-footer">
<el-button @click="$emit('close-dialog')" v-show="showColse" :size="size">{{ closeText }}</el-button>
<el-button type="primary" @click="ok" v-show="showOk" :size="size">{{ okText }}</el-button>
<slot name="otherControlButtons"></slot>
</span>
</slot>
</el-dialog>
</template>
<script>
import ValidateFormItem from './ValidateFormItem'
import { DIALOG_FORMID } from '../utils/Constants'
import watermark from '../utils/WarterMark'
import DictSelector from './DictSelector'
import JurisdictionTreeSelector from './JurisdictionTreeSelector'
export default {
name: 'BusinessEditDialog',
components: { JurisdictionTreeSelector, DictSelector, ValidateFormItem },
inject: ['$validator'],
props: {
bdid: {
type: String,
default: DIALOG_FORMID
},
mode: {
type: String,
default: 'add'
},
show: {
type: Boolean,
default: true
},
formData: {
default: () => {
return []
}
},
loading: {
type: Boolean,
default: false
},
fields: {
type: Array,
default: () => []
},
labelWidth: {
type: String,
default: ''
},
width: {
type: String,
default: '25%'
},
title: {
type: String,
default: '窗口'
},
closeText: {
type: String,
default: '关 闭'
},
okText: {
type: String,
default: '确 定'
},
showColse: {
type: Boolean,
default: true
},
center: {
type: Boolean,
default: false
},
size: {
type: String,
default: 'mini'
},
showOkFilter: {
type: Function,
default: (mode) => {
return mode === 'add' || mode === 'modify' || mode === 'cancel' || mode === 'review' ||
mode === 'reviewChange' || mode === 'repeal' || mode === 'audit' || mode === 'reply' || mode === 'scrap' || mode === 'cancelled'
}
},
closeAction: {
type: String,
default: 'hide' // 2 hide destroy v-if使
}
},
data () {
return {
showOk: true
}
},
inheritAttrs: false,
methods: {
ok () {
this.$emit('ok', this.formData, this, this.mode)
},
initDialog () {
this.$nextTick(() => {
this.$emit('init-dialog', { mode: this.mode, data: this.formData, dialog: this, form: this.$refs.form })
const inputEls = this.$el.querySelectorAll('input,textarea,select')
if (inputEls.length) {
inputEls[0].focus()
}
})
watermark.set('el-dialog', this.$store.getters['current_user'])
}
},
watch: {
show (val) {
if (val) {
this.initDialog()
}
},
mode (nMode) {
this.showOk = this.showOkFilter(nMode)
},
formData () {
this.showOk = this.showOkFilter(this.mode, this.fromData)
}
},
mounted () {
this.showOk = this.showOkFilter(this.mode)
if (this.closeAction === 'destroy') {
this.initDialog()
}
}
}
</script>

View File

@ -0,0 +1,457 @@
<template>
<div class="business-table">
<el-table
ref="tableRef"
:data="cData"
:height="'100%'"
:width="'100%'"
:cell-style="tableConfig.cellStyleFunc"
:row-style="tableConfig.rowStyleFunc"
:id="tableConfig.tableId || tableId"
:default-sort="tableConfig.defaultSort"
class="el-table--scrollable-y"
:show-summary="tableConfig.hasSummary"
:summary-method="tableConfig.summaryMethod"
:span-method="tableConfig.spanMethod"
:tree-props="tableConfig.treeProps"
:lazy="tableConfig.lazy"
:load="tableConfig.load"
:row-class-name="tableConfig.rowClassNameFun"
:row-key="tableConfig.rowKey"
header-row-class-name="table-header"
v-bind="$attrs"
@cell-click="tableConfig.cellClick"
@selection-change="handleSelectionChange"
@sort-change="sortChange"
>
<el-table-column
v-if="tableConfig.multipleSelect"
:reserve-selection="tableConfig.reserveSelect"
:selectable="tableConfig.selectable"
type="selection"
header-align="center"
align="center"
/>
<slot />
<slot
name="control-column"
:canDelete="mergePermissions.delete"
:canModify="mergePermissions.modify"
:canDetail="mergePermissions.detail"
:modifyMethod="modify"
:removeMethod="remove"
:detailMethod="showDetail"
>
<el-table-column
fixed="right"
align="center"
class-name="control-column"
label="操作"
:width="tableConfig.controlWidth"
v-if="tableConfig.hasControlColumn"
>
<template #default="scope">
<template v-if="mergePermissions.canModifyCustom(scope.row)">
<el-link
type="primary"
v-if="mergePermissions.modify"
@click="modify(scope.row)"
title="修改"
>
修改
</el-link>
</template>
<template v-if="mergePermissions.canDeleteCustom(scope.row)">
<el-link
type="primary"
v-if="mergePermissions.delete"
@click="remove(scope.row)"
title="删除"
>
删除
</el-link>
</template>
<template v-if="mergePermissions.canDetailCustom(scope.row)">
<el-link
type="primary"
@click="showDetail(scope.row)"
title="详情"
v-if="mergePermissions.detail"
>
详情
</el-link>
</template>
<slot
name="otherControlButtons"
:data="scope"
/>
</template>
</el-table-column>
</slot>
</el-table>
<el-pagination
v-if="hasPagerBar"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
v-model:current-page="pagination.page"
:page-sizes="tableConfig.pageSizes"
v-model:page-size="pagination.pageSize"
:layout="tableConfig.pagerLayout"
:total="pagination.total"
background
>
<template #default>
<span
class="pager total"
:class="{ link: pageUnlimitCondition }"
@click="queryUnLimit"
>
{{ pagerTotal }}
</span>
</template>
</el-pagination>
</div>
</template>
<script setup>
import {ref, reactive, computed, watch, onMounted, nextTick, getCurrentInstance} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import { DATA_FLAGS } from '../utils/Constants'
import watermark from '@/utils/WarterMark'
import { v4 as uuid } from 'uuid'
import { merge } from 'lodash'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { useUserStore } from '@/stores/modules/user'
import {
exportExcelFile,
toExportExcelFileByUrl,
toExportXmlAllFileByUrl,
toExportXmlSelectFileByUrl
} from "@/utils/ExportExcel.js";
const userStore = useUserStore()
const emit = defineEmits([
'query-params',
'do-query',
'show-detail',
'show-modify',
'do-remove',
'table-selection',
'do-customize',
])
const defaultConfig = {
tableLoading: false,
hasControlColumn: false,
controlWidth: '300',
pageSizes: [20, 40, 60, 100],
usePage: true,
multipleSelect: true,
selectable: null,
reserveSelect: false,
cellStyleFunc: null,
rowStyleFunc: null,
rowClassNameFun: null,
hasSummary: false,
summaryMethod: null,
spanMethod: null,
userInfo: {},
defaultSort: {},
treeProps: { children: 'children', hasChildren: 'hasChildren' },
useWaterMark: true,
istick: false,
pagerLayout: '->, slot, sizes, prev, pager, next'
}
const props = defineProps({
queryParams: {
type: Object,
default: () => ({}),
},
config: Object,
dataRes: {
type: Object,
default: () => ({}),
},
permissions: {
type: Object,
default: () => ({
modify: false,
delete: false,
detail: true,
canDeleteCustom: () => true,
canDetailCustom: () => true,
canModifyCustom: () => true,
}),
},
})
const defaultPermissions = {
modify: false,
delete: false,
detail: true,
canDeleteCustom: () => true,
canDetailCustom: () => true,
canModifyCustom: () => true,
}
const tableConfig = reactive(merge({}, defaultConfig, props.config))
const mergePermissions = reactive(merge({}, defaultPermissions, props.permissions))
//
const pagination = reactive({
page: 1,
pageSize: tableConfig.pageSizes[0] || 20,
total: 0,
})
const { proxy } = getCurrentInstance()
props.queryParams
const data = ref([])
const once = ref(true)
const tableHeaderLabel = ref([])
const tableHeaderProps = ref([])
//
const cData = computed(() => data.value.filter((d) => !d.dataFlag || d.dataFlag !== DATA_FLAGS.REMOVE))
const pagerTotal = computed(() => {
const totalVal = pagination.total || 0
return totalVal === 0 || props.dataRes.relation === 'eq' ? `${totalVal}` : `>=${totalVal}`
})
const pageUnlimitCondition = computed(() => props.dataRes.relation === 'gte' && props.queryParams.limit && !tableConfig.tableLoading)
const hasPagerBar = computed(() => tableConfig.usePage)
const tableId = computed(() => uuid())
//
onMounted(() => {
if (tableConfig.useWaterMark) {
watermark.set('el-table', { username: userStore.username, mobile: userStore.mobile })
}
initQueryParams()
})
//
const initQueryParams = () => {
emit('query-params', {
//
...props.queryParams,
//
page: pagination.page,
pageSize: pagination.pageSize,
//
pagesize: pagination.pageSize,
//
sort: tableConfig.defaultSort.prop,
dir: tableConfig.defaultSort.order,
})
}
const query = (pageNum) => {
emit('query-params', {
...props.queryParams,
page: pageNum,
total: pagination.total,
})
emit('do-query')
}
const handleSizeChange = (size) => {
pagination.pageSize = size
pagination.page = 1
emit('query-params', { pagesize: size, pageSize: size })
}
const handleCurrentChange = (current) => {
pagination.page = current
query(current)
}
const showDetail = (row) => {
if (mergePermissions.detail) {
emit('show-detail', row)
}
}
const modify = (row) => {
if (mergePermissions.modify) {
emit('show-modify', row)
}
}
const remove = (row) => {
if (mergePermissions.delete) {
ElMessageBox.confirm('是否要删除选中的数据?', '确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
}).then(() => emit('do-remove', row))
.catch(() => {})
}
}
const handleSelectionChange = (val) => {
if (tableConfig.multipleSelect) {
emit('table-selection', val)
}
}
const sortChange = ({ prop, order, column }) => {
const dir = order === 'descending' ? 'desc' : 'asc'
emit('query-params', {
...props.queryParams,
sort: column.sortBy || prop,
dir: dir,
})
if (!once.value) emit('do-query')
once.value = false
}
const queryUnLimit = () => {
if (pageUnlimitCondition.value) {
emit('query-params', {
...props.queryParams,
limit: false,
total: 0,
})
emit('do-query')
}
}
const resetQuery = () => {
//
emit('query-params', {
page: 1,
total: 0,
pageSize: pagination.pageSize,
})
pagination.page = 1
once.value = true
}
const exportExcelFileForCurrentPage = (tableId, excelFileName) => { // excel(访)
if (pagination.total === 0) {
ElMessage.warning('没有数据可以导出!')
return
}
exportExcelFile(tableId, excelFileName)
}
const exportExcelFileForAllData = (url, excelFileName, params) => { // tableexcel,params
if (pagination.total === 0) {
ElMessage.warning('没有数据可以导出!')
return
}
toExportExcelFileByUrl(url, tableHeaderLabel.value, tableHeaderProps.value, excelFileName, params, proxy)
}
const exportXmlSelectFileForData = (url, codeList) => {
if (codeList.length === 0) {
ElMessage.warning('没有数据可以导出!')
return
}
toExportXmlSelectFileByUrl(url, codeList, '企业XML格式数据', {}, proxy)
}
const exportXmlAllFileForData = (url, params) => {
toExportXmlAllFileByUrl(url, '企业XML格式数据', params, proxy)
}
//
watch(
() => props.dataRes,
(val) => {
if (val?.success) {
data.value = tableConfig.istick ? [] : val.data
pagination.total = val.total
if (once.value) {
once.value = false
}
}
},
{ immediate: true, deep: true }
)
watch(
() => pagination.page,
(newVal, oldVal) => {
if (newVal !== oldVal) query(newVal)
}
)
defineExpose({
query,
resetQuery,
exportExcelFileForCurrentPage,
exportExcelFileForAllData,
exportXmlSelectFileForData,
exportXmlAllFileForData
})
</script>
<style lang="scss" scoped>
.business-table {
//
border-radius: 8px;
overflow: hidden;
:deep(.el-table) {
//
border-radius: 8px;
overflow: hidden;
//
.el-table__header th {
background-color: #D1E5FB !important;
}
//
.el-table__row {
background-color: #ffffff;
//
&:nth-child(even) {
background-color: #D9E1F3;
}
}
.el-table__cell {
font-size: 1rem;
}
}
//
::v-deep(.el-pagination) {
border-radius: 8px;
overflow: hidden;
margin-top: 10px;
padding: 10px;
background-color: transparent;
}
.control-column {
.el-link {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
&.el-link--primary {
color: var(--el-color-primary);
}
&.el-link--danger {
color: var(--el-color-danger);
}
&.el-link--info {
color: var(--el-color-info);
}
svg {
margin-right: 2px;
}
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<el-cascader
v-model="cvalue"
:options="options"
:props="cascaderProps"
:placeholder="placeholder"
clearable
filterable
:loading="loading"
@change="handleChange"
@visible-change="handleVisibleChange"
ref="cascader"
v-bind="$attrs"
/>
</template>
<script setup>
import { ref, computed, watch, reactive, onMounted } from 'vue'
import dictItems from '../api/system/DictItems'
import { useConstantStore } from '../stores/modules/constant'
const props = defineProps({
//
dictCode: {
type: String,
required: true
},
// v-model
modelValue: {
type: [Array, String],
default: null
},
//
placeholder: {
type: String,
default: '请选择行业类别'
},
//
multiple: {
type: Boolean,
default: false
},
//
checkStrictly: {
type: Boolean,
default: false
},
//
separator: {
type: String,
default: '/'
}
})
const emit = defineEmits([
'update:modelValue',
'change'
])
// store
const constantStore = useConstantStore()
//
const loading = ref(false)
const options = ref([])
const cascader = ref(null)
//
const cascaderProps = computed(() => ({
multiple: props.multiple,
checkStrictly: props.checkStrictly,
emitPath: true,
expandTrigger: 'hover',
value: 'xxdm',
label: 'xxdmmc',
children: 'children'
}))
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
//
const handleChange = (value) => {
emit('change', value)
}
// /
const handleVisibleChange = (visible) => {
if (visible && options.value.length === 0) {
loadData()
}
}
//
const loadData = async () => {
if (!props.dictCode) return
loading.value = true
try {
console.log('正在加载字典数据dictCode:', props.dictCode)
// 使 store
const data = await constantStore.queryDictData({
dictCode: props.dictCode
})
//
options.value = data
console.log('级联选择器数据:', options.value)
} catch (error) {
console.error('行业类别数据获取异常:', error)
options.value = []
} finally {
loading.value = false
}
}
// dictCode
watch(() => props.dictCode, (newVal) => {
if (newVal) {
options.value = []
loadData()
}
})
//
onMounted(() => {
if (props.dictCode) {
loadData()
}
})
//
defineExpose({
loadData,
cascader
})
</script>
<style lang="scss" scoped>
.el-cascader {
width: 100%;
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<base-selector v-model="cvalue"
ref="bselect"
:config="config"
:optionProps="optionProps"
:data="data"
:total="total"
:disabled-options="disabledOptions"
:disabled-options-by="disabledOptionsBy"
:use-select-all="useSelectAll"
@show-more="showMore"
@remote-filter="queryDictItems"
@selected-clear="clear"
@visible-change="visibleChange"
v-bind="$attrs"
@change="change"
@select-load-option="initData">
</base-selector>
</template>
<script setup>
import BaseSelector from './BaseSelector.vue'
import { DICTS } from '@/utils/Constants'
import dictItems from '../api/system/DictItems'
import { computed, ref, watch, reactive, toRefs } from 'vue'
import { useConstantStore } from '@/stores/modules/constant'
const props = defineProps({
dictCode: String,
sort: { type: String, default: 'orderNum' },
dir: { type: String, default: 'asc' },
filterable: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
defaultOptions: { type: Array, default: () => [] },
modelValue: { // Vue3 v-model value
type: [Array, String, Number],
default: null
},
limit: {
type: Number,
default: 0
},
valueMarray: { type: String, default: null },
remote: { type: Boolean, default: false },
dictFilter: {
type: Function,
default: (dictItem, query, optionProps = { value: 'xxdm', label: 'xxdmmc' }) =>
!query ||
query === dictItem[optionProps.value] ||
dictItem[optionProps.label]?.includes(query) ||
(dictItem.simplePinyin?.includes(query)) ||
(dictItem.allPinyin?.includes(query))
},
banFilter: {
type: Function,
default: () => false
},
disabledOptions: Array,
disabledOptionsBy: Function,
useSelectAll: { type: Boolean, default: true }
})
const emit = defineEmits([
'update:modelValue',
'selected',
'visible-change',
'select-load-option'
])
const store = useConstantStore()
const bselect = ref(null)
//
const state = reactive({
params: {
dictCode: props.dictCode,
sort: props.sort,
dir: props.dir,
valueMarray: props.valueMarray
},
optionProps: { key: 'xxdmbh', label: 'xxdmmc', value: 'xxdm' },
page: 1,
lQueryParam: null,
cacheFirstData: { data: [], total: 0 },
data: [],
total: 0,
config: {
filterable: props.filterable,
multiple: props.multiple,
remote: props.remote
}
})
//
const localFilterMethod = (q) => {
if (q) {
if (props.filterMethod && typeof props.filterMethod === 'function') {
state.data = props.filterMethod(q, state.cacheFirstData.data)
} else {
state.data = state.cacheFirstData.data.filter(dictItem => {
const value = dictItem[state.optionProps.value] || ''
const label = dictItem[state.optionProps.label] || ''
return value.includes(q) ||
label.includes(q) ||
dictItem.simplePinyin?.includes(q) ||
dictItem.allPinyin?.includes(q)
})
}
} else {
state.data = state.cacheFirstData.data
}
}
const change = (v) => {
if (props.multiple) {
const temp = v.map(vl =>
state.data.find(d => d[state.optionProps.value] === vl)
)
emit('selected', v, temp, state.data)
} else {
emit('selected', v, state.data.find(d => d[state.optionProps.value] === v), state.data)
}
}
const visibleChange = (opened) => {
if (!opened && !state.data.length) {
queryDictItems()
}
}
const initData = () => {
if (props.remote && props.modelValue) {
queryDictItems(null, props.modelValue)
} else {
queryDictItems()
}
}
const queryDictItems = async (select, query, isShowMore = false) => {
if (!props.dictCode) return
if (state.lQueryParam !== query) {
state.page = 1
state.lQueryParam = query
}
try {
//
let res;
if (state.params.dictCode === DICTS.DM_XZQH) {
res = await dictItems.query({
page: state.page,
pagesize: props.limit > 0 ? props.limit : undefined,
...state.params,
queryParam: query
})
} else {
// 使 queryGzt
res = await dictItems.queryGzt({
...state.params,
queryParam: query
})
}
let pageable = null
let data = []
//
if (Array.isArray(res)) {
// store.queryDictData
data = res
} else if (res?.success) {
if (state.params.dictCode !== DICTS.DM_XZQH) {
// queryGzt
data = res.data || []
} else {
// dictItems.query
pageable = {
page: res.page,
pagesize: res.pagesize,
limit: res.pagesize,
total: res.total
}
data = res.data
}
}
if (state.params.dictCode !== DICTS.DM_XZQH && props.valueMarray) {
const valueArray = props.valueMarray.split(',')
data = data.filter(resmap =>
valueArray.some(va => resmap[state.optionProps.value].startsWith(va))
)
if (pageable) pageable.total = data.length
}
responseProcess([data, isShowMore, query, pageable])
} catch (error) {
console.error('字典项查询失败:', error)
}
}
const showMore = (select) => {
state.page += 1
queryDictItems(select, state.lQueryParam, true)
}
const clear = (select) => {
state.total = state.cacheFirstData.total
state.data = state.cacheFirstData.data
state.page = 1
queryDictItems(select, '', false)
}
const responseProcess = ([data, isShowMore, query, pageable]) => {
//
if (!Array.isArray(data)) {
console.error('字典数据格式错误:', data)
return
}
if (props.limit > 0) {
if (isShowMore) {
if (pageable) {
state.data = [...state.data, ...data]
state.total = pageable.total
} else {
const filterData = data
.filter(d => props.dictFilter(d, query, state.optionProps))
.filter(d => !props.banFilter(d))
state.data.push(...filterData.slice(
(state.page - 1) * props.limit,
state.page * props.limit
))
state.total = filterData.length
}
} else {
if (pageable) {
state.data = data
state.total = pageable.total
} else {
state.page = 1
const filterData = data
.filter(d => props.dictFilter(d, query, state.optionProps))
.filter(d => !props.banFilter(d))
state.data = filterData.slice(
(state.page - 1) * props.limit,
state.page * props.limit
)
state.total = filterData.length
state.cacheFirstData = { data: state.data, total: state.total }
}
}
} else {
state.data = data.filter(d => !props.banFilter(d))
state.total = 0
state.page = 1
state.cacheFirstData = { data: state.data, total: 0 }
}
}
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
//
defineExpose({
queryDictItems,
clear,
showMore
})
//
watch(() => props.dictCode, (code) => {
if (code) {
state.params.dictCode = code
queryDictItems(bselect.value)
}
})
watch(() => props.modelValue, (v) => {
if (props.remote && v) {
//
const hasValue = Array.isArray(v)
? v.every(item => state.data.some(d => d[state.optionProps.value] === item))
: state.data.some(d => d[state.optionProps.value] === v)
if (!hasValue) {
state.page = 1
queryDictItems(bselect.value, v)
}
}
})
watch(() => props.valueMarray, (v) => {
state.params.valueMarray = v
queryDictItems(bselect.value)
})
const {data,total,page,cacheFirstData,optionProps,config} = toRefs(state)
</script>

View File

@ -0,0 +1,208 @@
<template>
<el-dialog
v-model="opendia"
:title="title"
append-to-body
:show-close="false"
:top="top"
:style="{ width: dialogWidth }"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header>
<div class="cvi-dialog-header">
<div class="cvi-dialog-header-title">
<div class="dialog-line"></div>
{{ title }}
</div>
<i class="iconfont icon-zyyy_tcgb" @click="handleClose"></i>
</div>
</template>
<slot></slot>
<template #footer v-if="showFooter">
<div class="cvi-dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="$emit('ok')">{{okLabel || '确定'}}</el-button>
<slot name="footerOther" />
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ElButton } from 'element-plus'
import { ref, computed, watch } from 'vue'
const props = defineProps({
open: {
type: Boolean,
},
title: {
type: String,
},
type: {
type: String,
},
showFooter: {
type: Boolean,
default: true,
},
okLabel: {
type: String,
default: '确定',
}
})
const emit = defineEmits(['update:open', 'ok'])
const opendia = ref(props.open)
watch(
() => props.open,
(newValue) => {
opendia.value = newValue
}
)
const handleClose = () => {
opendia.value = false
emit('update:open', opendia.value)
}
const top = computed(() => {
switch (props.type) {
case 'medium':
return '100px'
case 'min':
return '100px'
case 'max':
return '100px'
case 'fullscreen':
return '0px'
default:
return '100px'
}
})
const dialogWidth = computed(() => {
switch (props.type) {
case 'medium':
return '960px'
case 'min':
return '796px'
case 'max':
return '1292px'
case 'fullscreen':
return window.innerWidth + 'px'
default:
return '960px'
}
})
</script>
<style>
.el-dialog {
--el-dialog-bg-color: #ffffff; /* 将 Element Plus 对话框背景色变量设置为白色 */
box-sizing: border-box;
background: var(--el-dialog-bg-color); /* 使用上面定义的变量来设置背景色 */
border: 1px solid rgba(255,255,255,1); /* 边框保持不变 (纯白边框) */
border-radius: 16px; /* 圆角保持不变 */
backdrop-filter: blur(1rem); /* 背景模糊效果,使对话框后面的内容模糊 */
}
.cvi-dialog-header {
display: flex !important;
justify-content: space-between;
padding-right: 26px;
height: 56px;
line-height: 56px;
font-weight: bold;
font-size: 16px;
font-style: normal;
border-bottom: 1px solid #ececec;
font-family: 'OPlusSans3-Bold', sans-serif;
/* color: #373E52; */
color: var(--el-text-color-regular);
}
.cvi-dialog-header .iconfont {
font-size: 12px !important;
cursor: pointer;
}
/* 关闭按钮样式 */
.cvi-dialog-header .icon-zyyy_tcgb {
color: var(--el-color-primary); /* 设置图标颜色为 Element Plus 主题色 */
background-color: var(--el-color-primary-light-9); /* 设置背景为淡淡的蓝色 (Element Plus 次级颜色) */
border-radius: 50%; /* 圆形背景 */
padding: 8px; /* 增加内边距,使光圈更大 */
display: inline-flex; /* 使 padding 和背景生效 */
align-items: center; /* 垂直居中图标 */
justify-content: center; /* 水平居中图标 */
align-self: center;
justify-self: center;
width: 30px; /* 调整宽度以适应新的padding */
height: 30px; /* 调整高度以适应新的padding */
box-sizing: border-box; /* padding 和 border 不会增加元素的总宽度和高度 */
}
.cvi-dialog-header-title {
display: flex;
color: var(--el-color-primary); /* 设置标题颜色为 Element Plus 主题色 */
}
.dialog-line {
width: 3px;
height: 20px;
margin: 18px 10px 0 24px;
background-color: transparent;
}
.el-dialog-header i {
font-size: 12px;
cursor: pointer;
}
.el-dialog .dialog-footer {
display: flex;
justify-content: flex-end;
padding-bottom: 16px;
padding-right: 25px;
}
.el-dialog .el-dialog__header {
padding-bottom: 0px;
}
.el-dialog .el-dialog__body {
padding: 20px 25px 16px 20px !important;
height: calc(100% - 95px);
overflow: auto;
}
.el-dialog .el-dialog__body > .el-form {
padding: 0 !important;
}
.el-dialog .el-dialog__body::-webkit-scrollbar {
width: 0;
}
.el-dialog .el-overlay {
background: rgba(0, 0, 0, 0.5);
}
.cvi-dialog-footer {
display: flex;
justify-content: center;
padding-bottom: 16px;
padding-right: 25px;
text-align: center;
}
/* 为所有对话框中的按钮添加圆角样式 */
.cvi-dialog-footer .el-button {
/* 设置圆角大小 */
border-radius: 20px;
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div>
<el-sub-menu v-if="item.children" :index="String(item.gncdbh)">
<template #title>
<i :class="'iconfont icon-' + (item.icon || item.meta.icon)"></i>
<span>{{ item.name }}</span>
</template>
<menu-item v-for="child in item.children" :key="child.gncdbh" :item="child" @select="handleClick" />
</el-sub-menu>
<el-menu-item v-else-if="!item.hidden" :index="item.path" @click="handleClick(item)">
<i :class="'iconfont icon-' + (item.icon || item.meta.icon)"></i>
{{ item.name }}
</el-menu-item>
</div>
</template>
<script>
import { defineComponent, ref, watch, defineEmits } from 'vue'
export default defineComponent({
name: 'MenuItem',
props: {
item: {
type: Object,
required: true,
},
},
setup(props, { emit }) {
const handleClick = (item) => {
emit('select', item)
}
return {
handleClick,
}
},
})
</script>
<style>
.el-menu {
width: 200px;
height: 100%;
border: none;
-webkit-transition: none !important;
transition: none !important;
border-right: none !important;
overflow: auto;
}
.el-sub-menu__title i {
padding: 0 !important;
margin-right: 5px;
}
.el-sub-menu {
width: 200px;
}
.el-menu-item i {
font-size: 16px !important;
}
.el-menu-item {
height: 100px !important;
padding-top: 20px !important;
padding-bottom: 20px !important;
}
.el-menu-item,
.el-sub-menu__title {
font-size: 16px !important;
height: 50px !important;
/* color: #ffffff !important; */
}
.el-sub-menu__title:hover,
.el-menu-item:hover {
height: 40px;
border-radius: 2px;
color: #057AFF !important;
background: #F1F6FE !important;
}
.el-menu-item.is-active {
border-radius: 2px;
color: #057AFF !important;
background: #F1F6FE !important;
/* margin :0 8px; */
}
.el-sub-menu__title i,
.el-menu-item i {
padding-right: 8px;
}
.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item,
.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-menu-item-group__title,
.el-menu--vertical:not(.el-menu--collapse):not(.el-menu--popup-container) .el-sub-menu__title {
--el-menu-base-level-padding: 24px;
--el-menu-level-padding: 24px;
padding-left: calc(var(--el-menu-base-level-padding) + var(--el-menu-level) * var(--el-menu-level-padding));
white-space: nowrap;
}
.el-menu:not(.el-menu--collapse) .el-sub-menu__title {
padding-right: calc(var(--el-menu-base-level-padding));
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<!-- 使用从 element-plus 导入的原始表格组件 -->
<el-original-table v-bind="$attrs">
<!-- 遍历父组件提供的所有插槽 -->
<!-- slotName 是插槽名, slotFn 是渲染函数 (我们这里不需要直接用它) -->
<template v-for="slotName in Object.keys($slots)" #[slotName]="slotProps">
<!-- 渲染父组件传入的同名插槽内容并将原始表格传出的 slotProps 透传过去 -->
<slot :name="slotName" v-bind="slotProps"></slot>
</template>
<!-- 定义自定义的 #empty 插槽 -->
<template #empty>
<div style="text-align: center; padding: 20px;">
<img :src="data_empty" alt="暂无数据" style="max-width: 200px; margin-bottom: 10px;" />
</div>
</template>
</el-original-table>
</template>
<script setup>
// main.js 'el-table'
// 使 Element Plus
//
import { ElTable as ElOriginalTable } from 'element-plus';
import data_empty from '@/assets/empty_images/data_empty.png';
// <script setup> $attrs $slots 使
// useAttrs 访 this.$slots
</script>
<style scoped>
/* 您可以根据需要添加样式 */
</style>

View File

@ -0,0 +1,169 @@
<template>
<div class="cvi-tabs">
<el-tabs v-model="activeTabs" @tab-remove="removeTab" @tab-click="handleTabClick">
<el-tab-pane
v-for="(item, index) in tabsdata"
:key="index"
:label="item.label || item.name"
:name="item.path"
:closable="shouldShowClose(item, index)"
></el-tab-pane>
</el-tabs>
<el-dropdown trigger="click">
<span class="el-dropdown-link text-color-opacity">
<i class="iconfont icon-zyyy_xxl"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="Refresh">
<i class="iconfont icon-zyyy_sx"></i>
刷新
</el-dropdown-item>
<el-dropdown-item @click="closeCurrent">
<i class="iconfont icon-zyyy_gb"></i>
关闭当前
</el-dropdown-item>
<el-dropdown-item @click="closeOther">
<i class="iconfont icon-zyyy_gbqt"></i>
关闭其它
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script>
import { defineComponent, ref, watch, defineEmits } from 'vue'
import { useRouteStore } from '@/stores/modules/router'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
props: {
tabsdata: {
type: Array,
required: true,
},
// activeTab: {
// type: String,
// required: true
// },
},
setup(props, { emit }) {
const routerstore = useRouteStore()
const router = useRouter()
const route = useRoute()
const activeTabs = ref(routerstore.currentRoute)
watch(
() => routerstore.currentRoute,
(newValue) => {
activeTabs.value = newValue
}
)
const handleTabClick = (item) => {
emit('selected-data', item)
}
const removeTab = (name) => {
emit('tab-remove', name, 'current')
}
const shouldShowClose = (item, index) => {
// return !item.noClosable && index !== 0
return true
}
const Refresh = () => {
router.replace({
path: routerstore.currentRoute,
query: {
...route.query,
_forceRefresh: Math.random(), //
},
})
}
const closeCurrent = () => {
open.value = false
emit('tab-remove', activeTabs.value, 'current')
}
const closeOther = () => {
open.value = false
emit('tab-remove', activeTabs.value, 'other')
}
return {
handleTabClick,
removeTab,
activeTabs,
shouldShowClose,
Refresh,
closeCurrent,
closeOther,
open,
}
},
})
</script>
<style scoped>
.el-tabs__header {
margin: 0 !important;
}
.cvi-tabs {
display: flex;
width: 100%;
height: 40px;
line-height: 40px;
position: relative;
margin-bottom: 20px;
}
.el-tabs {
width: calc(100% - 20px);
height: 40px;
}
.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2),
.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),
.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),
.el-tabs--top .el-tabs__item.is-top:nth-child(2) {
padding-left: 16px !important;
}
::v-deep(.el-tabs) {
.el-tabs__item {
font-size: 14px;
}
.el-tabs__active-bar {
margin-left: -5px;
height: 4px;
background-color: var(--el-menu-text-color);
}
.el-tabs__item {
--el-text-color-primary: var(--el-menu-text-color);
}
.el-tabs__item.is-active,
.el-tabs__item:hover {
color: var(--el-menu-text-color);
font-family: 'OPlusSans3-Bold';
font-weight: 700;
}
.el-tabs__nav-wrap::after {
background-color: transparent;
}
}
.el-dropdown-link {
box-shadow: inset 1px 0 0 0 #eef2fb;
width: 40px;
height: 40px;
position: relative;
cursor: pointer;
}
.el-dropdown-link .icon-zyyy_xxl {
color: #b7bdcf !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="index-title tcw">
<img class="title-style" :src="ts" alt="" />
{{ titleName }}
</div>
</template>
<script setup>
import { onMounted, defineProps } from 'vue'
import ts from '@/assets/image/title-style.png'
const props = defineProps({
titleName: {
type: String,
required: true,
},
})
onMounted(() => {})
</script>
<style lang="scss" scoped>
.index-title {
display: flex;
align-items: center;
margin-top: 16px;
font-size: 18px;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="industry-card-public radius-base">
<industry-title :titleObj></industry-title>
<slot></slot>
<div class="industry-card-bg"></div>
</div>
</template>
<script setup>
import { onMounted, defineProps } from 'vue'
const props = defineProps({
titleObj: {
type: Object,
required: true,
},
})
</script>
<style scoped>
.industry-card-public {
position: relative;
top: 0;
left: 0;
background-color: #fff;
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<div class="comprehensive-container">
<div class="comprehensive-top">
<div class="comprehensive-top-item border-base radius-base ccw" v-for="(item, index) in zhgkList" :key="index">
<img :src="item.img" alt="" />
<div class="comprehensive-top-text">
<div class="fs-16">{{ item.name }}</div>
<div v-if="item.lessValue" class="fs-22 ff-AvenirMedium">
<span style="color: #f4551c">{{ item.lessValue }}</span>
/
{{ item.value }}
</div>
<div v-else class="fs-22 ff-AvenirMedium">{{ item.value }}</div>
</div>
</div>
</div>
<index-title titleName="行业企业数"></index-title>
<public-chart ref="publicChart" :option="options" theme="vab-echarts-theme" />
</div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs } from 'vue'
import * as echarts from 'echarts'
import Comprehensive from './json/Comprehensive.json'
import qyzs from '@/assets/image/qyzs.png'
import jcqy from '@/assets/image/jcqy.png'
import byyj from '@/assets/image/byyj.png'
import wlw from '@/assets/image/wlw.png'
const state = reactive({
zhgkList: [
{
name: '企业总数',
value: 136,
img: qyzs,
},
{
name: '监测企业数',
value: 136,
img: jcqy,
},
{
name: '本月预警',
lessValue: 12,
value: 136,
img: byyj,
},
{
name: '物联网接入',
value: 136,
img: wlw,
},
],
options: Comprehensive,
})
const { zhgkList, options } = toRefs(state)
onMounted(() => {
options.value.series.itemStyle = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#1583FE' },
{ offset: 1, color: '#5CC8FC' },
]),
}
})
</script>
<style lang="scss" scoped>
.comprehensive-container {
height: calc(423px - 51px);
.echarts {
height: calc(48% - 43px);
}
.comprehensive-top {
display: flex;
flex-wrap: wrap;
width: 100%;
height: 52%;
padding: 4px 14px 0 14px;
box-sizing: border-box;
.comprehensive-top-item {
display: flex;
align-items: center;
width: calc(50% - 16px);
height: calc(50% - 16px);
padding: 12px 16px;
margin: 16px 8px 0 8px;
box-sizing: border-box;
img {
width: 56px;
height: 56px;
margin-right: 16px;
}
.comprehensive-top-text {
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="industry-map-container radius-base">
<div ref="mapRef" style="width: 100%; height: 100%; border-radius: 6px"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const mapRef = ref(null)
onMounted(() => {
//
const map = new window.BMap.Map(mapRef.value)
//
const geocoder = new window.BMap.Geocoder()
//
const defaultAddress = '西安市公安局'
//
geocoder.getPoint(
defaultAddress,
function (point) {
if (point) {
//
map.centerAndZoom(point, 15)
//
const marker = new window.BMap.Marker(point)
//
map.addOverlay(marker)
} else {
console.log('未能解析出地址对应的坐标')
}
},
'西安'
)
})
</script>
<style lang="scss" scoped>
.industry-map-container {
height: 667px;
padding: 16px;
background-color: #ffffff;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="industry-title tcw">
<img :src="titleObj.img" alt="" />
{{ titleObj.name }}
</div>
</template>
<script setup>
import { onMounted, defineProps } from 'vue'
const props = defineProps({
titleObj: {
type: Object,
required: true,
},
})
onMounted(() => {})
</script>
<style lang="scss" scoped>
.industry-title {
display: flex;
align-items: center;
padding: 12px 16px;
font-size: 18px;
box-shadow: 0px 2px 6px 0px rgba(42, 42, 42, 0.04);
img {
width: 24px;
height: 24px;
margin-right: 6px;
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div class="inherent-risk-container">
<div class="inherent-risk-top">
<div class="inherent-risk-top-item border-base radius-base ccw" v-for="(item, index) in gyfxList" :key="index">
<img :src="item.img" alt="" />
<div class="inherent-risk-top-text">
<div class="fs-16 tcw">{{ item.name }}</div>
<div v-if="item.lessValue" class="fs-22 ff-AvenirMedium">
<span style="color: #f4551c">{{ item.lessValue }}</span>
/
{{ item.value }}
</div>
<div v-else class="fs-22 ff-AvenirMedium">{{ item.value }}</div>
</div>
</div>
</div>
<index-title titleName="危险工艺"></index-title>
<div class="dangerous-charts">
<public-chart ref="publicChart" :option="wxgyOptions" theme="vab-echarts-theme" />
<div class="legend-content ccw">
<div v-for="(item, index) in wxgyList" :key="index" style="">
<span class="fs-14">
<span class="circle" :style="{ background: item.itemStyle.color }"></span>
{{ item.name }}
</span>
<span class="ff-AvenirMedium">{{ item.value }}</span>
</div>
</div>
</div>
<index-title titleName="风险对象统计"></index-title>
<div class="dangerous-charts">
<public-chart ref="publicChartone" :option="fxdxOptions" theme="vab-echarts-theme" />
<div class="legend-content ccw legendone-content">
<div v-for="(item, index) in fxdxList" :key="index" style="">
<span class="fs-14">
<span class="circle" :style="{ background: item.itemStyle.color }"></span>
{{ item.name }}
</span>
<span class="ff-AvenirMedium">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs } from 'vue'
import wxgy from './json/wxgy.json'
import fxdx from './json/fxdx.json'
import zdbw from '@/assets/image/zdbw.png'
import wxy from '@/assets/image/wxy.png'
const state = reactive({
gyfxList: [
{ name: '重点部位', value: 136, img: zdbw },
{ name: '危险源', value: 136, img: wxy },
],
wxgyList: [
{ name: '裂解(裂化)工艺', value: 78, itemStyle: { color: '#F59E0B' } },
{ name: '合成氨工艺', value: 12, itemStyle: { color: '#14B8A6' } },
{ name: '新型煤化工工艺', value: 19, itemStyle: { color: '#3B82F6' } },
{ name: '烷基化工艺', value: 25, itemStyle: { color: '#6366F1' } },
{ name: '氧化工艺', value: 43, itemStyle: { color: '#EC4899' } },
],
fxdxList: [
{ name: '重大风险', value: 78, itemStyle: { color: '#F59E0B' } },
{ name: '较大风险', value: 12, itemStyle: { color: '#14B8A6' } },
{ name: '一般风险', value: 19, itemStyle: { color: '#3B82F6' } },
{ name: '低风险', value: 43, itemStyle: { color: '#6366F1' } },
{ name: '未评估', value: 25, itemStyle: { color: '#EC4899' } },
],
wxgyOptions: wxgy,
fxdxOptions: fxdx,
})
const { gyfxList, wxgyList, fxdxList, wxgyOptions, fxdxOptions } = toRefs(state)
onMounted(() => {
wxgyOptions.value.series.data = wxgyList.value
fxdxOptions.value.series.data = fxdxList.value
})
</script>
<style lang="scss" scoped>
.inherent-risk-container {
height: calc(548px - 51px);
padding: 12px 14px;
box-sizing: border-box;
.inherent-risk-top {
display: flex;
flex-wrap: wrap;
width: 100%;
height: 18%;
.inherent-risk-top-item {
display: flex;
align-items: center;
width: calc(50% - 16px);
height: calc(100% - 8px);
padding: 12px 16px;
margin: 8px 8px 0 8px;
box-sizing: border-box;
img {
width: 56px;
height: 56px;
margin-right: 16px;
}
.inherent-risk-top-text {
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
}
.index-title {
margin-left: 0 !important;
}
.dangerous-charts {
display: flex;
width: 100%;
height: calc(41% - 43px);
.echarts {
width: 50%;
height: 100%;
}
.legend-content {
width: 50%;
display: flex;
flex-direction: column;
justify-content: center;
div {
display: flex;
justify-content: space-between;
align-items: center;
width: 78%;
margin-bottom: 8px;
}
.circle {
display: inline-block;
width: 8px;
height: 8px;
margin-right: 2px;
border-radius: 50%;
}
}
.legendone-content {
div {
width: 55%;
}
}
}
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="monitor-container">
<el-table :data="tableData" style="width: 100%" header-row-class-name="custom_header_className">
<el-table-column prop="zt" label="状态" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.zt" :type="typeArr[scope.row.type]">{{ scope.row.zt }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="yjqy" label="预警企业" show-overflow-tooltip>
<template #default="scope">
<div class="table-hidden ccw">{{ scope.row.yjqy }}</div>
</template>
</el-table-column>
<el-table-column prop="yjnr" label="预警内容" show-overflow-tooltip>
<template #default="scope">
<div class="table-hidden ccw">{{ scope.row.yjnr }}</div>
</template>
</el-table-column>
<el-table-column prop="yjsj" label="预警时间" width="180" show-overflow-tooltip>
<template #default="scope">
<div class="ccw ff-AvenirBook">{{ scope.row.yjsj }}</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs } from 'vue'
const typeArr = ['success', 'warning', 'danger']
const state = reactive({
tableData: [
{
zt: '未解除',
yjqy: '西安东郊供暖公司',
yjnr: '压力超限2级预警-一楼大厅压力20装置',
yjsj: '2025-02-11 16:30:21',
type: 2,
},
{
zt: '处置中',
yjqy: '陕西新奥燃气股份有限公司',
yjnr: '液位超限1级预警-17号储罐液位监测装置',
yjsj: '2025-02-11 16:22:58',
type: 1,
},
{
zt: '已解除',
yjqy: '西安东营化工材料有限公司',
yjnr: '温度超限2级预警-9号温度监测仪',
yjsj: '2025-02-11 12:41:35',
type: 0,
},
{
zt: '已解除',
yjqy: '西安钻石钨有色金属冶炼有限公司',
yjnr: '温度超限2级预警-A6号温度监测仪',
yjsj: '2025-02-10 16:28:32',
type: 0,
},
],
})
const { tableData } = toRefs(state)
onMounted(() => {})
</script>
<style lang="scss" scoped>
.monitor-container {
height: calc(304px - 51px);
padding: 16px 24px;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="special-type-container">
<div class="special-type-top">
<index-title titleName="特种作业操作持证人员"></index-title>
<public-chart ref="publicChart" :option="tzzy1Options" theme="vab-echarts-theme" />
</div>
<div class="special-type-bottom">
<index-title titleName="特种作业票"></index-title>
<public-chart ref="publicChartone" :option="tzzy2Options" theme="vab-echarts-theme" />
</div>
</div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs } from 'vue'
import tzzy1 from './json/tzzy1.json'
import tzzy2 from './json/tzzy2.json'
const state = reactive({
tzzy1Options: tzzy1,
tzzy2Options: tzzy2,
})
const { tzzy1Options, tzzy2Options } = toRefs(state)
onMounted(() => {})
</script>
<style lang="scss" scoped>
.special-type-container {
height: calc(612px - 67px);
.special-type-top,
.special-type-bottom {
height: 50%;
box-sizing: border-box;
}
.echarts {
height: calc(100% - 43px);
}
}
</style>

View File

@ -0,0 +1,70 @@
{
"grid": {
"left": "0%",
"right": "8%",
"bottom": "10%",
"top": "15%",
"containLabel": true
},
"tooltip": {
"trigger": "axis",
"borderWidth": 1,
"backgroundColor": "#FFFFFF",
"borderColor": "rgba(0,0,0,0.15)",
"textStyle": {
"color": "#3F4040"
},
"axisPointer": {
"animation": false,
"lineStyle": {
"color": "rgba(94, 161, 255, 0.15)"
}
}
},
"xAxis": {
"type": "category",
"data": ["冶金", "有色", "建材", "机械", "轻工", "纺织", "烟草", "商贸"],
"axisTick": {
"show": false
},
"axisLine": {
"lineStyle": {
"type": "solid",
"color": "#ECECEC"
},
"symbol": ["none"],
"symbolSize": [13, 20],
"symbolOffset": [0, 40]
},
"axisLabel": {
"margin": 16,
"interval": 0,
"fontSize": 16,
"fontFamily": "Avenir-Medium",
"color": "#3F4040"
},
"splitLine": {
"show": false
}
},
"yAxis": {
"show": false
},
"series": {
"name": "行业企业数",
"type": "bar",
"smooth": true,
"showSymbol": false,
"barWidth": 18,
"data": [12, 36, 20, 72, 30, 30, 30, 30],
"itemStyle": {
"color": "#4CBB14"
},
"label": {
"show": true,
"position": "top",
"fontSize": 14,
"fontFamily": "Avenir-Medium"
}
}
}

View File

@ -0,0 +1,48 @@
{
"title": {
"text": "177",
"subtext": "风险对象",
"left": "center",
"top": "32%",
"textStyle": {
"fontFamily": "ff-AvenirMedium",
"fontSize": 18,
"color": "#3F4040",
"align": "center"
},
"subtextStyle": {
"fontSize": 14,
"color": "#3F4040"
}
},
"tooltip": {
"trigger": "item"
},
"grid": {
"left": "2%",
"right": "4%",
"bottom": "0%",
"top": "0%",
"containLabel": true
},
"series": {
"name": "风险对象统计",
"type": "pie",
"radius": ["50%", "70%"],
"avoidLabelOverlap": false,
"padAngle": 2,
"label": {
"show": false,
"position": "center"
},
"emphasis": {
"label": {
"show": false
}
},
"labelLine": {
"show": false
},
"data": []
}
}

View File

@ -0,0 +1,82 @@
{
"grid": {
"left": "8%",
"right": "8%",
"bottom": "5%",
"top": "15%",
"containLabel": true
},
"tooltip": {
"trigger": "axis",
"borderWidth": 1,
"backgroundColor": "#FFFFFF",
"borderColor": "rgba(0,0,0,0.15)",
"textStyle": {
"color": "#3F4040"
},
"axisPointer": {
"animation": false,
"lineStyle": {
"color": "rgba(94, 161, 255, 0.15)"
}
}
},
"xAxis": {
"type": "category",
"data": ["电工证\n (高压)", "电工证\n (低压)", "焊工证", "高空证", "制冷证\n (冷库)", "制冷证 \n(空调)"],
"axisTick": {
"show": false
},
"axisLine": {
"lineStyle": {
"type": "solid",
"color": "#ECECEC"
},
"symbol": ["none"],
"symbolSize": [13, 20],
"symbolOffset": [0, 40]
},
"axisLabel": {
"margin": 16,
"interval": 0,
"fontSize": 16,
"fontFamily": "Avenir-Medium",
"color": "#3F4040"
},
"splitLine": {
"show": false
}
},
"yAxis": {
"type": "value",
"axisTick": {
"show": false
},
"axisLine": {
"show": false
},
"axisLabel": {
"fontSize": 16,
"fontFamily": "Avenir-Medium",
"color": "#3F4040"
},
"splitLine": {
"lineStyle": {
"type": "dashed",
"color": "#ECECEC"
},
"symbol": ["none"],
"symbolSize": [13, 20],
"symbolOffset": [0, 40]
}
},
"series": {
"type": "line",
"smooth": true,
"showSymbol": false,
"data": [100, 200, 200, 250, 300, 350, 360, 380, 300, 330, 350, 380],
"itemStyle": {
"color": "#4CBB14"
}
}
}

View File

@ -0,0 +1,120 @@
{
"grid": {
"left": "8%",
"right": "8%",
"bottom": "8%",
"top": "15%",
"containLabel": true
},
"tooltip": {
"trigger": "axis",
"borderWidth": 1,
"backgroundColor": "#FFFFFF",
"borderColor": "rgba(0,0,0,0.15)",
"textStyle": {
"color": "#3F4040"
},
"axisPointer": {
"animation": false,
"lineStyle": {
"color": "rgba(94, 161, 255, 0.15)"
}
}
},
"legend": {
"data": ["今日", "本月"],
"icon": "circle",
"height": 5,
"itemHeight": 8,
"itemGap": 10,
"textStyle": {
"color": "#3F4040",
"lineHeight": 5,
"fontSize": 16,
"padding": [0, 10, 0, -8]
},
"lineStyle": {
"color": "transparent"
}
},
"xAxis": {
"type": "category",
"data": ["动火", "受限\n空间", "盲板\n抽堵", "高处", "吊装", "临时\n用电", "动土", "断路"],
"axisTick": {
"show": false
},
"axisLine": {
"lineStyle": {
"type": "solid",
"color": "#ECECEC"
},
"symbol": ["none"],
"symbolSize": [13, 20],
"symbolOffset": [0, 40]
},
"axisLabel": {
"margin": 16,
"interval": 0,
"fontSize": 16,
"fontFamily": "Avenir-Medium",
"color": "#3F4040"
},
"splitLine": {
"show": false
}
},
"yAxis": {
"name": "单位",
"type": "value",
"nameTextStyle": {
"fontSize": 16,
"fontFamily": "Avenir-Medium",
"padding": [0, 50, 15, 0],
"color": "rgba(255, 255, 255, 0.90)"
},
"axisTick": {
"show": false
},
"axisLine": {
"show": false
},
"axisLabel": {
"fontSize": 16,
"fontFamily": "Avenir-Medium",
"color": "#3F4040"
},
"splitLine": {
"lineStyle": {
"type": "dashed",
"color": "#ECECEC"
},
"symbol": ["none"],
"symbolSize": [13, 20],
"symbolOffset": [0, 40]
}
},
"series": [
{
"name": "今日",
"type": "bar",
"smooth": true,
"showSymbol": false,
"barWidth": 18,
"data": [100, 80, 80, 150, 100, 150, 160, 180, 100, 110, 150, 180],
"itemStyle": {
"color": "#4CBB14"
}
},
{
"name": "本月",
"type": "line",
"smooth": true,
"showSymbol": false,
"color": "#FFB317",
"data": [200, 300, 200, 190, 200, 300, 200, 300, 400, 300, 300, 400],
"itemStyle": {
"color": "#FFD93E"
}
}
]
}

View File

@ -0,0 +1,48 @@
{
"title": {
"text": "177",
"subtext": "危险工艺",
"left": "center",
"top": "32%",
"textStyle": {
"fontFamily": "ff-AvenirMedium",
"fontSize": 18,
"color": "#3F4040",
"align": "center"
},
"subtextStyle": {
"fontSize": 14,
"color": "#3F4040"
}
},
"tooltip": {
"trigger": "item"
},
"grid": {
"left": "2%",
"right": "4%",
"bottom": "0%",
"top": "0%",
"containLabel": true
},
"series": {
"name": "危险工艺",
"type": "pie",
"radius": ["50%", "70%"],
"avoidLabelOverlap": false,
"padAngle": 2,
"label": {
"show": false,
"position": "center"
},
"emphasis": {
"label": {
"show": false
}
},
"labelLine": {
"show": false
},
"data": []
}
}

View File

@ -0,0 +1,49 @@
{
"title": {
"text": "2400",
"subtext": "处",
"left": "center",
"top": "40%",
"textStyle": {
"fontFamily": "ff-AvenirMedium",
"fontSize": 22,
"color": "#3F4040",
"align": "center"
},
"subtextStyle": {
"fontSize": 16,
"color": "#3F4040"
}
},
"tooltip": {
"trigger": "item"
},
"grid": {
"left": "2%",
"right": "4%",
"bottom": "0%",
"top": "0%",
"containLabel": true
},
"series": {
"name": "危险工艺",
"type": "pie",
"radius": ["50%", "70%"],
"center": ["50%", "50%"],
"avoidLabelOverlap": false,
"padAngle": 2,
"label": {
"show": false,
"position": "center"
},
"emphasis": {
"label": {
"show": false
}
},
"labelLine": {
"show": false
},
"data": []
}
}

View File

@ -0,0 +1,109 @@
<template>
<div class="lurking-peril-container ccw">
<div class="lurking-peril-top">
<public-chart ref="publicChart" :option="yhzlOptions" theme="vab-echarts-theme" />
<div class="legend-content ccw">
<span style="color: #8c8c8c; margin-bottom: 5px">隐患总数</span>
<span style="margin-bottom: 5px">
<span class="fs-22 ccw ff-AvenirMedium">2400</span>
<span>&nbsp;</span>
</span>
<div v-for="(item, index) in yhzlList" :key="index" style="">
<span>
<span class="circle" :style="{ background: item.itemStyle.color }"></span>
{{ item.name }}
</span>
<span class="ff-AvenirMedium" style="margin: 0 8px">{{ item.value }}</span>
</div>
</div>
</div>
<div class="lurking-peril-bottom">
<div class="lurking-peril-bottom-item radius-base" v-for="(item, index) in yxcyhList" :key="index">
<span class="fs-18 ff-AvenirMedium">{{ item.value }}</span>
<span>{{ item.name }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs } from 'vue'
import yhzl from './json/yhzl.json'
const state = reactive({
yhzlList: [
{ name: '企业自查', value: 12, itemStyle: { color: '#057AFF' } },
{ name: '监督检查', value: 12, itemStyle: { color: '#499EFE' } },
{ name: '公共检查', value: 12, itemStyle: { color: '#A5CFFF' } },
{ name: '风险检查', value: 12, itemStyle: { color: '#D8EAFF' } },
],
yxcyhList: [
{ name: '隐患已消除', value: 136 },
{ name: '整改中', value: 136 },
{ name: '逾期未处置', value: 136 },
{ name: '往期隐患', value: 136 },
],
yhzlOptions: yhzl,
})
const { yhzlList, yhzlOptions, yxcyhList } = toRefs(state)
onMounted(() => {
yhzlOptions.value.series.data = yhzlList.value
})
</script>
<style lang="scss" scoped>
.lurking-peril-container {
height: calc(359px - 51px);
.lurking-peril-top {
display: flex;
height: 70%;
.echarts {
width: 50%;
height: 100%;
}
.legend-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 50%;
padding-left: 15px;
box-sizing: border-box;
div {
display: flex;
align-items: center;
width: 80%;
margin-bottom: 8px;
}
.circle {
display: inline-block;
width: 8px;
height: 8px;
margin-right: 2px;
}
}
}
.lurking-peril-bottom {
display: flex;
height: 25%;
padding: 0 12px;
.lurking-peril-bottom-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: calc(25% - 8px);
margin: 0 4px;
background: #f8f8fa;
}
}
}
</style>

View File

@ -0,0 +1,555 @@
<template>
<div class="officer-selector-container">
<!-- 触发下拉框的输入框 -->
<el-popover
v-model:visible="popoverVisible"
placement="top-start"
:width="800"
trigger="click"
popper-class="officer-selector-popover"
:hide-after="0"
>
<!-- 下拉内容人员选择面板 -->
<template #default>
<el-card class="department-personnel-selector" shadow="never">
<div class="selector-content">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :span="8">
<el-card shadow="never" class="department-tree-card">
<template #header>
<div class="department-header">
<span>部门结构</span>
<el-button v-if="selectedDepartment" type="text" size="small" @click="selectedDepartment = null">
清除
</el-button>
</div>
</template>
<el-scrollbar height="300px">
<el-tree
:data="departmentsTree"
node-key="id"
:props="{
label: 'name',
children: 'children',
}"
:highlight-current="true"
@node-click="handleDepartmentSelect"
>
<template #default="{ node, data }">
<div class="custom-tree-node">
<span>{{ node.label }}</span>
<el-tag size="small" type="info" effect="plain">
{{ countPeopleInDepartment(data.path) }}
</el-tag>
</div>
</template>
</el-tree>
</el-scrollbar>
</el-card>
</el-col>
<!-- 人员列表 -->
<el-col :span="16">
<el-card shadow="never" class="personnel-list-card">
<div class="search-section">
<div class="search-row">
<el-input v-model="searchTerm" placeholder="搜索人员、职位或部门..." clearable>
<template #prefix>
<font-awesome-icon icon="fa-search" />
</template>
</el-input>
<!-- 全选和清空按钮 -->
<div class="selection-actions">
<el-button
type="primary"
size="small"
:disabled="filteredPeople.length === 0"
@click.stop="selectAll"
plain
>
<font-awesome-icon icon="fa-check-double" class="action-icon" />
全选
</el-button>
<el-button
type="danger"
size="small"
:disabled="selectedPeople.length === 0"
@click.stop="clearSelection"
plain
>
<font-awesome-icon icon="fa-times" class="action-icon" />
清空
</el-button>
</div>
</div>
<div v-if="selectedDepartment" class="selected-department">
<span class="label">当前部门:</span>
<el-tag closable @close="selectedDepartment = null">
{{ selectedDepartment.name }}
</el-tag>
</div>
</div>
<el-scrollbar height="300px" class="personnel-scrollbar">
<el-empty v-if="loading" description="加载中..." />
<el-empty v-else-if="filteredPeople.length === 0" description="未找到匹配的人员" />
<div v-else class="personnel-list">
<div
v-for="person in filteredPeople"
:key="person.id"
class="personnel-item"
:class="{ 'is-selected': isPersonSelected(person.id) }"
@click="togglePerson(person)"
>
<el-avatar :size="40" :src="person.image">
{{ person.name.slice(0, 1) }}
</el-avatar>
<div class="personnel-info">
<div class="personnel-name">{{ person.name }}</div>
<div class="personnel-details">
<el-tag size="small" effect="plain">{{ person.roleName || '未知角色' }}</el-tag>
<el-divider direction="vertical" />
<el-tooltip :content="person.departmentName" placement="top">
<span class="department-path">{{ person.departmentName }}</span>
</el-tooltip>
</div>
</div>
<font-awesome-icon v-if="isPersonSelected(person.id)" class="check-icon" icon="fa-check" />
</div>
</div>
</el-scrollbar>
</el-card>
</el-col>
</el-row>
<!-- 已选择人员 -->
<div v-if="selectedPeople.length > 0" class="selected-personnel">
<h3>已选择 ({{ selectedPeople.length }})</h3>
<div class="selected-tags">
<el-tag
v-for="person in selectedPeople"
:key="person.id"
closable
@close="removePerson(person.id)"
class="personnel-tag"
>
{{ person.name }}
<span class="department-label">({{ person.departmentName }} · {{ person.roleName || '未知角色' }})</span>
</el-tag>
</div>
</div>
<el-button
type="primary"
class="confirm-button"
@click.stop="confirmSelection"
>
确认选择 ({{ selectedPeople.length }})
</el-button>
</div>
</el-card>
</template>
<!-- 触发元素 -->
<template #reference>
<div class="reference-input">
<el-input
readonly
:placeholder="placeholder"
:value="displayValue"
@click.stop="handleInputClick"
class="selector-input"
>
<template #suffix>
<font-awesome-icon icon="fa-chevron-down" />
</template>
</el-input>
</div>
</template>
</el-popover>
</div>
</template>
<script setup>
import { ref, computed, defineProps, defineEmits, onMounted } from 'vue'
import { agencies } from '../api/lawenforcement/Agency'
import { officers } from '../api/lawenforcement/Officer'
// propsemits
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
placeholder: {
type: String,
default: '请选择人员',
},
agencyId:{
type: String,
default: '1'
}
})
const emit = defineEmits(['update:modelValue'])
//
const popoverVisible = ref(false)
//
function handleInputClick() {
popoverVisible.value = true
}
// - 使API
const departmentsTree = ref([])
const loading = ref(false)
// - API
const people = ref([])
//
// 使props.modelValueselectedPeoplev-model
const selectedPeople = ref([...props.modelValue])
const selectedDepartment = ref(null)
const searchTerm = ref('')
//
onMounted(async () => {
try {
loading.value = true
//
const agencyResponse = await agencies.tree()
if (agencyResponse.data && agencyResponse.data.length > 0) {
//
departmentsTree.value = processAgencyTree(agencyResponse.data)
}
//
await loadAllOfficers()
} catch (error) {
console.error('加载机构和人员数据失败:', error)
} finally {
loading.value = false
}
})
//
function processAgencyTree(agencies) {
return agencies.map(agency => ({
id: agency.agencyId, // 使 agencyId
agencyId: agency.agencyId, // agencyId
name: agency.agencyName, // 使 agencyName
path: agency.agencyPath || agency.agencyName, // 使 agencyPath agencyName
code: agency.agencyCode, // 使 agencyCode
level: agency.agencyLevel, // 使 agencyLevel
leaf: agency.leaf, // 使 leaf
parentId: agency.parent?.agencyId || '0', // 使 agencyId parentId
//
children: agency.children && agency.children.length > 0 ? processAgencyTree(agency.children) : []
}))
}
//
async function loadAllOfficers() {
try {
loading.value = true
// 使officers.querylist
const response = await officers.querylist()
if (response.success && response.data) {
//
const officersData = response.data.map(officer => ({
id: officer.officerId,
name: officer.officerName,
role: officer.role || 'zfry',
roleName: officer.roleName || '执法人员',
departmentName: officer.agency?.agencyName || '未知部门',
departmentPath: officer.agency?.agencyPath || '',
certificateNo: officer.certificateNo || '',
avatar: officer.avatar || ''
}))
//
const uniqueOfficers = officersData.filter(newOfficer =>
!people.value.some(existingOfficer => existingOfficer.id === newOfficer.id)
)
people.value = [...people.value, ...uniqueOfficers]
}
} catch (error) {
console.error('加载所有人员数据失败:', error)
} finally {
loading.value = false
}
}
//
const filteredPeople = computed(() => {
return people.value.filter((person) => {
const matchesSearch =
searchTerm.value === '' ||
person.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
person.role.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
person.departmentName.toLowerCase().includes(searchTerm.value.toLowerCase())
const matchesDepartment = !selectedDepartment.value || person.departmentPath.startsWith(selectedDepartment.value.path)
return matchesSearch && matchesDepartment
})
})
//
const displayValue = computed(() => {
if (!props.modelValue || props.modelValue.length === 0) return ''
if (props.modelValue.length === 1) return props.modelValue[0].name
return `已选择 ${props.modelValue.length}`
})
//
function handleDepartmentSelect(data) {
//
selectedDepartment.value = data
}
// v-model
function confirmSelection(e) {
emit('update:modelValue', [...selectedPeople.value])
popoverVisible.value = false
//
if (e) e.stopPropagation()
}
function togglePerson(person) {
const index = selectedPeople.value.findIndex((p) => p.id === person.id)
if (index === -1) {
selectedPeople.value.push(person)
} else {
selectedPeople.value.splice(index, 1)
}
// emit
}
function removePerson(personId) {
const index = selectedPeople.value.findIndex((p) => p.id === personId)
if (index !== -1) {
selectedPeople.value.splice(index, 1)
}
}
//
function selectAll() {
//
filteredPeople.value.forEach((person) => {
if (!isPersonSelected(person.id)) {
selectedPeople.value.push(person)
}
})
// v-model
}
//
function clearSelection() {
selectedPeople.value = []
// v-model
}
function isPersonSelected(personId) {
return selectedPeople.value.some((p) => p.id === personId)
}
function countPeopleInDepartment(departmentPath) {
return people.value.filter((p) => p.departmentPath.startsWith(departmentPath)).length
}
</script>
<style scoped>
.officer-selector-container {
width: 100%;
position: relative;
}
.selector-input {
width: 100%;
}
.selected-tags-preview {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.reference-input {
width: 100%;
}
:deep(.officer-selector-popover) {
max-width: 800px;
padding: 0;
}
.department-personnel-selector {
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
font-size: 18px;
margin: 0;
}
.selector-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.department-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.search-section {
margin-bottom: 16px;
}
.search-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.selection-actions {
display: flex;
gap: 5px;
white-space: nowrap;
}
.action-icon {
margin-right: 4px;
}
.selected-department {
margin-top: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.selected-department .label {
font-size: 13px;
color: #909399;
}
.personnel-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.personnel-item {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
position: relative;
}
.personnel-item:hover {
background-color: #f5f7fa;
}
.personnel-item.is-selected {
background-color: #ecf5ff;
}
.personnel-info {
margin-left: 12px;
flex: 1;
min-width: 0;
}
.personnel-name {
font-weight: 500;
margin-bottom: 4px;
}
.personnel-details {
font-size: 12px;
color: #909399;
display: flex;
align-items: center;
}
.department-path {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
display: inline-block;
}
.check-icon {
color: #409eff;
font-size: 16px;
margin-left: 8px;
}
.selected-personnel {
margin-top: 16px;
}
.selected-personnel h3 {
font-size: 14px;
margin-bottom: 10px;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.personnel-tag {
display: flex;
align-items: center;
gap: 4px;
}
.department-label {
font-size: 12px;
color: #909399;
}
.confirm-button {
margin-top: 16px;
width: 100%;
}
</style>
//

View File

@ -0,0 +1,142 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
/**
* 全局进度条组件
* 类似YouTube在点击链接跳转或加载组件时显示的顶部进度条
* 颜色使用渐变淡蓝到白色
*/
//
const progress = ref(0);
const isVisible = ref(false);
const timer = ref(null);
const router = useRouter();
//
const progressSpeed = 3; //
const progressIncrement = 5; //
const initialDelay = 50; // (ms)
/**
* 开始进度条动画
*/
const startProgress = () => {
isVisible.value = true;
progress.value = 0;
//
if (timer.value) {
clearInterval(timer.value);
}
//
setTimeout(() => {
//
timer.value = setInterval(() => {
//
if (progress.value < 30) {
progress.value += progressIncrement;
} else if (progress.value < 60) {
progress.value += progressIncrement * 0.8;
} else if (progress.value < 80) {
progress.value += progressIncrement * 0.5;
} else if (progress.value < 90) {
progress.value += progressIncrement * 0.2;
} else if (progress.value < 95) {
progress.value += progressIncrement * 0.1;
}
// 95%5%
if (progress.value >= 95) {
progress.value = 95;
clearInterval(timer.value);
}
}, progressSpeed * 100);
}, initialDelay);
};
/**
* 完成进度条动画
*/
const completeProgress = () => {
//
if (timer.value) {
clearInterval(timer.value);
}
//
progress.value = 100;
//
setTimeout(() => {
isVisible.value = false;
progress.value = 0;
}, 300);
};
//
onMounted(() => {
//
router.beforeEach((to, from, next) => {
startProgress();
next();
});
//
router.afterEach(() => {
completeProgress();
});
//
router.onError(() => {
completeProgress();
});
});
//
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<template>
<div
class="progress-bar-container"
:class="{ 'visible': isVisible }"
>
<div
class="progress-bar"
:style="{ width: `${progress}%` }"
></div>
</div>
</template>
<style lang="scss" scoped>
.progress-bar-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
&.visible {
opacity: 1;
}
.progress-bar {
height: 100%;
width: 0;
background: linear-gradient(to right, rgba(200, 200, 200, 0.9), rgba(255, 255, 255, 0.9));
box-shadow: 0 0 10px rgba(200, 200, 200, 0.7);
transition: width 0.3s ease;
}
}
</style>

View File

@ -0,0 +1,438 @@
!(function (e, t) {
'object' == typeof exports && 'object' == typeof module
? (module.exports = t(require('echarts')))
: 'function' == typeof define && define.amd
? define(['echarts'], t)
: 'object' == typeof exports
? (exports['echarts-liquidfill'] = t(require('echarts')))
: (e['echarts-liquidfill'] = t(e.echarts))
})(self, function (e) {
return (() => {
'use strict'
var t = {
245: (e, t, a) => {
a.r(t)
var i = a(83)
i.extendSeriesModel({
type: 'series.liquidFill',
optionUpdated: function () {
var e = this.option
e.gridSize = Math.max(Math.floor(e.gridSize), 4)
},
getInitialData: function (e, t) {
var a = i.helper.createDimensions(e.data, { coordDimensions: ['value'] }),
r = new i.List(a, this)
return r.initData(e.data), r
},
defaultOption: {
color: ['#294D99', '#156ACF', '#1598ED', '#45BDFF'],
center: ['50%', '50%'],
radius: '50%',
amplitude: '8%',
waveLength: '80%',
phase: 'auto',
period: 'auto',
direction: 'right',
shape: 'circle',
waveAnimation: !0,
animationEasing: 'linear',
animationEasingUpdate: 'linear',
animationDuration: 2e3,
animationDurationUpdate: 1e3,
outline: {
show: !0,
borderDistance: 8,
itemStyle: {
color: 'none',
borderColor: '#294D99',
borderWidth: 8,
shadowBlur: 20,
shadowColor: 'rgba(0, 0, 0, 0.25)',
},
},
backgroundStyle: { color: '#E3F7FF' },
itemStyle: { opacity: 0.95, shadowBlur: 50, shadowColor: 'rgba(0, 0, 0, 0.4)' },
label: {
show: !0,
color: '#294D99',
insideColor: '#fff',
fontSize: 50,
fontWeight: 'bold',
align: 'center',
baseline: 'middle',
position: 'inside',
},
emphasis: { itemStyle: { opacity: 0.8 } },
},
})
const r = i.graphic.extendShape({
type: 'ec-liquid-fill',
shape: {
waveLength: 0,
radius: 0,
radiusY: 0,
cx: 0,
cy: 0,
waterLevel: 0,
amplitude: 0,
phase: 0,
inverse: !1,
},
buildPath: function (e, t) {
null == t.radiusY && (t.radiusY = t.radius)
for (var a = Math.max(2 * Math.ceil(((2 * t.radius) / t.waveLength) * 4), 8); t.phase < 2 * -Math.PI; )
t.phase += 2 * Math.PI
for (; t.phase > 0; ) t.phase -= 2 * Math.PI
var i = (t.phase / Math.PI / 2) * t.waveLength,
r = t.cx - t.radius + i - 2 * t.radius
e.moveTo(r, t.waterLevel)
for (var l = 0, o = 0; o < a; ++o) {
var s = o % 4,
h = n((o * t.waveLength) / 4, s, t.waveLength, t.amplitude)
e.bezierCurveTo(
h[0][0] + r,
-h[0][1] + t.waterLevel,
h[1][0] + r,
-h[1][1] + t.waterLevel,
h[2][0] + r,
-h[2][1] + t.waterLevel
),
o === a - 1 && (l = h[2][0])
}
t.inverse
? (e.lineTo(l + r, t.cy - t.radiusY), e.lineTo(r, t.cy - t.radiusY), e.lineTo(r, t.waterLevel))
: (e.lineTo(l + r, t.cy + t.radiusY), e.lineTo(r, t.cy + t.radiusY), e.lineTo(r, t.waterLevel)),
e.closePath()
},
})
function n(e, t, a, i) {
return 0 === t
? [
[e + (0.5 * a) / Math.PI / 2, i / 2],
[e + (0.5 * a) / Math.PI, i],
[e + a / 4, i],
]
: 1 === t
? [
[e + ((0.5 * a) / Math.PI / 2) * (Math.PI - 2), i],
[e + ((0.5 * a) / Math.PI / 2) * (Math.PI - 1), i / 2],
[e + a / 4, 0],
]
: 2 === t
? [
[e + (0.5 * a) / Math.PI / 2, -i / 2],
[e + (0.5 * a) / Math.PI, -i],
[e + a / 4, -i],
]
: [
[e + ((0.5 * a) / Math.PI / 2) * (Math.PI - 2), -i],
[e + ((0.5 * a) / Math.PI / 2) * (Math.PI - 1), -i / 2],
[e + a / 4, 0],
]
}
var l = function (e, t) {
switch (e) {
case 'center':
case 'middle':
e = '50%'
break
case 'left':
case 'top':
e = '0%'
break
case 'right':
case 'bottom':
e = '100%'
}
return 'string' == typeof e
? ((a = e), a.replace(/^\s+|\s+$/g, '')).match(/%$/)
? (parseFloat(e) / 100) * t
: parseFloat(e)
: null == e
? NaN
: +e
var a
}
function o(e) {
return e && 0 === e.indexOf('path://')
}
i.extendChartView({
type: 'liquidFill',
render: function (e, t, a) {
var n = this,
s = this.group
s.removeAll()
var h = e.getData(),
d = h.getItemModel(0),
p = d.get('center'),
u = d.get('radius'),
c = a.getWidth(),
g = a.getHeight(),
v = Math.min(c, g),
f = 0,
y = 0,
m = e.get('outline.show')
m && ((f = e.get('outline.borderDistance')), (y = l(e.get('outline.itemStyle.borderWidth'), v)))
var w,
b,
x,
M = l(p[0], c),
P = l(p[1], g),
I = !1,
S = e.get('shape')
'container' === S
? ((I = !0),
(b = [(w = [c / 2, g / 2])[0] - y / 2, w[1] - y / 2]),
(x = [l(f, c), l(f, g)]),
(u = [Math.max(b[0] - x[0], 0), Math.max(b[1] - x[1], 0)]))
: ((b = (w = l(u, v) / 2) - y / 2), (x = l(f, v)), (u = Math.max(b - x, 0))),
m && ((Y().style.lineWidth = y), s.add(Y()))
var L = I ? 0 : M - u,
C = I ? 0 : P - u,
T = null
s.add(
(function () {
var t = E(u)
t.setStyle(e.getModel('backgroundStyle').getItemStyle()), (t.style.fill = null), (t.z2 = 5)
var a = E(u)
a.setStyle(e.getModel('backgroundStyle').getItemStyle()), (a.style.stroke = null)
var r = new i.graphic.Group()
return r.add(t), r.add(a), r
})()
)
var D = this._data,
F = []
function E(e, t) {
if (S) {
if (o(S)) {
var a = i.graphic.makePath(S.slice(7), {}),
r = a.getBoundingRect(),
n = r.width,
l = r.height
n > l ? ((l *= (2 * e) / n), (n = 2 * e)) : ((n *= (2 * e) / l), (l = 2 * e))
var s = t ? 0 : M - n / 2,
h = t ? 0 : P - l / 2
return (
(a = i.graphic.makePath(S.slice(7), {}, new i.graphic.BoundingRect(s, h, n, l))),
t && ((a.x = -n / 2), (a.y = -l / 2)),
a
)
}
if (I) {
var d = t ? -e[0] : M - e[0],
p = t ? -e[1] : P - e[1]
return i.helper.createSymbol('rect', d, p, 2 * e[0], 2 * e[1])
}
return (
(d = t ? -e : M - e),
(p = t ? -e : P - e),
'pin' === S ? (p += e) : 'arrow' === S && (p -= e),
i.helper.createSymbol(S, d, p, 2 * e, 2 * e)
)
}
return new i.graphic.Circle({ shape: { cx: t ? 0 : M, cy: t ? 0 : P, r: e } })
}
function Y() {
var t = E(w)
return (t.style.fill = null), t.setStyle(e.getModel('outline.itemStyle').getItemStyle()), t
}
function k(t, a, n) {
var o = I ? u[0] : u,
s = I ? g / 2 : u,
d = h.getItemModel(t),
p = d.getModel('itemStyle'),
c = d.get('phase'),
v = l(d.get('amplitude'), 2 * s),
f = l(d.get('waveLength'), 2 * o),
y = s - h.get('value', t) * s * 2
c = n ? n.shape.phase : 'auto' === c ? (t * Math.PI) / 4 : c
var m = p.getItemStyle()
if (!m.fill) {
var w = e.get('color'),
b = t % w.length
m.fill = w[b]
}
var x = new r({
shape: {
waveLength: f,
radius: o,
radiusY: s,
cx: 2 * o,
cy: 0,
waterLevel: y,
amplitude: v,
phase: c,
inverse: a,
},
style: m,
x: M,
y: P,
})
x.shape._waterLevel = y
var S = d.getModel('emphasis.itemStyle').getItemStyle()
;(S.lineWidth = 0), (x.ensureState('emphasis').style = S), i.helper.enableHoverEmphasis(x)
var L = E(u, !0)
return L.setStyle({ fill: 'white' }), x.setClipPath(L), x
}
function q(e, t, a) {
var i = h.getItemModel(e),
r = i.get('period'),
n = i.get('direction'),
l = h.get('value', e),
o = i.get('phase')
o = a ? a.shape.phase : 'auto' === o ? (e * Math.PI) / 4 : o
var s, d
s =
'auto' === r
? 0 === (d = h.count())
? 5e3
: 5e3 * (0.2 + ((d - e) / d) * 0.8)
: 'function' == typeof r
? r(l, e)
: r
var p = 0
'right' === n || null == n
? (p = Math.PI)
: 'left' === n
? (p = -Math.PI)
: 'none' === n
? (p = 0)
: console.error('Illegal direction value for liquid fill.'),
'none' !== n &&
i.get('waveAnimation') &&
t
.animate('shape', !0)
.when(0, { phase: o })
.when(s / 2, { phase: p + o })
.when(s, { phase: 2 * p + o })
.during(function () {
T && T.dirty(!0)
})
.start()
}
h
.diff(D)
.add(function (t) {
var a = k(t, !1),
r = a.shape.waterLevel
;(a.shape.waterLevel = I ? g / 2 : u),
i.graphic.initProps(a, { shape: { waterLevel: r } }, e),
(a.z2 = 2),
q(t, a, null),
s.add(a),
h.setItemGraphicEl(t, a),
F.push(a)
})
.update(function (t, a) {
for (
var r = D.getItemGraphicEl(a),
l = k(t, !1, r),
d = {},
p = ['amplitude', 'cx', 'cy', 'phase', 'radius', 'radiusY', 'waterLevel', 'waveLength'],
u = 0;
u < p.length;
++u
) {
var c = p[u]
l.shape.hasOwnProperty(c) && (d[c] = l.shape[c])
}
var v = {},
f = ['fill', 'opacity', 'shadowBlur', 'shadowColor']
for (u = 0; u < f.length; ++u) (c = f[u]), l.style.hasOwnProperty(c) && (v[c] = l.style[c])
I && (d.radiusY = g / 2),
i.graphic.updateProps(r, { shape: d, x: l.x, y: l.y }, e),
e.isUniversalTransitionEnabled && e.isUniversalTransitionEnabled()
? i.graphic.updateProps(r, { style: v }, e)
: r.useStyle(v)
var y = r.getClipPath(),
m = l.getClipPath()
r.setClipPath(l.getClipPath()),
(r.shape.inverse = l.inverse),
y &&
m &&
n._shape === S &&
!o(S) &&
i.graphic.updateProps(m, { shape: y.shape }, e, { isFrom: !0 }),
q(t, r, r),
s.add(r),
h.setItemGraphicEl(t, r),
F.push(r)
})
.remove(function (e) {
var t = D.getItemGraphicEl(e)
s.remove(t)
})
.execute(),
d.get('label.show') &&
s.add(
(function (t) {
var a = d.getModel('label')
var r,
n,
l,
o = {
z2: 10,
shape: { x: L, y: C, width: 2 * (I ? u[0] : u), height: 2 * (I ? u[1] : u) },
style: { fill: 'transparent' },
textConfig: { position: a.get('position') || 'inside' },
silent: !0,
},
s = {
style: {
text:
((r = e.getFormattedLabel(0, 'normal')),
(n = 100 * h.get('value', 0)),
(l = h.getName(0) || e.name),
isNaN(n) || (l = n.toFixed(0) + '%'),
null == r ? l : r),
textAlign: a.get('align'),
textVerticalAlign: a.get('baseline'),
},
}
Object.assign(s.style, i.helper.createTextStyle(a))
var p = new i.graphic.Rect(o),
c = new i.graphic.Rect(o)
;(c.disableLabelAnimation = !0), (p.disableLabelAnimation = !0)
var g = new i.graphic.Text(s),
v = new i.graphic.Text(s)
p.setTextContent(g), c.setTextContent(v)
var f = a.get('insideColor')
v.style.fill = f
var y = new i.graphic.Group()
y.add(p), y.add(c)
var m = E(u, !0)
return (
(T = new i.graphic.CompoundPath({ shape: { paths: t }, x: M, y: P })).setClipPath(m),
c.setClipPath(T),
y
)
})(F)
),
(this._shape = S),
(this._data = h)
},
dispose: function () {},
})
},
83: (t) => {
t.exports = e
},
},
a = {}
function i(e) {
if (a[e]) return a[e].exports
var r = (a[e] = { exports: {} })
return t[e](r, r.exports, i), r.exports
}
return (
(i.r = (e) => {
'undefined' != typeof Symbol &&
Symbol.toStringTag &&
Object.defineProperty(e, Symbol.toStringTag, { value: 'Module' }),
Object.defineProperty(e, '__esModule', { value: !0 })
}),
i(245)
)
})()
})
//# sourceMappingURL=echarts-liquidfill.min.js.map

View File

@ -0,0 +1,277 @@
<template>
<div class="echarts" />
</template>
<script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import theme from './theme/vab-echarts-theme.json'
import debounce from 'lodash/debounce'
import { addListener, removeListener } from 'resize-detector'
import { useThemeStore } from '@/stores/modules/theme'
const INIT_TRIGGERS = ['theme', 'initOptions', 'autoResize']
const REWATCH_TRIGGERS = ['manualUpdate', 'watchShallow']
const themeStore = useThemeStore()
const props = defineProps({
option: {
type: Object,
default: () => {},
},
theme: {
type: [String, Object],
default: () => {},
},
initOptions: {
type: Object,
default: () => {},
},
group: {
type: String,
default: '',
},
autoResize: {
type: Boolean,
default: true,
},
watchShallow: {
type: Boolean,
default: false,
},
manualUpdate: {
type: Boolean,
default: false,
},
})
const state = reactive({
chart: null,
lastArea: 0,
manualOptions: {},
})
const { chart, lastArea, manualOptions } = toRefs(state)
const { option, theme, group, initOptions, autoResize, watchShallow, manualUpdate } = toRefs(props)
// watch: {
// group(group) {
// chart.value.group = group
// },
// themeStore: {
// handler(data) {
// console.log(data)
// },
// deep: true,
// immediate: true,
// },
// },
onMounted(() => {
initOptionsWatcher()
INIT_TRIGGERS.forEach((prop) => {
this.$watch(
prop,
() => {
this.refresh()
},
{ deep: true }
)
})
REWATCH_TRIGGERS.forEach((prop) => {
this.$watch(prop, () => {
initOptionsWatcher()
this.refresh()
})
})
if (option.value) {
echarts.registerTheme('vab-echarts-theme', theme)
init()
}
})
const activated = () => {
if (autoResize.value) {
chart.value && chart.value.resize()
}
}
const destroyed = () => {
if (chart.value) {
this.destroy()
}
}
const mergeOptions = (option, notMerge, lazyUpdate) => {
if (manualUpdate.value) {
manualOptions.value = option
}
if (!chart.value) {
init(option)
} else {
delegateMethod('setOption', option, notMerge, lazyUpdate)
}
}
const appendData = (params) => {
delegateMethod('appendData', params)
}
const resize = (option) => {
delegateMethod('resize', option)
}
const dispatchAction = (payload) => {
delegateMethod('dispatchAction', payload)
}
const convertToPixel = (finder, value) => {
return delegateMethod('convertToPixel', finder, value)
}
const convertFromPixel = (finder, value) => {
return delegateMethod('convertFromPixel', finder, value)
}
const containPixel = (finder, value) => {
return delegateMethod('containPixel', finder, value)
}
const showLoading = (type, option) => {
delegateMethod('showLoading', type, option)
}
const hideLoading = () => {
delegateMethod('hideLoading')
}
const getDataURL = (option) => {
return delegateMethod('getDataURL', option)
}
const getConnectedDataURL = (option) => {
return delegateMethod('getConnectedDataURL', option)
}
const clear = () => {
delegateMethod('clear')
}
const dispose = () => {
delegateMethod('dispose')
}
const delegateMethod = (name, ...args) => {
if (!chart.value) {
init()
}
return chart.value[name](...args)
}
const delegateGet = (methodName) => {
if (!chart.value) {
init()
}
return chart.value[methodName]()
}
const getArea = () => {
return this.$el.offsetWidth * this.$el.offsetHeight
}
const init = (option) => {
if (chart.value) {
return
}
const newchart = echarts.init(this.$el, theme.value, initOptions.value)
if (group.value) {
newchart.group = group.value
}
newchart.setOption(option || manualOptions.value || option.value || {}, true)
Object.keys(this.$attrs).forEach((event) => {
const handler = this.$attrs[event]
if (event.indexOf('zr:') === 0) {
newchart.getZr().on(event.slice(3), handler)
} else {
newchart.on(event, handler)
}
})
if (autoResize.value) {
lastArea.value = getArea()
this.__resizeHandler = debounce(
() => {
if (lastArea.value === 0) {
this.mergeOptions({}, true)
this.resize()
this.mergeOptions(option.value || manualOptions.value || {}, true)
} else {
this.resize()
}
lastArea.value = getArea()
},
100,
{ leading: true }
)
addListener(this.$el, this.__resizeHandler)
}
Object.defineProperties(this, {
width: {
configurable: true,
get: () => {
return this.delegateGet('getWidth')
},
},
height: {
configurable: true,
get: () => {
return this.delegateGet('getHeight')
},
},
isDisposed: {
configurable: true,
get: () => {
return !!this.delegateGet('isDisposed')
},
},
computedOptions: {
configurable: true,
get: () => {
return this.delegateGet('getOption')
},
},
})
chart.value = chart
}
const initOptionsWatcher = () => {
if (this.__unwatchOptions) {
this.__unwatchOptions()
this.__unwatchOptions = null
}
if (!manualUpdate.value) {
this.__unwatchOptions = this.$watch(
'option',
(val, oldVal) => {
if (!chart.value && val) {
init()
} else {
chart.value.setOption(val, val !== oldVal)
}
},
{ deep: !this.watchShallow }
)
}
}
const destroy = () => {
if (autoResize.value) {
removeListener(this.$el, this.__resizeHandler)
}
this.dispose()
chart.value = null
}
const refresh = () => {
if (chart.value) {
this.destroy()
init()
}
}
const connect = (group) => {
if (typeof group !== 'string') {
group = group.map((chart) => chart.chart)
}
echarts.connect(group)
}
const disconnect = (group) => {
echarts.disConnect(group)
}
const getMap = (mapName) => {
return echarts.getMap(mapName)
}
// graphic: echarts.graphic,
</script>
<style>
.echarts {
/* width: 600px; */
height: 400px;
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<div class="echarts" />
</template>
<script>
import * as echarts from 'echarts'
import theme from './theme/vab-echarts-theme.json'
import debounce from 'lodash/debounce'
import { addListener, removeListener } from 'resize-detector'
import 'echarts-liquidfill'
const INIT_TRIGGERS = ['theme', 'initOptions', 'autoResize']
const REWATCH_TRIGGERS = ['manualUpdate', 'watchShallow']
export default {
props: {
option: {
type: Object,
default: () => {},
},
theme: {
type: [String, Object],
default: () => {},
},
initOptions: {
type: Object,
default: () => {},
},
group: {
type: String,
default: '',
},
autoResize: {
type: Boolean,
default: true,
},
watchShallow: {
type: Boolean,
default: false,
},
manualUpdate: {
type: Boolean,
default: false,
},
},
data() {
return {
lastArea: 0,
}
},
watch: {
group(group) {
this.chart.group = group
},
},
created() {
this.initOptionsWatcher()
INIT_TRIGGERS.forEach((prop) => {
this.$watch(
prop,
() => {
this.refresh()
},
{ deep: true }
)
})
REWATCH_TRIGGERS.forEach((prop) => {
this.$watch(prop, () => {
this.initOptionsWatcher()
this.refresh()
})
})
},
mounted() {
if (this.option) {
echarts.registerTheme('vab-echarts-theme', theme)
this.init()
}
},
activated() {
if (this.autoResize) {
this.chart && this.chart.resize()
}
},
destroyed() {
if (this.chart) {
this.destroy()
}
},
methods: {
mergeOptions(option, notMerge, lazyUpdate) {
if (this.manualUpdate) {
this.manualOptions = option
}
if (!this.chart) {
this.init(option)
} else {
this.delegateMethod('setOption', option, notMerge, lazyUpdate)
}
},
appendData(params) {
this.delegateMethod('appendData', params)
},
resize(option) {
this.delegateMethod('resize', option)
},
dispatchAction(payload) {
this.delegateMethod('dispatchAction', payload)
},
convertToPixel(finder, value) {
return this.delegateMethod('convertToPixel', finder, value)
},
convertFromPixel(finder, value) {
return this.delegateMethod('convertFromPixel', finder, value)
},
containPixel(finder, value) {
return this.delegateMethod('containPixel', finder, value)
},
showLoading(type, option) {
this.delegateMethod('showLoading', type, option)
},
hideLoading() {
this.delegateMethod('hideLoading')
},
getDataURL(option) {
return this.delegateMethod('getDataURL', option)
},
getConnectedDataURL(option) {
return this.delegateMethod('getConnectedDataURL', option)
},
clear() {
this.delegateMethod('clear')
},
dispose() {
this.delegateMethod('dispose')
},
delegateMethod(name, ...args) {
if (!this.chart) {
this.init()
}
return this.chart[name](...args)
},
delegateGet(methodName) {
if (!this.chart) {
this.init()
}
return this.chart[methodName]()
},
getArea() {
return this.$el.offsetWidth * this.$el.offsetHeight
},
init(option) {
if (this.chart) {
return
}
const chart = echarts.init(this.$el, this.theme, this.initOptions)
if (this.group) {
chart.group = this.group
}
chart.setOption(option || this.manualOptions || this.option || {}, true)
Object.keys(this.$attrs).forEach((event) => {
const handler = this.$attrs[event]
if (event.indexOf('zr:') === 0) {
chart.getZr().on(event.slice(3), handler)
} else {
chart.on(event, handler)
}
})
if (this.autoResize) {
this.lastArea = this.getArea()
this.__resizeHandler = debounce(
() => {
if (this.lastArea === 0) {
this.mergeOptions({}, true)
this.resize()
this.mergeOptions(this.option || this.manualOptions || {}, true)
} else {
this.resize()
}
this.lastArea = this.getArea()
},
100,
{ leading: true }
)
addListener(this.$el, this.__resizeHandler)
}
Object.defineProperties(this, {
width: {
configurable: true,
get: () => {
return this.delegateGet('getWidth')
},
},
height: {
configurable: true,
get: () => {
return this.delegateGet('getHeight')
},
},
isDisposed: {
configurable: true,
get: () => {
return !!this.delegateGet('isDisposed')
},
},
computedOptions: {
configurable: true,
get: () => {
return this.delegateGet('getOption')
},
},
})
this.chart = chart
},
initOptionsWatcher() {
if (this.__unwatchOptions) {
this.__unwatchOptions()
this.__unwatchOptions = null
}
if (!this.manualUpdate) {
this.__unwatchOptions = this.$watch(
'option',
(val, oldVal) => {
if (!this.chart && val) {
this.init()
} else {
this.chart.setOption(val, val !== oldVal)
}
},
{ deep: !this.watchShallow }
)
}
},
destroy() {
if (this.autoResize) {
removeListener(this.$el, this.__resizeHandler)
}
this.dispose()
this.chart = null
},
refresh() {
if (this.chart) {
this.destroy()
this.init()
}
},
},
connect(group) {
if (typeof group !== 'string') {
group = group.map((chart) => chart.chart)
}
echarts.connect(group)
},
disconnect(group) {
echarts.disConnect(group)
},
getMap(mapName) {
return echarts.getMap(mapName)
},
registerMap(mapName, geoJSON, specialAreas) {
echarts.registerMap(mapName, geoJSON, specialAreas)
},
graphic: echarts.graphic,
}
</script>
<style>
.echarts {
/* width: 600px; */
height: 400px;
}
</style>

View File

@ -0,0 +1,317 @@
{
"color": ["#1890FF", "#36CBCB", "#4ECB73", "#FBD437", "#F2637B", "#975FE5"],
"backgroundColor": "rgba(252,252,252,0)",
"textStyle": {},
"title": {
"textStyle": {
"color": "#666666"
},
"subtextStyle": {
"color": "#999999"
}
},
"line": {
"itemStyle": {
"borderWidth": "2"
},
"lineStyle": {
"normal": {
"width": "3"
}
},
"symbolSize": "8",
"symbol": "emptyCircle",
"smooth": false
},
"radar": {
"itemStyle": {
"borderWidth": "2"
},
"lineStyle": {
"normal": {
"width": "3"
}
},
"symbolSize": "8",
"symbol": "emptyCircle",
"smooth": false
},
"bar": {
"itemStyle": {
"barBorderWidth": 0,
"barBorderColor": "#ccc"
}
},
"pie": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"scatter": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"boxplot": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"parallel": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"sankey": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"funnel": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"gauge": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
}
},
"candlestick": {
"itemStyle": {
"color": "#e6a0d2",
"color0": "transparent",
"borderColor": "#e6a0d2",
"borderColor0": "#1890FF",
"borderWidth": "2"
}
},
"graph": {
"itemStyle": {
"borderWidth": 0,
"borderColor": "#ccc"
},
"lineStyle": {
"normal": {
"width": "1",
"color": "#cccccc"
}
},
"symbolSize": "8",
"symbol": "emptyCircle",
"smooth": false,
"color": ["#1890FF", "#36CBCB", "#4ECB73", "#FBD437", "#F2637B", "#975FE5"],
"label": {
"color": "#ffffff"
}
},
"map": {
"itemStyle": {
"areaColor": "#eeeeee",
"borderColor": "#aaaaaa",
"borderWidth": 0.5
},
"label": {
"color": "#ffffff"
}
},
"geo": {
"itemStyle": {
"areaColor": "#eeeeee",
"borderColor": "#aaaaaa",
"borderWidth": 0.5
},
"label": {
"color": "#ffffff"
}
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"color": "#999999"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": ["#eeeeee"]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
}
}
},
"valueAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisLabel": {
"show": true,
"color": "#999999"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": ["#eeeeee"]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
}
}
},
"logAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"color": "#999999"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": ["#eeeeee"]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
}
}
},
"timeAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc"
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"show": true,
"color": "#999999"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": ["#eeeeee"]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": ["rgba(250,250,250,0.05)", "rgba(200,200,200,0.02)"]
}
}
},
"toolbox": {
"iconStyle": {
"borderColor": "#999999"
}
},
"legend": {
"textStyle": {
"color": "#999999"
}
},
"tooltip": {
"axisPointer": {
"lineStyle": {
"color": "#ffffff",
"width": 1
},
"crossStyle": {
"color": "#ffffff",
"width": 1
}
}
},
"timeline": {
"lineStyle": {
"color": "#4ECB73",
"width": 1
},
"itemStyle": {
"color": "#4ECB73",
"borderWidth": 1
},
"controlStyle": {
"color": "#4ECB73",
"borderColor": "#4ECB73",
"borderWidth": 0.5
},
"checkpointStyle": {
"color": "#1890FF",
"borderColor": "rgba(63,177,227,0.15)"
},
"label": {
"color": "#4ECB73"
}
},
"visualMap": {
"color": ["#1890FF", "#afe8ff"]
},
"dataZoom": {
"backgroundColor": "rgba(255,255,255,0)",
"dataBackgroundColor": "rgba(222,222,222,1)",
"fillerColor": "rgba(114,230,212,0.25)",
"handleColor": "#cccccc",
"handleSize": "100%",
"textStyle": {
"color": "#999999"
}
},
"markPoint": {
"label": {
"color": "#ffffff"
}
}
}

View File

@ -0,0 +1,48 @@
<template>
<i
:class="`iconfont icon-${isFullscreen ? 'suoxiao' : 'bt_qp'}`"
:title="isFullscreen ? '退出全屏' : '进入全屏'"
@click="click"
></i>
</template>
<script setup>
import { reactive, onMounted, onUnmounted, defineEmits, toRefs } from 'vue'
import screenfull from 'screenfull'
const state = reactive({
isFullscreen: false,
})
const { isFullscreen } = toRefs(state)
const emit = defineEmits(['refresh'])
const click = () => {
if (!screenfull.isEnabled) {
this.$baseMessage('开启全屏失败', 'error', 'vab-hey-message-error')
}
screenfull.toggle()
emit('refresh')
}
const init = () => {
if (screenfull.isEnabled) screenfull.on('change', change)
}
const change = () => {
isFullscreen.value = screenfull.isFullscreen
}
const destroy = () => {
if (screenfull.isEnabled) screenfull.off('change', change)
}
onMounted(() => {
init()
})
onUnmounted(() => {
destroy()
})
</script>

View File

@ -0,0 +1,110 @@
<template>
<BaseSelector
v-model="cvalue"
:config="{
filterable: true,
remote: false,
multiple: multiple
}"
:option-props="{
key: 'itemId',
label: 'itemName',
value: 'itemId'
}"
:data="filteredCheckItems"
:placeholder="placeholder"
@change="handleChange"
@select-load-option="loadCheckItems"
/>
</template>
<script setup>
// API
import { ref, computed, watch, onMounted } from 'vue'
import BaseSelector from './BaseSelector.vue'
import checkItemApi from '@/api/lawenforcement/CheckItem'
// props
const props = defineProps({
//
multiple: {
type: Boolean,
default: false
},
//
placeholder: {
type: String,
default: '请选择检查项'
},
// v-model
modelValue: {
type: [Array, String, Number],
default: () => []
},
})
// emit
const emit = defineEmits(['update:modelValue', 'change'])
//
const checkItems = ref([])
const loading = ref(false)
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => {
//
let formattedValue = val
//
if (!props.multiple && Array.isArray(formattedValue) && formattedValue.length > 0) {
formattedValue = formattedValue[0]
}
//
if (props.multiple && !Array.isArray(formattedValue)) {
formattedValue = formattedValue ? [formattedValue] : []
}
emit('update:modelValue', formattedValue)
}
})
//
const filteredCheckItems = computed(() => checkItems.value)
//
const loadCheckItems = async () => {
loading.value = true
try {
const res = await checkItemApi.getCheckItems()
checkItems.value = res.data || []
loading.value = false
} catch (error) {
console.error('获取检查项数据失败:', error)
loading.value = false
}
}
//
const handleChange = (val) => {
//
let selectedCheckItems
if (Array.isArray(val)) {
selectedCheckItems = checkItems.value.filter(checkItem => val.includes(checkItem.itemId))
} else {
const selectedCheckItem = checkItems.value.find(checkItem => checkItem.itemId === val)
selectedCheckItems = selectedCheckItem ? [selectedCheckItem] : []
}
// change
emit('change', props.multiple ? selectedCheckItems : (selectedCheckItems[0] || null))
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,200 @@
<template>
<BaseSelector
v-model="cvalue"
:config="{
filterable: true,
remote: false,
multiple: multiple
}"
:option-props="{
key: 'enterpriseId',
label: 'unitName',
value: 'enterpriseId'
}"
:data="filteredEnterprises"
:filter-method="filterMethod"
:placeholder="placeholder"
@change="handleChange"
@select-load-option="loadEnterprises">
<template v-slot:loadMore>
<div class="left-info-bar" @click="loadMore"><span>还有条 {{leftTotal}} 记录</span></div>
</template>
</BaseSelector>
</template>
<script setup>
// API
import { ref, computed, watch, onMounted } from 'vue'
import enterpriseApi from '@/api/lawenforcement/Enterprise'
import BaseSelector from './BaseSelector.vue'
// props
const props = defineProps({
//
multiple: {
type: Boolean,
default: false
},
//
placeholder: {
type: String,
default: '请选择企业'
},
// v-model
modelValue: {
type: [Array, String, Number],
default: () => []
},
// Id
agencyId: {
type: [String, Number],
default: ''
},
// code
agencyCode: {
type: [String],
default: ''
},
// defaultEnterpriseIds
defaultEnterpriseIds: {
type: [Array, String, Number],
default: () => []
},
//
isShowAll: {
type: Boolean,
default: true
}
})
// emit
const emit = defineEmits(['update:modelValue', 'change'])
//
const query = ref({
page: 1,
pagesize: 20
})
const enterprises = ref([])
const leftTotal = ref(0)
const loading = ref(false)
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => {
//
let formattedValue = val
//
if (!props.multiple && Array.isArray(formattedValue) && formattedValue.length > 0) {
formattedValue = formattedValue[0]
}
//
if (props.multiple && !Array.isArray(formattedValue)) {
formattedValue = formattedValue ? [formattedValue] : []
}
emit('update:modelValue', formattedValue)
}
})
//
const filteredEnterprises = computed(() => {return enterprises.value})
//
const loadEnterprises = async () => {
loading.value = true
try {
if (!props.agencyId && !props.agencyCode && !props.isShowAll) {
return
}
query.value.agencyId = props.agencyId || '1'
query.value.agencyCode = props.agencyCode || '1'
query.value.enterpriseIds = props.defaultEnterpriseIds || []
const res = await enterpriseApi.findEnterprisesByAgencyId(query.value)
if (res.success) {
enterprises.value = mergeAndDeduplicate(enterprises.value, res.data)
leftTotal.value = res.total - enterprises.value.length
}
loading.value = false
} catch (error) {
console.error('获取企业数据失败:', error)
loading.value = false
}
}
function mergeAndDeduplicate(existing, newItems) {
let idMap = new Map()
existing.forEach(item => idMap.set(item.enterpriseId, item))
newItems.forEach(item => { if (!idMap.has(item.enterpriseId)) idMap.set(item.enterpriseId, item) })
return Array.from(idMap.values())
}
//
const handleChange = (val) => {
//
let selectedEnterprises
if (Array.isArray(val)) {
selectedEnterprises = enterprises.value.filter(enterprise => val.includes(enterprise.enterpriseId))
} else {
const selectedEnterprise = enterprises.value.find(enterprise => enterprise.enterpriseId === val)
selectedEnterprises = selectedEnterprise ? [selectedEnterprise] : []
}
// change
emit('change', props.multiple ? selectedEnterprises : (selectedEnterprises[0] || null))
}
//
const loadMore = () => {
query.value.page += 1
loadEnterprises()
}
//
const filterMethod = (val) => {
if (val) {
if (props.defaultEnterpriseIds.length > 0) {
let enterpriseMap = new Map(enterprises.value.map(item => [item.enterpriseId, item]))
let filteredEnterprises = props.defaultEnterpriseIds.map(enterpriseId => enterpriseMap.get(enterpriseId))
enterprises.value = filteredEnterprises
} else {
enterprises.value = []
}
query.value.page = 1
query.value.unitName = val
loadEnterprises()
}
}
// agencyId
watch(() => [props.agencyId, props.defaultEnterpriseIds], () => {
enterprises.value = []
query.value.page = 1
query.value.unitName = ''
loadEnterprises()
},{ deep: true })
//
onMounted(() => {
//
// select-load-option
})
</script>
<style lang="scss" scoped>
.left-info-bar {
color: #0D88FC;
}
</style>

View File

@ -0,0 +1,207 @@
<template>
<BaseSelector v-model="cvalue"
:config="baseConfig"
:option-props="selectConfig[simpleConfig.confName].optionProps"
:data="filterDatas"
:placeholder="placeholder"
@change="handleChange"
@select-load-option="loadOptions"
/>
</template>
<script setup>
// API
import {computed, onMounted, reactive, ref, toRefs, watch} from 'vue'
import BaseSelector from './BaseSelector.vue'
import {merge} from "lodash"
import documents from "@/api/lawenforcement/Document.js"
import deliveryMethods from "@/api/lawenforcement/DeliveryMethod.js"
import recipientInfos from "@/api/lawenforcement/RecipientInfo.js"
import cases from "@/api/lawenforcement/Case.js"
import checklists from "@/api/lawenforcement/Checklist.js"
// props
const props = defineProps({
//
multiple: {
type: Boolean,
default: false
},
remote: {
type: Boolean,
default: false
},
//
placeholder: {
type: String,
default: '请选择'
},
// v-model
modelValue: {
type: [Array, String, Number],
default: () => []
},
caseId: {
type: [String, Number],
default: ''
},
simpleConfig: {
type: Object
}
})
const state = reactive({
baseConfig: {
filterable: true,
remote: props.remote,
multiple: props.multiple,
filterMethod: filterMethod
},
selectConfig: {
case: {//
filterable: true,
api: cases,
key: 'caseId',
parmas: {},
optionProps: {key: 'caseId', label: 'caseName', value: 'caseId'}
},
document: {//
filterable: true,
api: documents,
key: 'documentId',
parmas: {status: 'done'},
optionProps: {key: 'documentId', label: 'documentName', value: 'documentId'}
},
recipient: {//
filterable: false,
api: recipientInfos,
parmas: {isDeleted: false},
key: 'recipientId',
optionProps: {key: 'recipientId', label: 'name', value: 'recipientId'}
},
method: {//
filterable: false,
api: deliveryMethods,
parmas: {isDeleted: false, enabled: true},
key: 'deliveryMethodId',
optionProps: {key: 'deliveryMethodId', label: 'methodName', value: 'deliveryMethodId'}
},
checklist: {//
filterable: false,
api: checklists,
parmas: {isRemove: false, enabled: true},
key: 'checklistId',
optionProps: {key: 'checklistId', label: 'checklistName', value: 'checklistId'}
}
}
})
const {baseConfig, selectConfig} = toRefs(state)
// emit
const emit = defineEmits(['update:modelValue', 'change'])
//
const allDatas = ref([])
const datas = ref([])
const loading = ref(false)
const parmas = reactive(merge({}, props.simpleConfig?.parmas || selectConfig.value[props.simpleConfig?.confName]?.parmas))
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => {
//
let formattedValue = val
//
if (!props.multiple && Array.isArray(formattedValue) && formattedValue.length > 0) {
formattedValue = formattedValue[0]
}
//
if (props.multiple && !Array.isArray(formattedValue)) {
formattedValue = formattedValue ? [formattedValue] : []
}
emit('update:modelValue', formattedValue)
}
})
//
const filterDatas = computed(() => {
return datas.value
})
async function listData() {
loading.value = true
try {
const res = await selectConfig.value[props.simpleConfig?.confName]?.api.querylist(parmas)
datas.value = res.data || []
allDatas.value = res.data || []
loading.value = false
} catch (error) {
console.error('获取数据失败:', error)
loading.value = false
}
}
//
const loadOptions = async () => {
if (props.simpleConfig?.confName === 'document') {
if (props.caseId) {
parmas.caseId = props.caseId
await listData()
}
} else
await listData()
}
//
const handleChange = (val) => {
//
let selecteds
if (Array.isArray(val)) {
selecteds = datas.value.filter(o => val.includes(o[selectConfig.value[props.simpleConfig?.confName]?.key]))
} else {
const selected = datas.value.find(o => o[selectConfig.value[props.simpleConfig?.confName]?.key] === val)
selecteds = selected ? [selected] : []
}
// change
emit('change', props.multiple ? selecteds : (selecteds[0] || null))
}
function filterMethod(query) {
if (selectConfig.value[props.simpleConfig?.confName]?.filterable) {//
parmas.conditionlike = query
loadOptions()
} else {
if (!query) datas.value = allDatas.value
else {
datas.value = allDatas.value.filter((option) => {
const label = option[selectConfig.value[props.simpleConfig?.confName]?.optionProps?.label]
const value = option[selectConfig.value[props.simpleConfig?.confName]?.optionProps?.value]
return label.toLowerCase().includes(query.toLowerCase()) || value.toString().toLowerCase().includes(query.toLowerCase())
})
}
}
}
// caseId
watch(() => props.caseId, async (nv) => {
if (nv) {
parmas.caseId = nv
if (props.simpleConfig?.confName === 'document')
await listData()
}
datas.value = []
})
//
onMounted(() => {
//
// select-load-option
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,237 @@
<template>
<BaseSelector
v-model="cvalue"
:config="{
filterable: true,
remote: false,
multiple: multiple,
filterMethod
}"
:option-props="{
key: 'officerId',
label: 'officerName',
value: 'officerId'
}"
:data="filteredOfficers"
:placeholder="placeholder"
@change="handleChange"
@select-load-option="loadOfficers"
/>
</template>
<script setup>
// API
import { ref, computed, watch, onMounted } from 'vue'
import BaseSelector from './BaseSelector.vue'
import officerApi from '../api/lawenforcement/Officer'
// props
const props = defineProps({
//
multiple: {
type: Boolean,
default: false
},
//
placeholder: {
type: String,
default: '请选择执法人员'
},
// v-model
modelValue: {
type: [Array, String, Number],
default: () => []
},
// ID
agencyId: {
type: [String, Number],
default: ''
}
})
// emit
const emit = defineEmits(['update:modelValue', 'change'])
//
const officers = ref([])
const loading = ref(false)
//
const cvalue = computed({
get: () => props.modelValue,
set: (val) => {
//
let formattedValue = val
//
if (!props.multiple && Array.isArray(formattedValue) && formattedValue.length > 0) {
formattedValue = formattedValue[0]
}
//
if (props.multiple && !Array.isArray(formattedValue)) {
formattedValue = formattedValue ? [formattedValue] : []
}
emit('update:modelValue', formattedValue)
}
})
//
const filteredOfficers = ref([])
//
//
const loadOfficers = async () => {
loading.value = true
try {
// API使
// API
const res = await officerApi.getOfficersByAgency(props.agencyId || '1')
officers.value = res.data || []
filteredOfficers.value = officers.value
// 使
// officers.value = [
// {
// id: '1',
// name: '',
// departmentId: '610102000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '2',
// name: '',
// departmentId: '610102000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '3',
// name: '',
// departmentId: '610103000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '4',
// name: '',
// departmentId: '610104000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '5',
// name: '',
// departmentId: '610111000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '6',
// name: '',
// departmentId: '610112000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '7',
// name: '',
// departmentId: '610113000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// },
// {
// id: '8',
// name: '',
// departmentId: '610114000000',
// departmentPath: '西.',
// departmentName: '',
// role: '',
// }
// ]
loading.value = false
} catch (error) {
console.error('获取执法人员数据失败:', error)
loading.value = false
}
}
//
const handleChange = (val) => {
//
let selectedOfficers
if (Array.isArray(val)) {
selectedOfficers = officers.value.filter(officer => val.includes(officer.id))
} else {
const selectedOfficer = officers.value.find(officer => officer.id === val)
selectedOfficers = selectedOfficer ? [selectedOfficer] : []
}
// change
emit('change', props.multiple ? selectedOfficers : (selectedOfficers[0] || null))
}
/**
* 过滤方法实现姓名和角色的复合搜索
* 搜索规则
* 1. 输入单个词"张三"匹配姓名或角色
* 2. 输入多个词"执法员 张三"要求同时匹配姓名和角色OR 逻辑
* @param {string} query - 搜索关键词
*/
const filterMethod = (query) => {
//
if (!officers.value || !Array.isArray(officers.value)) {
filteredOfficers.value = [];
return;
}
//
if (!query) {
filteredOfficers.value = officers.value;
return;
}
const searchTerms = query.trim().toLowerCase().split(/\s+/);
filteredOfficers.value = officers.value.filter((option) => {
const label = option.officerName?.toLowerCase() || '';
const role = option.role?.toLowerCase() || '';
const roleName = option.roleName?.toLowerCase() || '';
//
return searchTerms.some(term => {
//
return label.includes(term) ||
role.includes(term) ||
roleName.includes(term);
});
});
}
// agencyId
watch(() => props.agencyId, (newVal) => {
// ID
// 使APIAPI
// 使
loadOfficers()
})
//
onMounted(() => {
//
// select-load-option
})
</script>
<style lang="scss" scoped>
// SimpleOfficerSelector
</style>

View File

@ -0,0 +1,155 @@
<template>
<el-container :direction="direction" class="toolbar">
<slot name="bottomPanel"></slot>
<el-row class="top-panel">
<slot name="button" :toolbar-context="{ deleteIsDisabled }">
<el-col :span="21">
<el-button type="primary" v-show="permissions.add" @click="add" title="新增">
<template #icon>
<font-awesome-icon icon="plus" />
</template>
新增
</el-button>
<el-button type="danger" plain v-show="permissions.deleteAll" :disabled="deleteIsDisabled" @click="handleBatchDelete">
<template #icon>
<font-awesome-icon icon="trash" />
</template>
删除 ({{ selection.length }})
</el-button>
<el-button type="success" v-show="permissions.importFile" @click="importFile" title="导入">
<template #icon>
<font-awesome-icon icon="file-import" />
</template>
导入
</el-button>
<el-button type="primary" v-show="permissions.exportFile" @click="exportFile" title="导出">
<template #icon>
<font-awesome-icon icon="file-export" />
</template>
导出
</el-button>
<el-button type="primary" v-show="permissions.exportSelectFile" @click="exportSelectFile" title="选择导出">
<template #icon>
<font-awesome-icon icon="file-export" />
</template>
选择导出
</el-button>
<el-button type="warning" v-show="permissions.downloadTemp" @click="downloadTemp" title="下载样表">
<template #icon>
<font-awesome-icon icon="download" />
</template>
下载样表
</el-button>
<el-button type="primary" v-show="permissions.senior" @click="senior" title="高级检索">
<template #icon>
<font-awesome-icon icon="angle-double-right" rotation="90" v-if="!showSenior"/>
<font-awesome-icon icon="angle-double-left" rotation="90" v-if="showSenior"/>
</template>
高级检索
</el-button>
<slot name="extraButton" />
</el-col>
</slot>
<slot name="otherButton">
<el-col :span="3" class="other-button">
<el-button type="primary" v-show="permissions.query" @click="query" title="查询">
<template #icon>
<font-awesome-icon icon="search" />
</template>
查询
</el-button>
<el-button type="info" v-show="permissions.query" @click="$emit('do-reset')" title="重置">
<template #icon>
<font-awesome-icon icon="undo" />
</template>
重置
</el-button>
<el-button type="danger" v-show="permissions.help" @click="$emit('help')" title="帮助">
<template #icon>
<font-awesome-icon icon="question" />
</template>
帮助
</el-button>
</el-col>
</slot>
</el-row>
</el-container>
</template>
<script setup>
import {computed, ref} from 'vue'
import { ElMessageBox } from 'element-plus'
const emit = defineEmits([
'do-add',
'batch-delete',
'do-query',
'do-importFile',
'do-exportFile',
'do-exportSelectFile',
'do-downloadTemp',
'help',
'do-reset',
'do-senior'
])
const props = defineProps({
selection: {
type: Array,
default: () => [],
},
direction: {
type: String,
default: 'vertical',
},
permissions: {
type: Object,
default: () => ({
add: false,
delete: false,
deleteAll: false,
query: false,
importFile: false,
exportFile: false,
exportSelectFile: false,
downloadTemp: false,
help: false,
senior: false
}),
},
})
const showSenior = ref(false)
const deleteIsDisabled = computed(() => props.selection.length === 0)
const add = () => emit('do-add')
const handleBatchDelete = () => {
ElMessageBox.confirm(`确定要删除选中的 ${props.selection.length} 项记录吗?`, {
title: '确认删除',
type: 'warning',
})
.then(() => {
emit('batch-delete')
})
.catch(() => {})
}
const query = () => emit('do-query')
const senior = () => {
showSenior.value = !showSenior.value
emit('do-senior', showSenior.value)
}
const importFile = () => emit('do-importFile')
const exportFile = () => emit('do-exportFile')
const exportSelectFile = () => emit('do-exportSelectFile')
const downloadTemp = () => emit('do-downloadTemp')
</script>
<style scoped lang="scss">
.other-button {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,354 @@
<template>
<el-popover
popper-class="tree-selector"
ref="popover"
placement="bottom-start"
trigger="click"
width="auto"
:disabled="disabled"
@show="showTree = true"
@hide="showTree = false">
<div class="popover-panel" v-if="showTree" :style="popoverPanelStyle">
<el-input type="text" name="query" v-model="query" v-if="!lazyTree" :placeholder="queryPlaceHolder"
class="query-input">
<template #prefix>
<FontAwesomeIcon :icon="['fas','search']"></FontAwesomeIcon>
</template>
</el-input>
<el-tree
class="tree-panel"
:filter-node-method="filterMethod"
:props="treeProps"
:lazy="lazyTree"
:load="loadNode"
:node-key="nodeKey"
:show-checkbox="multiple"
:data="data"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKeys"
:default-checked-keys="modelValue instanceof Array ? modelValue.map(v => v[nodeKey]) : [modelValue ? modelValue[nodeKey]: null]"
@node-click="nodeClickHandler"
@check="nodeCheckHandler"
@current-change="currentNodeChange"
@node-expand="nodeExpandHandler"
ref="treeRef"></el-tree>
</div>
<template #reference>
<div class="el-select">
<div
class="el-select__tags"
v-if="multiple"
ref="tags">
<span v-if="modelValue && modelValue.length">
<el-tag
:closable="false"
:hit="modelValue[0].hitState"
type="info"
size="small"
disable-transitions>
<span class="el-select__tags-text">{{ getNodeLabel(modelValue[0]) }}</span>
</el-tag>
<el-tag
v-if="modelValue.length > 1"
:closable="false"
size="small"
type="info"
disable-transitions>
<span class="el-select__tags-text">+ {{ modelValue.length - 1 }}</span>
</el-tag>
</span>
</div>
<el-input
ref="select"
:disabled="disabled"
:placeholder="placeholder"
:readonly="true"
:model-value="inputValue">
<template #suffix>
<font-awesome-icon v-if="showClose" :icon="['fas', 'times-circle']" class="el-select__caret" @click.stop="handleClearClick" />
<font-awesome-icon v-if="!showClose" :icon="['fas', 'chevron-down']" class="el-select__caret" :class="iconClass" />
</template>
</el-input>
</div>
</template>
</el-popover>
</template>
<script setup>
// API
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { ref, computed, watch, nextTick, onMounted, onUpdated, useAttrs } from 'vue'
// props
const props = defineProps({
filterMethod: Function,
multiple: {
type: Boolean,
default: false
},
lazyTree: {
type: Boolean,
default: false
},
loadNode: {
type: Function,
default: () => {}
},
nodeKey: {
type: String,
default: (props) => {
return props.valueProp && typeof props.valueProp === 'string' ? props.valueProp : ''
}
},
treeProps: {
type: Object,
default: () => ({})
},
data: {
type: Array,
default: () => []
},
defaultExpandedKeys: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: ''
},
queryPlaceHolder: {
type: String,
default: '请输入筛选内容'
},
modelValue: {
type: [Array, String, Number, Object],
default: () => []
},
valueProp: {
type: [Object, String],
default: null
},
selectionFilter: {
type: Function,
default: (data, node, tree) => true
}
})
// emit
const emit = defineEmits(['update:modelValue'])
// attrs
const attrs = useAttrs()
//
const popoverPanelStyle = ref({})
const query = ref(null)
const showTree = ref(false)
const showClose = ref(false)
const popShow = ref(false)
// refs
const popover = ref(null)
const treeRef = ref(null)
const select = ref(null)
const tags = ref(null)
//
const iconClass = computed(() => {
return (showTree.value ? 'is-reverse' : '')
})
//
const getNodeLabel = (node) => {
if (!node) return '';
// treeProps.label
if (typeof props.treeProps.label === 'function') {
return props.treeProps.label(node);
}
// treeProps.label
return node[props.treeProps.label] || '';
};
const inputValue = computed(() => {
let value
let nodeKey
if (!props.multiple) {
if (props.modelValue) {
if (props.modelValue instanceof Object) {
// 使 getNodeLabel
value = getNodeLabel(props.modelValue);
if (!value) {
nodeKey = props.modelValue[props.nodeKey]
}
} else {
nodeKey = props.modelValue
}
if (!value) {
const valueObj = findFirstTreeNodeData(props.data, data => data[props.nodeKey] === nodeKey)
value = valueObj ? getNodeLabel(valueObj) : null
}
}
}
toggleShowClose(value)
return value
})
// query
watch(query, (val) => {
treeRef.value.filter(val)
})
//
const findFirstTreeNodeData = (treeData, queryPredicate) => {
//
const findData = treeData.find(queryPredicate)
if (!findData) {
return treeData.map(td => td[props.treeProps.children] || []).reduce((result, next) => !result ? findFirstTreeNodeData(next, queryPredicate) : result, null)
}
return findData
}
const nodeClickHandler = (data, node, tree) => {
if (!props.multiple) {
if (selection(data, node, tree)) {
closeSelectWindow()
}
} else {
//
treeRef.value.setChecked(node, !node.checked, true)
//
const checkedNodes = treeRef.value.getCheckedNodes()
//
selection(checkedNodes)
}
}
const nodeExpandHandler = (data, node, self) => {
//
}
const nodeCheckHandler = (data, { checkedKeys, checkedNodes, halfCheckedKeys, halfCheckedNodes }) => {
selection(checkedNodes)
}
const currentNodeChange = (data, node) => {
//
console.log(data, node);
}
const selection = (data, node, tree) => {
const result = props.selectionFilter(data, node, tree)
if (result) {
updateModelValue(data)
}
return result
}
const updateModelValue = (val) => {
const value = val instanceof Array ? val.map(computedValue) : computedValue(val)
emit('update:modelValue', value)
toggleShowClose(value)
}
const computedValue = (data) => {
if (props.valueProp) {
if (props.valueProp instanceof Object) {
return Object.fromEntries(Object.entries(props.valueProp).map(([valueProperty, dataProperty]) => {
return [valueProperty, data[dataProperty]]
}))
} else {
return data[props.valueProp]
}
} else {
return data
}
}
const closeSelectWindow = () => {
popover.value.hide()
}
const handleClearClick = () => {
if (props.disabled) {
return false
}
showTree.value = false
//
if (treeRef.value) {
treeRef.value.setCheckedKeys([])
}
if (props.multiple) {
updateModelValue([])
} else {
updateModelValue('')
}
}
const toggleShowClose = (val) => {
showClose.value = props.multiple ? val?.length > 0 : !!val
}
const expandDefaultKeys = () => {
if (!treeRef.value || !props.defaultExpandedKeys || !props.defaultExpandedKeys.length) return
props.defaultExpandedKeys.forEach((key) => {
const node = treeRef.value.getNode(key)
if (node && !node.expanded) {
if (typeof node.expand === 'function') {
node.expand()
} else {
node.expanded = true
}
}
})
}
watch(() => showTree.value, async (v) => {
if (v) {
await nextTick()
expandDefaultKeys()
}
})
watch(() => props.data, async () => {
if (showTree.value) {
await nextTick()
expandDefaultKeys()
}
})
//
onMounted(() => {
nextTick(() => {
popoverPanelStyle.value = {
'min-width': select.value.$el.clientWidth + 'px'
}
})
})
onUpdated(() => {
if (showTree.value) {
if (props.modelValue && props.multiple) {
treeRef.value.setCheckedKeys([...props.modelValue].map(n => n[props.nodeKey]))
}
}
})
</script>
<style lang="scss">
.tree-selector {
.tree-panel {
max-height: 300px;
overflow: auto;
}
.query-input {
margin-bottom: 4px;
}
}
</style>