字典、配置、websocket

This commit is contained in:
陈红丽 2024-12-18 14:28:43 +08:00
parent aa8920f487
commit 05dd125357
6 changed files with 436 additions and 203 deletions

View File

@ -27,24 +27,29 @@
:y-offset="60"
:rotate="-15"
/>
<global-websocket :uri="'/api/websocket/' + userInfo.id" @rollback="rollback" />
</NConfigProvider>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted } from 'vue';
import { computed, onMounted, onUnmounted, defineAsyncComponent, h } from 'vue';
import { darkTheme, dateZhCN, zhCN } from 'naive-ui';
import { LockScreen } from '@/components/Lockscreen';
import { AppProvider } from '@/components/Application';
import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useRoute } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { useDesignSettingStore } from '@/store/modules/designSetting';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { initWebSocket, sendWebSocket } from '@/components/Websocket/index';
const GlobalWebsocket = defineAsyncComponent(() => import('@/components/Websocket/index.vue'));
import { lighten } from '@/utils';
const route = useRoute();
const useLockscreen = useLockscreenStore();
const designStore = useDesignSettingStore();
const userStore = useUserStore();
const userInfo: object = userStore.getUserInfo || {};
const isLock = computed(() => useLockscreen.isLock);
const lockTime = computed(() => useLockscreen.lockTime);
@ -95,6 +100,13 @@
}
}, 1000);
};
const rollback = (msg) => {
$notification.info({
title: '通知',
content: () => h('div', msg),
duration: 5000,
});
};
onMounted(() => {
// document.addEventListener('mousedown', timekeeping);

View File

@ -0,0 +1,155 @@
<template>
<div></div>
</template>
<script setup lang="ts" name="global-websocket">
import { reactive, ref, computed,onMounted,onUnmounted } from 'vue';
import { useUserStore } from '@/store/modules/user';
const emit = defineEmits(['rollback']);
const props = defineProps({
uri: {
type: String,
},
});
const state = reactive({
webSocket: ref(), // webSocket
lockReconnect: false, //
maxReconnect: 6, // -1
reconnectTime: 0, //
heartbeat: {
interval: 30 * 1000, //
timeout: 10 * 1000, //
pingTimeoutObj: ref(), //
pongTimeoutObj: ref(), //
pingMessage: JSON.stringify({ type: 'ping' }), //
},
});
const token = computed(() => {
return useUserStore().getToken;
});
const tenant = computed(() => {
return Session.getTenant();
});
onMounted(() => {
initWebSocket();
});
onUnmounted(() => {
state.webSocket.close();
clearTimeoutObj(state.heartbeat);
});
const initWebSocket = () => {
// ws
let host = window.location.host;
let wsUri =`${location.protocol === 'https:' ? 'wss' : 'ws'}://${host}${props.uri}`;
//
state.webSocket = new WebSocket(wsUri);
//
state.webSocket.onopen = onOpen;
//
state.webSocket.onerror = onError;
//
state.webSocket.onmessage = onMessage;
//
state.webSocket.onclose = onClose;
};
const reconnect = () => {
if (!token) {
return;
}
if (state.lockReconnect || (state.maxReconnect !== -1 && state.reconnectTime > state.maxReconnect)) {
return;
}
state.lockReconnect = true;
setTimeout(() => {
state.reconnectTime++;
//
initWebSocket();
state.lockReconnect = false;
}, 5000);
};
/**
* 清空定时器
*/
const clearTimeoutObj = (heartbeat: any) => {
heartbeat.pingTimeoutObj && clearTimeout(heartbeat.pingTimeoutObj);
heartbeat.pongTimeoutObj && clearTimeout(heartbeat.pongTimeoutObj);
};
/**
* 开启心跳
*/
const startHeartbeat = () => {
const webSocket = state.webSocket;
const heartbeat = state.heartbeat;
//
clearTimeoutObj(heartbeat);
//
heartbeat.pingTimeoutObj = setTimeout(() => {
//
if (webSocket.readyState === 1) {
//
webSocket.send(heartbeat.pingMessage);
//
heartbeat.pongTimeoutObj = setTimeout(() => {
webSocket.close();
}, heartbeat.timeout);
} else {
//
reconnect();
}
}, heartbeat.interval);
};
/**
* 连接成功事件
*/
const onOpen = () => {
//
startHeartbeat();
state.reconnectTime = 0;
};
/**
* 连接失败事件
* @param e
*/
const onError = () => {
//
reconnect();
};
/**
* 连接关闭事件
* @param e
*/
const onClose = () => {
//
reconnect();
};
/**
* 接收服务器推送的信息
* @param msgEvent
*/
const onMessage = (msgEvent: any) => {
//
startHeartbeat();
// if (msgEvent.data.indexOf('pong') > 0) {
// return;
// }
// let text =''
// try{
// text = JSON.parse(msgEvent.data);
// }catch(e){
// return
// }
emit('rollback', msgEvent.data);
};
</script>

View File

@ -6,12 +6,6 @@ export const columns = [
width: 50,
fixed: 'left',
},
{
title: 'ID',
key: 'id',
fixed: 'left',
width: 50,
},
{
title: '配置名称',
key: 'name',

View File

@ -1,9 +1,8 @@
<template>
<PageWrapper>
<n-grid x-gap="12" cols="1 s:1 m:1 l:24 xl:24 2xl:24" responsive="screen">
<n-grid-item span="7">
<n-grid-item span="8">
<n-card shadow="hover" class="border-0">
<template #header>
<n-space>
<n-input
type="text"
@ -11,13 +10,7 @@
placeholder="请输入配置名称"
clearable
/>
<n-button
type="primary"
@click="
pager.page = 1;
loadDataTable();
"
>
<n-button type="primary" @click="reloadTable">
<template #icon>
<n-icon>
<SearchOutlined />
@ -35,14 +28,12 @@
</template>
新建
</n-button>
<n-button type="warning" @click="handleEdit" v-perm="['sys:config:edit']">
<template #icon>
<n-icon>
<FormOutlined />
</n-icon> </template
>编辑
</n-button>
<n-button type="error" @click="handleDelete()" v-perm="['sys:config:delete']">
<n-button
type="error"
@click="handleDelete()"
v-perm="['sys:config:delete']"
:disabled="!selectionData.length"
>
<template #icon>
<n-icon>
<DeleteOutlined />
@ -51,31 +42,23 @@
</n-button>
</n-space>
</div>
</template>
<template #default>
<div :style="{ height: fwbHeight + 'px' }" class="dict-list-box">
<div
v-for="(item, index) in configDataList"
:key="index"
@click="onCheckedRow(item)"
class="dict-item"
:class="item.id == configId ? 'active' : ''"
<BasicTable
:columns="columns"
:actionColumn="actionColumn"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
:showTableSetting="false"
@update:checked-row-keys="onSelectionChange"
:pagination="{ showQuickJumper: false, showSizePicker: false }"
:row-props="rowProps"
:row-class-name="getRowClassName"
:autoScrollX="true"
>
<span class="t1"
>{{ item.name }}<span class="t2">({{ item.code }})</span></span
>
</div>
</div>
<pagination
style="justify-content: flex-end"
class="mt-10 flex"
@change="loadDataTable"
v-model="pager"
/>
</template>
</BasicTable>
</n-card>
</n-grid-item>
<n-grid-item span="17">
<n-grid-item span="16">
<n-card shadow="hover" class="mb-4 border-0 proCard">
<configItem :configId="configId" v-if="configItemShow" />
</n-card>
@ -86,23 +69,27 @@
:configId="configId"
v-model:visible="editVisible"
ref="createModalRef"
@success="loadDataTable()"
@success="reloadTable('noRefresh')"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineAsyncComponent, onMounted } from 'vue';
import { ref, nextTick, defineAsyncComponent, onMounted, reactive, h } from 'vue';
import { getConfigList, configDelete } from '@/api/data/config';
import { PlusOutlined, FormOutlined, DeleteOutlined, SearchOutlined } from '@vicons/antd';
import editDialog from './edit.vue';
import configItem from './configItem.vue';
import { TableAction } from '@/components/Table';
import { columns } from './columns';
import { renderIcon } from '@/utils';
import { useMessage, useDialog } from 'naive-ui';
/**
* 定义参数变量
*/
const configId = ref(0);
const tableRef = ref();
const createModalRef = ref();
const configItemShow = ref(false);
const editVisible = ref(false);
@ -115,18 +102,47 @@
const params = ref({
name: '',
});
const configDataList = ref([]);
const selectionData = ref([]);
const actionColumn = reactive({
width: 200,
title: '操作',
align: 'center',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
icon: renderIcon(FormOutlined),
type: 'warning',
auth: ['sys:config:update'],
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
icon: renderIcon(DeleteOutlined),
type: 'error',
auth: ['sys:config:delete'],
onClick: handleDelete.bind(null, record),
},
],
select: (key) => {
message.info(`您点击了,${key} 按钮`);
},
});
},
});
/**
* 定义分页参数
* 刷新配置项值列表
* @param noRefresh 参数
*/
const pager = ref({
page: 1,
size: 10,
count: configDataList.value.length,
});
const fwbHeight = document.body.clientHeight - 335;
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
/**
* 执行添加
*/
@ -140,52 +156,58 @@
/**
* 执行编辑
*/
const handleEdit = async () => {
const handleEdit = async (row) => {
configId.value = row.id;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
};
/**
* 数据行选中事件
* @param row 参数
* 选项发生变化
* @param value 参数
*/
function onCheckedRow(row) {
configId.value = row.id;
function onSelectionChange(value) {
selectionData.value = value;
}
/**
* 加载数据列表
*/
const loadDataTable = async () => {
let result = await getConfigList({
...params.value,
pageNo: pager.value.page,
pageSize: pager.value.size,
});
const loadDataTable = async (res) => {
const result = await getConfigList({ ...params.value, ...res });
configId.value = result?.records[0]?.id;
configItemShow.value = true;
configDataList.value = result.records;
pager.value.count = result.total;
return result;
};
/**
* 执行删除
*/
async function handleDelete() {
async function handleDelete(row) {
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
configDelete(configId.value);
row ? await configDelete(row.id) : await configBatchDelete(selectionData.value);
message.success('删除成功');
pager.value.page = 1;
loadDataTable();
reloadTable();
},
});
}
const getRowClassName = (row) => {
return configId.value === row.id ? 'clickRowStyle' : '';
};
const rowProps = (row) => {
return {
style: 'cursor: pointer;',
onClick: () => {
configId.value = row.id;
},
};
};
/**
* 钩子函数
@ -243,3 +265,18 @@
}
}
</style>
<style lang="less">
.n-data-table-tbody {
.n-data-table-tr.clickRowStyle,
.n-data-table-tr.clickRowStyle {
td {
background-color: #e7eeff !important;
}
&:hover {
td {
background-color: #e7eeff !important;
}
}
}
}
</style>

View File

@ -6,16 +6,14 @@ export const columns = [
width: 50,
fixed: 'left',
},
{
title: 'ID',
key: 'id',
},
{
title: '字典名称',
key: 'name',
width: 100,
},
{
title: '字典编码',
key: 'code',
width: 100,
},
];

View File

@ -1,9 +1,8 @@
<template>
<PageWrapper>
<n-grid x-gap="12" cols="1 s:1 m:1 l:24 xl:24 2xl:24" responsive="screen">
<n-grid-item span="7">
<n-grid-item span="8">
<n-card shadow="hover" class="border-0" size="small">
<template #header>
<n-space :size="4">
<n-input
type="text"
@ -11,25 +10,19 @@
placeholder="请输入字典名称"
clearable
/>
<n-button
type="primary"
@click="
pager.page = 1;
loadDataTable();
"
>
<n-button type="primary" @click="reloadTable">
<template #icon>
<n-icon>
<SearchOutlined />
</n-icon> </template
>查询
</n-button>
<n-button type="primary" @click="dictRefresh" v-perm="['sys:dict:cache']">
<template #icon> <RedoOutlined /> </template>刷新缓存</n-button
>
</n-space>
<div style="margin-top: 15px">
<n-space>
<n-button type="primary" @click="dictRefresh" v-perm="['sys:dict:cache']">
<template #icon> <RedoOutlined /> </template>刷新缓存</n-button
>
<n-button type="primary" @click="handleAdd" v-perm="['sys:dict:add']">
<template #icon>
<n-icon>
@ -38,14 +31,12 @@
</template>
新建
</n-button>
<n-button type="warning" @click="handleEdit" v-perm="['sys:dict:edit']">
<template #icon>
<n-icon>
<FormOutlined />
</n-icon> </template
>编辑
</n-button>
<n-button type="error" @click="handleDelete()" v-perm="['sys:dict:delete']">
<n-button
type="error"
@click="handleDelete()"
v-perm="['sys:dict:delete']"
:disabled="!selectionData.length"
>
<template #icon>
<n-icon>
<DeleteOutlined />
@ -54,31 +45,23 @@
</n-button>
</n-space>
</div>
</template>
<template #default>
<div :style="{ height: fwbHeight + 'px' }" class="dict-list-box">
<div
v-for="(item, index) in dictDataList"
:key="index"
@click="onCheckedRow(item)"
class="dict-item"
:class="item.id == dictId ? 'active' : ''"
<BasicTable
:columns="columns"
:actionColumn="actionColumn"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
:showTableSetting="false"
@update:checked-row-keys="onSelectionChange"
:pagination="{ showQuickJumper: false, showSizePicker: false }"
:row-props="rowProps"
:row-class-name="getRowClassName"
:autoScrollX="true"
>
<span class="t1"
>{{ item.name }}<span class="t2">({{ item.code }})</span></span
>
</div>
</div>
<pagination
style="justify-content: flex-end"
class="mt-10 flex"
@change="loadDataTable"
v-model="pager"
/>
</template>
</BasicTable>
</n-card>
</n-grid-item>
<n-grid-item span="17">
<n-grid-item span="16">
<n-card shadow="hover" class="border-0 proCard" size="small">
<dictItem :dictId="dictId" v-if="dictItemShow" />
</n-card>
@ -89,13 +72,13 @@
:dictId="dictId"
v-model:visible="editVisible"
ref="createModalRef"
@success="loadDataTable()"
@success="reloadTable('noRefresh')"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineAsyncComponent, onMounted } from 'vue';
import { ref, nextTick, defineAsyncComponent, onMounted, reactive, h } from 'vue';
import { getDictList, refreshCache, dictDelete } from '@/api/data/dictionary';
import {
PlusOutlined,
@ -104,37 +87,71 @@
SearchOutlined,
RedoOutlined,
} from '@vicons/antd';
import { TableAction } from '@/components/Table';
import { columns } from './columns';
import editDialog from './edit.vue';
import dictItem from './dictItem.vue';
import { renderIcon } from '@/utils';
import { useMessage, useDialog } from 'naive-ui';
/**
* 定义参数变量
*/
const dictId = ref(0);
const tableRef = ref();
const createModalRef = ref();
const dictItemShow = ref(false);
const editVisible = ref(false);
const selectionData = ref([]);
const message = useMessage();
const dialog = useDialog();
const actionColumn = reactive({
width: 200,
title: '操作',
align: 'center',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
icon: renderIcon(FormOutlined),
type: 'warning',
auth: ['sys:dict:update'],
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
icon: renderIcon(DeleteOutlined),
type: 'error',
auth: ['sys:dict:delete'],
onClick: handleDelete.bind(null, record),
},
],
select: (key) => {
message.info(`您点击了,${key} 按钮`);
},
});
},
});
/**
* 定义查询参数
*/
const params = ref({
name: '',
});
const dictDataList = ref([]);
/**
* 定义分页参数
* 刷新配置项值列表
* @param noRefresh 参数
*/
const pager = ref({
page: 1,
size: 10,
count: dictDataList.value.length,
});
const fwbHeight = document.body.clientHeight - 335;
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
/**
* 执行添加
@ -149,7 +166,8 @@
/**
* 执行编辑
*/
const handleEdit = async () => {
const handleEdit = async (row) => {
dictId.value = row.id;
editVisible.value = true;
await nextTick();
createModalRef.value.openModal();
@ -163,46 +181,50 @@
}
/**
* 数据行选中事件
* @param row 参数
* 选项发生变化
* @param value 参数
*/
function onCheckedRow(row) {
dictId.value = row.id;
function onSelectionChange(value) {
selectionData.value = value;
}
/**
* 加载数据列表
*/
const loadDataTable = async () => {
let result = await getDictList({
...params.value,
pageNo: pager.value.page,
pageSize: pager.value.size,
});
const loadDataTable = async (res) => {
const result = await getDictList({ ...params.value, ...res });
dictId.value = result?.records[0]?.id;
dictItemShow.value = true;
dictDataList.value = result.records;
pager.value.count = result.total;
return result;
};
/**
* 执行删除
*/
async function handleDelete() {
async function handleDelete(row) {
dialog.warning({
title: '提示',
content: '确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
dictDelete(dictId.value);
row ? await dictDelete(row.id) : await dictBatchDelete(selectionData.value);
message.success('删除成功');
pager.value.page = 1;
loadDataTable();
reloadTable();
},
});
}
const getRowClassName = (row) => {
return dictId.value === row.id ? 'clickRowStyle' : '';
};
const rowProps = (row) => {
return {
style: 'cursor: pointer;',
onClick: () => {
dictId.value = row.id;
},
};
};
/**
* 钩子函数
*/
@ -259,3 +281,18 @@
}
}
</style>
<style lang="less">
.n-data-table-tbody {
.n-data-table-tr.clickRowStyle,
.n-data-table-tr.clickRowStyle {
td {
background-color: #e7eeff !important;
}
&:hover {
td {
background-color: #e7eeff !important;
}
}
}
}
</style>