feat:人员打卡配置

This commit is contained in:
dzc committed 2026-06-29 11:01:03 +08:00
1 parent cdb7496ade
commit 866a11277e
21 files changed
+1357 -269

No files matched your search

+1 -1
View File
@@ -120,4 +120,4 @@
"type": "git",
"url": "https://gitee.com/log4j/pig"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import request from "/@/utils/request";
export function change(data?: { userCodeList: string[]; enable: "1" | "0" }) {
return request({
url: "/admin/sysPersonClockConfig/change",
method: "post",
data,
});
}
+1 -1
View File
@@ -109,7 +109,7 @@ const props = withDefaults(
nodeClickable: true,
switchInactiveText: "当前级",
switchActiveText: "当前级及子级",
defaultTreeWidth: 20,
defaultTreeWidth: 15,
},
)
const emit = defineEmits(["getSelectNode"])
@@ -112,7 +112,7 @@ const props = withDefaults(
nodeClickable: true,
switchInactiveText: "当前级",
switchActiveText: "当前级及子级",
defaultTreeWidth: 20,
defaultTreeWidth: 15,
},
)
@@ -121,7 +121,7 @@ const props = withDefaults(
nodeClickable: true,
switchInactiveText: "当前级",
switchActiveText: "当前级及子级",
defaultTreeWidth: 20,
defaultTreeWidth: 15,
windowNum: null
},
)
+1 -1
View File
@@ -109,7 +109,7 @@ const props = withDefaults(
nodeClickable: true,
switchInactiveText: "当前级",
switchActiveText: "当前级及子级",
defaultTreeWidth: 20,
defaultTreeWidth: 15,
},
)
const emit = defineEmits(["getSelectNode"])
+32 -3
View File
@@ -3,9 +3,9 @@
label: 'treeLabel',
children: 'deviceChannelList',
}" node-key="treeValue" v-model="state.selectId" :placeholder="props.placeholder" :clearable="props.clearable"
:loading="state.loading" :data="state.options" :render-after-expand="false" show-checkbox multiple
style="width: 100%" filterable :filter-node-method="filterNode" default-expand-all
@visible-change="(e: boolean) => e && getOptions()">
:loading="state.loading" :data="state.options" :render-after-expand="true" show-checkbox multiple
style="width: 100%" filterable :filter-node-method="filterNode"
@visible-change="(e: boolean) => e && getOptions(true)">
<template #default="{ data, node }">
<div v-if="node.level == 1" class="flex items-center gap-2">
<el-tag size="small" effect="dark" color="var(--el-color-primary)">硬盘录像机</el-tag>
@@ -111,8 +111,29 @@ onMounted(() => {
}
})
const getCacheKey = () => `SelectTreeChannel_Cache_${JSON.stringify(props.apiParams || {})}`
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
if (props.autoFirst && state.options.length != 0) {
state.selectId = state.options[0].id
emit("change", state.selectId)
}
return
}
} catch (e) {
console.error('Failed to parse cached tree data', e)
}
}
if (loadOptionsTask) return loadOptionsTask
loadOptionsTask = getOptions(force).finally(() => {
loadOptionsTask = null
@@ -131,6 +152,14 @@ async function getOptions(force = false) {
const { data } = await DeviceManageApi.channelTree(_pa)
state.options = data ?? []
state.loaded = true
try {
const cacheKey = getCacheKey()
localStorage.setItem(cacheKey, JSON.stringify(state.options))
} catch (e) {
console.error('Failed to cache tree data', e)
}
if (props.autoFirst && state.options.length != 0) {
state.selectId = state.options[0].id
emit("change", state.selectId)
+218 -216
View File
@@ -1,251 +1,253 @@
import { CellStyle, ElMessage } from 'element-plus';
import other from '/@/utils/other';
import { CellStyle, ElMessage } from "element-plus";
import other from "/@/utils/other";
/**
* 表格组件基础配置属性
*/
export interface BasicTableProps {
// 是否在创建页面时即调用数据列表接口,默认为true
createdIsNeed?: boolean;
// 是否需要分页,默认为true
isPage?: boolean;
// 查询条件表单对象,类型为any
queryForm?: any;
// 数据列表数组
dataList?: any[];
// 分页属性对象
pagination?: Pagination;
// 数据列表,loading状态标志,默认为false
dataListLoading?: boolean;
// 数据列表多选项数组
dataListSelections?: any[];
// 数据列表查询接口api方法,接收任意数量参数,返回Promise
pageList?: (...arg: any) => Promise<any>;
// loading标志,默认为false
loading?: Boolean;
// 多选结果数组
selectObjs?: any[];
// 排序字段数组
descs?: string[];
// 排序方式数组
ascs?: string[];
// props属性对象,类型为any
props?: any;
// 是否在创建页面时即调用数据列表接口,默认为true
createdIsNeed?: boolean;
// 是否需要分页,默认为true
isPage?: boolean;
// 查询条件表单对象,类型为any
queryForm?: any;
// 数据列表数组
dataList?: any[];
// 分页属性对象
pagination?: Pagination;
// 数据列表,loading状态标志,默认为false
dataListLoading?: boolean;
// 数据列表多选项数组
dataListSelections?: any[];
// 数据列表查询接口api方法,接收任意数量参数,返回Promise
pageList?: (...arg: any) => Promise<any>;
// loading标志,默认为false
loading?: Boolean;
// 多选结果数组
selectObjs?: any[];
// 排序字段数组
descs?: string[];
// 排序方式数组
ascs?: string[];
// props属性对象,类型为any
props?: any;
}
/**
* 表格样式。
*/
export interface TableStyle {
cellStyle: CellStyle<any>;
headerCellStyle: CellStyle<any>;
cellStyle: CellStyle<any>;
headerCellStyle: CellStyle<any>;
}
/**
* 分页属性配置接口
*/
export interface Pagination {
// 当前页码,默认为1
current?: number;
// 每页显示条数,默认为10
size?: number;
// 总条数,默认为0
total?: number;
// 每页显示条数选择器的选项数组,默认为[10,20,30,40]
pageSizes?: any[];
// 分页组件布局方式,可选值有 total,sizes,prev,jump,next,默认为'total,sizes,prev,jump,next'
layout?: String;
// 当前页码,默认为1
current?: number;
// 每页显示条数,默认为10
size?: number;
// 总条数,默认为0
total?: number;
// 每页显示条数选择器的选项数组,默认为[10,20,30,40]
pageSizes?: any[];
// 分页组件布局方式,可选值有 total,sizes,prev,jump,next,默认为'total,sizes,prev,jump,next'
layout?: String;
}
export function useTable(options?: BasicTableProps) {
const defaultOptions: BasicTableProps = {
// 列表数据是否正在加载中,默认为false
dataListLoading: false,
// 是否需要自动请求创建接口来获取表格数据,默认为true
createdIsNeed: true,
// 是否展示分页组件,默认为true
isPage: true,
// 查询表单对象,用于提交条件查询时的参数传递,默认为空对象
queryForm: {},
// 表格展示的数据数组,默认为空数组
dataList: [],
// 分页组件属性配置,如当前页码、每页展示数据条数等,默认值为 {current:1, size:10,total:0,pageSizes:[1, 10, 20, 50, 100, 200],layout:'total, sizes, prev, pager, next, jumper'}
pagination: {
current: 1,
size: 20,
total: 0,
pageSizes: [1, 10, 20, 50, 100, 200],
layout: 'total, sizes, prev, pager, next, jumper',
} as Pagination,
// 当前选中的数据项,默认为空数组
dataListSelections: [],
// 是否正在从服务器加载数据,默认为false
loading: false,
// 表格数据项的选择数据,默认为空数组
selectObjs: [],
// 排序时使用的字段名数组,如 ['id','name'],默认为空数组
descs: [],
// 排序方向数组,如 ['asc', 'desc'],默认为空数组
ascs: [],
// props属性配置对象,用于自定义数据属性,默认值为 {item:'records',totalCount:'total'}
props: {
item: 'records',
totalCount: 'total',
},
};
const defaultOptions: BasicTableProps = {
// 列表数据是否正在加载中,默认为false
dataListLoading: false,
// 是否需要自动请求创建接口来获取表格数据,默认为true
createdIsNeed: true,
// 是否展示分页组件,默认为true
isPage: true,
// 查询表单对象,用于提交条件查询时的参数传递,默认为空对象
queryForm: {},
// 表格展示的数据数组,默认为空数组
dataList: [],
// 分页组件属性配置,如当前页码、每页展示数据条数等,默认值为 {current:1, size:10,total:0,pageSizes:[1, 10, 20, 50, 100, 200],layout:'total, sizes, prev, pager, next, jumper'}
pagination: {
current: 1,
size: 20,
total: 0,
pageSizes: [1, 10, 20, 50, 100],
layout: "total, sizes, prev, pager, next, jumper",
} as Pagination,
// 当前选中的数据项,默认为空数组
dataListSelections: [],
// 是否正在从服务器加载数据,默认为false
loading: false,
// 表格数据项的选择数据,默认为空数组
selectObjs: [],
// 排序时使用的字段名数组,如 ['id','name'],默认为空数组
descs: [],
// 排序方向数组,如 ['asc', 'desc'],默认为空数组
ascs: [],
// props属性配置对象,用于自定义数据属性,默认值为 {item:'records',totalCount:'total'}
props: {
item: "records",
totalCount: "total",
},
};
/**
* 合并默认属性配置和自定义属性配置
* @param options 默认属性配置对象
* @param props 自定义属性配置对象
* @returns 合并后的属性配置对象
*/
const mergeDefaultOptions = (options: any, props: any): BasicTableProps => {
for (const key in options) {
if (!Object.getOwnPropertyDescriptor(props, key)) {
props[key] = options[key];
}
}
return props;
};
/**
* 合并默认属性配置和自定义属性配置
* @param options 默认属性配置对象
* @param props 自定义属性配置对象
* @returns 合并后的属性配置对象
*/
const mergeDefaultOptions = (options: any, props: any): BasicTableProps => {
for (const key in options) {
if (!Object.getOwnPropertyDescriptor(props, key)) {
props[key] = options[key];
}
}
return props;
};
// 覆盖默认值
const state = mergeDefaultOptions(defaultOptions, options);
// 覆盖默认值
const state = mergeDefaultOptions(defaultOptions, options);
/**
* 发起分页查询,并设置表格数据和分页信息
*/
const query = async () => {
// 判断是否存在state.pageList属性
if (state.pageList) {
try {
// 开始加载数据,设置state.loading为true
state.loading = true;
/**
* 发起分页查询,并设置表格数据和分页信息
*/
const query = async () => {
// 判断是否存在state.pageList属性
if (state.pageList) {
try {
// 开始加载数据,设置state.loading为true
state.loading = true;
// 调用state.pageList方法发起分页查询
const res = await state.pageList({
...state.queryForm,
current: state.pagination?.current,
size: state.pagination?.size,
descs: state.descs?.join(','),
ascs: state.ascs?.join(','),
});
// 调用state.pageList方法发起分页查询
const res = await state.pageList({
...state.queryForm,
current: state.pagination?.current,
size: state.pagination?.size,
descs: state.descs?.join(","),
ascs: state.ascs?.join(","),
});
// 设置表格展示的数据数组
state.dataList = state.isPage ? res.data[state.props.item] : res.data;
// 设置分页信息中的总数据条数
state.pagination!.total = state.isPage ? res.data[state.props.totalCount] : 0;
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
} finally {
// 结束加载数据,设置state.loading为false
state.loading = false;
}
}
};
// 设置表格展示的数据数组
state.dataList = state.isPage ? res.data[state.props.item] : res.data;
// 设置分页信息中的总数据条数
state.pagination!.total = state.isPage
? res.data[state.props.totalCount]
: 0;
} catch (err: any) {
// 捕获异常并显示错误提示
ElMessage.error(err.msg || err.data.msg);
} finally {
// 结束加载数据,设置state.loading为false
state.loading = false;
}
}
};
onMounted(() => {
if (state.createdIsNeed) {
query();
}
});
onMounted(() => {
if (state.createdIsNeed) {
query();
}
});
/**
* 分页大小改变事件处理函数
* @param val 新的分页大小
*/
const sizeChangeHandle = (val: number) => {
// 修改state.pagination中的size属性
state.pagination!.size = val;
// 再次发起查询操作
query();
};
/**
* 分页大小改变事件处理函数
* @param val 新的分页大小
*/
const sizeChangeHandle = (val: number) => {
// 修改state.pagination中的size属性
state.pagination!.size = val;
// 再次发起查询操作
query();
};
/**
* 当前页码改变事件处理函数
* @param val 新的页码
*/
const currentChangeHandle = (val: number) => {
// 修改state.pagination中的current属性
state.pagination!.current = val;
// 再次发起查询操作
query();
};
/**
* 当前页码改变事件处理函数
* @param val 新的页码
*/
const currentChangeHandle = (val: number) => {
// 修改state.pagination中的current属性
state.pagination!.current = val;
// 再次发起查询操作
query();
};
// 排序触发事件
const sortChangeHandle = (column: any) => {
const prop = other.toUnderline(column.prop);
if (column.order === 'descending') {
state.descs?.push(prop);
if (state.ascs!.includes(prop)) {
state.ascs?.splice(state.ascs.indexOf(prop), 1);
}
} else if (column.order === 'ascending') {
state.ascs?.push(prop);
if (state.descs!.includes(prop)) {
state.descs?.splice(state.descs.indexOf(prop), 1);
}
} else {
if (state.ascs!.includes(prop)) {
state.ascs?.splice(state.ascs.indexOf(prop), 1);
}
if (state.descs!.includes(prop)) {
state.descs?.splice(state.descs.indexOf(prop), 1);
}
}
query();
};
// 排序触发事件
const sortChangeHandle = (column: any) => {
const prop = other.toUnderline(column.prop);
if (column.order === "descending") {
state.descs?.push(prop);
if (state.ascs!.includes(prop)) {
state.ascs?.splice(state.ascs.indexOf(prop), 1);
}
} else if (column.order === "ascending") {
state.ascs?.push(prop);
if (state.descs!.includes(prop)) {
state.descs?.splice(state.descs.indexOf(prop), 1);
}
} else {
if (state.ascs!.includes(prop)) {
state.ascs?.splice(state.ascs.indexOf(prop), 1);
}
if (state.descs!.includes(prop)) {
state.descs?.splice(state.descs.indexOf(prop), 1);
}
}
query();
};
/**
* 获取数据列表,并可选择是否刷新当前页码
* 刷新后不跳转第一页,则入参 getDataList(false)
* @param refresh 是否刷新当前页码
*/
const getDataList = async (refresh?: any) => {
// 如果需要刷新,则将state.pagination.current重置为1
if (refresh !== false) {
state.pagination!.current = 1;
}
// 再次发起查询操作
await query();
};
/**
* 获取数据列表,并可选择是否刷新当前页码
* 刷新后不跳转第一页,则入参 getDataList(false)
* @param refresh 是否刷新当前页码
*/
const getDataList = async (refresh?: any) => {
// 如果需要刷新,则将state.pagination.current重置为1
if (refresh !== false) {
state.pagination!.current = 1;
}
// 再次发起查询操作
await query();
};
/**
* 下载文件
* @param url 文件下载地址
* @param query 请求参数(可能包含token)
* @param fileName 文件名
* @returns 返回一个Promise对象,用于异步处理结果
*/
const downBlobFile = (url: string, query: any, fileName: string) => {
return other.downBlobFile(url, query, fileName);
};
/**
* 下载文件
* @param url 文件下载地址
* @param query 请求参数(可能包含token)
* @param fileName 文件名
* @returns 返回一个Promise对象,用于异步处理结果
*/
const downBlobFile = (url: string, query: any, fileName: string) => {
return other.downBlobFile(url, query, fileName);
};
const postDownBlobFile = (url: string, body: any, fileName: string) => {
return other.poseDownBlobFile(url, body, fileName);
};
const postDownBlobFile = (url: string, body: any, fileName: string) => {
return other.poseDownBlobFile(url, body, fileName);
};
/**
* 定义表格通用样式
* @returns css
*/
const tableStyle: TableStyle = {
cellStyle: { textAlign: 'center' },
headerCellStyle: {
textAlign: 'center',
background: 'var(--el-table-row-hover-bg-color)',
color: 'var(--el-text-color-primary)',
},
};
/**
* 定义表格通用样式
* @returns css
*/
const tableStyle: TableStyle = {
cellStyle: { textAlign: "center" },
headerCellStyle: {
textAlign: "center",
background: "var(--el-table-row-hover-bg-color)",
color: "var(--el-text-color-primary)",
},
};
return {
tableStyle,
getDataList,
sizeChangeHandle,
currentChangeHandle,
sortChangeHandle,
downBlobFile,
postDownBlobFile
};
return {
tableStyle,
getDataList,
sizeChangeHandle,
currentChangeHandle,
sortChangeHandle,
downBlobFile,
postDownBlobFile,
};
}
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="layout-padding">
<draggable-dept-tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
:defaultTreeWidth="15" :nodeClickable="false">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
@@ -0,0 +1,35 @@
<template>
<div class="editable-area" :class="{ 'is-hover': hover }" @mouseenter="hover = true" @mouseleave="hover = false">
<slot></slot>
<div class="edit-btn-wrap" v-if="hover">
<el-button type="primary" size="small" @click="$emit('edit')">编辑</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const hover = ref(false)
defineEmits(['edit'])
</script>
<style scoped>
.editable-area {
position: relative;
transition: all 0.3s;
}
.editable-area.is-hover {
outline: 2px dashed var(--el-color-primary);
outline-offset: 4px;
border-radius: 4px;
z-index: 10;
}
.edit-btn-wrap {
position: absolute;
top: -12px;
right: -12px;
z-index: 100;
}
</style>
@@ -0,0 +1,109 @@
<template>
<el-dialog v-model="visible" :title="title" width="900px" append-to-body destroy-on-close>
<div class="list-editor">
<el-button type="primary" plain style="margin-bottom: 16px;" @click="handleAdd">添加一项</el-button>
<el-collapse v-model="activeName" accordion @change="handleChangeActive">
<el-collapse-item v-for="(item, index) in listData" :key="index" :name="index">
<template #title>
<div
style="display: flex; justify-content: space-between; width: 100%; align-items: center; padding-right: 16px;">
<span>第 {{ index + 1 }} 项</span>
<el-button type="danger" link @click.stop="handleRemove(index)">删除</el-button>
</div>
</template>
<el-form :model="item" label-width="100px" style="margin-top: 16px;">
<template v-for="col in columns" :key="col.prop">
<el-form-item :label="col.label">
<template v-if="col.type === 'richtext'">
<div style="width: 100%; border: 1px solid #dcdfe6; border-radius: 4px;"
v-if="visible && activeName === index">
<Editor :getHtml="item[col.prop]"
@update:getHtml="val => { if (val) item[col.prop] = val }" height="200" />
</div>
</template>
<template v-else-if="col.type === 'textarea'">
<el-input type="textarea" v-model="item[col.prop]" :rows="3" />
</template>
<template v-else>
<el-input v-model="item[col.prop]" />
</template>
</el-form-item>
</template>
</el-form>
</el-collapse-item>
</el-collapse>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { cloneDeep } from 'lodash'
import Editor from '/@/components/Editor/index.vue'
const props = defineProps({
title: {
type: String,
default: '编辑列表'
},
columns: {
type: Array as () => any[],
default: () => []
}
})
const emit = defineEmits(['confirm'])
const visible = ref(false)
const listData = ref<any[]>([])
const activeName = ref(0)
const handleChangeActive = (val: number | string) => {
// 强制触发重新渲染
const currentList = cloneDeep(listData.value)
listData.value = []
nextTick(() => {
listData.value = currentList
activeName.value = val as number
})
}
const open = (initialData: any[]) => {
listData.value = cloneDeep(initialData)
activeName.value = 0
visible.value = true
}
const handleAdd = () => {
const newItem: any = {}
props.columns.forEach(col => {
newItem[col.prop] = ''
})
listData.value.push(newItem)
activeName.value = listData.value.length - 1
}
const handleRemove = (index: number) => {
listData.value.splice(index, 1)
}
const handleConfirm = () => {
emit('confirm', listData.value)
visible.value = false
}
defineExpose({ open })
</script>
<style scoped>
.list-editor {
max-height: 60vh;
overflow-y: auto;
}
</style>
@@ -0,0 +1,48 @@
<template>
<el-dialog v-model="visible" :title="title" width="800px" append-to-body destroy-on-close>
<div class="editor-container" v-if="visible">
<Editor v-model:getHtml="content" height="400" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import Editor from '/@/components/Editor/index.vue'
const props = defineProps({
title: {
type: String,
default: '编辑内容'
}
})
const emit = defineEmits(['confirm'])
const visible = ref(false)
const content = ref('')
const open = (initialContent: string) => {
content.value = initialContent
visible.value = true
}
const handleConfirm = () => {
emit('confirm', content.value)
visible.value = false
}
defineExpose({ open })
</script>
<style scoped>
.editor-container {
min-height: 400px;
}
</style>
+509
View File
@@ -0,0 +1,509 @@
<template>
<div class="camp-intro">
<section class="hero-panel">
<EditableArea class="hero-stats" @edit="openListEditor('stats', stats, statsColumns)">
<div v-for="item in stats" :key="item.label" class="stat-card">
<div class="stat-value">{{ item.value }}</div>
<div class="stat-label">{{ item.label }}</div>
<div class="stat-note" v-html="item.note"></div>
</div>
</EditableArea>
<div class="hero-copy">
<div class="hero-tag">园区介绍</div>
<h1 class="hero-title">中国人民解放军雷达领域高等学府</h1>
<EditableArea @edit="openRichTextEditor('heroDesc', heroDesc)">
<div class="hero-desc" v-html="heroDesc"></div>
</EditableArea>
</div>
</section>
<section class="content-grid">
<article class="panel overview-panel">
<div class="panel-title">学校概况</div>
<div class="panel-line"></div>
<EditableArea @edit="openRichTextEditor('overviewHtml', overviewHtml)">
<div class="panel-paragraph" v-html="overviewHtml"></div>
</EditableArea>
</article>
<article class="panel feature-panel">
<div class="panel-title">办学特色</div>
<div class="panel-line"></div>
<EditableArea class="feature-list" @edit="openListEditor('features', features, featuresColumns)">
<div v-for="feature in features" :key="feature.title" class="feature-card">
<div class="feature-index">{{ feature.index }}</div>
<div class="feature-body">
<div class="feature-title">{{ feature.title }}</div>
<div class="feature-text" v-html="feature.text"></div>
</div>
</div>
</EditableArea>
</article>
</section>
<section class="panel timeline-panel">
<div class="panel-title">历史沿革</div>
<div class="panel-line"></div>
<EditableArea class="timeline" @edit="openListEditor('timeline', timeline, timelineColumns)">
<div v-for="item in timeline" :key="item.year" class="timeline-item">
<div class="timeline-year">{{ item.year }}</div>
<div class="timeline-dot"></div>
<div class="timeline-card">
<div class="timeline-title">{{ item.title }}</div>
<div class="timeline-text" v-html="item.text"></div>
</div>
</div>
</EditableArea>
</section>
<RichTextEditorDialog ref="richTextDialogRef" @confirm="handleRichTextConfirm" />
<ListEditorDialog ref="listDialogRef" :columns="currentListColumns" @confirm="handleListConfirm" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useMessage } from "/@/hooks/message"
import * as ComDhSettingApi from "/@/api/com/comDhSetting";
import EditableArea from './components/EditableArea.vue'
import RichTextEditorDialog from './components/RichTextEditorDialog.vue'
import ListEditorDialog from './components/ListEditorDialog.vue'
const heroDesc = ref<any>("")
const stats = ref<any>([]);
const overviewHtml = ref<any>("");
const features = ref<any>([]);
const timeline = ref<any>([]);
// Dialogs
const richTextDialogRef = ref<any>(null)
const listDialogRef = ref<any>(null)
const currentEditField = ref('')
const currentListColumns = ref([])
const state = reactive<any>({
loading: false,
})
const form = reactive({
id: '',
})
onMounted(() => {
getSetting()
})
const openRichTextEditor = (field: any, content: any) => {
currentEditField.value = field
richTextDialogRef.value.open(content)
}
const handleRichTextConfirm = async (content: any) => {
console.log("handleRichTextConfirm-content:", content);
if (currentEditField.value === 'heroDesc') {
heroDesc.value = content
} else if (currentEditField.value === 'overviewHtml') {
overviewHtml.value = content
}
await onSubmit()
}
const statsColumns = [
{ prop: 'value', label: '数值' },
{ prop: 'label', label: '标题' },
{ prop: 'note', label: '描述(富文本)', type: 'richtext' }
]
const featuresColumns = [
{ prop: 'index', label: '序号' },
{ prop: 'title', label: '标题' },
{ prop: 'text', label: '描述(富文本)', type: 'richtext' }
]
const timelineColumns = [
{ prop: 'year', label: '年份' },
{ prop: 'title', label: '标题' },
{ prop: 'text', label: '描述(富文本)', type: 'richtext' }
]
const openListEditor = (field: any, data: any, columns: any) => {
currentEditField.value = field
currentListColumns.value = columns
listDialogRef.value.open(data)
}
const handleListConfirm = async (data: any) => {
console.log("handleListConfirm-data:", data);
if (currentEditField.value === 'stats') {
stats.value = data
} else if (currentEditField.value === 'features') {
features.value = data
} else if (currentEditField.value === 'timeline') {
timeline.value = data
}
await onSubmit()
}
const getSetting = async () => {
state.loading = true
try {
const { data } = await ComDhSettingApi.getObj()
if (data) {
Object.assign(form, data)
if (data.campIntroJson) {
const obj = JSON.parse(data.campIntroJson)
if (obj.heroDesc) {
heroDesc.value = obj.heroDesc
}
if (obj.stats) {
stats.value = obj.stats
}
if (obj.overviewHtml) {
overviewHtml.value = obj.overviewHtml
}
if (obj.features) {
features.value = obj.features
}
if (obj.timeline) {
timeline.value = obj.timeline
}
}
}
} catch (err: any) {
console.error(err);
} finally {
state.loading = false
}
}
const onSubmit = async () => {
// 收集所有数据
const _form = {
// Hero 区域富文本描述
heroDesc: heroDesc.value,
// 顶部统计卡片数据 (数组)
stats: stats.value,
// 学校概况富文本内容
overviewHtml: overviewHtml.value,
// 办学特色列表数据 (数组)
features: features.value,
// 历史沿革时间轴数据 (数组)
timeline: timeline.value
}
console.log("_form:", _form);
state.loading = true
try {
await ComDhSettingApi.putObj({ id: form.id, campIntroJson: JSON.stringify(_form) })
useMessage().success('修改成功');
} catch (error: any) {
console.error(error);
useMessage().error(error.msg);
} finally {
state.loading = false
}
}
defineExpose({
getSetting,
onSubmit
})
</script>
<style scoped lang="scss">
.camp-intro {
position: relative;
width: 100%;
height: 100%;
padding: 0.56vw 0.62vw;
color: #d6ebff;
background:
radial-gradient(circle at top right, rgba(27, 102, 188, 0.18), transparent 26%),
radial-gradient(circle at left center, rgba(18, 68, 126, 0.16), transparent 24%),
linear-gradient(135deg, rgba(6, 18, 33, 0.96), rgba(8, 24, 44, 0.92));
overflow: auto;
box-sizing: border-box;
-ms-overflow-style: none;
scrollbar-width: none;
}
.camp-intro::-webkit-scrollbar {
display: none;
}
.hero-panel,
.panel {
position: relative;
border: 1px solid rgba(124, 198, 255, 0.13);
background: linear-gradient(to right, rgba(10, 82, 154, 0.18), rgba(10, 82, 154, 0.06), rgba(10, 16, 26, 0.84));
box-shadow: inset 0 0 22px rgba(14, 66, 124, 0.12), 0 8px 22px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(3px);
overflow: hidden;
}
.hero-panel {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(420px, 1fr);
gap: 1vw;
min-height: 11.8vw;
padding: 0.76vw 0.88vw;
border-radius: 12px;
}
.hero-panel::after,
.panel::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(124, 198, 255, 0.06), transparent);
}
.hero-copy {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.42vw;
}
.hero-tag {
width: fit-content;
padding: 0.16vw 0.48vw;
border: 1px solid rgba(124, 198, 255, 0.22);
border-radius: 999px;
font-size: 0.5vw;
letter-spacing: 0.1vw;
color: #9fd7ff;
background: rgba(124, 198, 255, 0.08);
text-shadow: 0 0 8px rgba(124, 198, 255, 0.4);
}
.hero-title {
margin: 0;
font-size: 1.42vw;
font-weight: 700;
letter-spacing: 0.05vw;
color: #f4fbff;
text-shadow: 0 0 12px rgba(124, 198, 255, 0.25);
}
.hero-desc {
margin: 0;
max-width: 39vw;
line-height: 1.62;
font-size: 0.66vw;
color: rgba(214, 235, 255, 0.82);
}
.hero-desc :deep(p) {
margin: 0;
}
.hero-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5vw;
align-content: center;
}
.stat-card {
min-height: 7.2vw;
padding: 0.62vw 0.64vw;
border-radius: 10px;
border: 1px solid rgba(124, 198, 255, 0.12);
background: linear-gradient(180deg, rgba(10, 40, 68, 0.82), rgba(7, 18, 31, 0.88));
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.24vw;
}
.stat-value {
font-size: 1.16vw;
font-weight: 700;
color: #7cc6ff;
text-shadow: 0 0 12px rgba(124, 198, 255, 0.35);
}
.stat-label {
font-size: 0.62vw;
color: #f3faff;
letter-spacing: 0.05vw;
}
.stat-note {
font-size: 0.54vw;
line-height: 1.42;
color: rgba(202, 238, 255, 0.68);
}
.stat-note :deep(p) {
margin: 0;
}
.content-grid {
margin-top: 0.62vw;
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
gap: 0.62vw;
}
.panel {
border-radius: 12px;
padding: 0.74vw 0.78vw 0.68vw;
}
.panel-title {
font-size: 0.74vw;
font-weight: 600;
color: #f5fbff;
letter-spacing: 0.06vw;
text-shadow: 0 0 10px rgba(124, 198, 255, 0.3);
}
.panel-line {
width: 100%;
height: 1px;
margin: 0.45vw 0 0.55vw;
background: linear-gradient(90deg, rgba(124, 198, 255, 0.55), rgba(10, 82, 154, 0.4), transparent);
}
.panel-paragraph :deep(p) {
margin: 0 0 0.46vw;
font-size: 0.64vw;
line-height: 1.62;
color: rgba(214, 235, 255, 0.82);
text-indent: 2em;
}
.panel-paragraph :deep(p:last-child) {
margin-bottom: 0;
}
.feature-list {
display: flex;
flex-direction: column;
gap: 0.5vw;
}
.feature-card {
display: grid;
grid-template-columns: 2.2vw 1fr;
gap: 0.58vw;
align-items: start;
padding: 0.58vw 0.62vw;
border-radius: 10px;
border: 1px solid rgba(124, 198, 255, 0.1);
background: linear-gradient(135deg, rgba(9, 31, 56, 0.86), rgba(7, 18, 31, 0.78));
}
.feature-index {
width: 2.2vw;
height: 2.2vw;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7vw;
font-weight: 700;
color: #7cc6ff;
background: radial-gradient(circle, rgba(124, 198, 255, 0.24), rgba(10, 37, 62, 0.88));
box-shadow: 0 0 12px rgba(124, 198, 255, 0.14);
}
.feature-body {
min-width: 0;
}
.feature-title {
margin-bottom: 0.2vw;
font-size: 0.68vw;
font-weight: 600;
color: #f4fbff;
}
.feature-text {
line-height: 1.5;
font-size: 0.6vw;
color: rgba(202, 238, 255, 0.72);
}
.feature-text :deep(p) {
margin: 0;
}
.timeline-panel {
margin-top: 0.62vw;
}
.timeline {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.62vw;
}
.timeline-item {
position: relative;
padding-top: 0.68vw;
}
.timeline-year {
margin-bottom: 0.4vw;
font-size: 0.72vw;
font-weight: 700;
color: #7cc6ff;
letter-spacing: 0.06vw;
}
.timeline-dot {
position: absolute;
top: 0;
left: 0.1vw;
width: 0.44vw;
height: 0.44vw;
border-radius: 50%;
background: #7cc6ff;
box-shadow: 0 0 12px rgba(124, 198, 255, 0.6);
}
.timeline-card {
min-height: 6.4vw;
padding: 0.62vw;
border-radius: 10px;
border: 1px solid rgba(124, 198, 255, 0.1);
background: linear-gradient(180deg, rgba(9, 31, 56, 0.86), rgba(7, 18, 31, 0.8));
}
.timeline-title {
margin-bottom: 0.22vw;
font-size: 0.66vw;
font-weight: 600;
color: #f4fbff;
}
.timeline-text {
line-height: 1.54;
font-size: 0.58vw;
color: rgba(202, 238, 255, 0.72);
}
.timeline-text :deep(p) {
margin: 0;
}
@media (max-width: 1440px) {
.hero-panel {
grid-template-columns: 1fr;
}
.hero-desc {
max-width: none;
}
.content-grid,
.timeline {
grid-template-columns: 1fr;
}
}
</style>
+13
View File
@@ -29,6 +29,9 @@
<div class="w-full" v-if="state.currentTab === 'DEVICE_ALARM'">
<DeviceAlarmSetting ref="deviceAlarmSettingRef" />
</div>
<div class="w-full" v-if="state.currentTab === 'CAMP_INTRO'">
<CampIntro ref="campIntro" v-if="state.currentTab === 'CAMP_INTRO'" />
</div>
<div class="h-20" />
</div>
<div class="flex justify-center">
@@ -39,6 +42,7 @@
</template>
<script setup lang="ts" name="deviceLog">
const CampIntro = defineAsyncComponent(() => import('./campIntro/index.vue'));
const CarSetting = defineAsyncComponent(() => import('./car/car-setting.vue'));
const CleaningData = defineAsyncComponent(() => import('./cleaningData/cleaning-data.vue'));
const DoorSetting = defineAsyncComponent(() => import('./door/door-setting.vue'));
@@ -82,9 +86,14 @@ const data = {
label: "设备报警配置",
name: "DEVICE_ALARM",
},
{
label: "园区介绍",
name: "CAMP_INTRO",
},
]
}
const campIntro = ref()
const doorSetting = ref()
const carSetting = ref()
const panelSetting = ref()
@@ -135,5 +144,9 @@ function onSubmit() {
deviceAlarmSettingRef.value.onSubmit()
return
}
if (state.currentTab == "CAMP_INTRO") {
campIntro.value.onSubmit()
return
}
}
</script>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="layout-padding-auto layout-padding-view">
<draggable-org-Tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
:defaultTreeWidth="15" :nodeClickable="false">
<template #default>
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<draggable-org-Tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="20"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="15"
:nodeClickable="false">
<template #default>
<el-row shadow="hover" v-show="showSearch" class="ml10">
@@ -154,6 +154,7 @@ const resetQuery = () => {
function openDialog(query: any) {
state.queryForm = query
state.queryForm.isDkErrorList = true
state.queryForm.allFindAlarmTypeList = true
visible.value = true
getDataList();
}
+7 -1
View File
@@ -16,6 +16,9 @@
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="User" size="small" type="primary" @click="doPersonClockConfig">
人员打卡配置
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="false" @exportExcel="exportExcel"
class="ml10 mr20" style="float: right;" @queryTable="getDataList"></right-toolbar>
</div>
@@ -23,7 +26,6 @@
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
@sort-change="sortChangeHandle" show-summary :summary-method="getSummaries">
<el-table-column type="selection" width="80" align="center" />
<el-table-column type="index" label="#" width="80" />
<el-table-column min-width="140" label="日期">
<template #default="{ row }">
@@ -165,6 +167,10 @@ const selectionChangHandle = (objs: { id: string }[]) => {
multiple.value = !objs.length;
};
const doPersonClockConfig = () => {
push({ path: "/door/personClockConfig/index" })
}
const getSummaries = (param: any) => {
const { columns, data } = param
const totalColumns = ["reignCount", "carReignCount", "totalReignCount", "notReignCount", "lateCount", "earlyCount"]
+327
View File
@@ -0,0 +1,327 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<draggable-dept-tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="15" :nodeClickable="false">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item style="width: 250px;" label="姓名" prop="name">
<el-input v-model="state.queryForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item style="width: 250px;" label="手机号" prop="phone">
<el-input v-model="state.queryForm.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item style="width: 250px;" label="证件号码" prop="paperNumber">
<el-input v-model="state.queryForm.paperNumber" placeholder="请输入证件号码" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">
{{ $t('common.resetBtn') }}
</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div v-auth="'admin_personClockConfig_change'" class="mb8" style="width: 100%">
<el-button icon="CircleCheck" type="primary" class="ml10" :disabled="multiple"
v-auth="'admin_sysPerson_add'" @click="handleChangeClockConfig(true)">
加入打卡
</el-button>
<el-button plain :disabled="multiple" icon="CircleClose" type="primary"
v-auth="'admin_sysPerson_del'" @click="handleChangeClockConfig(false)">
移除打卡
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'admin_sysPerson_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right;"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table ref="tableRef" :data="state.dataList" v-loading="state.loading" row-key="code" border
size="small" :cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" :reserve-selection="true" />
<el-table-column type="index" label="#" width="64" />
<el-table-column width="100" label="是否加入打卡">
<template #default="{ row }">
<el-tag :type="row.clockEnable === '1' ? 'success' : 'danger'">
{{ row.clockEnable === '1' ? '已加入' :
'未加入' }}
</el-tag>
</template>
</el-table-column>
<el-table-column width="250" label="人员信息">
<template #default="{ row }">
<div class="flex items-center gap-2 py-1">
<div class="w-[52px] shrink-0">
<preview-image width="42px" height="42px" :src="row.avatar" />
</div>
<div class="flex flex-1 flex-col justify-center gap-1 overflow-hidden pr-2">
<div class="flex items-center gap-1 overflow-hidden leading-5">
<span class="truncate font-medium">{{ row.name }}</span>
<span v-if="row.age" class="shrink-0 text-xs text-gray-500">{{ row.age
}}岁</span>
<tag-sex :code="row.sex" />
</div>
<div class="flex items-center gap-1 text-xs text-gray-500 leading-5">
<span class="shrink-0">编码</span>
<div class="flex-1 text-ellipsis text-start">{{ row.code || '--' }}</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column width="220" label="证件信息">
<template #default="{ row }">
<div
class="flex flex-col justify-start items-start gap-1 py-1 text-start text-xs leading-5">
<div class="flex w-full gap-2">
<div class="w-16 shrink-0 text-gray-500">证件类型</div>
<div class="flex-1 overflow-hidden text-ellipsis">
<dict-tag :options="dh_paper_type" :value="row.paperType" />
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-16 shrink-0 text-gray-500">证件号码</div>
<div class="flex-1 overflow-hidden text-ellipsis">{{ row.paperNumber || '--' }}
</div>
</div>
<div class="flex w-full gap-2">
<div class="w-16 shrink-0 text-gray-500">证件地址</div>
<div class="flex-1 overflow-hidden text-ellipsis">{{ row.paperAddress || '--' }}
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="deptName" width="132" label="部门名称" show-overflow-tooltip />
<el-table-column prop="phone" width="140" label="手机号码" show-overflow-tooltip />
<el-table-column prop="email" width="170" label="电子邮箱" show-overflow-tooltip />
<el-table-column prop="status" width="110" label="人员状态" show-overflow-tooltip>
<template #default="{ row }">
<tag-user-status :code="row.status" />
</template>
</el-table-column>
<el-table-column prop="isSync" width="100" label="同步">
<template #default="{ row }">
<tag-sync :isSync="row.isSync == 1" />
</template>
</el-table-column>
<el-table-column label="同步配置" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<dict-tag :options="user_sync_config" :value="row.syncConfigType" />
</template>
</el-table-column>
<el-table-column v-auth="'admin_personClockConfig_change'" label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 py-1 text-xs">
<el-button style="margin: 0; padding: 0;" icon="CircleCheck" text type="primary"
v-auth="'admin_personClockConfig_change'"
@click="handleChangeClockConfig(true, [row])">
加入打卡
</el-button>
<el-button style="margin: 0; padding: 0;" icon="CircleClose" text type="primary"
v-auth="'admin_personClockConfig_change'"
@click="handleChangeClockConfig(false, [row])">
移除打卡
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle"
v-bind="state.pagination" />
</draggable-dept-tree>
</div>
</div>
</template>
<script setup lang="ts" name="systemSysPerson">
import { BasicTableProps, useTable } from "/@/hooks/table";
import { fetchList } from '/@/api/sys/sysPerson'
import { change } from '/@/api/sys/sysPersonClockConfig'
import { useMessage, useMessageBox } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
import dayjs from "dayjs";
import other from '/@/utils/other'
// 定义查询字典
const { dh_paper_type, user_sync_config } = useDict('dh_paper_type', 'user_sync_config')
// 定义变量内容
const tableRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const draggableDepTreeRef = ref()
const selectionRowMap = ref(new Map<string, any>())
const isRestoringSelection = ref(false)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList
})
// table hook
const {
getDataList: baseGetDataList,
postDownBlobFile,
tableStyle
} = useTable(state)
const syncSelectionState = () => {
selectObjs.value = Array.from(selectionRowMap.value.values()).map((item: any) => item.id)
multiple.value = selectionRowMap.value.size === 0
}
const clearSelectionState = () => {
selectionRowMap.value.clear()
selectObjs.value = []
multiple.value = true
tableRef.value?.clearSelection()
}
const restoreSelection = async () => {
if (!tableRef.value) return
const currentPageRows = state.dataList ?? []
isRestoringSelection.value = true
tableRef.value.clearSelection()
currentPageRows.forEach((row: any) => {
if (selectionRowMap.value.has(row.code)) {
tableRef.value.toggleRowSelection(row, true)
}
})
await nextTick()
isRestoringSelection.value = false
}
const getDataList = async (refresh?: any) => {
await baseGetDataList(refresh)
await nextTick()
await restoreSelection()
}
const currentChangeHandle = async (val: number) => {
state.pagination!.current = val
await getDataList(false)
}
const sizeChangeHandle = async (val: number) => {
state.pagination!.size = val
await getDataList(false)
}
const sortChangeHandle = async (column: any) => {
const prop = other.toUnderline(column.prop)
if (column.order === 'descending') {
state.descs = [prop]
state.ascs = state.ascs?.filter((item) => item !== prop) ?? []
} else if (column.order === 'ascending') {
state.ascs = [prop]
state.descs = state.descs?.filter((item) => item !== prop) ?? []
} else {
state.ascs = state.ascs?.filter((item) => item !== prop) ?? []
state.descs = state.descs?.filter((item) => item !== prop) ?? []
}
await getDataList(false)
}
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
// 清空多选
clearSelectionState()
// 清空树状选择
state.queryForm.deptId = ""
state.queryForm.deptCodes = []
draggableDepTreeRef.value.resetData()
getDataList()
}
// 导出excel
const exportExcel = () => {
const timeSuffix = dayjs().format('YYYY-MM-DD_HH-mm-ss')
postDownBlobFile('/admin/sysPerson/export', Object.assign(state.queryForm, { ids: selectObjs }), `人员信息管理_${timeSuffix}.xlsx`)
}
// 多选事件
const selectionChangHandle = (rows: any[]) => {
if (isRestoringSelection.value) return
const currentPageRows = state.dataList ?? []
const currentPageCodeSet = new Set(currentPageRows.map((item: any) => item.code))
const selectedCodeSet = new Set(rows.map((item: any) => item.code))
currentPageCodeSet.forEach((code) => {
if (!selectedCodeSet.has(code)) {
selectionRowMap.value.delete(code)
}
})
rows.forEach((row: any) => {
if (row.code) {
selectionRowMap.value.set(row.code, row)
}
})
syncSelectionState()
};
// 树节点选择时
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: any) => {
state.queryForm.deptId = curClickedNode.id
state.queryForm.deptCodes = checkedTreeNodes.map((item: any) => item.id)
state.queryForm.isInclude = !isDeepTree
getDataList()
}
const handleChangeClockConfig = async (enable: boolean, rows?: any[]) => {
const selectedRows = rows?.length ? rows : Array.from(selectionRowMap.value.values())
const userCodeList = selectedRows
.map((item: any) => item.code)
.filter((item: string) => !!item)
if (!userCodeList.length) {
useMessage().error('请先选择需要操作的人员')
return
}
const actionText = enable ? '加入打卡' : '移除打卡'
const userNameListText = selectedRows
.map((item: any) => item.name || item.code)
.filter((item: string) => !!item)
.join('、')
try {
await useMessageBox().confirm(`是否确认${actionText}${userCodeList.length}名人员?\n${userNameListText}`);
} catch {
return
}
try {
await change({ userCodeList, enable: enable ? '1' : '0' })
if (!rows?.length) {
clearSelectionState()
}
await getDataList(false)
useMessage().success(`${actionText}成功`)
} catch (err: any) {
useMessage().error(err.msg)
}
}
</script>
<style lang="scss" scoped>
.text-ellipsis {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
+1 -1
View File
@@ -117,7 +117,7 @@ const getThemeConfig = computed(() => {
*/
const jumpAdminPanel = () => {
const isDev = import.meta.env.MODE == "development"
const baseAdminUrl = isDev ? 'http://localhost:5173/device' : 'http://123.157.250.162:7001/device';
const baseAdminUrl = isDev ? 'http://localhost:5173/device' : 'http://192.168.50.10:7001/device';
const roleCodes = useUserInfoStore.userInfos?.roleCodes ?? []
if (!roleCodes || !roleCodes.length) {
openPlanUrl(baseAdminUrl, true)
+40 -40
View File
@@ -2,7 +2,7 @@
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<draggable-dept-tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="20"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="15"
:nodeClickable="false">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
@@ -39,83 +39,83 @@
class="ml10 mr20" style="float: right;" @queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle" @selection-change="selectionChangHandle"
@sort-change="sortChangeHandle">
<el-table :data="state.dataList" v-loading="state.loading" border size="small"
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle" @sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="80" />
<el-table-column width="280" label="人员信息">
<el-table-column type="index" label="#" width="64" />
<el-table-column width="250" label="人员信息">
<template #default="{ row }">
<div class="flex items-center gap-2">
<div class="w-[60px]">
<preview-image width="50px" height="50px" :src="row.avatar" />
<div class="flex items-center gap-2 py-1">
<div class="w-[52px] shrink-0">
<preview-image width="42px" height="42px" :src="row.avatar" />
</div>
<div class="flex-1 flex-col justify-center pr-2">
<div class="flex items-center gap-2">
<span> {{ row.name }}</span>
<span v-if="row.age"> {{ row.age }}岁 </span>
<div class="flex flex-1 flex-col justify-center gap-1 overflow-hidden pr-2">
<div class="flex items-center gap-1 overflow-hidden leading-5">
<span class="truncate font-medium">{{ row.name }}</span>
<span v-if="row.age" class="shrink-0 text-xs text-gray-500">{{ row.age }}岁</span>
<tag-sex :code="row.sex" />
</div>
<div class="h-2" />
<div class="flex items-center gap-2">
<div class="flex-1 text-ellipsis text-start"> {{ row.code }}</div>
<div class="flex items-center gap-1 text-xs text-gray-500 leading-5">
<span class="shrink-0">编码</span>
<div class="flex-1 text-ellipsis text-start">{{ row.code || '--' }}</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column width="250" label="人员编码">
<el-table-column width="220" label="证件信息">
<template #default="{ row }">
<div class="flex-col justify-start items-start text-start">
<div class="flex gap-2">
<div class="w-20">证件类型:</div>
<div class="flex flex-col justify-start items-start gap-1 py-1 text-start text-xs leading-5">
<div class="flex w-full gap-2">
<div class="w-16 shrink-0 text-gray-500">证件类型</div>
<div class="flex-1 overflow-hidden text-ellipsis">
<dict-tag :options="dh_paper_type" :value="row.paperType" />
</div>
</div>
<div class="flex gap-2">
<div class="w-20">证件号码:</div>
<div class="flex-1 overflow-hidden text-ellipsis">{{ row.paperNumber }}</div>
<div class="flex w-full gap-2">
<div class="w-16 shrink-0 text-gray-500">证件号码</div>
<div class="flex-1 overflow-hidden text-ellipsis">{{ row.paperNumber || '--' }}</div>
</div>
<div class="flex gap-2">
<div class="w-20">证件地址:</div>
<div class="flex-1 overflow-hidden text-ellipsis">{{ row.paperAddress }}</div>
<div class="flex w-full gap-2">
<div class="w-16 shrink-0 text-gray-500">证件地址</div>
<div class="flex-1 overflow-hidden text-ellipsis">{{ row.paperAddress || '--' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="deptName" width="150" label="部门名称" show-overflow-tooltip />
<el-table-column prop="phone" width="160" label="手机号码" show-overflow-tooltip />
<el-table-column prop="email" width="150" label="电子邮箱" show-overflow-tooltip />
<el-table-column prop="status" width="150" label="人员状态" show-overflow-tooltip>
<el-table-column prop="deptName" width="132" label="部门名称" show-overflow-tooltip />
<el-table-column prop="phone" width="140" label="手机号码" show-overflow-tooltip />
<el-table-column prop="email" width="170" label="电子邮箱" show-overflow-tooltip />
<el-table-column prop="status" width="110" label="人员状态" show-overflow-tooltip>
<template #default="{ row }">
<tag-user-status :code="row.status" />
</template>
</el-table-column>
<el-table-column prop="isSync" width="150" label="是否为同步">
<el-table-column prop="isSync" width="100" label="同步">
<template #default="{ row }">
<tag-sync :isSync="row.isSync == 1" />
</template>
</el-table-column>
<el-table-column label="数据同步配置" min-width="150">
<el-table-column label="同步配置" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<dict-tag :options="user_sync_config" :value="row.syncConfigType" />
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right"
<el-table-column label="操作" width="170" fixed="right"
v-auths="['admin_sysPerson_edit', 'admin_sysPerson_del']">
<template #default="{ row }">
<div class="grid grid-cols-2 gap-2 w-full">
<el-button style="margin: 0;" icon="Reading" text type="primary" v-auth="'admin_sysPerson_edit'"
@click="syncConfigFormRef.openDialog(row.id)">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 py-1 text-xs">
<el-button style="margin: 0; padding: 0;" icon="Reading" text type="primary"
v-auth="'admin_sysPerson_edit'" @click="syncConfigFormRef.openDialog(row.id)">
同步设置
</el-button>
<el-button style="margin: 0;" icon="edit-pen" text type="primary" v-auth="'admin_sysPerson_edit'"
@click="formDialogRef.openDialog(row.id)">
<el-button style="margin: 0; padding: 0;" icon="edit-pen" text type="primary"
v-auth="'admin_sysPerson_edit'" @click="formDialogRef.openDialog(row.id)">
编辑
</el-button>
<el-button style="margin: 0;" icon="delete" text type="primary" v-auth="'admin_sysPerson_del'"
@click="handleDelete([row.id])">
<el-button style="margin: 0; padding: 0;" icon="delete" text type="primary"
v-auth="'admin_sysPerson_del'" @click="handleDelete([row.id])">
删除
</el-button>
</div>