Compare commits

...

2 Commits

Author SHA1 Message Date
fyy
d03743f996 Merge branch 'master' of http://47.109.37.87:3000/by2025/admin-vben5
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
2025-07-23 14:27:36 +08:00
fyy
fefce85997 feat: 完成考勤排版管理页面交互 2025-07-23 14:26:56 +08:00
6 changed files with 378 additions and 254 deletions

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import {ref, shallowRef} from 'vue';
import {useVbenModal} from '@vben/common-ui';
import { ref, shallowRef } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import {
Button,
Checkbox,
@ -11,16 +11,16 @@ import {
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import {renderDict} from "#/utils/render";
import {groupInfo} from "#/api/property/attendanceManagement/attendanceGroupSettings";
import type {GroupVO} from "#/api/property/attendanceManagement/attendanceGroupSettings/model";
import { renderDict } from '#/utils/render';
import { groupInfo } from '#/api/property/attendanceManagement/attendanceGroupSettings';
import type { GroupVO } from '#/api/property/attendanceManagement/attendanceGroupSettings/model';
import {
infoCycleColumns,
infoClockingColumns,
infoNoClockingColumns,
infoWeekdayColumns
} from "#/views/property/attendanceManagement/attendanceGroupSettings/data";
import holidayCalendar from './holiday-calendar.vue'
infoWeekdayColumns,
} from '#/views/property/attendanceManagement/attendanceGroupSettings/data';
import holidayCalendar from './holiday-calendar.vue';
dayjs.extend(duration);
dayjs.extend(relativeTime);
@ -34,17 +34,17 @@ const [BasicModal, modalApi] = useVbenModal({
const groupDetail = shallowRef<null | GroupVO>(null);
const weekdayData = ref<any[]>([])
const cycleData = ref<any[]>([])
const unCheckInData = ref<any[]>([])
const checkInData = ref<any[]>([])
const weekdayData = ref<any[]>([]);
const cycleData = ref<any[]>([]);
const unCheckInData = ref<any[]>([]);
const checkInData = ref<any[]>([]);
async function handleOpenChange(open: boolean) {
if (!open) {
return null;
}
modalApi.modalLoading(true);
const {id} = modalApi.getData() as { id: number | string };
const { id } = modalApi.getData() as { id: number | string };
groupDetail.value = await groupInfo(id);
modalApi.modalLoading(false);
}
@ -57,74 +57,122 @@ const [HolidayCalendar, holidayApi] = useVbenModal({
* 查看法定节假日日历
*/
async function showHoliday() {
holidayApi.open()
holidayApi.open();
}
</script>
<template>
<BasicModal :footer="false" :fullscreen-button="false" title="考勤组信息" class="w-[70%]">
<BasicModal
:footer="false"
:fullscreen-button="false"
title="考勤组信息"
class="w-[70%]"
>
<div v-if="groupDetail" class="des-container">
<Descriptions size="small" :column="1" :labelStyle="{width:'100px'}">
<Descriptions size="small" :column="1" :labelStyle="{ width: '100px' }">
<DescriptionsItem label="考勤组名称">
{{ groupDetail.groupName }}
</DescriptionsItem>
<DescriptionsItem label="考勤类型">
<component
:is="groupDetail.attendanceType ? renderDict(groupDetail.attendanceType,'wy_kqlx') : ''"
:is="
groupDetail.attendanceType
? renderDict(groupDetail.attendanceType, 'wy_kqlx')
: ''
"
/>
</DescriptionsItem>
<DescriptionsItem label="状态">
<component
:is="renderDict(groupDetail.status,'wy_state')"
/>
<component :is="renderDict(groupDetail.status, 'wy_state')" />
</DescriptionsItem>
<DescriptionsItem label="工作日设置" v-if="groupDetail.attendanceType==0">
<div class="item-font" style="width: 100%;">
<Table style="width: 90%" bordered :columns="infoWeekdayColumns"
:data-source="weekdayData"
size="small" :pagination="false">
<DescriptionsItem
label="工作日设置"
v-if="groupDetail.attendanceType == 0"
>
<div class="item-font" style="width: 100%">
<Table
style="width: 90%"
bordered
:columns="infoWeekdayColumns"
:data-source="weekdayData"
size="small"
:pagination="false"
>
</Table>
<Checkbox class="item-padding-top" v-model:checked="groupDetail.isAutomatic">
<Checkbox
class="item-padding-top"
v-model:checked="groupDetail.isAutomatic"
>
法定节假日自动排休
</Checkbox>
<Button type="link" @click="showHoliday">查看法定节假日日历</Button>
<p class="item-padding-top item-font-weight">特殊日期</p>
<p class="item-padding">无需打卡日期</p>
<Table style="width: 75%" bordered :columns="infoNoClockingColumns"
:data-source="unCheckInData"
size="small" :pagination="false">
<template #bodyCell="{ column,record,index }">
<template v-if="column.dataIndex==='dateTime'">
<span v-if="record.dateType==0">{{ record.startDate }}</span>
<span v-else>{{ record.startDate + '~' + record.endDate }}</span>
<Table
style="width: 75%"
bordered
:columns="infoNoClockingColumns"
:data-source="unCheckInData"
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'dateTime'">
<span v-if="record.dateType == 0">{{
record.startDate
}}</span>
<span v-else>{{
record.startDate + '~' + record.endDate
}}</span>
</template>
</template>
</Table>
<p class="item-padding">必须打卡日期</p>
<Table style="width: 75%" bordered :columns="infoClockingColumns"
:data-source="checkInData"
size="small" :pagination="false">
<template #bodyCell="{ column,record,index }">
<template v-if="column.dataIndex==='dateTime'">
<span v-if="record.dateType==0">{{ record.startDate }}</span>
<span v-else>{{ record.startDate + '~' + record.endDate }}</span>
<Table
style="width: 75%"
bordered
:columns="infoClockingColumns"
:data-source="checkInData"
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'dateTime'">
<span v-if="record.dateType == 0">{{
record.startDate
}}</span>
<span v-else>{{
record.startDate + '~' + record.endDate
}}</span>
</template>
</template>
</Table>
</div>
</DescriptionsItem>
<DescriptionsItem label="排班周期" v-if="groupDetail.attendanceType==1">
<p class="item-font">周期天数
<span class="item-font-weight item-font-color">{{ cycleData.length }}</span>
</p>
<Table style="width: 80%;margin-top: 5px" bordered :columns="infoCycleColumns" :data-source="cycleData"
size="small" :pagination="false">
<template #bodyCell="{ column,record,index }">
<template v-if="column.dataIndex==='day'">
<DescriptionsItem
label="排班周期"
v-if="groupDetail.attendanceType == 1"
>
<p class="item-font">
周期天数
<span class="item-font-weight item-font-color">{{
cycleData.length
}}</span>
</p>
<Table
style="width: 80%; margin-top: 5px"
bordered
:columns="infoCycleColumns"
:data-source="cycleData"
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'day'">
<span>{{ '第' + (index + 1) + '天' }}</span>
</template>
<template v-if="column.dataIndex==='shiftId'">
<template v-if="column.dataIndex === 'shiftId'">
<!-- {{item.name+'\xa0'}}-->
<!-- <span v-if="item.isRest">{{item.startTime+'~'+item.restStartTime+'\xa0'+item.restEndTime+'~'+item.endTime}}</span>-->
<!-- <span v-else>{{item.startTime+'~'+item.endTime}}</span>-->
@ -159,7 +207,11 @@ async function showHoliday() {
padding: 1.1rem 0 0.5rem 0;
}
:deep(.ant-descriptions .ant-descriptions-item-container .ant-descriptions-item-content) {
:deep(
.ant-descriptions
.ant-descriptions-item-container
.ant-descriptions-item-content
) {
display: block;
}
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, reactive,onMounted } from 'vue';
import { computed, reactive,onMounted } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
@ -17,6 +17,9 @@ import unitPersonModal from './unit-person-modal.vue';
import {groupList} from '#/api/property/attendanceManagement/attendanceGroupSettings'
import {getDictOptions} from "#/utils/dict";
import dayjs from 'dayjs';
import type { PersonVO } from './type';
import { ref, h } from 'vue';
import { Tag,Table } from 'ant-design-vue';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
@ -24,7 +27,77 @@ const title = computed(() => {
});
const groupOptions = ref<any[]>([]);
const groupMap = ref<Record<string, any>>({}); //
const tableData = reactive<{dept: {unitId: string | number,unitName: string}, users: PersonVO[]}[]>([]);
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
customRender: ({ index }: { index: number }) => index + 1,
},
{
title: '部门',
dataIndex: ['dept', 'unitName'],
key: 'dept',
width: 120,
customRender: ({ record }: { record: any }) => record.dept.unitName,
},
{
title: '已选人数',
dataIndex: 'users',
key: 'userCount',
width: 100,
customRender: ({ record }: { record: any }) => record.users.length,
},
{
title: '人员',
dataIndex: 'users',
key: 'users',
customRender: ({ record }: { record: any }) =>
record.users.map((user: any) =>
h(
Tag,
{
color: 'blue',
closable: true,
onClose: (e) => {
e.preventDefault();
handleRemoveUser(record, user);
},
style: 'margin-right: 4px;',
},
() => user.userName
)
),
},
{
title: '操作',
key: 'action',
width: 80,
customRender: ({ record, index }: { record: any, index: number }) =>
h(
'a',
{
style: 'color: #ff4d4f; font-size: 18px; margin-left: 8px; cursor: pointer;',
onClick: () => handleRemoveRow(index),
},
'移除'
),
},
];
function handleRemoveUser(row: any, user: any) {
row.users = row.users.filter((u: any) => u.id !== user.id);
}
function handleRemoveRow(index: number) {
tableData.splice(index, 1);
}
function handleTableData(newTableData: any) {
tableData.splice(0, tableData.length, ...newTableData);
}
//
const formModel = reactive<{
id: string;
@ -111,68 +184,7 @@ const [BasicModal, modalApi] = useVbenModal({
modalApi.modalLoading(false);
},
});
// mock
const tableData = ref([
{ dept: 'xx部门', users: ['张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三'] },
{ dept: 'xx部门', users: ['张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三'] },
{ dept: '', users: [] },
]);
const staticData = [
{ id: 1, dept: 'xx部门', name: '张三' },
{ id: 2, dept: 'yy部门', name: '李四' },
];
const totalSelected = computed(() => tableData.value.reduce((sum, row) => sum + row.users.length, 0));
const gridOptions: VxeGridProps = {
checkboxConfig: {
//
highlight: true,
//
reserve: true,
//
// trigger: 'row',
},
// 使i18ngetter
columns: attendanceColumns,
// columns,
// height: 'auto',
keepSource: true,
data:staticData,
pagerConfig: {},
proxyConfig: {
// ajax: {
// query: async ({ page }, formValues = {}) => {
// return await arrangementList({
// pageNum: page.currentPage,
// pageSize: page.pageSize,
// ...formValues,
// });
// },
// },
},
rowConfig: {
keyField: 'id',
},
//
id: 'property-arrangement-index',
toolbarConfig: {
// "/" redo
refresh: false,
zoom: false, //
custom: false, //
},
};
const [BasicTable, tableApi] = useVbenVxeGrid({
gridOptions,
});
async function handleEdit(row: any){
modalApi.setData({ id: row.id });
modalApi.open();
}
async function handleDelete(row: any) {
console.log(12);
}
const totalSelected:number = 0;
function handleAdd() {
unitPersonmodalApi.setData({});
@ -303,27 +315,15 @@ onMounted(() => {
<span>考勤组人员已选 <span class="text-red-500">{{ totalSelected }}</span> </span>
<a-button type="primary" size="small" class="ml-4" @click="handleAdd">+ 添加人员</a-button>
</div>
<BasicTable>
<template #action="{ row }">
<Space>
<Popconfirm
placement="left"
title="确认移除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['property:arrangement:remove']"
@click.stop=""
>
移除
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
<Table
:columns="columns"
:dataSource="tableData"
:pagination="false"
rowKey="dept.unitId"
bordered
/>
</div>
<UnitPersonModal />
<UnitPersonModal @reload="handleTableData"/>
<!-- 底部按钮区 -->
</BasicModal>
</template>

View File

@ -5,7 +5,7 @@ import {ref} from 'vue';
import { Dayjs } from 'dayjs';
import arrangementModal from './arrangement-modal.vue';
import {useVbenModal} from '@vben/common-ui';
import type { PersonVO } from './type';
const emit = defineEmits<{
(e: 'changeView',value:boolean):void
}>();
@ -77,6 +77,7 @@ function handleAdd() {
modalApi.setData({});
modalApi.open();
}
</script>
<template>
<div class="h-full flex flex-col">

View File

@ -174,17 +174,17 @@ export const unitColumns: VxeGridProps['columns'] = [
},
{
title: '姓名',
field: 'scheduleName',
field: 'userName',
minWidth:120
},
{
title: '所属单位',
field: 'scheduleName',
field: 'unitName',
width:'auto',
},
{
title: '电话',
field: 'groupId',
field: 'phone',
minWidth:120
}
];

View File

@ -0,0 +1,68 @@
//列表数据,用于展示人员列表
export interface PersonVO {
/**
* id
*/
id: string | number;
/**
* id
*/
userId: string | number;
/**
*
*/
userName: string;
/**
*
*/
phone: string;
/**
*
*/
gender: number;
/**
*
*/
img: string;
/**
* id
*/
unitId: string | number;
/**
*
*/
unitName: string;
/**
*
*/
locathon: string;
/**
*
*/
time: string;
/**
*
*/
carNumber: string;
/**
*
*/
state: number|string;
/**
*
*/
remark: string;
}

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, reactive, onMounted } from 'vue';
import { computed, ref, reactive, onMounted, watch } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { Tree, InputSearch, Skeleton, Empty, Button } from 'ant-design-vue';
import { Tree, InputSearch, Skeleton, Empty, Button,Tag } from 'ant-design-vue';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
@ -16,7 +16,9 @@ import {
import { modalSchema,unitColumns,unitQuerySchema } from './data';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { resident_unitList } from '#/api/property/resident/unit';
const emit = defineEmits<{ reload: [] }>();
import { personList } from '#/api/property/resident/person';
import type { PersonVO } from './type';
const emit = defineEmits<{ reload: [tableData: {dept: {unitId: string | number,unitName: string}, users: PersonVO[]}[]] }>();
const selectDeptId = ref<string[]>([]);
const isUpdate = ref(false);
const title = computed(() => {
@ -27,28 +29,29 @@ const searchValue = ref('');
const selectedDeptIds = ref<string[]>([]);
const showTreeSkeleton = ref(false);
//
const deptTree = reactive([
{
id: '',
label: '全部单位',
children: [
{ id: '1-3', label: '人事部' },
],
},
const deptTree = reactive<{
id: string;
label: string;
children: {
id: string;
label: string;
}[];
}[]>([
{
id: '',
label: '全部单位',
children: [
],
},
]);
//
const selectedUsers = ref<PersonVO[]>([]);
//
const tableData = ref([
{ dept: 'xx部门', users: ['张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三'] },
{ dept: 'xx部门', users: ['张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三', '张三'] },
{ dept: '', users: [] },
]);
//
const staticData = [
{ id: 1, dept: 'xx部门', name: '张三' },
{ id: 2, dept: 'yy部门', name: '李四' },
];
const tableData = reactive<{
dept: {unitId: string | number,unitName: string},
users: PersonVO[]}[]>([]);
function handleReload() {
showTreeSkeleton.value = true;
setTimeout(() => {
@ -57,7 +60,7 @@ function handleReload() {
}
const filteredDeptTree = computed(() => {
if (!searchValue.value) return deptTree.value;
if (!searchValue.value) return deptTree;
//
function filter(tree: any[]): any[] {
return tree
@ -71,27 +74,9 @@ const filteredDeptTree = computed(() => {
})
.filter(Boolean);
}
return filter(deptTree.value);
return filter(deptTree);
});
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: unitQuerySchema(),
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
// RangePicker /
//
// fieldMappingTime: [
// [
// 'createTime',
// ['params[beginTime]', 'params[endTime]'],
// ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
// ],
// ],
};
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
//
@ -127,72 +112,35 @@ const [BasicModal, modalApi] = useVbenModal({
return null;
}
modalApi.modalLoading(true);
console.log(12)
const { id } = modalApi.getData() as { id?: number | string };
console.log(2)
isUpdate.value = !!id;
if (isUpdate.value && id) {
console.log(3)
const record = await arrangementInfo(id);
await formApi.setValues(record);
}
console.log(4)
// await markInitialized();
console.log(5)
modalApi.modalLoading(false);
console.log(6)
},
});
async function handleConfirm() {
try {
modalApi.lock(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// getValuesreadonly
const data = cloneDeep(await formApi.getValues());
await (isUpdate.value ? arrangementUpdate(data) : arrangementAdd(data));
resetInitialized();
emit('reload');
modalApi.close();
} catch (error) {
console.error(error);
} finally {
modalApi.lock(false);
}
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
const formModel = reactive({
group: '',
type: '',
dateType: 'long',
dateSingle: null,
dateLong: null,
dateRange: [null, null],
startDate:'',
endDate:''
});
const totalSelected = computed(() => tableData.value.reduce((sum, row) => sum + row.users.length, 0));
function addRow() {
tableData.value.push({ dept: '', users: [] });
}
const formOptions: VbenFormProps = {
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
schema: unitQuerySchema(),
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
// RangePicker /
//
// fieldMappingTime: [
// [
// 'createTime',
// ['params[beginTime]', 'params[endTime]'],
// ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
// ],
// ],
};
const gridOptions: VxeGridProps = {
checkboxConfig: {
//
@ -207,18 +155,18 @@ const gridOptions: VxeGridProps = {
// columns,
// height: 'auto',
keepSource: true,
data:staticData,
// data:selectedUsers,
pagerConfig: {},
proxyConfig: {
// ajax: {
// query: async ({ page }, formValues = {}) => {
// return await arrangementList({
// pageNum: page.currentPage,
// pageSize: page.pageSize,
// ...formValues,
// });
// },
// },
ajax: {
query: async ({ page }) => {
return await personList({
pageNum: page.currentPage,
pageSize: page.pageSize,
unitId: selectedDeptIds.value[0],
});
},
},
},
rowConfig: {
keyField: 'id',
@ -232,21 +180,73 @@ const gridOptions: VxeGridProps = {
custom: false, //
},
};
const gridEvents = {
checkboxChange:async (params:any) => {
const currentRecords = await params.$grid.getCheckboxRecords()??[];
const reserveRecords = await params.$grid.getCheckboxReserveRecords()??[];
selectedUsers.value = [...reserveRecords, ...currentRecords];
},
checkboxAll:async (params:any) => {
const currentRecords = await params.$grid.getCheckboxRecords()??[];
const reserveRecords = await params.$grid.getCheckboxReserveRecords()??[];
selectedUsers.value = [...reserveRecords, ...currentRecords];
},
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
gridEvents
});
async function getUnitList(){
const res = await resident_unitList();
for(const item of res.rows){
deptTree.
async function handleConfirm() {
try {
for(const user of selectedUsers.value){
if(tableData.find(item=>item.dept.unitId === user.unitId)){
tableData.find(item=>item.dept.unitId===user.unitId)!.users.push(user);
}else{
tableData.push({ dept:{unitId:user.unitId,unitName:user.unitName}, users: [user] });
}
}
console.log(tableData);
resetInitialized();
emit('reload',tableData);
modalApi.close();
} catch (error) {
console.error(error);
} finally {
modalApi.lock(false);
}
console.log(res);
}
async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
async function getUnitList(){
deptTree[0]!.children = [];
const res = await resident_unitList();
if (deptTree[0] && Array.isArray(deptTree[0].children)) {
for(const item of res.rows){
deptTree[0].children.push({
id: item.id as string,
label: item.name as string,
});
}
}
}
//
async function fetchDeptUsers() {
tableApi.query();
}
watch(selectedDeptIds, (newVal) => {
if (newVal && newVal.length > 0) {
fetchDeptUsers();
}
});
onMounted(() => {
getUnitList();
console.log(tableData.value);
});
</script>
@ -263,7 +263,7 @@ onMounted(() => {
style="flex:1;"
>
<template #enterButton>
<Button type="text" @click="handleReload">
<Button type="text" @click="getUnitList">
<SyncOutlined class="text-primary" />
</Button>
</template>
@ -297,6 +297,9 @@ onMounted(() => {
<div class="flex-1">
<div class="flex mb-2 overflow-auto" style="max-height: 80px;font-size: 14px;">
已选人员
<span v-for="user in selectedUsers" :key="user.id" style="margin-right: 8px;">
<Tag>{{ user.userName }}</Tag>
</span>
</div>
<BasicTable>
</BasicTable>