This commit is contained in:
2026-03-27 11:58:17 +08:00
parent 485f24c26e
commit 798bf82740
26 changed files with 639 additions and 79 deletions

View File

@@ -192,7 +192,7 @@ interface LogoutParams {
token: string; token: string;
redirect_uri: string; redirect_uri: string;
} }
export const logout = (params: LogoutParams) => { export const logout = (params?: LogoutParams) => {
return request({ return request({
// url: '/auth/token/logout', // url: '/auth/token/logout',
url: '/auth-service/v1/logout', url: '/auth-service/v1/logout',

View File

@@ -97,12 +97,16 @@ const list = computed(() => {
return [...formatChildDepartments.value, ...formatUserList.value]; return [...formatChildDepartments.value, ...formatUserList.value];
}); });
const cacheList = computed(() => {
return list.value.filter((item) => (!disabledIdList.includes(item?.id) && item.type === SourcePickerTypeEnum.dept ? !props.deptDisabled : true));
});
const selectAllChecked = computed(() => { const selectAllChecked = computed(() => {
return list.value.filter((item) => !disabledIdList.includes(item?.id)).every((item) => item.checked); return cacheList.value.every((item) => item.checked);
}); });
const indeterminate = computed(() => { const indeterminate = computed(() => {
const checkedCount = list.value.filter((item) => !disabledIdList.includes(item?.id) && item.checked).length; const checkedCount = cacheList.value.filter((item) => item.checked).length;
return checkedCount > 0 && checkedCount < list.value.filter((item) => !disabledIdList.includes(item?.id)).length; return checkedCount > 0 && checkedCount < cacheList.value.length;
}); });
onMounted(() => { onMounted(() => {

View File

@@ -258,6 +258,7 @@ export default {
simulationProcessDetail: 'Simulation Process Detail', simulationProcessDetail: 'Simulation Process Detail',
spdmSystemApplication: 'Application Center', spdmSystemApplication: 'Application Center',
spdmCompetenceCenterCondition: 'Condition Map Library', spdmCompetenceCenterCondition: 'Condition Map Library',
spdmCompetenceCenterConditionDetail: 'Condition Map Library Detail',
spdmCompetenceCenterStandardScene: 'Standard Scene Library', spdmCompetenceCenterStandardScene: 'Standard Scene Library',
spdmCompetenceCenterKnowledge: 'Knowledge Library', spdmCompetenceCenterKnowledge: 'Knowledge Library',
spdmCompetenceCenterParameter: 'Parameter Library', spdmCompetenceCenterParameter: 'Parameter Library',

View File

@@ -155,6 +155,7 @@ export default {
simulationProcessDetail: '仿真流程详情', simulationProcessDetail: '仿真流程详情',
spdmSystemApplication: '应用中心', spdmSystemApplication: '应用中心',
spdmCompetenceCenterCondition: '仿真地图库', spdmCompetenceCenterCondition: '仿真地图库',
spdmCompetenceCenterConditionDetail: '仿真地图库详情',
spdmCompetenceCenterStandardScene: '标准场景库', spdmCompetenceCenterStandardScene: '标准场景库',
spdmCompetenceCenterKnowledge: '仿真标准库', spdmCompetenceCenterKnowledge: '仿真标准库',
spdmCompetenceCenterParameter: '仿真参数库', spdmCompetenceCenterParameter: '仿真参数库',

View File

@@ -11,8 +11,33 @@ import mittBus from '/@/utils/mitt';
import {useTenantInfo} from '/@/stores/tenant'; import {useTenantInfo} from '/@/stores/tenant';
import {useRoutesList} from '/@/stores/routesList'; import {useRoutesList} from '/@/stores/routesList';
import LayoutDefault from '/@/layout/main/defaults.vue'; import LayoutDefault from '/@/layout/main/defaults.vue';
import {useUserInfo} from '/@/stores/userInfo'; import {logout, useUserInfo} from '/@/stores/userInfo';
import {useMsg} from '/@/stores/msg'; import {useMsg} from '/@/stores/msg';
import { slidingTimeout } from '/@/utils/slidingTimeout';
defineOptions({
beforeRouteEnter(_to, _from, next) {
/**
* 处理超时登出逻辑
*/
const handleTimeoutLogout = async () => {
console.log('超时登出');
try {
await logout();
slidingTimeout.reset();
} catch (error) {
console.error('超时登出失败:', error);
}
};
slidingTimeout.start();
if (!slidingTimeout.checkInactiveTimeout()) {
next();
slidingTimeout.on('timeout', handleTimeoutLogout);
} else {
next('/login');
}
},
});
// 定义变量内容 // 定义变量内容
const route = useRoute(); const route = useRoute();

View File

@@ -143,6 +143,12 @@ export default [
component: () => import('/@/spdm/views/index.vue'), component: () => import('/@/spdm/views/index.vue'),
meta: {"icon":"fa fa-map","code":"spdm_CompetenceCenterCondition_view"}, meta: {"icon":"fa fa-map","code":"spdm_CompetenceCenterCondition_view"},
}, },
{
path: '/spdm/competenceCenter/condition/detail',
name: 'moduleRoutes.spdmCompetenceCenterConditionDetail',
component: () => import('/@/spdm/views/index.vue'),
meta: {"icon":"fa fa-map","code":"spdm_CompetenceCenterCondition_view"},
},
{ {
path: '/spdm/competenceCenter/standardScene', path: '/spdm/competenceCenter/standardScene',
name: 'moduleRoutes.spdmCompetenceCenterStandardScene', name: 'moduleRoutes.spdmCompetenceCenterStandardScene',

View File

@@ -377,6 +377,16 @@ export const appList = [
icon: 'fa fa-map', icon: 'fa fa-map',
}, },
}, },
{
name: 'spdmCompetenceCenterConditionDetail',
zhCn: '仿真地图库详情',
en: 'Condition Map Library Detail',
path: '/spdm/competenceCenter/condition/detail',
component: () => import('/@/spdm/views/index.vue'),
meta: {
icon: 'fa fa-map',
},
},
], ],
}, },
{ {

View File

@@ -223,6 +223,14 @@ export const pageMap = {
code: 'spdmCompetenceCenter_view', code: 'spdmCompetenceCenter_view',
}, },
}, },
spdmCompetenceCenterConditionDetail: {
path: '/spdm/competenceCenter/condition/detail',
name: 'moduleRoutes.spdmCompetenceCenterConditionDetail',
meta: {
icon: 'fa fa-map',
code: 'spdm_CompetenceCenterCondition_view',
},
},
spdmCompetenceCenterStandardScene: { spdmCompetenceCenterStandardScene: {
path: '/spdm/competenceCenter/standardScene', path: '/spdm/competenceCenter/standardScene',
name: 'moduleRoutes.spdmCompetenceCenterStandardScene', name: 'moduleRoutes.spdmCompetenceCenterStandardScene',

View File

@@ -1,11 +1,20 @@
import {defineStore} from 'pinia';
import {REFRESH_TOKEN_KEY, Session, Token} from '/@/utils/storage';
import {getUserInfo, login, loginByMobile, loginBySocial, refreshTokenApi, tenantAuthCheck, loginByAccount} from '/@/api/login/index';
import {useMessage} from '/@/hooks/message';
import {avatarFormat} from '/@/utils/commonFunction';
import {ITenant} from '/@/api/admin/tenant';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { loginHeartbeat } from '/@/spdm/utils/index'; import { loginHeartbeat } from '/@/spdm/utils/index';
import { defineStore } from 'pinia';
import { REFRESH_TOKEN_KEY, Session, Token } from '/@/utils/storage';
import { getUserInfo, login, loginByMobile, loginBySocial, logout as logoutApi, refreshTokenApi, tenantAuthCheck } from '/@/api/login/index';
import { useMessage } from '/@/hooks/message';
import { avatarFormat } from '/@/utils/commonFunction';
import { ITenant } from '/@/api/admin/tenant';
import { slidingTimeout } from '/@/utils/slidingTimeout';
export const logout = async () => {
await logoutApi();
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
};
/** /**
* @function useUserInfo * @function useUserInfo
@@ -56,13 +65,14 @@ export const useUserInfo = defineStore('userInfo', {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
login(data) login(data)
.then(({data: res}) => { .then(({ data: res }) => {
// SPDM CODE // SPDM CODE
Cookies.set('cid_user_id', res.user_id) Cookies.set('cid_user_id', res.user_id);
Session.setTenant(res.tenant_id); Session.setTenant(res.tenant_id);
// 存储token 信息 // 存储token 信息
Token.set(res.access_token); Token.set(res.access_token);
Session.set(REFRESH_TOKEN_KEY, res.refresh_token); Session.set(REFRESH_TOKEN_KEY, res.refresh_token);
slidingTimeout.updateTimeout();
resolve(res); resolve(res);
}) })
.catch((err) => { .catch((err) => {
@@ -85,25 +95,25 @@ export const useUserInfo = defineStore('userInfo', {
login({ login({
grant_type: 'remote', grant_type: 'remote',
scope: 'server', scope: 'server',
...resp.data ...resp.data,
}) })
.then(({data: res}) => { .then(({ data: res }) => {
Session.setTenant(res.tenant_id); Session.setTenant(res.tenant_id);
// 存储token 信息 // 存储token 信息
Token.set(res.access_token); Token.set(res.access_token);
Session.set(REFRESH_TOKEN_KEY, res.refresh_token); Session.set(REFRESH_TOKEN_KEY, res.refresh_token);
resolve(res); resolve(res);
}) })
.catch((err) => { .catch((err) => {
useMessage().error(err?.msg || '系统异常请联系管理员'); useMessage().error(err?.msg || '系统异常请联系管理员');
reject(err); reject(err);
}); });
}) })
.catch((err) => { .catch((err) => {
const codeMsg: Record<string, string> = { const codeMsg: Record<string, string> = {
'TENANT-0057': '校验同步用户账号密码失败(员工编号或密码不存在)!', 'TENANT-0057': '校验同步用户账号密码失败(员工编号或密码不存在)!',
'TENANT-0058': '校验同步用户账号密码失败(员工停用,无法登录)!"', 'TENANT-0058': '校验同步用户账号密码失败(员工停用,无法登录)!"',
} };
useMessage().error(codeMsg[err?.errorCode] || '系统异常请联系管理员'); useMessage().error(codeMsg[err?.errorCode] || '系统异常请联系管理员');
reject(err); reject(err);
}); });

34
src/utils/eventBinder.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* 绑定操作事件
* @param events 监听的事件列表
* @param handler 事件处理函数
*/
export const bindEvents = (events: Array<keyof DocumentEventMap>, handler: () => void): void => {
events.forEach((event) => {
document.addEventListener(event, handler, { passive: true });
});
// 页面可见性变化事件
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
handler();
}
});
};
/**
* 解绑操作事件
* @param events 监听的事件列表
* @param handler 事件处理函数
*/
export const unbindEvents = (events: Array<keyof DocumentEventMap>, handler: () => void): void => {
events.forEach((event) => {
document.removeEventListener(event, handler);
});
document.removeEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
handler();
}
});
};

