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

This commit is contained in:
FLL
2025-07-23 15:43:32 +08:00
8 changed files with 507 additions and 388 deletions

View File

@@ -1,25 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive,onMounted } from 'vue'; import { computed, reactive, onMounted } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { arrangementAdd, arrangementInfo, arrangementUpdate } from '#/api/property/attendanceManagement/arrangement';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { import {
useVbenVxeGrid, arrangementAdd,
type VxeGridProps arrangementInfo,
} from '#/adapter/vxe-table'; arrangementUpdate,
import { modalSchema,attendanceColumns } from './data'; } from '#/api/property/attendanceManagement/arrangement';
import { Radio,DatePicker,Form,Select,FormItem,RadioGroup, message } from 'ant-design-vue'; import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { modalSchema, attendanceColumns } from './data';
import {
Radio,
DatePicker,
Form,
Select,
FormItem,
RadioGroup,
message,
} from 'ant-design-vue';
import unitPersonModal from './unit-person-modal.vue'; import unitPersonModal from './unit-person-modal.vue';
import {groupList} from '#/api/property/attendanceManagement/attendanceGroupSettings' import { groupList } from '#/api/property/attendanceManagement/attendanceGroupSettings';
import {getDictOptions} from "#/utils/dict"; import { getDictOptions } from '#/utils/dict';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { PersonVO } from './type'; import type { PersonVO } from './type';
import { ref, h } from 'vue'; import { ref, h } from 'vue';
import { Tag,Table } from 'ant-design-vue'; import { Tag, Table } from 'ant-design-vue';
const emit = defineEmits<{ reload: [] }>(); const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false); const isUpdate = ref(false);
const title = computed(() => { const title = computed(() => {
@@ -27,7 +36,9 @@ const title = computed(() => {
}); });
const groupOptions = ref<any[]>([]); const groupOptions = ref<any[]>([]);
const groupMap = ref<Record<string, any>>({}); // 用于快速查找 const groupMap = ref<Record<string, any>>({}); // 用于快速查找
const tableData = reactive<{dept: {unitId: string | number,unitName: string}, users: PersonVO[]}[]>([]); const tableData = reactive<
{ dept: { unitId: string | number; unitName: string }; users: PersonVO[] }[]
>([]);
const columns = [ const columns = [
{ {
@@ -68,22 +79,23 @@ const columns = [
}, },
style: 'margin-right: 4px;', style: 'margin-right: 4px;',
}, },
() => user.userName () => user.userName,
) ),
), ),
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 80, width: 80,
customRender: ({ record, index }: { record: any, index: number }) => customRender: ({ record, index }: { record: any; index: number }) =>
h( h(
'a', 'a',
{ {
style: 'color: #ff4d4f; font-size: 18px; margin-left: 8px; cursor: pointer;', style:
'color: #ff4d4f; font-size: 18px; margin-left: 8px; cursor: pointer;',
onClick: () => handleRemoveRow(index), onClick: () => handleRemoveRow(index),
}, },
'移除' '移除',
), ),
}, },
]; ];
@@ -110,20 +122,20 @@ const formModel = reactive<{
}>({ }>({
id: '', id: '',
groupId: '', groupId: '',
attendanceType:'',//考勤组类型 attendanceType: '', //考勤组类型
dateType: undefined,//日期类型1-单个日期2-长期有效3-期间有效 dateType: undefined, //日期类型1-单个日期2-长期有效3-期间有效
// dateSingle: null, // dateSingle: null,
// dateLong: null, // dateLong: null,
// dateRange: [null, null], // dateRange: [null, null],
startDate:'',//开始日期 startDate: '', //开始日期
endDate:'',//结束日期 endDate: '', //结束日期
userGroupList:[ userGroupList: [
// scheduleId:undefined,//排班ID // scheduleId:undefined,//排班ID
// employeeId:undefined,//员工ID // employeeId:undefined,//员工ID
// employeeName:undefined,//员工姓名 // employeeName:undefined,//员工姓名
// deptId:undefined,//部门ID // deptId:undefined,//部门ID
// deptName:undefined,//部门名称 // deptName:undefined,//部门名称
]//考勤组人员列表 ], //考勤组人员列表
}); });
const singleDate = ref(''); const singleDate = ref('');
const longDate = ref(''); const longDate = ref('');
@@ -143,7 +155,7 @@ const [BasicForm, formApi] = useVbenForm({
// 通用配置项 会影响到所有表单项 // 通用配置项 会影响到所有表单项
componentProps: { componentProps: {
class: 'w-full', class: 'w-full',
} },
}, },
schema: modalSchema(), schema: modalSchema(),
showDefaultActions: false, showDefaultActions: false,
@@ -184,37 +196,37 @@ const [BasicModal, modalApi] = useVbenModal({
modalApi.modalLoading(false); modalApi.modalLoading(false);
}, },
}); });
const totalSelected:number = 0; const totalSelected: number = 0;
function handleAdd() { function handleAdd() {
unitPersonmodalApi.setData({}); unitPersonmodalApi.setData({});
unitPersonmodalApi.open(); unitPersonmodalApi.open();
} }
async function getGroupList(){ async function getGroupList() {
const res = await groupList({ const res = await groupList({
pageSize:1000000000, pageSize: 1000000000,
pageNum:1, pageNum: 1,
status:1//0:停用 1:启用 status: 1, //0:停用 1:启用
}); });
groupOptions.value = (res.rows || []).map(item=>({ groupOptions.value = (res.rows || []).map((item) => ({
label:item.groupName, label: item.groupName,
value:item.id value: item.id,
})); }));
// 构建 id 到 group 对象的映射 // 构建 id 到 group 对象的映射
groupMap.value = {}; groupMap.value = {};
(res.rows || []).forEach(item => { (res.rows || []).forEach((item) => {
groupMap.value[item.id] = item; groupMap.value[item.id] = item;
}); });
} }
function chooseGroup(value:any){ function chooseGroup(value: any) {
console.log(value); console.log(value);
const group = groupMap.value[value]; const group = groupMap.value[value];
if(group){ if (group) {
formModel.attendanceType = String(group.attendanceType); formModel.attendanceType = String(group.attendanceType);
} }
} }
async function handleDateTypeChange(value:any){ async function handleDateTypeChange(value: any) {
formModel.dateType = value; formModel.dateType = value;
} }
async function handleConfirm() { async function handleConfirm() {
@@ -222,36 +234,35 @@ async function handleConfirm() {
modalApi.lock(true); modalApi.lock(true);
// await formRef.value.validate(); // 先校验 // await formRef.value.validate(); // 先校验
const data = formModel; const data = formModel;
const { id } = modalApi.getData() as { id?:string }; const { id } = modalApi.getData() as { id?: string };
data.id = id? id:''; data.id = id ? id : '';
console.log(data); console.log(data);
if(data.dateType == 1){ if (data.dateType == 1) {
if(singleDate.value){ if (singleDate.value) {
data.startDate = dayjs(singleDate.value).format('YYYY-MM-DD'); data.startDate = dayjs(singleDate.value).format('YYYY-MM-DD');
}else{ } else {
message.error('请选择单个日期') message.error('请选择单个日期');
return; return;
} }
}else if(data.dateType == 2){ } else if (data.dateType == 2) {
if(longDate.value){ if (longDate.value) {
data.startDate = dayjs(longDate.value).format('YYYY-MM-DD'); data.startDate = dayjs(longDate.value).format('YYYY-MM-DD');
}else{ } else {
message.error('请选择起始日期') message.error('请选择起始日期');
return; return;
} }
}else if(data.dateType == 3){ } else if (data.dateType == 3) {
if(!rangeDate.value[0]){ if (!rangeDate.value[0]) {
message.error('请选择开始日期') message.error('请选择开始日期');
return; return;
}else if(!rangeDate.value[1]){ } else if (!rangeDate.value[1]) {
message.error('请选择结束日期') message.error('请选择结束日期');
return; return;
}else{ } else {
data.startDate = dayjs(rangeDate.value[0]).format('YYYY-MM-DD'); data.startDate = dayjs(rangeDate.value[0]).format('YYYY-MM-DD');
data.endDate = dayjs(rangeDate.value[1]).format('YYYY-MM-DD'); data.endDate = dayjs(rangeDate.value[1]).format('YYYY-MM-DD');
} }
} }
// await (isUpdate.value ? arrangementUpdate(data) : arrangementAdd(data)); // await (isUpdate.value ? arrangementUpdate(data) : arrangementAdd(data));
resetInitialized(); resetInitialized();
@@ -268,41 +279,69 @@ async function handleClosed() {
await formApi.resetForm(); await formApi.resetForm();
resetInitialized(); resetInitialized();
} }
onMounted(() => { onMounted(() => {});
})
</script> </script>
<template> <template>
<BasicModal :title="title"> <BasicModal :title="title">
<!-- 顶部表单区 --> <!-- 顶部表单区 -->
<Form :model="formModel" :rules="rules" ref="formRef" layout="horizontal" class="mb-4" > <Form
:model="formModel"
:rules="rules"
ref="formRef"
layout="horizontal"
class="mb-4"
>
<div class="grid grid-cols-2 gap-x-8 gap-y-2"> <div class="grid grid-cols-2 gap-x-8 gap-y-2">
<FormItem name="groupId" label="请选择考勤组" class="mb-0"> <FormItem name="groupId" label="请选择考勤组" class="mb-0">
<Select v-model:value="formModel.groupId" :options="groupOptions" placeholder="请选择" @change="chooseGroup"/> <Select
v-model:value="formModel.groupId"
:options="groupOptions"
placeholder="请选择"
@change="chooseGroup"
/>
</FormItem> </FormItem>
<FormItem name="attendanceType" label="考勤类型" class="mb-0"> <FormItem name="attendanceType" label="考勤类型" class="mb-0">
<Select :disabled="true" :options="getDictOptions('wy_kqlx')" v-model:value="formModel.attendanceType" placeholder="请选择考勤组" /> <Select
:disabled="true"
:options="getDictOptions('wy_kqlx')"
v-model:value="formModel.attendanceType"
placeholder="请选择考勤组"
/>
</FormItem> </FormItem>
<FormItem label="排班日期" name="dateType" class="col-span-2 mb-0"> <FormItem label="排班日期" name="dateType" class="col-span-2 mb-0">
<RadioGroup v-model:value="formModel.dateType" class="mr-4"> <RadioGroup v-model:value="formModel.dateType" class="mr-4">
<Radio value=1> <Radio value="1">
<span> <span>
单个日期 单个日期
<DatePicker v-model:value="singleDate" @click="handleDateTypeChange(1)"/> <DatePicker
v-model:value="singleDate"
@click="handleDateTypeChange(1)"
/>
</span> </span>
</Radio> </Radio>
<Radio value=2> <Radio value="2">
<span> <span>
从此日起长期有效 从此日起长期有效
<DatePicker v-model:value="longDate" @click="handleDateTypeChange(2)"/> <DatePicker
v-model:value="longDate"
@click="handleDateTypeChange(2)"
/>
</span> </span>
</Radio> </Radio>
<Radio value=3> <Radio value="3">
<span> <span>
在此期间有效 在此期间有效
<DatePicker v-model:value="rangeDate[0]" class="ml-2" @click="handleDateTypeChange(3)"/> <DatePicker
v-model:value="rangeDate[0]"
class="ml-2"
@click="handleDateTypeChange(3)"
/>
<span class="mx-2">~</span> <span class="mx-2">~</span>
<DatePicker v-model:value="rangeDate[1]" @click="handleDateTypeChange(3)"/> <DatePicker
v-model:value="rangeDate[1]"
@click="handleDateTypeChange(3)"
/>
</span> </span>
</Radio> </Radio>
</RadioGroup> </RadioGroup>
@@ -311,9 +350,14 @@ onMounted(() => {
</Form> </Form>
<!-- 考勤组人员表格区 --> <!-- 考勤组人员表格区 -->
<div class="mb-2"> <div class="mb-2">
<div class="flex items-center mb-2"> <div class="mb-2 flex items-center">
<span>考勤组人员已选 <span class="text-red-500">{{ totalSelected }}</span> </span> <span
<a-button type="primary" size="small" class="ml-4" @click="handleAdd">+ 添加人员</a-button> >考勤组人员已选
<span class="text-red-500">{{ totalSelected }}</span> </span
>
<a-button type="primary" size="small" class="ml-4" @click="handleAdd"
>+ 添加人员</a-button
>
</div> </div>
<Table <Table
:columns="columns" :columns="columns"
@@ -323,7 +367,7 @@ onMounted(() => {
bordered bordered
/> />
</div> </div>
<UnitPersonModal @reload="handleTableData"/> <UnitPersonModal @reload="handleTableData" />
<!-- 底部按钮区 --> <!-- 底部按钮区 -->
</BasicModal> </BasicModal>
</template> </template>

View File

@@ -1,36 +1,43 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Radio, Button,Calendar,DatePicker } from 'ant-design-vue'; import { Radio, Button, Calendar, DatePicker } from 'ant-design-vue';
import type { RadioChangeEvent,BadgeProps, } from 'ant-design-vue'; import type { RadioChangeEvent, BadgeProps } from 'ant-design-vue';
import {ref} from 'vue'; import { ref } from 'vue';
import { Dayjs } from 'dayjs'; import { Dayjs } from 'dayjs';
import arrangementModal from './arrangement-modal.vue'; import arrangementModal from './arrangement-modal.vue';
import {useVbenModal} from '@vben/common-ui'; import { useVbenModal } from '@vben/common-ui';
import type { PersonVO } from './type'; import type { PersonVO } from './type';
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'changeView',value:boolean):void (e: 'changeView', value: boolean): void;
}>(); }>();
const props = defineProps<{ const props = defineProps<{
viewMode:'calender' | 'schedule' viewMode: 'calender' | 'schedule';
}>(); }>();
const selectedDate = ref(); const selectedDate = ref();
// 切换视图模式 // 切换视图模式
function handleViewModeChange(e: RadioChangeEvent): void { function handleViewModeChange(e: RadioChangeEvent): void {
// 将父组件的isCalenderView变为true // 将父组件的isCalenderView变为true
emit('changeView',e.target.value) emit('changeView', e.target.value);
} }
// 日历模拟数据 // 日历模拟数据
const scheduleData:{ date: string; list: { type: BadgeProps['status']; content: string }[] }[] = [ const scheduleData: {
{ date: '2025-07-08', list: [ { type: 'warning', content: '7月8日事件' } ] }, date: string;
{ date: '2025-07-06', list: [ { type: 'success', content: '8月8日事件' } ] }, list: { type: BadgeProps['status']; content: string }[];
}[] = [
{ date: '2025-07-08', list: [{ type: 'warning', content: '7月8日事件' }] },
{ date: '2025-07-06', list: [{ type: 'success', content: '8月8日事件' }] },
// ... // ...
]; ];
const getListData2 = (current: Dayjs): { type: BadgeProps['status']; content: string }[] => { const getListData2 = (
current: Dayjs,
): { type: BadgeProps['status']; content: string }[] => {
const dateStr = current.format('YYYY-MM-DD'); const dateStr = current.format('YYYY-MM-DD');
const found = scheduleData.find(item => item.date === dateStr); const found = scheduleData.find((item) => item.date === dateStr);
return found ? found.list : []; return found ? found.list : [];
}; };
const getListData = (value: Dayjs):{ type: BadgeProps['status']; content: string }[] => { const getListData = (
value: Dayjs,
): { type: BadgeProps['status']; content: string }[] => {
let listData: { type: BadgeProps['status']; content: string }[] | undefined; let listData: { type: BadgeProps['status']; content: string }[] | undefined;
switch (value.date()) { switch (value.date()) {
case 8: case 8:
@@ -77,49 +84,63 @@ function handleAdd() {
modalApi.setData({}); modalApi.setData({});
modalApi.open(); modalApi.open();
} }
</script> </script>
<template> <template>
<div class="h-full flex flex-col"> <div class="flex h-full flex-col">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="flex gap-7"> <div class="flex gap-7">
<Radio.Group v-model:value="props.viewMode" @change="handleViewModeChange"> <Radio.Group
v-model:value="props.viewMode"
@change="handleViewModeChange"
>
<Radio.Button value="calender">日历视图</Radio.Button> <Radio.Button value="calender">日历视图</Radio.Button>
<Radio.Button value="schedule">班表视图</Radio.Button> <Radio.Button value="schedule">班表视图</Radio.Button>
</Radio.Group> </Radio.Group>
<div class="my-auto flex gap-1"> <div class="my-auto flex gap-1">
<div class="my-auto">排班日历</div> <div class="my-auto">排班日历</div>
<DatePicker picker="month" v-model:value="selectedDate" /> <DatePicker picker="month" v-model:value="selectedDate" />
</div> </div>
</div> </div>
<div> <div>
<Button type="primary" @click="handleAdd">新增排班</Button> <Button type="primary" @click="handleAdd">新增排班</Button>
</div> </div>
</div> </div>
<div class="flex-1 p-1"> <div class="flex-1 p-1">
<Calendar v-model:value="selectedDate" :mode="'month'" :headerRender="customHeader"> <Calendar
v-model:value="selectedDate"
:mode="'month'"
:headerRender="customHeader"
>
<template #dateCellRender="{ current }"> <template #dateCellRender="{ current }">
<ul class="events"> <ul class="events">
<li v-for="item in getListData2(current)" :key="item.content"> <li v-for="item in getListData2(current)" :key="item.content">
<span> <span>
<!-- <Badge :status="item.type" :text="item.content" /> --> <!-- <Badge :status="item.type" :text="item.content" /> -->
<span>{{ item.content }}</span> <span>{{ item.content }}</span>
<a style="margin-left: 4px; color: #1890ff; cursor: pointer;">编辑</a> <a style="margin-left: 4px; color: #1890ff; cursor: pointer"
<a style="margin-left: 4px; color: #ff4d4f; cursor: pointer;">删除</a> >编辑</a
</span> >
</li> <a style="margin-left: 4px; color: #ff4d4f; cursor: pointer"
<a v-if="getListData2(current).length > 0" style="margin-left: 4px; color: #1890ff; cursor: pointer;">详情</a> >删除</a
</ul> >
</template> </span>
<template #monthCellRender="{ current }"> </li>
<div v-if="getMonthData(current)" class="notes-month"> <a
<section>{{ getMonthData(current) }}</section> v-if="getListData2(current).length > 0"
<span>Backlog number</span> style="margin-left: 4px; color: #1890ff; cursor: pointer"
</div> >详情</a
</template> >
</ul>
</template>
<template #monthCellRender="{ current }">
<div v-if="getMonthData(current)" class="notes-month">
<section>{{ getMonthData(current) }}</section>
<span>Backlog number</span>
</div>
</template>
</Calendar> </Calendar>
</div> </div>
<ArrangementModal @reload=""/> <ArrangementModal @reload="" />
</div> </div>
</template> </template>
<style scoped> <style scoped>

View File

@@ -1,7 +1,6 @@
import type { FormSchemaGetter } from '#/adapter/form'; import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
export const querySchema: FormSchemaGetter = () => [ export const querySchema: FormSchemaGetter = () => [
{ {
component: 'Input', component: 'Input',
@@ -15,15 +14,13 @@ export const querySchema: FormSchemaGetter = () => [
}, },
{ {
component: 'Select', component: 'Select',
componentProps: { componentProps: {},
},
fieldName: 'scheduleType', fieldName: 'scheduleType',
label: '排班类型1-固定班制2-排班制', label: '排班类型1-固定班制2-排班制',
}, },
{ {
component: 'Select', component: 'Select',
componentProps: { componentProps: {},
},
fieldName: 'dateType', fieldName: 'dateType',
label: '日期类型1-单个日期2-长期有效3-期间有效', label: '日期类型1-单个日期2-长期有效3-期间有效',
}, },
@@ -75,33 +72,33 @@ export const columns: VxeGridProps['columns'] = [
{ {
title: '序号', title: '序号',
field: 'id', field: 'id',
slots:{ slots: {
default:({rowIndex}) => { default: ({ rowIndex }) => {
return (rowIndex + 1).toString(); return (rowIndex + 1).toString();
} },
}, },
width:60 width: 60,
}, },
{ {
title: '人员', title: '人员',
field: 'scheduleName', field: 'scheduleName',
width:120, width: 120,
// width: 'auto', // width: 'auto',
}, },
{ {
title: '单位', title: '单位',
field: 'scheduleName', field: 'scheduleName',
width:'auto', width: 'auto',
}, },
{ {
title: '排班名称', title: '排班名称',
field: 'scheduleName', field: 'scheduleName',
minWidth:120 minWidth: 120,
}, },
{ {
title: '考勤组', title: '考勤组',
field: 'groupId', field: 'groupId',
minWidth:120 minWidth: 120,
}, },
{ {
title: '考勤类型', title: '考勤类型',
@@ -111,8 +108,7 @@ export const columns: VxeGridProps['columns'] = [
{ {
title: '考勤时间', title: '考勤时间',
field: 'dateType', field: 'dateType',
minWidth:200 minWidth: 200,
}, },
{ {
field: 'action', field: 'action',
@@ -128,27 +124,27 @@ export const attendanceColumns: VxeGridProps['columns'] = [
{ {
title: '序号', title: '序号',
field: 'id', field: 'id',
slots:{ slots: {
default:({rowIndex}) => { default: ({ rowIndex }) => {
return (rowIndex + 1).toString(); return (rowIndex + 1).toString();
} },
}, },
width:60 width: 60,
}, },
{ {
title: '单位', title: '单位',
field: 'scheduleName', field: 'scheduleName',
width:'auto', width: 'auto',
}, },
{ {
title: '已选人数', title: '已选人数',
field: 'scheduleName', field: 'scheduleName',
width:'auto', width: 'auto',
}, },
{ {
title: '人员', title: '人员',
field: 'dateType', field: 'dateType',
minWidth:200 minWidth: 200,
}, },
{ {
field: 'action', field: 'action',
@@ -165,28 +161,28 @@ export const unitColumns: VxeGridProps['columns'] = [
{ {
title: '序号', title: '序号',
field: 'id', field: 'id',
slots:{ slots: {
default:({rowIndex}) => { default: ({ rowIndex }) => {
return (rowIndex + 1).toString(); return (rowIndex + 1).toString();
} },
}, },
width:60 width: 60,
}, },
{ {
title: '姓名', title: '姓名',
field: 'userName', field: 'userName',
minWidth:120 minWidth: 120,
}, },
{ {
title: '所属单位', title: '所属单位',
field: 'unitName', field: 'unitName',
width:'auto', width: 'auto',
}, },
{ {
title: '电话', title: '电话',
field: 'phone', field: 'phone',
minWidth:120 minWidth: 120,
} },
]; ];
export const modalSchema: FormSchemaGetter = () => [ export const modalSchema: FormSchemaGetter = () => [
{ {
@@ -212,15 +208,13 @@ export const modalSchema: FormSchemaGetter = () => [
label: '排班类型1-固定班制2-排班制', label: '排班类型1-固定班制2-排班制',
fieldName: 'scheduleType', fieldName: 'scheduleType',
component: 'Select', component: 'Select',
componentProps: { componentProps: {},
},
}, },
{ {
label: '日期类型1-单个日期2-长期有效3-期间有效', label: '日期类型1-单个日期2-长期有效3-期间有效',
fieldName: 'dateType', fieldName: 'dateType',
component: 'Select', component: 'Select',
componentProps: { componentProps: {},
},
}, },
{ {
label: '开始日期', label: '开始日期',

View File

@@ -1,20 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import calendarView from './calendarView.vue' import calendarView from './calendarView.vue';
import scheduleView from './scheduleView.vue'; import scheduleView from './scheduleView.vue';
const viewMode = ref<'calender' | 'schedule'>('calender'); const viewMode = ref<'calender' | 'schedule'>('calender');
// const isCalenderView = ref(true); // const isCalenderView = ref(true);
function handleViewModeChange(mode:'calender' | 'schedule'){ function handleViewModeChange(mode: 'calender' | 'schedule') {
viewMode.value = mode viewMode.value = mode;
} }
</script> </script>
<template> <template>
<div class="p-4 h-full"> <div class="h-full p-4">
<div class="p-4 h-full bg-white"> <div class="h-full bg-white p-4">
<calendarView v-show="viewMode === 'calender'" :viewMode="viewMode" @changeView="handleViewModeChange"/> <calendarView
<scheduleView v-show="viewMode === 'schedule'" :viewMode="viewMode" @changeView="handleViewModeChange"/> v-show="viewMode === 'calender'"
</div> :viewMode="viewMode"
@changeView="handleViewModeChange"
/>
<scheduleView
v-show="viewMode === 'schedule'"
:viewMode="viewMode"
@changeView="handleViewModeChange"
/>
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@@ -1,13 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Radio, Button, Calendar } from 'ant-design-vue'; import { Radio, Button, Calendar } from 'ant-design-vue';
import type { RadioChangeEvent } from 'ant-design-vue'; import type { RadioChangeEvent } from 'ant-design-vue';
import {ref} from 'vue'; import { ref } from 'vue';
import { Dayjs } from 'dayjs'; import { Dayjs } from 'dayjs';
import { columns } from './data'; import { columns } from './data';
import { import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
useVbenVxeGrid,
type VxeGridProps
} from '#/adapter/vxe-table';
import { getVxePopupContainer } from '@vben/utils'; import { getVxePopupContainer } from '@vben/utils';
import { import {
arrangementList, arrangementList,
@@ -17,9 +14,9 @@ import arrangementModal from './arrangement-modal.vue';
import type { ArrangementForm } from '#/api/property/attendanceManagement/arrangement/model'; import type { ArrangementForm } from '#/api/property/attendanceManagement/arrangement/model';
import { Page, useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { Popconfirm, Space } from 'ant-design-vue'; import { Popconfirm, Space } from 'ant-design-vue';
const emit = defineEmits<{(e:'changeView',value:boolean):void}>(); const emit = defineEmits<{ (e: 'changeView', value: boolean): void }>();
const props = defineProps<{ const props = defineProps<{
viewMode:'calender' | 'schedule' viewMode: 'calender' | 'schedule';
}>(); }>();
const value = ref<Dayjs>(); const value = ref<Dayjs>();
const onPanelChange = (value: Dayjs, mode: string) => { const onPanelChange = (value: Dayjs, mode: string) => {
@@ -28,10 +25,9 @@ const onPanelChange = (value: Dayjs, mode: string) => {
// 切换视图模式 // 切换视图模式
function handleViewModeChange(e: RadioChangeEvent): void { function handleViewModeChange(e: RadioChangeEvent): void {
// 将父组件的isCalenderView变为false // 将父组件的isCalenderView变为false
emit('changeView',e.target.value) emit('changeView', e.target.value);
} }
const gridOptions: VxeGridProps = { const gridOptions: VxeGridProps = {
checkboxConfig: { checkboxConfig: {
// 高亮 // 高亮
@@ -66,7 +62,7 @@ const gridOptions: VxeGridProps = {
toolbarConfig: { toolbarConfig: {
// 隐藏"刷新/重置"按钮(对应 redo // 隐藏"刷新/重置"按钮(对应 redo
refresh: false, refresh: false,
zoom: false, // 显示全屏 zoom: false, // 显示全屏
custom: false, // 隐藏列设置 custom: false, // 隐藏列设置
}, },
}; };
@@ -93,55 +89,68 @@ async function handleDelete(row: Required<ArrangementForm>) {
} }
</script> </script>
<template> <template>
<div class="h-full "> <div class="h-full">
<div class="flex justify-between"> <div class="flex justify-between">
<Radio.Group v-model:value="props.viewMode" @change="handleViewModeChange"> <Radio.Group
<Radio.Button value="calender">日历视图</Radio.Button> v-model:value="props.viewMode"
<Radio.Button value="schedule">班表视图</Radio.Button> @change="handleViewModeChange"
>
<Radio.Button value="calender">日历视图</Radio.Button>
<Radio.Button value="schedule">班表视图</Radio.Button>
</Radio.Group> </Radio.Group>
<div> <div>
<Button type="primary" @click="handleAdd">新增排班</Button> <Button type="primary" @click="handleAdd">新增排班</Button>
</div> </div>
</div> </div>
<div class="flex p-2 h-full"> <div class="flex h-full p-2">
<div style="padding-top: 48.4px;"> <div style="padding-top: 48.4px">
<div :style="{ width: '300px', border: '1px solid #d9d9d9', borderRadius: '4px' }"> <div
<Calendar v-model:value="value" :fullscreen="false" :mode="'month'" @panelChange="onPanelChange"/> :style="{
width: '300px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
}"
>
<Calendar
v-model:value="value"
:fullscreen="false"
:mode="'month'"
@panelChange="onPanelChange"
/>
</div> </div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<Page :auto-content-height="true"> <Page :auto-content-height="true">
<BasicTable> <BasicTable>
<template #action="{ row }"> <template #action="{ row }">
<Space> <Space>
<ghost-button
v-access:code="['property:arrangement:edit']"
@click.stop="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</ghost-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button <ghost-button
danger v-access:code="['property:arrangement:edit']"
v-access:code="['property:arrangement:remove']" @click.stop="handleEdit(row)"
@click.stop=""
> >
{{ $t('pages.common.delete') }} {{ $t('pages.common.edit') }}
</ghost-button> </ghost-button>
</Popconfirm> <Popconfirm
</Space> :get-popup-container="getVxePopupContainer"
</template> placement="left"
</BasicTable> title="确认删除?"
@confirm="handleDelete(row)"
>
<ghost-button
danger
v-access:code="['property:arrangement:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</ghost-button>
</Popconfirm>
</Space>
</template>
</BasicTable>
</Page> </Page>
</div> </div>
</div> </div>
<ArrangementModal @reload="tableApi.query()"/> <ArrangementModal @reload="tableApi.query()" />
</div> </div>
</template> </template>
<style> <style></style>
</style>

View File

@@ -1,68 +1,67 @@
//列表数据,用于展示人员列表 //列表数据,用于展示人员列表
export interface PersonVO { export interface PersonVO {
/** /**
* 主键id * 主键id
*/ */
id: string | number; id: string | number;
/** /**
* 用户id * 用户id
*/ */
userId: string | number; userId: string | number;
/** /**
* 用户名称 * 用户名称
*/ */
userName: string; userName: string;
/** /**
* 联系电话 * 联系电话
*/ */
phone: string; phone: string;
/** /**
* 性别 * 性别
*/ */
gender: number; gender: number;
/** /**
* 人脸图片 * 人脸图片
*/ */
img: string; img: string;
/** /**
* 所属单位id * 所属单位id
*/ */
unitId: string | number; unitId: string | number;
/** /**
* 所属单位名称 * 所属单位名称
*/ */
unitName: string; unitName: string;
/** /**
* 入驻位置 * 入驻位置
*/ */
locathon: string; locathon: string;
/** /**
* 入驻时间 * 入驻时间
*/ */
time: string; time: string;
/** /**
* 车牌号码 * 车牌号码
*/ */
carNumber: string; carNumber: string;
/** /**
* 状态 * 状态
*/ */
state: number|string; state: number | string;
/** /**
* 备注 * 备注
*/ */
remark: string; remark: string;
}
}

View File

@@ -1,24 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, reactive, onMounted, watch } from 'vue'; import { computed, ref, reactive, onMounted, watch } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue'; import { SyncOutlined } from '@ant-design/icons-vue';
import { Tree, InputSearch, Skeleton, Empty, Button,Tag } from 'ant-design-vue'; import {
Tree,
InputSearch,
Skeleton,
Empty,
Button,
Tag,
} from 'ant-design-vue';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils'; import { cloneDeep } from '@vben/utils';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { arrangementAdd, arrangementInfo, arrangementUpdate } from '#/api/property/attendanceManagement/arrangement';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { import {
useVbenVxeGrid, arrangementAdd,
type VxeGridProps arrangementInfo,
} from '#/adapter/vxe-table'; arrangementUpdate,
import { modalSchema,unitColumns,unitQuerySchema } from './data'; } from '#/api/property/attendanceManagement/arrangement';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { useVbenVxeGrid, type VxeGridProps } from '#/adapter/vxe-table';
import { modalSchema, unitColumns, unitQuerySchema } from './data';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui'; import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { resident_unitList } from '#/api/property/resident/unit'; import { resident_unitList } from '#/api/property/resident/unit';
import { personList } from '#/api/property/resident/person'; import { personList } from '#/api/property/resident/person';
import type { PersonVO } from './type'; import type { PersonVO } from './type';
const emit = defineEmits<{ reload: [tableData: {dept: {unitId: string | number,unitName: string}, users: PersonVO[]}[]] }>(); const emit = defineEmits<{
reload: [
tableData: {
dept: { unitId: string | number; unitName: string };
users: PersonVO[];
}[],
];
}>();
const selectDeptId = ref<string[]>([]); const selectDeptId = ref<string[]>([]);
const isUpdate = ref(false); const isUpdate = ref(false);
const title = computed(() => { const title = computed(() => {
@@ -29,29 +44,33 @@ const searchValue = ref('');
const selectedDeptIds = ref<string[]>([]); const selectedDeptIds = ref<string[]>([]);
const showTreeSkeleton = ref(false); const showTreeSkeleton = ref(false);
//单位树 //单位树
const deptTree = reactive<{ const deptTree = reactive<
id: string; {
label: string;
children: {
id: string; id: string;
label: string; label: string;
}[]; children: {
}[]>([ id: string;
{ label: string;
id: '', }[];
label: '全部单位', }[]
children: [ >([
], {
}, id: '',
label: '全部单位',
children: [],
},
]); ]);
//勾选内容 //勾选内容
const selectedUsers = ref<PersonVO[]>([]); const selectedUsers = ref<PersonVO[]>([]);
//已选内容,需要展示到父组件 //已选内容,需要展示到父组件
const tableData = reactive<{ const tableData = reactive<
dept: {unitId: string | number,unitName: string}, {
users: PersonVO[]}[]>([]); dept: { unitId: string | number; unitName: string };
users: PersonVO[];
}[]
>([]);
function handleReload() { function handleReload() {
showTreeSkeleton.value = true; showTreeSkeleton.value = true;
setTimeout(() => { setTimeout(() => {
@@ -64,7 +83,7 @@ const filteredDeptTree = computed(() => {
// 递归过滤树 // 递归过滤树
function filter(tree: any[]): any[] { function filter(tree: any[]): any[] {
return tree return tree
.map(node => { .map((node) => {
if (node.label.includes(searchValue.value)) return node; if (node.label.includes(searchValue.value)) return node;
if (node.children) { if (node.children) {
const children = filter(node.children); const children = filter(node.children);
@@ -86,7 +105,7 @@ const [BasicForm, formApi] = useVbenForm({
// 通用配置项 会影响到所有表单项 // 通用配置项 会影响到所有表单项
componentProps: { componentProps: {
class: 'w-full', class: 'w-full',
} },
}, },
schema: modalSchema(), schema: modalSchema(),
showDefaultActions: false, showDefaultActions: false,
@@ -176,41 +195,47 @@ const gridOptions: VxeGridProps = {
toolbarConfig: { toolbarConfig: {
// 隐藏"刷新/重置"按钮(对应 redo // 隐藏"刷新/重置"按钮(对应 redo
refresh: false, refresh: false,
zoom: false, // 显示全屏 zoom: false, // 显示全屏
custom: false, // 隐藏列设置 custom: false, // 隐藏列设置
}, },
}; };
const gridEvents = { const gridEvents = {
checkboxChange:async (params:any) => { checkboxChange: async (params: any) => {
const currentRecords = await params.$grid.getCheckboxRecords()??[]; const currentRecords = (await params.$grid.getCheckboxRecords()) ?? [];
const reserveRecords = await params.$grid.getCheckboxReserveRecords()??[]; const reserveRecords =
(await params.$grid.getCheckboxReserveRecords()) ?? [];
selectedUsers.value = [...reserveRecords, ...currentRecords]; selectedUsers.value = [...reserveRecords, ...currentRecords];
}, },
checkboxAll:async (params:any) => { checkboxAll: async (params: any) => {
const currentRecords = await params.$grid.getCheckboxRecords()??[]; const currentRecords = (await params.$grid.getCheckboxRecords()) ?? [];
const reserveRecords = await params.$grid.getCheckboxReserveRecords()??[]; const reserveRecords =
(await params.$grid.getCheckboxReserveRecords()) ?? [];
selectedUsers.value = [...reserveRecords, ...currentRecords]; selectedUsers.value = [...reserveRecords, ...currentRecords];
}, },
}; };
const [BasicTable, tableApi] = useVbenVxeGrid({ const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions, formOptions,
gridOptions, gridOptions,
gridEvents gridEvents,
}); });
async function handleConfirm() { async function handleConfirm() {
try { try {
for(const user of selectedUsers.value){ for (const user of selectedUsers.value) {
if(tableData.find(item=>item.dept.unitId === user.unitId)){ if (tableData.find((item) => item.dept.unitId === user.unitId)) {
tableData.find(item=>item.dept.unitId===user.unitId)!.users.push(user); tableData
}else{ .find((item) => item.dept.unitId === user.unitId)!
tableData.push({ dept:{unitId:user.unitId,unitName:user.unitName}, users: [user] }); .users.push(user);
} else {
tableData.push({
dept: { unitId: user.unitId, unitName: user.unitName },
users: [user],
});
} }
} }
console.log(tableData); console.log(tableData);
resetInitialized(); resetInitialized();
emit('reload',tableData); emit('reload', tableData);
modalApi.close(); modalApi.close();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -224,11 +249,11 @@ async function handleClosed() {
resetInitialized(); resetInitialized();
} }
async function getUnitList(){ async function getUnitList() {
deptTree[0]!.children = []; deptTree[0]!.children = [];
const res = await resident_unitList(); const res = await resident_unitList();
if (deptTree[0] && Array.isArray(deptTree[0].children)) { if (deptTree[0] && Array.isArray(deptTree[0].children)) {
for(const item of res.rows){ for (const item of res.rows) {
deptTree[0].children.push({ deptTree[0].children.push({
id: item.id as string, id: item.id as string,
label: item.name as string, label: item.name as string,
@@ -251,59 +276,79 @@ onMounted(() => {
</script> </script>
<template> <template>
<BasicModal :title="title"> <BasicModal :title="title">
<div class="mb-2 flex"> <div class="mb-2 flex">
<div> <div>
<Skeleton :loading="showTreeSkeleton" :paragraph="{ rows: 8 }" active class="p-[8px]"> <Skeleton
<div class="bg-background z-100 sticky left-0 top-0 p-[8px] flex items-center"> :loading="showTreeSkeleton"
<InputSearch :paragraph="{ rows: 8 }"
v-model:value="searchValue" active
placeholder="搜索单位" class="p-[8px]"
size="small" >
style="flex:1;" <div
class="bg-background z-100 sticky left-0 top-0 flex items-center p-[8px]"
> >
<template #enterButton> <InputSearch
<Button type="text" @click="getUnitList"> v-model:value="searchValue"
<SyncOutlined class="text-primary" /> placeholder="搜索单位"
</Button> size="small"
</template> style="flex: 1"
</InputSearch> >
</div> <template #enterButton>
<div class="h-full overflow-x-hidden px-[8px]"> <Button type="text" @click="getUnitList">
<Tree <SyncOutlined class="text-primary" />
v-if="filteredDeptTree.length > 0" </Button>
v-model:selectedKeys="selectedDeptIds" </template>
:field-names="{ title: 'label', key: 'id' }" </InputSearch>
:show-line="{ showLeafIcon: false }"
:tree-data="filteredDeptTree"
:virtual="false"
default-expand-all
>
<template #title="{ label }">
<span v-if="searchValue && label.indexOf(searchValue) > -1">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
{{ label.substring(label.indexOf(searchValue) + searchValue.length) }}
</span>
<span v-else>{{ label }}</span>
</template>
</Tree>
<div v-else class="mt-5">
<Empty :image="Empty.PRESENTED_IMAGE_SIMPLE" description="无单位数据" />
</div> </div>
<div class="h-full overflow-x-hidden px-[8px]">
<Tree
v-if="filteredDeptTree.length > 0"
v-model:selectedKeys="selectedDeptIds"
:field-names="{ title: 'label', key: 'id' }"
:show-line="{ showLeafIcon: false }"
:tree-data="filteredDeptTree"
:virtual="false"
default-expand-all
>
<template #title="{ label }">
<span v-if="searchValue && label.indexOf(searchValue) > -1">
{{ label.substring(0, label.indexOf(searchValue)) }}
<span style="color: #f50">{{ searchValue }}</span>
{{
label.substring(
label.indexOf(searchValue) + searchValue.length,
)
}}
</span>
<span v-else>{{ label }}</span>
</template>
</Tree>
<div v-else class="mt-5">
<Empty
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="无单位数据"
/>
</div>
</div>
</Skeleton>
</div>
<div class="flex-1">
<div
class="mb-2 flex 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> </div>
</Skeleton> <BasicTable> </BasicTable>
</div> </div>
<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>
</div>
</div> </div>
</BasicModal> </BasicModal>
</template> </template>

View File

@@ -1,18 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Recordable } from '@vben/types';
import { ref } from 'vue';
import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui'; import { Page, useVbenModal, type VbenFormProps } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils'; import { getVxePopupContainer } from '@vben/utils';
import { Modal, Popconfirm, Space } from 'ant-design-vue'; import { Modal, Popconfirm, Space } from 'ant-design-vue';
import dayjs from 'dayjs';
import { import {
useVbenVxeGrid, useVbenVxeGrid,
vxeCheckboxChecked, vxeCheckboxChecked,
type VxeGridProps type VxeGridProps,
} from '#/adapter/vxe-table'; } from '#/adapter/vxe-table';
import { import {
@@ -76,7 +70,7 @@ const gridOptions: VxeGridProps = {
keyField: 'id', keyField: 'id',
}, },
// 表格全局唯一表示 保存列配置需要用到 // 表格全局唯一表示 保存列配置需要用到
id: 'property-costMeterWater-index' id: 'property-costMeterWater-index',
}; };
const [BasicTable, tableApi] = useVbenVxeGrid({ const [BasicTable, tableApi] = useVbenVxeGrid({
@@ -118,9 +112,14 @@ function handleMultiDelete() {
} }
function handleDownloadExcel() { function handleDownloadExcel() {
commonDownloadExcel(costMeterWaterExport, '费用-水电抄数据', tableApi.formApi.form.values, { commonDownloadExcel(
fieldMappingTime: formOptions.fieldMappingTime, costMeterWaterExport,
}); '费用-水电抄数据',
tableApi.formApi.form.values,
{
fieldMappingTime: formOptions.fieldMappingTime,
},
);
} }
</script> </script>
@@ -140,7 +139,8 @@ function handleDownloadExcel() {
danger danger
type="primary" type="primary"
v-access:code="['property:costMeterWater:remove']" v-access:code="['property:costMeterWater:remove']"
@click="handleMultiDelete"> @click="handleMultiDelete"
>
{{ $t('pages.common.delete') }} {{ $t('pages.common.delete') }}
</a-button> </a-button>
<a-button <a-button