feat:对照需求表

This commit is contained in:
dzc committed 2026-07-02 17:47:59 +08:00
1 parent 5685ee9136
commit 8cc6972f1c
10 files changed
+1015 -320

No files matched your search

+6 -10
View File
@@ -2,13 +2,9 @@
<div class="top-right-btn" :style="style">
<el-row>
<!-- 搜索框控制 -->
<el-tooltip
class="item"
effect="dark"
:content="showSearch ? $t('queryTree.hideSearch') : $t('queryTree.displayTheSearch')"
placement="top"
v-if="search"
>
<el-tooltip class="item" effect="dark"
:content="showSearch ? $t('queryTree.hideSearch') : $t('queryTree.displayTheSearch')" placement="top"
v-if="search">
<el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip>
@@ -22,9 +18,9 @@
<el-button circle icon="Refresh" @click="handleRefresh()" />
</el-tooltip>
<!-- 插槽 -->
<slot></slot>
</el-row>
<!-- 插槽 -->
<slot></slot>
</el-row>
</div>
</template>
+360
View File
@@ -0,0 +1,360 @@
<template>
<el-tree-select :class="['select-org-tree', { 'select-org-tree--single': !props.multiple }]" :props="treeProps"
node-key="id" v-model="state.innerValue" :placeholder="props.placeholder" :clearable="props.clearable"
:disabled="props.disabled" :loading="state.loading" :data="state.options" :render-after-expand="false"
:show-checkbox="props.multiple" :multiple="props.multiple" :check-strictly="props.checkStrictly"
:check-on-click-node="props.multiple" :expand-on-click-node="false" :filter-node-method="filterNode" filterable
default-expand-all style="width: 100%" @node-click="handleNodeClick" @check="handleCheck"
@visible-change="handleVisibleChange" @clear="handleClear">
<template #default="{ data, node }">
<div class="flex items-center gap-2 w-full min-w-0">
<el-tag v-if="node.level === 1" size="small" effect="dark" color="var(--el-color-primary)">顶级组织</el-tag>
<span class="font-medium truncate">{{ data.treeLabel || data.name || data.orgName || node.label
}}</span>
<span v-if="data.orgCode" class="text-gray-400 truncate">{{ data.orgCode }}</span>
</div>
</template>
</el-tree-select>
</template>
<script setup lang="ts" name="select-org-tree">
import { onMounted, reactive, watch } from 'vue'
import * as SysOrgApi from '/@/api/sys/sysOrg'
const emit = defineEmits(['change'])
const modelValue = defineModel<any>()
type OrgTreeNode = {
id: string | number
primaryId?: string | number
name?: string
orgName?: string
orgCode?: string
treeLabel?: string
visible?: boolean
children?: OrgTreeNode[]
[key: string]: any
}
type Props = {
placeholder?: string
disabled?: boolean
clearable?: boolean
autoFirst?: boolean
multiple?: boolean
checkStrictly?: boolean
valueKey?: string
apiParams?: Record<string, any>
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择组织',
disabled: false,
clearable: true,
autoFirst: false,
multiple: false,
checkStrictly: false,
valueKey: 'id',
apiParams: () => ({}),
})
const treeProps = {
label: 'treeLabel',
children: 'children',
value: 'id',
}
const state = reactive<{
loading: boolean
innerValue: any
options: OrgTreeNode[]
loaded: boolean
syncingFromModel: boolean
}>({
loading: false,
innerValue: props.multiple ? [] : null,
options: [],
loaded: false,
syncingFromModel: false,
})
let loadOptionsTask: Promise<void> | null = null
const hasSelectedValue = (value: unknown) => {
if (Array.isArray(value)) {
return value.some((item) => item !== null && item !== undefined && `${item}`.trim() !== '')
}
return value !== null && value !== undefined && `${value}`.trim() !== ''
}
const shouldLoadOptionsOnInit = (value: unknown) => props.autoFirst || hasSelectedValue(value)
const filterVisibleNodes = (nodes: OrgTreeNode[]): OrgTreeNode[] => {
if (!Array.isArray(nodes)) return []
return nodes.reduce<OrgTreeNode[]>((filtered, node) => {
if (node.visible !== false) {
const filteredNode: OrgTreeNode = {
...node,
treeLabel: node.name || node.orgName || `${node.id}`,
}
if (node.children?.length) {
filteredNode.children = filterVisibleNodes(node.children)
}
filtered.push(filteredNode)
return filtered
}
if (node.children?.length) {
const filteredChildren = filterVisibleNodes(node.children)
if (filteredChildren.length > 0) {
filtered.push({
...node,
treeLabel: node.name || node.orgName || `${node.id}`,
children: filteredChildren,
})
}
}
return filtered
}, [])
}
const filterNode = (value: string, data: OrgTreeNode, node: any) => {
const kw = (value ?? '').toString().trim().toLowerCase()
if (!kw) return true
const selfName = (data?.treeLabel ?? '').toString().toLowerCase()
const selfCode = (data?.orgCode ?? '').toString().toLowerCase()
const parentName = (node?.parent?.data?.treeLabel ?? '').toString().toLowerCase()
if (selfName && selfName.includes(kw)) return true
if (selfCode && selfCode.includes(kw)) return true
if (parentName && parentName.includes(kw)) return true
return false
}
const getCacheKey = () => `SelectOrgTree_Cache_${JSON.stringify(props.apiParams || {})}`
const flattenNodes = (nodes: OrgTreeNode[]): OrgTreeNode[] => {
return nodes.reduce<OrgTreeNode[]>((result, node) => {
result.push(node)
if (node.children?.length) {
result.push(...flattenNodes(node.children))
}
return result
}, [])
}
const findNodeById = (nodes: OrgTreeNode[], nodeId: string | number): OrgTreeNode | null => {
for (const node of nodes) {
if (node.id === nodeId) return node
if (node.children?.length) {
const childNode = findNodeById(node.children, nodeId)
if (childNode) return childNode
}
}
return null
}
const getLocalEmitValue = (node: OrgTreeNode) => {
if (props.valueKey === 'id') return node.id
return node?.[props.valueKey]
}
const resolveEmitValue = async (node: OrgTreeNode) => {
const localValue = getLocalEmitValue(node)
if (localValue !== undefined && localValue !== null && `${localValue}`.trim() !== '') {
return localValue
}
if (props.valueKey === 'id' || !node.primaryId) return node.id
try {
const res: any = await SysOrgApi.getObj(String(node.primaryId))
const resolvedValue = res?.data?.[props.valueKey]
if (resolvedValue !== undefined && resolvedValue !== null && `${resolvedValue}`.trim() !== '') {
node[props.valueKey] = resolvedValue
if (res?.data?.orgCode) node.orgCode = res.data.orgCode
if (res?.data?.orgName && !node.treeLabel) node.treeLabel = res.data.orgName
return resolvedValue
}
} catch (error) {
console.error('Failed to resolve org node value', error)
}
return node.id
}
const findNodeByModelValue = (nodes: OrgTreeNode[], value: any): OrgTreeNode | null => {
const flatNodes = flattenNodes(nodes)
return flatNodes.find((node) => getLocalEmitValue(node) === value) || null
}
const syncInnerValueByModel = async (value: any) => {
state.syncingFromModel = true
try {
if (props.multiple) {
const values = Array.isArray(value) ? value : []
const nodeIds = values
.map((item) => findNodeByModelValue(state.options, item)?.id)
.filter((item) => item !== undefined && item !== null)
state.innerValue = nodeIds
return
}
if (!hasSelectedValue(value)) {
state.innerValue = null
return
}
const matchedNode = findNodeByModelValue(state.options, value)
state.innerValue = matchedNode?.id ?? null
} finally {
state.syncingFromModel = false
}
}
const emitChange = (value: any) => {
modelValue.value = value
emit('change', value)
}
const findFirstSelectableNode = (nodes: OrgTreeNode[]): OrgTreeNode | null => {
for (const node of nodes) {
if (!props.checkStrictly || !node.children?.length) return node
if (node.children?.length) {
const childNode = findFirstSelectableNode(node.children)
if (childNode) return childNode
}
}
return null
}
watch(
modelValue,
async (newVal, oldValue) => {
if (newVal === oldValue && state.loaded) return
if (shouldLoadOptionsOnInit(newVal)) {
await ensureOptionsLoaded()
}
await syncInnerValueByModel(newVal)
},
{ immediate: true, deep: true },
)
watch(
() => props.apiParams,
async () => {
if (shouldLoadOptionsOnInit(modelValue.value) || state.loaded) {
state.loaded = false
await ensureOptionsLoaded(true)
await syncInnerValueByModel(modelValue.value)
}
},
{ deep: true },
)
onMounted(() => {
if (shouldLoadOptionsOnInit(modelValue.value)) {
void ensureOptionsLoaded()
}
})
async function ensureOptionsLoaded(force = false) {
if (state.loaded && !force) return
if (!force) {
try {
const cacheKey = getCacheKey()
const cached = localStorage.getItem(cacheKey)
if (cached) {
state.options = JSON.parse(cached)
state.loaded = true
return
}
} catch (e) {
console.error('Failed to parse cached org tree data', e)
}
}
if (loadOptionsTask) return loadOptionsTask
loadOptionsTask = getOptions(force).finally(() => {
loadOptionsTask = null
})
return loadOptionsTask
}
async function getOptions(force = false) {
if (state.loaded && !force) return
try {
state.loading = true
const { data } = await SysOrgApi.getTree({
...(props.apiParams || {}),
})
state.options = filterVisibleNodes((data ?? []) as OrgTreeNode[])
state.loaded = true
try {
const cacheKey = getCacheKey()
localStorage.setItem(cacheKey, JSON.stringify(state.options))
} catch (e) {
console.error('Failed to cache org tree data', e)
}
if (props.autoFirst && !hasSelectedValue(modelValue.value) && state.options.length > 0) {
const firstNode = findFirstSelectableNode(state.options)
if (firstNode) {
state.innerValue = props.multiple ? [firstNode.id] : firstNode.id
const emitValue = await resolveEmitValue(firstNode)
emitChange(props.multiple ? [emitValue] : emitValue)
}
}
} finally {
state.loading = false
}
}
const handleNodeClick = async (data: OrgTreeNode, node: any) => {
if (props.multiple) return
if (props.checkStrictly && node?.childNodes?.length > 0) return
state.innerValue = data.id
const emitValue = await resolveEmitValue(data)
emitChange(emitValue)
}
const handleCheck = async (_: OrgTreeNode, checkStatus: any) => {
if (!props.multiple || state.syncingFromModel) return
const checkedNodes = (checkStatus?.checkedNodes || []) as OrgTreeNode[]
const values = await Promise.all(checkedNodes.map((node) => resolveEmitValue(node)))
emitChange(values)
}
const handleVisibleChange = (visible: boolean) => {
if (!visible) return
void ensureOptionsLoaded()
}
const handleClear = () => {
state.innerValue = props.multiple ? [] : null
emitChange(props.multiple ? [] : null)
}
defineExpose({
refresh: () => ensureOptionsLoaded(true),
clearSelection: handleClear,
getSelectedNode: () => {
if (props.multiple) {
return (Array.isArray(state.innerValue) ? state.innerValue : [])
.map((id) => findNodeById(state.options, id))
.filter(Boolean)
}
return findNodeById(state.options, state.innerValue)
},
})
</script>
<style lang="scss">
.select-org-tree {
.el-tree-node__content {
min-height: 36px;
}
}
.select-org-tree--single {
.el-tree-node__content .el-checkbox {
display: none;
}
}
</style>
+109 -104
View File
@@ -1,130 +1,135 @@
import Pagination from '/@/components/Pagination/index.vue';
import RightToolbar from '/@/components/RightToolbar/index.vue';
import DictTag from '/@/components/DictTag/index.vue';
import DictSelect from '/@/components/DictTag/Select.vue';
import UploadExcel from '/@/components/Upload/Excel.vue';
import UploadFile from '/@/components/Upload/index.vue';
import UploadImg from '/@/components/Upload/Image.vue';
import DelWrap from '/@/components/DelWrap/index.vue';
import Editor from '/@/components/Editor/index.vue';
import Tip from '/@/components/Tip/index.vue';
import TagList from '/@/components/TagList/index.vue';
import SvgIcon from '/@/components/SvgIcon/index.vue';
import Sign from '/@/components/Sign/index.vue';
import ChinaArea from '/@/components/ChinaArea/index.vue';
import OrgSelector from '/@/components/OrgSelector/index.vue';
import Pagination from "/@/components/Pagination/index.vue";
import RightToolbar from "/@/components/RightToolbar/index.vue";
import DictTag from "/@/components/DictTag/index.vue";
import DictSelect from "/@/components/DictTag/Select.vue";
import UploadExcel from "/@/components/Upload/Excel.vue";
import UploadFile from "/@/components/Upload/index.vue";
import UploadImg from "/@/components/Upload/Image.vue";
import DelWrap from "/@/components/DelWrap/index.vue";
import Editor from "/@/components/Editor/index.vue";
import Tip from "/@/components/Tip/index.vue";
import TagList from "/@/components/TagList/index.vue";
import SvgIcon from "/@/components/SvgIcon/index.vue";
import Sign from "/@/components/Sign/index.vue";
import ChinaArea from "/@/components/ChinaArea/index.vue";
import OrgSelector from "/@/components/OrgSelector/index.vue";
import DraggableDeptTree from "/@/components/DraggableDeptTree/index.vue"
import DraggableOrgTree from "/@/components/DraggableOrgTree/index.vue"
import DraggableDeviceGroup from "/@/components/DraggableDeviceGroup/index.vue"
import DraggableDeviceGroupDraggable from "/@/components/DraggableDeviceGroupDraggable/index.vue"
import DraggableDeptTree from "/@/components/DraggableDeptTree/index.vue";
import DraggableOrgTree from "/@/components/DraggableOrgTree/index.vue";
import DraggableDeviceGroup from "/@/components/DraggableDeviceGroup/index.vue";
import DraggableDeviceGroupDraggable from "/@/components/DraggableDeviceGroupDraggable/index.vue";
import PreviewImage from "/@/components/PreviewImage/index.vue"
import PreviewImage from "/@/components/PreviewImage/index.vue";
import TagOnline from "/@/components/Tag/TagOnline.vue"
import TagSex from "/@/components/Tag/TagSex.vue"
import TagSync from "/@/components/Tag/TagSync.vue"
import TagUserStatus from "/@/components/Tag/TagUserStatus.vue"
import TagOnline from "/@/components/Tag/TagOnline.vue";
import TagSex from "/@/components/Tag/TagSex.vue";
import TagSync from "/@/components/Tag/TagSync.vue";
import TagUserStatus from "/@/components/Tag/TagUserStatus.vue";
import SelectArea from "/@/components/Select/SelectArea.vue"
import SelectDevice from "/@/components/Select/SelectDevice.vue"
import SelectDeviceGroup from "/@/components/Select/SelectDeviceGroup.vue"
import SelectDuty from "/@/components/Select/SelectDuty.vue"
import SelectDutyType from "/@/components/Select/SelectDutyType.vue"
import SelectTreeChannel from "/@/components/Select/SelectTreeChannel.vue"
import SelectTreeChannelRadio from "/@/components/Select/SelectTreeChannelRadio.vue"
import SelectTreeDept from "/@/components/Select/SelectTreeDept.vue"
import SelectDeviceTree from "/@/components/Select/SelectDeviceTree.vue"
import SelectAreaType from "/@/components/Select/SelectAreaType.vue"
import SelectPersonCarType from "/@/components/Select/SelectPersonCarType.vue"
import SelectPerson from "/@/components/Select/SelectPerson.vue"
import SelectArea from "/@/components/Select/SelectArea.vue";
import SelectDevice from "/@/components/Select/SelectDevice.vue";
import SelectDeviceGroup from "/@/components/Select/SelectDeviceGroup.vue";
import SelectDuty from "/@/components/Select/SelectDuty.vue";
import SelectDutyType from "/@/components/Select/SelectDutyType.vue";
import SelectTreeChannel from "/@/components/Select/SelectTreeChannel.vue";
import SelectTreeChannelRadio from "/@/components/Select/SelectTreeChannelRadio.vue";
import SelectTreeDept from "/@/components/Select/SelectTreeDept.vue";
import SelectOrgTree from "/@/components/Select/SelectOrgTree.vue";
import SelectDeviceTree from "/@/components/Select/SelectDeviceTree.vue";
import SelectAreaType from "/@/components/Select/SelectAreaType.vue";
import SelectPersonCarType from "/@/components/Select/SelectPersonCarType.vue";
import SelectPerson from "/@/components/Select/SelectPerson.vue";
import ColumnBtn from "/@/components/Column/ColumnBtn.vue"
import ColumnBtn from "/@/components/Column/ColumnBtn.vue";
import PreviewJson from "/@/components/PreviewJson/index.vue"
import PreviewJson from "/@/components/PreviewJson/index.vue";
import RtspPluginVideo from "/@/components/rtsp-plugin-video/video-plugin.vue"
import RtspPluginVideo from "/@/components/rtsp-plugin-video/video-plugin.vue";
// 第三方组件
import ElementPlus from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import 'element-plus/dist/index.css';
import { Pane, Splitpanes } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import ElementPlus from "element-plus";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import "element-plus/dist/index.css";
import { Pane, Splitpanes } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
// 日历组件
import { setupCalendar } from 'v-calendar';
import { setupCalendar } from "v-calendar";
// 部门树组件
import vue3TreeOrg from 'vue3-tree-org';
import 'vue3-tree-org/lib/vue3-tree-org.css';
import vue3TreeOrg from "vue3-tree-org";
import "vue3-tree-org/lib/vue3-tree-org.css";
// 导入 FcDesigner
import FcDesigner from 'form-create-designer';
import FcDesigner from "form-create-designer";
// 导入声明
import { App } from 'vue';
import { App } from "vue";
export default {
install(app: App) {
app.component('DictTag', DictTag);
app.component('DictSelect', DictSelect);
app.component('Pagination', Pagination);
app.component('RightToolbar', RightToolbar);
app.component('uploadExcel', UploadExcel);
app.component('UploadFile', UploadFile);
app.component('UploadImg', UploadImg);
app.component('Editor', Editor);
app.component('Tip', Tip);
app.component('DelWrap', DelWrap);
app.component('TagList', TagList);
app.component('SvgIcon', SvgIcon);
app.component('ChinaArea', ChinaArea);
app.component('OrgSelector', OrgSelector);
app.component('Sign', Sign);
app.component('PreviewImage', PreviewImage)
install(app: App) {
app.component("DictTag", DictTag);
app.component("DictSelect", DictSelect);
app.component("Pagination", Pagination);
app.component("RightToolbar", RightToolbar);
app.component("uploadExcel", UploadExcel);
app.component("UploadFile", UploadFile);
app.component("UploadImg", UploadImg);
app.component("Editor", Editor);
app.component("Tip", Tip);
app.component("DelWrap", DelWrap);
app.component("TagList", TagList);
app.component("SvgIcon", SvgIcon);
app.component("ChinaArea", ChinaArea);
app.component("OrgSelector", OrgSelector);
app.component("Sign", Sign);
app.component("PreviewImage", PreviewImage);
app.component('DraggableDeptTree', DraggableDeptTree)
app.component('DraggableOrgTree', DraggableOrgTree)
app.component('DraggableDeviceGroup', DraggableDeviceGroup)
app.component('DraggableDeviceGroupDraggable', DraggableDeviceGroupDraggable)
app.component("DraggableDeptTree", DraggableDeptTree);
app.component("DraggableOrgTree", DraggableOrgTree);
app.component("DraggableDeviceGroup", DraggableDeviceGroup);
app.component(
"DraggableDeviceGroupDraggable",
DraggableDeviceGroupDraggable
);
app.component('TagOnline', TagOnline);
app.component('TagSex', TagSex);
app.component('TagSync', TagSync);
app.component('TagUserStatus', TagUserStatus);
app.component("TagOnline", TagOnline);
app.component("TagSex", TagSex);
app.component("TagSync", TagSync);
app.component("TagUserStatus", TagUserStatus);
app.component('SelectArea', SelectArea);
app.component('SelectDevice', SelectDevice);
app.component('SelectDeviceGroup', SelectDeviceGroup)
app.component('SelectDuty', SelectDuty);
app.component('SelectDutyType', SelectDutyType);
app.component('SelectTreeChannel', SelectTreeChannel);
app.component('SelectTreeChannelRadio', SelectTreeChannelRadio);
app.component('SelectTreeDept', SelectTreeDept);
app.component('SelectDeviceTree', SelectDeviceTree);
app.component('SelectAreaType', SelectAreaType);
app.component('SelectPersonCarType', SelectPersonCarType);
app.component('SelectPerson', SelectPerson);
app.component("SelectArea", SelectArea);
app.component("SelectDevice", SelectDevice);
app.component("SelectDeviceGroup", SelectDeviceGroup);
app.component("SelectDuty", SelectDuty);
app.component("SelectDutyType", SelectDutyType);
app.component("SelectTreeChannel", SelectTreeChannel);
app.component("SelectTreeChannelRadio", SelectTreeChannelRadio);
app.component("SelectTreeDept", SelectTreeDept);
app.component("SelectOrgTree", SelectOrgTree);
app.component("SelectDeviceTree", SelectDeviceTree);
app.component("SelectAreaType", SelectAreaType);
app.component("SelectPersonCarType", SelectPersonCarType);
app.component("SelectPerson", SelectPerson);
app.component('ColumnBtn', ColumnBtn);
app.component("ColumnBtn", ColumnBtn);
app.component('PreviewJson', PreviewJson);
app.component("PreviewJson", PreviewJson);
app.component('RtspPluginVideo', RtspPluginVideo)
app.component("RtspPluginVideo", RtspPluginVideo);
// 导入全部的elmenet-plus的图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
// 兼容性
app.component(`ele-${key}`, component);
}
// 导入布局插件
app.component('Splitpanes', Splitpanes);
app.component('Pane', Pane);
app.use(ElementPlus); // ELEMENT 组件
app.use(setupCalendar, {}); // 日历组件
app.use(vue3TreeOrg); // 组织架构组件
app.use(FcDesigner);
app.use(FcDesigner.formCreate);
},
// 导入全部的elmenet-plus的图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
// 兼容性
app.component(`ele-${key}`, component);
}
// 导入布局插件
app.component("Splitpanes", Splitpanes);
app.component("Pane", Pane);
app.use(ElementPlus); // ELEMENT 组件
app.use(setupCalendar, {}); // 日历组件
app.use(vue3TreeOrg); // 组织架构组件
app.use(FcDesigner);
app.use(FcDesigner.formCreate);
},
};
+31 -50
View File
@@ -2,25 +2,19 @@
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 顶部折线图-->
<log-line-chart/>
<log-line-chart />
<el-row class="mt-4 ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('syslog.logType')" prop="logType">
<el-select :placeholder="$t('syslog.inputLogTypeTip')" clearable
v-model="state.queryForm.logType">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in log_type"/>
<el-select :placeholder="$t('syslog.inputLogTypeTip')" clearable v-model="state.queryForm.logType">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in log_type" />
</el-select>
</el-form-item>
<el-form-item :label="$t('syslog.createTime')" prop="createTime">
<el-date-picker
:end-placeholder="$t('syslog.inputEndPlaceholderTip')"
:start-placeholder="$t('syslog.inputStartPlaceholderTip')"
range-separator="To"
type="datetimerange"
v-model="state.queryForm.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<el-date-picker :end-placeholder="$t('syslog.inputEndPlaceholderTip')"
:start-placeholder="$t('syslog.inputStartPlaceholderTip')" range-separator="To" type="datetimerange"
v-model="state.queryForm.createTime" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }}</el-button>
@@ -32,31 +26,18 @@
<el-row>
<div class="mb-2" style="width: 100%">
<el-button :disabled="multiple" v-auth="'sys_log_del'" @click="handleDelete(selectObjs)" class="ml10"
icon="Delete" type="primary">
icon="Delete" type="primary">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_log_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
<right-toolbar :export="'sys_log_export'" @exportExcel="exportExcel" @queryTable="getDataList" class="ml10"
style="float: right; margin-right: 20px" v-model:showSearch="showSearch"></right-toolbar>
</div>
</el-row>
<el-table
ref="tableRef"
:data="state.dataList"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40"/>
<el-table-column :label="$t('syslog.index')" type="index" width="60"/>
<el-table ref="tableRef" :data="state.dataList" @selection-change="handleSelectionChange"
@sort-change="sortChangeHandle" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="$t('syslog.index')" type="index" width="60" />
<el-table-column :label="$t('syslog.logType')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="log_type" :value="scope.row.logType"></dict-tag>
@@ -71,44 +52,44 @@
</template>
</el-table-column>
<el-table-column :label="$t('syslog.createTime')" prop="createTime" show-overflow-tooltip sortable="custom"
width="200"></el-table-column>
width="200"></el-table-column>
<el-table-column :label="$t('syslog.createBy')" prop="createBy" show-overflow-tooltip sortable="custom"
width="200"></el-table-column>
width="200"></el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="view" @click="LogDetailRef.openDialog(scope.row)" size="small" text type="primary">
{{ $t('common.detailBtn') }}
</el-button>
<el-button v-auth="'sys_log_del'" icon="delete" @click="handleDelete([scope.row.id])" size="small" text
type="primary">
type="primary">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle"
v-bind="state.pagination"></pagination>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination">
</pagination>
<log-detail ref="LogDetailRef"></log-detail>
</div>
</div>
</template>
<script lang="ts" setup>
import {BasicTableProps, useTable} from '/@/hooks/table';
import {delObj, pageList} from '/@/api/admin/log';
import {useI18n} from 'vue-i18n';
import {useMessage, useMessageBox} from '/@/hooks/message';
import {useDict} from '/@/hooks/dict';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, pageList } from '/@/api/admin/log';
import { useI18n } from 'vue-i18n';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
const LogDetail = defineAsyncComponent(() => import('./detail.vue'));
const LogLineChart = defineAsyncComponent(() => import('./line-chart.vue'));
const LogDetailRef = ref();
const {log_type} = useDict('log_type');
const { log_type } = useDict('log_type');
const {t} = useI18n();
const { t } = useI18n();
// 定义变量内容
const queryRef = ref();
@@ -123,7 +104,7 @@ let tableRef = ref(null);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
logType: '',
createTime: '',
createTime: [],
serviceId: '',
},
selectObjs: [],
@@ -134,7 +115,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
// table hook
const {
downBlobFile,
postDownBlobFile,
getDataList,
currentChangeHandle: baseCurrentChangeHandle,
sortChangeHandle,
@@ -158,12 +139,12 @@ const resetQuery = () => {
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/log/export', state.queryForm, 'log.xlsx');
postDownBlobFile('/admin/log/export', state.queryForm, 'log.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({id}) => id);
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
@@ -187,7 +168,7 @@ const handleDelete = async (ids: string[]) => {
// onMounted 通过路由参数给 serviceId 赋值
const route = useRoute();
onMounted(() => {
const {serviceId} = route.query;
const { serviceId } = route.query;
if (serviceId) {
state.queryForm.serviceId = serviceId;
}
@@ -0,0 +1,196 @@
<template>
<el-dialog v-model="visible" :title="form.id ? '编辑摄像头' : '新增摄像头'" width="760px" destroy-on-close
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="100px" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item label="设备编码" prop="deviceCode">
<el-input v-model="form.deviceCode" disabled placeholder="自动带入当前设备编码" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="通道编号" prop="channelCode">
<el-input v-model="form.channelCode" clearable placeholder="请输入通道编号" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="通道名称" prop="channelName">
<el-input v-model="form.channelName" clearable placeholder="请输入通道名称" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="摄像头类型" prop="cameraType">
<dict-select v-model="form.cameraType" :options="camera_type" clearable
placeholder="请选择摄像头类型" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="接入状态" prop="access">
<dict-select v-model="form.access" :options="accessOptions" clearable placeholder="请选择接入状态" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="启用状态" prop="stat">
<dict-select v-model="form.stat" :options="statOptions" clearable placeholder="请选择启用状态" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="通道能力" prop="capability">
<el-input v-model="form.capability" type="textarea" :rows="3" placeholder="请输入通道能力集" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="通道描述" prop="memo">
<el-input v-model="form.memo" type="textarea" :rows="3" placeholder="请输入通道描述" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="onSubmit">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="deviceChannerForm">
import { useDict } from '/@/hooks/dict'
import { useMessage } from '/@/hooks/message'
import { addObj, getObj, putObj } from '/@/api/device/deviceChannel'
import { rule } from '/@/utils/validate'
const emit = defineEmits(['refresh'])
const dataFormRef = ref()
const visible = ref(false)
const loading = ref(false)
const currentDeviceCode = ref('')
const { camera_type, yes_no_type } = useDict('camera_type', 'yes_no_type')
const accessOptions = [
{ label: '未接入', value: '0' },
{ label: '已接入', value: '1' },
]
const statOptions = [
{ label: '禁用', value: '0' },
{ label: '启用', value: '1' },
]
const initialForm = () => ({
id: '',
deviceCode: '',
unitId: '',
channelCode: '',
channelName: '',
channelSeq: '',
channelSn: '',
channelType: '',
capability: '',
cameraType: '',
access: '',
gpsX: '',
gpsY: '',
gpsZ: '',
stat: '1',
chExt: '',
memo: '',
sortOrder: 0,
isSync: '',
installLocation: '',
})
const form = reactive(initialForm())
const dataRules = ref({
deviceCode: [{ required: true, message: '缺少设备编码', trigger: 'blur' }],
channelCode: [
{ required: true, message: '请输入通道编号', trigger: 'blur' },
{ validator: rule.checkSpace, trigger: 'blur' },
{ validator: rule.overLength, trigger: 'blur' },
],
channelName: [
{ required: true, message: '请输入通道名称', trigger: 'blur' },
{ validator: rule.checkSpace, trigger: 'blur' },
{ validator: rule.overLength, trigger: 'blur' },
],
cameraType: [{ required: true, message: '请选择摄像头类型', trigger: 'change' }],
sortOrder: [{ required: true, message: '请输入排序', trigger: 'blur' }],
memo: [{ validator: rule.overLength, trigger: 'blur' }],
})
const resetForm = (deviceCode = '') => {
Object.assign(form, initialForm(), {
deviceCode,
})
nextTick(() => {
dataFormRef.value?.clearValidate()
})
}
const getDeviceChannelData = async (id: string) => {
loading.value = true
try {
const res: any = await getObj(id)
Object.assign(form, {
...initialForm(),
...res.data,
sortOrder: res.data?.sortOrder !== null && res.data?.sortOrder !== undefined && res.data?.sortOrder !== ''
? Number(res.data.sortOrder)
: 0,
deviceCode: res.data?.deviceCode || currentDeviceCode.value,
})
} finally {
loading.value = false
}
}
const openDialog = (id = '', deviceCode = '') => {
visible.value = true
currentDeviceCode.value = deviceCode || ''
resetForm(currentDeviceCode.value)
nextTick(() => {
dataFormRef.value?.resetFields()
})
if (id) {
form.id = id
void getDeviceChannelData(id)
}
}
const onSubmit = async () => {
const valid = await dataFormRef.value?.validate().catch(() => undefined)
if (!valid) return false
try {
loading.value = true
const submitForm = {
...form,
sortOrder: form.sortOrder?.toString?.() ?? '0',
}
form.id ? await putObj(submitForm) : await addObj(submitForm)
useMessage().success(form.id ? '修改成功' : '新增成功')
visible.value = false
emit('refresh')
} catch (err: any) {
useMessage().error(err.msg)
} finally {
loading.value = false
}
}
defineExpose({
openDialog,
})
</script>
<style lang="scss" scoped>
.w100 {
width: 100%;
}
</style>
@@ -1,7 +1,17 @@
<template>
<div class="containner">
<div class="toolbar">
<div class="toolbar-left">
<el-button icon="FolderAdd" type="primary" @click="handleAdd"
v-auth="'admin_deviceChannel_add'">新增</el-button>
<el-button icon="Delete" plain :disabled="multiple" @click="handleDelete(selectObjs)"
v-auth="'admin_deviceChannel_del'">删除</el-button>
</div>
<el-button icon="Refresh" :loading="state.loading" circle @click="getDataList()" />
</div>
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle">
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle">
<el-table-column type="selection" width="48" align="center" />
<el-table-column type="index" label="#" width="80" />
<el-table-column prop="channelCode" align="left" width="150" label="通道编号" />
<el-table-column prop="channelName" align="left" width="250" label="通道名称" />
@@ -26,14 +36,28 @@
<div class="color-danger" v-else>未安装</div>
</template>
</el-table-column>
<el-table-column prop="isSync" width="150" label="同步">
<template #default="{ row }">
<tag-sync :isSync="row.isSync == 1" />
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button icon="EditPen" text type="primary" @click="handleEdit(row.id)"
v-auth="'admin_deviceChannel_edit'">编辑</el-button>
<el-button icon="Delete" text type="primary" @click="handleDelete([row.id])"
v-auth="'admin_deviceChannel_del'">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<div style="height: 15px;"></div>
<device-channer-form ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="deviceChannelList">
import { ref, onMounted, reactive } from "vue"
import { reactive } from "vue"
import { BasicTableProps, useTable } from "/@/hooks/table";
@@ -41,6 +65,7 @@ import * as DeviceChannelApi from "/@/api/device/deviceChannel";
import { useDict } from '/@/hooks/dict';
import { parseDeviceCapability } from "/@/utils/dispose"
import { useMessage, useMessageBox } from "/@/hooks/message";
const props = withDefaults(defineProps<{
deviceCode: string
@@ -48,11 +73,16 @@ const props = withDefaults(defineProps<{
deviceCode: ""
})
const DeviceChannerForm = defineAsyncComponent(() => import('./device-channer-form.vue'))
const formDialogRef = ref()
const selectObjs = ref<string[]>([])
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
createdIsNeed: false,
queryForm: {
installLocation: true
installLocation: true,
deviceCode: props.deviceCode,
},
pageList: DeviceChannelApi.fetchList
})
@@ -64,24 +94,100 @@ const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile,
tableStyle
} = useTable(state)
const selectionChangHandle = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id)
multiple.value = !objs.length
}
const handleAdd = () => {
if (!props.deviceCode) {
useMessage().warning('当前设备编码为空,无法新增摄像头')
return
}
formDialogRef.value?.openDialog?.('', props.deviceCode)
}
const handleEdit = (id: string) => {
formDialogRef.value?.openDialog?.(id, props.deviceCode)
}
const getDeleteConfirmMessage = (ids: string[]) => {
const matchedRows = (state.dataList || []).filter((item: any) => ids.includes(item.id))
const channelLabels = matchedRows
.map((item: any) => {
const channelName = item.channelName || '未命名通道'
const channelCode = item.channelCode || '无编号'
return `${channelName}(${channelCode})`
})
.filter(Boolean)
if (channelLabels.length === 1) {
return `确定要删除通道“${channelLabels[0]}”吗`
}
if (channelLabels.length > 1) {
return `确定要删除以下通道吗: ${channelLabels.join('、')}`
}
return ids.length === 1 ? '确定要删除该摄像头吗' : `确定要删除这 ${ids.length} 个摄像头吗`
}
const handleDelete = async (ids: string[]) => {
if (!ids.length) {
useMessage().warning('请先选择要删除的摄像头')
return
}
try {
await useMessageBox().confirm(getDeleteConfirmMessage(ids))
} catch {
return
}
try {
await DeviceChannelApi.delObjs(ids)
selectObjs.value = []
multiple.value = true
getDataList()
useMessage().success('删除成功')
} catch (err: any) {
useMessage().error(err.msg)
}
}
watch(
() => props.deviceCode,
(newVal, oldVal) => {
if (newVal !== oldVal) {
state.queryForm.deviceCode = newVal
getDataList()
if (newVal === oldVal && newVal) return
state.queryForm.deviceCode = newVal
selectObjs.value = []
multiple.value = true
if (!newVal) {
state.dataList = []
return
}
getDataList()
}, { immediate: true }
)
</script>
<style lang="scss" scoped>
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
&-left {
display: flex;
align-items: center;
gap: 8px;
}
}
.tag-container {
display: flex;
align-items: center;
@@ -92,4 +198,4 @@ watch(
width: 100%;
height: 100%;
}
</style>
</style>
+9
View File
@@ -16,6 +16,9 @@
<el-form-item style="width: 250px;" label="状态" prop="isOnline">
<dict-select v-model="state.queryForm.isOnline" :options="is_nline" />
</el-form-item>
<el-form-item style="width: 250px;" label="同步状态" prop="isSync">
<dict-select v-model="state.queryForm.isSync" :options="is_sync" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
@@ -34,6 +37,9 @@
v-auth="'admin_deviceManage_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<el-button plain icon="upload-filled" class="ml10" @click="uploadExcelRef.show()">
导 入
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'admin_deviceManage_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right;"
@queryTable="getDataList"></right-toolbar>
@@ -113,6 +119,8 @@
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<preview-video-dialog ref="previewVideoRef" />
<replay-video-dialog ref="replayVideoDialogRef" />
<upload-excel ref="uploadExcelRef" title="设备导入" url="/admin/deviceManage/read-excel"
temp-url="/admin/sys-file/local/file/device_import_temp.xlsx" @refreshDataList="getDataList(false)" />
</div>
</template>
@@ -138,6 +146,7 @@ const formDialogRef = ref()
const previewVideoRef = ref()
const replayVideoDialogRef = ref()
// 搜索变量
const uploadExcelRef = ref()
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
+3 -3
View File
@@ -30,13 +30,13 @@
v-auth="'admin_deviceManage_add'">
新 增
</el-button>
<el-button plain icon="upload-filled" class="ml10" @click="uploadExcelRef.show()">
导 入
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'admin_deviceManage_del'"
@click="handleDelete(selectObjs)">
删除
</el-button>
<el-button plain icon="upload-filled" class="ml10" @click="uploadExcelRef.show()">
导 入
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'admin_deviceManage_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right;"
@queryTable="getDataList"></right-toolbar>
+41 -76
View File
@@ -2,96 +2,41 @@
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item label="上级组织" prop="orgPreCode">
<SelectOrgTree v-model="form.orgPreCode" value-key="orgCode" placeholder="请选择上级组织" clearable />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="组织编码" prop="orgCode">
<el-input v-model="form.orgCode" placeholder="请输入组织编码" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="组织名称" prop="orgName">
<el-input v-model="form.orgName" placeholder="请输入组织名称" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="唯一标识码" prop="orgSn">
<el-input v-model="form.orgSn" placeholder="请输入唯一标识码" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="组织结构类型(字典)" prop="orgType">
<el-input v-model="form.orgType" placeholder="请输入组织结构类型(字典)" />
<el-form-item label="组织类型" prop="orgType">
<dict-select disabled v-model="form.orgType" :options="dh_org_type" placeholder="请选择组织类型" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="省" prop="province">
<el-input v-model="form.province" placeholder="请输入省" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="市" prop="city">
<el-input v-model="form.city" placeholder="请输入市" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="县" prop="county">
<el-input v-model="form.county" placeholder="请输入县" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="域ID" prop="domainId">
<el-input v-model="form.domainId" placeholder="请输入域ID" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="状态" prop="stat">
<el-input v-model="form.stat" placeholder="请输入状态" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="排序码" prop="sort">
<el-input v-model="form.sort" placeholder="请输入排序码" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="所有组排序织" prop="allSort">
<el-input v-model="form.allSort" placeholder="请输入所有组排序织" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="子系统服务编码,默认evo-thirdParty" prop="service">
<el-input v-model="form.service" placeholder="请输入子系统服务编码,默认evo-thirdParty" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="禁止绑定用户 :0-可绑定用户,1-不可绑定用户" prop="forbidBindUser">
<el-input v-model="form.forbidBindUser" placeholder="请输入禁止绑定用户 :0-可绑定用户,1-不可绑定用户" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="禁止绑定用户服务" prop="forbidBindService">
<el-input v-model="form.forbidBindService" placeholder="请输入禁止绑定用户服务" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="是否为同步" prop="isSync">
<el-input v-model="form.isSync" placeholder="请输入是否为同步" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -107,7 +52,6 @@
import { useDict } from '/@/hooks/dict';
import { useMessage } from "/@/hooks/message";
import { getObj, addObj, putObj } from '/@/api/sys/sysOrg'
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
// 定义变量内容
@@ -115,14 +59,15 @@ const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
// 定义字典
const { dh_org_type } = useDict("dh_org_type")
// 提交表单数据
const form = reactive({
const defaultForm = {
id: '',
orgCode: '',
orgName: '',
orgSn: '',
orgType: '',
orgType: 'org_base',
orgPreCode: '',
province: '',
city: '',
county: '',
@@ -134,16 +79,30 @@ const form = reactive({
forbidBindUser: '',
forbidBindService: '',
isSync: '',
});
}
const createForm = () => ({
...defaultForm,
})
// 提交表单数据
const form = reactive(createForm());
// 定义校验规则
const dataRules = ref({
orgCode: [{ required: true, message: '组织编码不能为空', trigger: 'blur' }],
orgName: [{ required: true, message: '组织名称不能为空', trigger: 'blur' }],
orgType: [{ required: true, message: '组织类型不能为空', trigger: 'change' }],
})
const resetForm = () => {
Object.assign(form, createForm());
}
// 打开弹窗
const openDialog = (id: string) => {
const openDialog = (id = '') => {
visible.value = true
form.id = ''
resetForm()
// 重置表单数据
nextTick(() => {
@@ -177,18 +136,24 @@ const onSubmit = async () => {
// 初始化表单数据
const getsysOrgData = (id: string) => {
// 获取数据
loading.value = true
getObj(id).then((res: any) => {
const getsysOrgData = async (id: string) => {
try {
loading.value = true
const res: any = await getObj(id)
// default
if (!res.data.orgType) {
res.data.orgType = defaultForm.orgType
}
Object.assign(form, res.data)
}).finally(() => {
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false
})
}
};
// 暴露变量
defineExpose({
openDialog
});
</script>
</script>
+145 -68
View File
@@ -1,10 +1,11 @@
<template>
<div class="layout-padding">
<div class="h-full flex flex-row gap-2">
<div class="h-full flex flex-row gap-2" v-loading="pageLoading">
<div class="w-[250px]">
<div class="layout-padding-auto layout-padding-view">
<div :class="data.selectTopIndex == i ? 'border-blue-400' : ''"
class="flex gap-4 items-center p-3 border-2 rounded-lg mt-1" v-for="(v, i) in data.topList">
class="flex gap-4 items-center p-3 border-2 rounded-lg mt-1 cursor-pointer transition-colors hover:border-blue-300"
v-for="(v, i) in data.topList" :key="getNodeKey(v, i)" @click="handleTopOrgClick(v, i)">
<el-icon size="40">
<OfficeBuilding />
</el-icon>
@@ -43,6 +44,18 @@
<div class="flex-1 overflow-hidden">
<div class="layout-padding-auto layout-padding-view">
<div class="flex items-center justify-between mb20">
<div class="text-base font-medium">组织详情</div>
<div class="flex items-center gap-2">
<el-button icon="FolderAdd" type="primary" @click="handleAdd">新增</el-button>
<el-button icon="Edit" type="primary" plain :disabled="!data.currentOrgId"
@click="handleEdit">编辑</el-button>
<el-button icon="Delete" type="danger" plain :disabled="!data.currentOrgId"
@click="handleDeleteCurrent">删除</el-button>
<right-toolbar :search="false" :export="'admin_sysPerson_export'" @exportExcel="exportExcel"
class="ml10 mr20" style="float: right;" @queryTable="handleRefresh" />
</div>
</div>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px"
v-loading="data.formLoading">
<el-col :span="24" class="mb20">
@@ -65,6 +78,11 @@
<dict-select v-model="form.orgType" disabled :options="dh_org_type" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="是否同步" prop="isSync">
<dict-select v-model="form.isSync" disabled :options="yes_no_type" />
</el-form-item>
</el-col>
</el-form>
</div>
</div>
@@ -72,49 +90,55 @@
</div>
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<form-dialog ref="formDialogRef" @refresh="handleRefresh" />
</div>
</template>
<script setup lang="ts" name="systemSysOrg">
import { BasicTableProps, useTable } from "/@/hooks/table";
import * as SysOrgApi from "/@/api/sys/sysOrg";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
import other from "/@/utils/other"
import dayjs from 'dayjs';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
// 定义查询字典
const { dh_org_type } = useDict("dh_org_type")
const { dh_org_type, yes_no_type } = useDict("dh_org_type", "yes_no_type")
const orgTreeRef = ref()
// 定义变量内容
const formDialogRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
onMounted(() => {
getTopOrg()
getTree()
})
const createDetailForm = () => ({
id: '',
orgPreName: '',
orgCode: '',
orgName: '',
orgType: '',
})
const data = reactive<any>({
selectTopIndex: 0,
topList: [],
treeList: [],
treeSearchName: '',
topLoading: false,
treeLoading: false,
formLoading: false,
refreshLoading: false,
currentNodeKey: '',
currentOrgId: '',
})
const form = reactive<any>({
const form = reactive<any>(createDetailForm())
})
const pageLoading = computed(() => data.refreshLoading || data.topLoading || data.treeLoading || data.formLoading)
const dataRules = {
orgPreName: [
@@ -131,59 +155,69 @@ const dataRules = {
],
}
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: SysOrgApi.fetchList
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile,
tableStyle
} = useTable(state)
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
// 清空多选
selectObjs.value = []
getDataList()
const resetDetailForm = () => {
Object.assign(form, createDetailForm())
}
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/sysOrg/export', Object.assign(state.queryForm, { ids: selectObjs }), 'sysOrg.xlsx')
const getOrgId = (node: any) => node?.primaryId || node?.id || ''
const getNodeKey = (node: any, index: string | number = 0) => node?.id || node?.primaryId || `${node?.orgCode || 'org'}-${index}`
const findTreeNode = (nodes: Array<any>, orgId: string): any | null => {
if (!orgId || !Array.isArray(nodes)) return null
for (const node of nodes) {
if (getOrgId(node) === orgId) return node
if (node.children?.length) {
const childNode: any | null = findTreeNode(node.children, orgId)
if (childNode) return childNode
}
}
return null
}
// 多选事件
const selectionChangHandle = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
const loadOrgDetail = async (orgId: string) => {
if (!orgId) {
data.currentOrgId = ''
resetDetailForm()
return
}
try {
data.formLoading = true
const res = await SysOrgApi.getObj(orgId)
if (res.code != 0) throw new Error()
data.currentOrgId = orgId
resetDetailForm()
Object.assign(form, res.data)
if (!form.orgPreName) form.orgPreName = '无'
} catch (err: any) {
useMessage().error(err.msg);
} finally {
data.formLoading = false
}
}
// 点击树
const handleNodeClick = async (e: any) => {
// 查询详情
const res = await SysOrgApi.getObj(e.primaryId)
if (res.code == 0) {
Object.assign(form, res.data)
if (!form.orgPreName) form.orgPreName = '无'
}
data.currentNodeKey = e.id
await loadOrgDetail(getOrgId(e))
}
const handleTopOrgClick = async (row: any, index: string | number) => {
data.selectTopIndex = index
await loadOrgDetail(getOrgId(row))
}
// 获取顶级组织
const getTopOrg = async () => {
try {
data.topLoading = true
const { code, data: orgData } = await SysOrgApi.getTopOrg();
if (code != 0) throw new Error()
data.topList = orgData
} catch (err: any) {
useMessage().error(err.msg);
} finally {
data.topLoading = false
}
}
@@ -209,50 +243,93 @@ const filterVisibleNodes = (nodes: Array<any>) => {
}
// 获取组织树状结构
const getTree = async () => {
const getTree = async (selectedOrgId = data.currentOrgId) => {
try {
data.treeLoading = true
const _data = {
orgName: data.treeSearchName
}
const { code, data: orgTree } = await SysOrgApi.getTree(_data);
if (code != 0) throw new Error()
data.treeList = orgTree
if (orgTree && orgTree.length) {
data.treeList = filterVisibleNodes(orgTree)
}
// 默认选择第一个
if (orgTree && orgTree.length) {
nextTick(() => {
orgTreeRef.value.setCurrentKey(orgTree[0].id)
})
// 查询详情
const res = await SysOrgApi.getObj(orgTree[0].primaryId)
if (res.code == 0) {
Object.assign(form, res.data)
if (!form.orgPreName) form.orgPreName = '无'
}
data.treeList = Array.isArray(orgTree) ? filterVisibleNodes(orgTree) : []
const selectedNode = findTreeNode(data.treeList, selectedOrgId) || data.treeList[0]
if (!selectedNode) {
data.currentNodeKey = ''
await loadOrgDetail('')
return
}
data.currentNodeKey = selectedNode.id
nextTick(() => {
orgTreeRef.value?.setCurrentKey(selectedNode.id)
})
await loadOrgDetail(getOrgId(selectedNode))
} catch (err: any) {
useMessage().error(err.msg);
} finally {
data.treeLoading = false
}
}
const handleAdd = () => {
formDialogRef.value?.openDialog?.()
}
const handleEdit = () => {
if (!data.currentOrgId) {
useMessage().warning('请先选择组织');
return
}
formDialogRef.value?.openDialog?.(data.currentOrgId)
}
const handleRefresh = async () => {
try {
data.refreshLoading = true
await getTopOrg()
await getTree()
} finally {
data.refreshLoading = false
}
}
const handleDeleteCurrent = async () => {
if (!data.currentOrgId) {
useMessage().warning('请先选择组织');
return
}
await handleDelete([data.currentOrgId])
}
const getDeleteConfirmMessage = (ids: string[]) => {
if (ids.length === 1) {
const currentOrgName = ids[0] === data.currentOrgId ? form.orgName : ''
return currentOrgName ? `确定要删除组织“${currentOrgName}”吗` : '确定要删除该组织吗'
}
return `确定要删除这 ${ids.length} 个组织吗`
}
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
await useMessageBox().confirm(getDeleteConfirmMessage(ids));
} catch {
return;
}
try {
await SysOrgApi.delObjs(ids);
getDataList();
await handleRefresh();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 导出excel
const exportExcel = () => {
const timeSuffix = dayjs().format('YYYY-MM-DD_HH-mm-ss')
other.poseDownBlobFile('/admin/sysOrg/export', {}, `组织管理_${timeSuffix}.xlsx`)
}
</script>