360
src/utils/slidingTimeout.ts Normal file
View File

@@ -0,0 +1,360 @@
import { bindEvents, unbindEvents } from '/@/utils/eventBinder';
import { Local } from '/@/utils/storage';
import { debounce, isFunction, isString } from 'lodash-es';
/**
* 滑动超时支持的事件类型
*/
type SlidingTimeoutEvent = 'timeout' | 'activity';
/**
* 滑动超时配置项移除onTimeout/onActivity改为事件监听
* @interface SlidingTimeoutOptions
* @property {string} [storageKey=EXPIRY_TIME_KEY] - 存储最后活动时间的本地存储key
* @property {number} [timeout=1000 * 60 * 60 * 8] - 默认8小时 超时时间(毫秒)
* @property {Array<keyof DocumentEventMap>} [events=['mousemove', 'click', 'keydown', 'scroll', 'touchstart']] - 需要监听的用户操作事件列表
* @property {number} [debounceDelay=500] - 事件防抖延迟时间(毫秒),避免频繁触发检查
* @property {boolean} [checkOnStart=true] - 是否在调用start方法时立即检查超时状态
*/
interface SlidingTimeoutOptions {
storageKey?: string;
timeout?: number;
events?: Array<keyof DocumentEventMap>;
debounceDelay?: number;
checkOnStart?: boolean;
}
/**
* 滑动超时监听类:监听用户操作事件,每次操作重置超时时间,超时后触发指定事件
* 核心逻辑:用户无操作达到指定时间后触发'timeout'事件,有操作则触发'activity'事件
* @example
* // 事件监听模式用法(核心修改点)
* const slidingTimeout = new SlidingTimeout({
* timeout: 8 * 60 * 60 * 1000,
* });
*
* // 绑定超时事件
* slidingTimeout.on('timeout', () => {
* console.log('用户超时了');
* await logout();
* });
*
* // 绑定用户活动事件
* slidingTimeout.on('activity', () => {
* console.log('用户操作了');
* });
*
* // 启动监听
* slidingTimeout.start();
*
* @example
* // 高级用法:解绑事件/暂停恢复
* // 绑定事件并保存回调引用
* const activityHandler = () => console.log('用户有操作');
* slidingTimeout.on('activity', activityHandler);
*
* // 解绑指定事件
* slidingTimeout.off('activity', activityHandler);
*
* // 暂停监听
* slidingTimeout.pause();
* // 恢复监听
* slidingTimeout.resume();
*
* // 页面销毁时清理
* onUnmounted(() => slidingTimeout.destroy());
*/
class SlidingTimeout {
private static readonly DEFAULT_ACTIVITY_EVENTS: Array<keyof DocumentEventMap> = ['mousemove', 'click', 'keydown', 'scroll', 'touchstart'];
private static readonly DEFAULT_STORAGE_KEY = 'EXPIRY_TIME_KEY';
private static readonly EXPIRE_TIME = 8 * 60 * 60 * 1000;
// 配置项
private storageKey: string;
private timeout: number;
private events: Array<keyof DocumentEventMap>;
private debounceDelay: number;
private checkOnStart: boolean;
// 状态管理
private isListenerActive = false;
private isPaused = false;
private isTimeoutTriggered = false; // 防止重复触发timeout事件
// 事件注册表:存储不同事件的回调函数列表
private eventHandlers: Record<SlidingTimeoutEvent, Array<() => void | Promise<void>>> = {
timeout: [],
activity: [],
};
private debouncedCheckTimeout: ReturnType<typeof debounce>;
constructor(options: SlidingTimeoutOptions) {
const {
storageKey = SlidingTimeout.DEFAULT_STORAGE_KEY,
timeout = SlidingTimeout.EXPIRE_TIME,
events = SlidingTimeout.DEFAULT_ACTIVITY_EVENTS,
debounceDelay = 500,
checkOnStart = true,
} = options;
// 初始化配置
this.storageKey = storageKey;
this.timeout = timeout;
this.events = events;
this.debounceDelay = debounceDelay;
this.checkOnStart = checkOnStart;
// 初始化防抖函数
this.debouncedCheckTimeout = debounce(this.executeTimeoutCheck.bind(this), this.debounceDelay, { leading: true, trailing: true, maxWait: 1000 });
}
/**
* 绑定事件监听
* @param event 事件类型('timeout' | 'activity'
* @param handler 事件回调函数
*/
public on(event: SlidingTimeoutEvent, handler: () => void | Promise<void>): void {
if (!isString(event) || !['timeout', 'activity'].includes(event)) {
throw new TypeError(`事件类型必须是 'timeout' 或 'activity',当前传入:${event}`);
}
if (!isFunction(handler)) {
throw new TypeError('事件回调必须是一个函数');
}
console.log(`SlidingTimeout: 绑定 ${event} 事件`);
this.eventHandlers[event].push(handler);
}
/**
* 解绑事件监听
* @param event 事件类型('timeout' | 'activity'
* @param handler 要解绑的回调函数(不传则解绑该事件的所有回调)
*/
public off(event: SlidingTimeoutEvent, handler?: () => void | Promise<void>): void {
if (!isString(event) || !['timeout', 'activity'].includes(event)) {
throw new TypeError(`事件类型必须是 'timeout' 或 'activity',当前传入:${event}`);
}
if (handler) {
// 解绑指定回调
this.eventHandlers[event] = this.eventHandlers[event].filter((h) => h !== handler);
} else {
// 解绑该事件的所有回调
this.eventHandlers[event] = [];
}
}
/**
* 触发指定事件(内部使用)
* @param event 事件类型
*/
private emit(event: SlidingTimeoutEvent): void {
const handlers = this.eventHandlers[event];
if (!handlers.length) return;
// 依次执行所有回调,捕获单个回调的错误(不影响其他回调)
for (const handler of handlers) {
try {
handler();
} catch (error) {
console.error(`SlidingTimeout: 执行 ${event} 事件回调失败`, error);
}
}
}
/**
* 重置超时时间(清空本地存储的最后活动时间)
*/
public reset(): void {
try {
Local.remove(this.storageKey);
} catch (error) {
console.warn('滑动超时:清空最后操作时间失败', error);
}
}
/**
* 销毁所有监听和计时器,重置状态
*/
public destroy(): void {
console.log('SlidingTimeout: 销毁所有监听和计时器');
// 取消防抖函数
this.debouncedCheckTimeout.cancel();
// 解绑用户操作事件
if (this.isListenerActive) {
try {
unbindEvents(this.events, this.debouncedCheckTimeout);
} catch (error) {
console.error('滑动超时:解绑事件监听失败', error);
}
this.isListenerActive = false;
}
// 清空事件回调注册表
this.eventHandlers = { timeout: [], activity: [] };
// 重置状态
this.isPaused = false;
this.isTimeoutTriggered = false;
}
/**
* 启动用户活动监听
* 1. 先销毁已有监听
* 2. 检查超时状态
* 3. 初始化事件监听
*/
public start(): void {
if (this.isListenerActive) {
this.destroy();
}
if (this.checkOnStart) {
this.executeTimeoutCheck();
}
this.initActivityListener();
}
/**
* 暂停监听
*/
public pause(): void {
if (this.isPaused || this.isTimeoutTriggered) return;
this.debouncedCheckTimeout.cancel();
if (this.isListenerActive) {
try {
unbindEvents(this.events, this.debouncedCheckTimeout);
} catch (error) {
console.error('滑动超时:暂停解绑事件失败', error);
}
}
this.isPaused = true;
}
/**
* 恢复监听
*/
public resume(): void {
if (!this.isPaused || this.isTimeoutTriggered) return;
this.isPaused = false;
if (this.checkOnStart) {
this.executeTimeoutCheck();
}
this.initActivityListener();
}
/**
* 手动触发超时逻辑立即执行timeout事件并销毁监听
*/
public triggerTimeout(): void {
this.handleTimeout();
}
/**
* 更新最后活动时间为当前时间
*/
public updateTimeout(): void {
try {
Local.set(this.storageKey, Date.now());
} catch (error) {
console.warn('滑动超时:更新最后操作时间失败', error);
}
}
private get lastActivityTime(): number | null {
try {
return Number(Local.get(this.storageKey));
} catch (error) {
console.warn('滑动超时:获取最后操作时间失败', error);
return null;
}
}
/**
* 检查是否超时未操作
* @returns {boolean} 是否超时
*/
public checkInactiveTimeout(): boolean {
if (this.isPaused || this.isTimeoutTriggered) return false;
try {
if (!this.lastActivityTime) return true;
const currentTime = Date.now();
const timeElapsed = currentTime - this.lastActivityTime;
return timeElapsed > this.timeout;
} catch (error) {
console.error('滑动超时:检查超时状态失败', error);
return false;
}
}
/**
* 处理用户活动逻辑
*/
private handleActivity(): void {
if (this.isPaused || this.isTimeoutTriggered) return;
this.updateTimeout();
this.emit('activity'); // 触发activity事件
}
/**
* 处理超时逻辑触发timeout事件
*/
private handleTimeout(): void {
if (this.isTimeoutTriggered) return; // 防止重复触发
this.isTimeoutTriggered = true;
this.reset();
this.emit('timeout'); // 触发timeout事件
}
/**
* 执行超时检查
* @returns {void} 是否超时
*/
private executeTimeoutCheck(): void {
const isTimeout = this.checkInactiveTimeout();
if (isTimeout) {
this.handleTimeout();
return;
}
this.handleActivity();
}
/**
* 初始化事件监听
*/
private initActivityListener(): void {
if (this.isListenerActive || this.isPaused || this.isTimeoutTriggered) return;
try {
bindEvents(this.events, this.debouncedCheckTimeout);
this.isListenerActive = true;
} catch (error) {
console.error('滑动超时:绑定事件监听失败', error);
}
}
}
const EXPIRY_TIME_KEY = 'EXPIRY_TIME_KEY';
const DEFAULT_TIMEOUT = 8 * 60 * 60 * 1000;
export const slidingTimeout = new SlidingTimeout({
storageKey: EXPIRY_TIME_KEY,
timeout: DEFAULT_TIMEOUT,
checkOnStart: false,
});
// // 绑定timeout事件
// slidingTimeout.on('timeout', () => {
// console.log('用户超时了');
// });
// // 绑定activity事件
// slidingTimeout.on('activity', () => {
// console.log('用户操作了');
// });

View File

@@ -25,7 +25,11 @@ export const Local = {
// 获取永久缓存 // 获取永久缓存
get(key: string) { get(key: string) {
let json = <string>window.localStorage.getItem(Local.setKey(key)); let json = <string>window.localStorage.getItem(Local.setKey(key));
return JSON.parse(json); try {
return JSON.parse(json);
} catch (error) {
return '';
}
}, },
// 移除永久缓存 // 移除永久缓存
remove(key: string) { remove(key: string) {

View File

@@ -1,5 +1,5 @@
<template> <template>
<Echarts class="h-80 w-full" :option="option"/> <Echarts class="h-full w-full" :option="option"/>
</template> </template>
<script setup lang="ts" name="log-line-chart"> <script setup lang="ts" name="log-line-chart">
import Echarts from '/@/components/Echarts/index.vue'; import Echarts from '/@/components/Echarts/index.vue';

View File

@@ -0,0 +1,2 @@
/** 根用户id */
export const DEFAULT_ROOT_ID = '0';

View File

@@ -1,42 +1,61 @@
<template> <template>
<div class="layout-padding-auto layout-padding-view"> <div class="layout-padding-auto layout-padding-view">
<div> <div>
<el-input v-model="searchName" suffix-icon="search" :placeholder="$t('staffManage.searchDeptTip')" @change="getDeptData" /> <el-input
v-model="searchName"
suffix-icon="search"
:placeholder="t('staffManage.searchDeptTip')"
class="input-with-select"
clearable
@change="getDeptData"
>
<template #append>
<el-button :icon="Search" @click="getDeptData" />
</template>
</el-input>
</div>
<div class="deptTreeContainer">
<el-tree
class="mt20"
:default-expand-all="false"
:data="tree"
:props="{ children: 'children', label: 'deptName', value: 'deptId' }"
:expand-on-click-node="false"
ref="deptTreeRef"
node-key="deptId"
:loading="loading"
:current-node-key="deptId || DEFAULT_ROOT_ID"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ node, data }" v-if="$slots.default">
<slot :node="node" :data="data"></slot>
</template>
</el-tree>
</div> </div>
<el-tree
class="mt20"
:data="tree"
:props="{children: 'children', label: 'deptName', value: 'deptId'}"
:expand-on-click-node="false"
ref="deptTreeRef"
node-key="id"
:loading="loading"
highlight-current
default-expand-all
@node-click="handleNodeClick"
>
<template #default="{node, data}" v-if="$slots.default">
<slot :node="node" :data="data"></slot>
</template>
</el-tree>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {getDeptList, IDeptIntroTree} from '/@/api/admin/deptManage'; import { DEFAULT_ROOT_ID } from './constant';
import {useTenantInfo} from '/@/stores/tenant'; import { getDeptList, IDeptIntroTree } from '/@/api/admin/deptManage';
import { useTenantInfo } from '/@/stores/tenant';
import { Search } from '@element-plus/icons-vue';
const props = defineProps<{
deptId?: string;
}>();
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['nodeClick']); const emit = defineEmits(['nodeClick']);
const tenantStore = useTenantInfo(); const tenantStore = useTenantInfo();
const {currentTenant} = storeToRefs(tenantStore); const { currentTenant } = storeToRefs(tenantStore);
const treeData = ref<IDeptIntroTree[]>([]); const treeData = ref<IDeptIntroTree[]>([]);
const loading = ref(false); const loading = ref(false);
const searchName = ref(''); const searchName = ref('');
const getDeptData = async () => { const getDeptData = async () => {
loading.value = true; loading.value = true;
try { try {
const {data} = await getDeptList({ const { data } = await getDeptList({
deptName: searchName.value, deptName: searchName.value,
}); });
@@ -48,10 +67,10 @@ const getDeptData = async () => {
} }
}; };
const tree = computed(() => { const tree = computed(() => {
const {companyName} = currentTenant.value; const { companyName } = currentTenant.value;
const root: IDeptIntroTree = { const root: IDeptIntroTree = {
deptId: '0', deptId: DEFAULT_ROOT_ID,
deptName: companyName, deptName: companyName,
children: [], children: [],
}; };
@@ -59,7 +78,7 @@ const tree = computed(() => {
return [root]; return [root];
}); });
const handleNodeClick = (item: IDeptIntroTree) => { const handleNodeClick = (item: IDeptIntroTree) => {
if (item.deptId === '0') { if (item.deptId === DEFAULT_ROOT_ID) {
emit('nodeClick'); emit('nodeClick');
} else { } else {
emit('nodeClick', item.deptId); emit('nodeClick', item.deptId);
@@ -70,3 +89,14 @@ onMounted(() => {
getDeptData(); getDeptData();
}); });
</script> </script>
<style lang="scss" scoped>
.deptTreeContainer {
height: calc(100vh - 190px);
overflow: auto;
:deep(.el-tree) {
--el-tree-node-content-height: 40px;
}
}
</style>

View File

@@ -52,8 +52,9 @@
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import {getDeptList, IDeptIntroTree} from '/@/api/admin/deptManage'; import { DEFAULT_ROOT_ID } from './constant';
import {getStaffDetail, updateStaff} from '/@/api/admin/staffManage'; import { getDeptList, IDeptIntroTree } from '/@/api/admin/deptManage';
import { getStaffDetail, updateStaff } from '/@/api/admin/staffManage';
import { useMessage } from '/@/hooks/message'; import { useMessage } from '/@/hooks/message';
import {useTenantInfo} from '/@/stores/tenant'; import {useTenantInfo} from '/@/stores/tenant';
const { t } = useI18n(); const { t } = useI18n();
@@ -96,7 +97,7 @@ const staffForm = ref<{
email: '', email: '',
posts: [], posts: [],
roles: [], roles: [],
dept: '', dept: DEFAULT_ROOT_ID,
}); });
const dataRules = ref({ const dataRules = ref({
@@ -142,7 +143,7 @@ const getDetail = async (id: string) => {
email, email,
posts, posts,
roles, roles,
dept: depts[0], dept: depts[0] || DEFAULT_ROOT_ID,
}; };
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -156,7 +157,7 @@ const getDeptTree = async () => {
} }
const {companyName} = currentTenant.value; const {companyName} = currentTenant.value;
const root: IDeptIntroTree = { const root: IDeptIntroTree = {
deptId: '0', deptId: DEFAULT_ROOT_ID,
deptName: companyName, deptName: companyName,
children: [], children: [],
}; };

View File

@@ -2,7 +2,7 @@
<div class="layout-padding"> <div class="layout-padding">
<div class="flex"> <div class="flex">
<div class="tree"> <div class="tree">
<DeptTree @node-click="handleNodeClick" /> <DeptTree :dept-id="deptId" @node-click="handleNodeClick" />
</div> </div>
<div class="table ml-2"> <div class="table ml-2">
<StaffTable :dept-id="deptId" :posts="posts" :roles="roles" /> <StaffTable :dept-id="deptId" :posts="posts" :roles="roles" />

View File

@@ -81,6 +81,7 @@
</el-button> </el-button>
</div> </div>
<TableRender <TableRender
v-loading="loading"
class="min-h-[440px]" class="min-h-[440px]"
:data="tableData" :data="tableData"
:columns="tableColumns" :columns="tableColumns"
@@ -104,7 +105,11 @@ import {ElButton} from 'element-plus';
import EditForm from './editForm.vue'; import EditForm from './editForm.vue';
import InviteForm from './inviteForm.vue'; import InviteForm from './inviteForm.vue';
import { useMessage, useMessageBox } from '/@/hooks/message'; import { useMessage, useMessageBox } from '/@/hooks/message';
const {t} = useI18n(); import { onlyResolvesLast } from '/@/utils/onlyResolvesLast';
const getStaffListApi = onlyResolvesLast(getStaffList);
const { t } = useI18n();
const editFormRef = ref<InstanceType<typeof EditForm>>(); const editFormRef = ref<InstanceType<typeof EditForm>>();
const inviteFormRef = ref<InstanceType<typeof InviteForm>>(); const inviteFormRef = ref<InstanceType<typeof InviteForm>>();
@@ -132,6 +137,15 @@ type QueryForm = {
roles: string[]; roles: string[];
posts: string[]; posts: string[];
}; };
const loading = ref(false);
const setTrue = () => {
loading.value = true;
};
const setFalse = () => {
loading.value = false;
};
const initialQueryForm = (): QueryForm => { const initialQueryForm = (): QueryForm => {
return { return {
phone: '', phone: '',
@@ -277,8 +291,9 @@ const queryStaffs = () => {
}; };
const getDataList = async () => { const getDataList = async () => {
setTrue();
try { try {
const {data} = await getStaffList(queryParams.value); const { data } = await getStaffListApi({ ...queryParams.value, deptId: props.deptId });
pagination.size = data.size; pagination.size = data.size;
pagination.current = data.current; pagination.current = data.current;
pagination.total = data.total; pagination.total = data.total;
@@ -303,6 +318,8 @@ const getDataList = async () => {
console.log(toRaw(tableData.value)); console.log(toRaw(tableData.value));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
setFalse();
} }
}; };

View File

@@ -1,3 +1,10 @@
<script lang="ts">
export default {
w: 12,
h: 3,
static: true
};
</script>
<script lang="ts" setup name="TopNav"> <script lang="ts" setup name="TopNav">
import {getTaskCountApi} from '/@/api/admin/tenant'; import {getTaskCountApi} from '/@/api/admin/tenant';
import {getTenantAssignmentsListByUsername} from '/@/api/admin/user'; import {getTenantAssignmentsListByUsername} from '/@/api/admin/user';

View File

@@ -1,14 +1,18 @@
<script lang="ts"> <script lang="ts">
import {i18n} from "/@/i18n"
export default { export default {
title: "components.audit-log.0910665-0", // 审计日志 title: 'components.audit-log.0910665-0', // 审计日志
icon: 'DocumentCopy', icon: 'DocumentCopy',
description: "components.audit-log.0910665-1" // 审计日志列表 description: 'components.audit-log.0910665-1', // 审计日志列表
w: 6,
h: 10,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>
<el-card class="box-card h-[400px]"> <el-card class="box-card h-full w-full">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>{{ $t('home.auditLogsTip') }}</span> <span>{{ $t('home.auditLogsTip') }}</span>

View File

@@ -3,11 +3,17 @@ export default {
title: 'components.calendar.0910666-0', // 日程管理 title: 'components.calendar.0910666-0', // 日程管理
icon: 'Calendar', icon: 'Calendar',
description: 'components.calendar.0910666-1', // 日历组件展示 description: 'components.calendar.0910666-1', // 日历组件展示
w: 6,
h: 14,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>
<el-card class="h-[400px] box-card"> <el-card class="h-full w-full box-card">
<Calendar <Calendar
ref="calendar" ref="calendar"
view="weekly" view="weekly"
@@ -22,8 +28,8 @@ export default {
@dayclick="dayClick" @dayclick="dayClick"
/> />
<div v-if="calendar" class="py-4 px-6 w-full h-[18rem] overflow-y-auto"> <div v-if="calendar" class="py-4 px-6 w-full h-[18rem] overflow-y-auto">
<template v-for="{day, cells} in Object.values(dayCells)"> <template v-for="{ day, cells } in Object.values(dayCells)">
<ul v-if="cells.length > 0" :key="day" class="py-2 space-y-2"> <ul v-if="cells.length > 0" class="py-2 space-y-2" :key="day">
<li v-for="cell in cells" :key="cell"> <li v-for="cell in cells" :key="cell">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<!--Icon--> <!--Icon-->
@@ -58,18 +64,18 @@ export default {
</template> </template>
<script setup lang="ts" name="scheduleCalendar"> <script setup lang="ts" name="scheduleCalendar">
import {Calendar} from 'v-calendar'; import { Calendar } from 'v-calendar';
import 'v-calendar/style.css'; import 'v-calendar/style.css';
import {useThemeConfig} from '/@/stores/themeConfig'; import { useThemeConfig } from '/@/stores/themeConfig';
import {parseDate} from '/@/utils/formatTime'; import { parseDate } from '/@/utils/formatTime';
import {list, putObj} from '/@/api/admin/schedule'; import { list, putObj } from '/@/api/admin/schedule';
const ScheduleForm = defineAsyncComponent(() => import('/@/views/home/schedule/form.vue')); const ScheduleForm = defineAsyncComponent(() => import('/@/views/home/schedule/form.vue'));
const Schedule = defineAsyncComponent(() => import('/@/views/home/schedule/index.vue')); const Schedule = defineAsyncComponent(() => import('/@/views/home/schedule/index.vue'));
// 获取当前国际化方言 // 获取当前国际化方言
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const {themeConfig} = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
const locale = computed(() => { const locale = computed(() => {
return themeConfig.value.globalI18n; return themeConfig.value.globalI18n;
}); });
@@ -109,16 +115,16 @@ const weeknumberClick = (page: any) => {
//处理日程安排事件,若当前日期下没有日程则打开表单对话框,否则打开日程详情页面 //处理日程安排事件,若当前日期下没有日程则打开表单对话框,否则打开日程详情页面
const dayClick = (day: any) => { const dayClick = (day: any) => {
if (filterCellSelected(day.id)) { if (filterCellSelected(day.id)) {
scheduleRef.value.open({date: parseDate(day.id, null)}); scheduleRef.value.open({ date: parseDate(day.id, null) });
} else { } else {
scheduleFormRef.value.openDialog(null, {date: parseDate(day.id, null)}); scheduleFormRef.value.openDialog(null, { date: parseDate(day.id, null) });
} }
}; };
// 修改开关状态 // 修改开关状态
const changeSwitch = async (id: string) => { const changeSwitch = async (id: string) => {
// 修改对象的状态为'3' // 修改对象的状态为'3'
await putObj({id: id, state: '3'}); await putObj({ id: id, state: '3' });
// 初始化调度列表 // 初始化调度列表
initscheduleList(); initscheduleList();
}; };

View File

@@ -3,6 +3,12 @@ export default {
title: 'components.favorite-flow.0910773-0', // '常用流程' title: 'components.favorite-flow.0910773-0', // '常用流程'
icon: 'Star', icon: 'Star',
description: 'components.favorite-flow.0910773-1', // '常用流程收藏' description: 'components.favorite-flow.0910773-1', // '常用流程收藏'
w: 6,
h: 9,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>

View File

@@ -3,6 +3,12 @@ export default {
title: 'components.favorite-menu.0910774-0', // '常用功能' title: 'components.favorite-menu.0910774-0', // '常用功能'
icon: 'Star', icon: 'Star',
description: 'components.favorite-menu.0910774-1', // '常用功能收藏' description: 'components.favorite-menu.0910774-1', // '常用功能收藏'
w: 6,
h: 5,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>

View File

@@ -3,10 +3,16 @@ export default {
title: 'components.news.0910777-0', // '系统公告' title: 'components.news.0910777-0', // '系统公告'
icon: 'MuteNotification', icon: 'MuteNotification',
description: 'components.news.0910777-1', // '系统公告展示' description: 'components.news.0910777-1', // '系统公告展示'
w: 6,
h: 10,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>
<el-card class="h-[400px]"> <el-card class="h-full w-full">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>{{ $t('home.newsletterTip') }}</span> <span>{{ $t('home.newsletterTip') }}</span>
@@ -15,7 +21,7 @@ export default {
</template> </template>
<el-timeline v-if="newsList.length > 0"> <el-timeline v-if="newsList.length > 0">
<div class="relative"> <div class="relative">
<el-timeline-item v-for="(item, index) in newsList" :key="index" :timestamp="item.createTime" @click="contentRef.openDialog(item)"> <el-timeline-item v-for="(item, index) in newsList" :key="index" :timestamp="item.intervalTime">
<button class="absolute right-0 px-3 rotate-[10deg] -top-2 border bg-primary text-white font-bold"> <button class="absolute right-0 px-3 rotate-[10deg] -top-2 border bg-primary text-white font-bold">
{{ item.readFlag === '1' ? $t('msg.readed') : $t('msg.unread') }} {{ item.readFlag === '1' ? $t('msg.readed') : $t('msg.unread') }}
</button> </button>
@@ -50,7 +56,7 @@ const newsList = ref([]);
// 获取用户的信息 // 获取用户的信息
const getUserMessage = () => { const getUserMessage = () => {
// 取前三条数据 // 取前三条数据
return fetchUserMessageList({ current: 1, size: 3, category: '0', msgType: '1', sendFlag: '1'}).then((res) => { return fetchUserMessageList({ current: 1, size: 3, category: '0', msgType: '1', sendFlag: '1' }).then((res) => {
newsList.value = res.data.records; newsList.value = res.data.records;
}); });
}; };

View File

@@ -3,6 +3,12 @@ export default {
title: 'components.sys-log-line.0910980-0', // '请求折线图' title: 'components.sys-log-line.0910980-0', // '请求折线图'
icon: 'DocumentCopy', icon: 'DocumentCopy',
description: 'components.sys-log-line.0910980-1', // '折线图示例' description: 'components.sys-log-line.0910980-1', // '折线图示例'
w: 6,
h: 14,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>

View File

@@ -3,6 +3,12 @@ export default {
title: 'components.sys-log.0910779-0', // '系统日志' title: 'components.sys-log.0910779-0', // '系统日志'
icon: 'Monitor', icon: 'Monitor',
description: 'components.sys-log.0910779-1', // '系统日志列表' description: 'components.sys-log.0910779-1', // '系统日志列表'
w: 6,
h: 10,
minW: 3,
minH: 5,
maxW: 12,
maxH: 16,
}; };
</script> </script>
<template> <template>