增加业务菜单
This commit is contained in:
parent
c5073ebd6c
commit
53bd29677b
|
|
@ -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) => {
|
||||
// 当外部modelValue变化时,如果与内部值不同,触发node-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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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) => { // 导出table页中所有数据,需要传入到后台进行查询并组装excel,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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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> 处</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>
|
||||
|
|
@ -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'
|
||||
|
||||
// 定义props和emits
|
||||
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.modelValue初始化selectedPeople,确保与v-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>
|
||||
//
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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变化时,可能需要重新加载或过滤执法人员
|
||||
// 如果使用远程API,这里可以重新调用API获取数据
|
||||
// 现在使用本地过滤,不需要额外处理
|
||||
loadOfficers()
|
||||
})
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 组件挂载时加载执法人员数据
|
||||
// 实际上在select-load-option事件中已经处理了
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 可以添加特定于SimpleOfficerSelector的样式
|
||||
</style>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue