Browse Source

init: 排班管理

cfort 1 year ago
parent
commit
cc7881d2c0

+ 194 - 0
src/api/business/schedule.js

@@ -0,0 +1,194 @@
+import request from '@/utils/request'
+import { BUSINESS_BASE_URL } from '@/api/baseUrl'
+
+/**
+ * 获取排班配置
+ * @param {*} params
+ */
+export function getScheduleConfig (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/scheduleConfig/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 获取排班配置列表
+ * @param {*} params
+ */
+export function queryScheduleConfig (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/scheduleConfig/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 保存排班配置
+ * @param {*} params
+ */
+export function saveScheduleConfig (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/scheduleConfig/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除排班配置
+ * @param {*} params
+ */
+export function removeScheduleConfig (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/scheduleConfig/remove',
+        method: 'post',
+        params
+    })
+}
+
+/**
+ * 获取排班记录详情
+ * @param {*} params
+ */
+export function getStaffSchedule (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffSchedule/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 获取排班记录列表
+ * @param {*} params
+ */
+export function queryStaffSchedule (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffSchedule/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 保存排班
+ * @param {*} params
+ */
+export function saveStaffSchedule (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffSchedule/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除排班
+ * @param {*} params
+ */
+export function removeStaffSchedule (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffSchedule/remove',
+        method: 'post',
+        params
+    })
+}
+
+/**
+ * 获取排班记录详情
+ * @param {*} params
+ */
+export function getStaffScheduleDetail (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffScheduleDetail/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 获取排班记录列表
+ * @param {*} params
+ */
+export function queryStaffScheduleDetail (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffScheduleDetail/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 保存排班
+ * @param {*} params
+ */
+export function saveStaffScheduleDetail (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffScheduleDetail/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除排班
+ * @param {*} params
+ */
+export function removeStaffScheduleDetail (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/staffScheduleDetail/remove',
+        method: 'post',
+        params
+    })
+}
+
+/**
+ * 获取调班记录详情
+ * @param {*} params
+ */
+export function getAdjustment (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/adjustment/get',
+        method: 'get',
+        params
+    })
+}
+
+/**
+ * 获取调班记录列表
+ * @param {*} params
+ */
+export function queryAdjustment (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/adjustment/query',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 调班申请
+ * @param {*} params
+ */
+export function saveAdjustment (data) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/adjustment/save',
+        method: 'post',
+        data
+    })
+}
+
+/**
+ * 删除调班记录
+ * @param {*} params
+ */
+export function removeAdjustment (params) {
+    return request({
+        url: BUSINESS_BASE_URL() + '/employee/adjustment/remove',
+        method: 'post',
+        params
+    })
+}

+ 21 - 0
src/assets/styles/pages/dashboard.scss

@@ -311,6 +311,27 @@ $transition: all .5s;
             }
         }
     }
+
+    [alias="userInfo"] {
+        .el-card__body {
+            padding: 10px 20px;
+            .ibps-item-content {
+                .ibps-item-content-title {
+                    line-height: 20px;
+                    margin-bottom: 6px;
+                }
+                .ibps-item-content-label {
+                    > div {
+                        margin-bottom: 2px;
+                        &:last-child {
+                            margin-bottom: 0;
+                        }
+                    }
+                }
+            }
+        }
+
+    }
     
     .pending-business {
         .ibps-list-col-main {

+ 195 - 0
src/views/business/​scheduleManage/adjust.vue

@@ -0,0 +1,195 @@
+<template>
+    <div class="main-container">
+        <ibps-crud
+            ref="crud"
+            :display-field="title"
+            :height="height"
+            :data="listData"
+            :toolbars="listConfig.toolbars"
+            :search-form="listConfig.searchForm"
+            :pk-key="pkKey"
+            :columns="listConfig.columns"
+            :row-handle="listConfig.rowHandle"
+            :pagination="pagination"
+            :loading="loading"
+            @action-event="handleAction"
+            @sort-change="handleSortChange"
+            @pagination-change="handlePaginationChange"
+            @row-dblclick="handleRowDblclick"
+        >
+            <template slot="dateRange" slot-scope="scope">
+                <span>{{ `${scope.row.startDate} 至 ${scope.row.endDate}` }}</span>
+            </template>
+        </ibps-crud>
+        <adjust-edit
+            v-if="showAdjustEdit"
+            :visible.sync="showAdjustEdit"
+            :params="params"
+            @refresh="loadData"
+            @close="() => showAdjustEdit = false"
+        />
+    </div>
+</template>
+
+<script>
+import { queryAdjustment, removeAdjustment } from '@/api/business/schedule'
+import { stateType } from '@/views/constants/schedule'
+import ActionUtils from '@/utils/action'
+import FixHeight from '@/mixins/height'
+
+export default {
+    components: {
+        AdjustEdit: () => import('./components/adjust-edit')
+    },
+    mixins: [FixHeight],
+    data () {
+        const { userList = [] } = this.$store.getters || {}
+        const userOption = userList.map(item => ({ label: item.userName, value: item.userId }))
+        return {
+            userOption,
+            stateType,
+            title: '调班记录',
+            pkKey: 'id', // 主键  如果主键不是pk需要传主键
+            loading: true,
+            height: document.clientHeight,
+            listData: [],
+            pagination: {},
+            sorts: {},
+            showAdjustEdit: false,
+            readonly: false,
+            params: {},
+            listConfig: {
+                toolbars: [
+                    { key: 'search', icon: 'ibps-icon-search', label: '查询', type: 'primary', hidden: false },
+                    { key: 'create', icon: 'ibps-icon-plus', label: '申请', type: 'success', hidden: false },
+                    { key: 'remove', icon: 'ibps-icon-close', label: '删除', type: 'danger', hidden: false }
+                ],
+                searchForm: {
+                    labelWidth: 80,
+                    itemWidth: 180,
+                    forms: [
+                        { prop: 'Q^reason_^SL', label: '调班原因' },
+                        { prop: 'Q^status_^SL', label: '状态', fieldType: 'select', options: stateType },
+                        { prop: ['Q^create_time_^DL', 'Q^create_time_^DG'], label: '申请时间', fieldType: 'daterange', itemWidth: 200 }
+                    ]
+                },
+                // 表格字段配置
+                columns: [
+                    { prop: 'createBy', label: '申请人', tags: userOption, width: 100 },
+                    { prop: 'createTime', label: '申请时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 140 },
+                    { prop: 'executor', label: '审批人', tags: userOption, width: 100 },
+                    { prop: 'executeDate', label: '审批时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 140 },
+                    { prop: 'reason', label: '调班原因', width: 150 },
+                    { prop: 'status', label: '状态', tags: stateType, width: 100 },
+                    { prop: 'overview', label: '概览', minWidth: 200 }
+                ],
+                rowHandle: {
+                    effect: 'display',
+                    actions: [
+                        { key: 'edit', label: '编辑', type: 'primary', icon: 'ibps-icon-edit' },
+                        { key: 'detail', label: '详情', type: 'primary', icon: 'ibps-icon-list-alt' }
+                    ]
+                }
+            }
+        }
+    },
+    created () {
+        this.loadData()
+    },
+    methods: {
+        // 加载数据
+        loadData () {
+            this.loading = true
+            queryAdjustment(this.getSearchFormData()).then(res => {
+                ActionUtils.handleListData(this, res.data)
+                this.loading = false
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        /**
+         * 获取格式化参数
+         */
+        getSearchFormData () {
+            return ActionUtils.formatParams(
+                this.$refs['crud'] ? this.$refs['crud'].getSearcFormData() : {},
+                this.pagination,
+                this.sorts
+            )
+        },
+        /**
+         * 处理分页事件
+         */
+        handlePaginationChange (page) {
+            ActionUtils.setPagination(this.pagination, page)
+            this.loadData()
+        },
+        /**
+         * 处理排序
+         */
+        handleSortChange (sort) {
+            ActionUtils.setSorts(this.sorts, sort)
+            this.loadData()
+        },
+        /**
+         * 查询
+         */
+        search () {
+            this.loadData()
+        },
+        /**
+         * 处理按钮事件
+         */
+        handleAction (command, position, selection, data) {
+            switch (command) {
+                case 'search':
+                    ActionUtils.setFirstPagination(this.pagination)
+                    this.search()
+                    break
+                case 'create':
+                    this.handleEdit(command, {})
+                    break
+                case 'edit':
+                    this.handleEdit(command, data)
+                    break
+                case 'detail':
+                    this.handleEdit(command, data)
+                    break
+                case 'remove':
+                    ActionUtils.removeRecord(selection).then((ids) => {
+                        this.handleRemove(ids)
+                    }).catch(() => {})
+                    break
+                default:
+                    break
+            }
+        },
+        /**
+         * 处理编辑
+         */
+        async handleEdit (key, { id, scheduleId }) {
+            this.params = {
+                id,
+                scheduleId
+            }
+            this.readonly = key === 'detail'
+            this.showAdjustEdit = true
+        },
+        /**
+         * 处理删除
+         */
+        handleRemove (ids) {
+            // return this.$message.warning('避免误删测试数据,联系开发删除')
+            removeAdjustment({ ids }).then(() => {
+                ActionUtils.removeSuccessMessage()
+                this.search()
+            }).catch(() => {})
+        },
+        handleRowDblclick (row) {
+            this.handleEdit(row, 'detail')
+        }
+    }
+}
+</script>
+<style lang="scss">
+</style>

+ 376 - 0
src/views/business/​scheduleManage/components/add-classes.vue

@@ -0,0 +1,376 @@
+<template>
+    <el-dialog
+        :title="title"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        width="60%"
+        class="dialog classes-dialog"
+        top="5vh"
+        @close="closeDialog"
+    >
+        <el-form
+            ref="form"
+            :label-width="formLabelWidth"
+            :model="formData"
+            :rules="rules"
+            label-position="left"
+            class="classes-form"
+            @submit.native.prevent
+        >
+            <el-form-item label="班次名称" prop="name">
+                <el-input
+                    v-model="formData.name"
+                    type="text"
+                    clearable
+                    show-word-limit
+                    :maxlength="16"
+                    :disabled="readonly"
+                    placeholder="请输入"
+                />
+            </el-form-item>
+            <el-form-item label="班次别名" prop="alias">
+                <el-input
+                    v-model="formData.alias"
+                    type="text"
+                    clearable
+                    show-word-limit
+                    :maxlength="5"
+                    :disabled="readonly"
+                    placeholder="请输入"
+                />
+            </el-form-item>
+            <el-form-item label="对应岗位" prop="positions">
+                <el-select
+                    v-model="formData.positions"
+                    filterable
+                    multiple
+                    clearable
+                    :disabled="readonly"
+                    placeholder="请选择"
+                >
+                    <el-option
+                        v-for="item in positionList"
+                        :key="item.positionId"
+                        :label="item.positionName"
+                        :value="item.positionName"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-row :gutter="20" class="form-row">
+                <el-col :span="12">
+                    <el-form-item
+                        label="颜色"
+                        prop="color"
+                        required
+                        :show-message="false"
+                    >
+                        <el-color-picker
+                            v-model="formData.color"
+                            :predefine="predefine"
+                            size="mini"
+                        />
+                    </el-form-item>
+                </el-col>
+                <el-col :span="12">
+                    <el-form-item label="是否可用" prop="isEnabled" :show-message="false">
+                        <el-switch v-model="formData.isEnabled" active-value="Y" inactive-value="N" />
+                    </el-form-item>
+                </el-col>
+            </el-row>
+            <el-form-item
+                v-for="(item, index) in formData.dateRange"
+                :key="`${index}`"
+                class="date-range"
+                required
+                :show-message="false"
+            >
+                <template slot="label">
+                    <div class="custom-label">{{ `班次时间${index + 1}` }}</div>
+                </template>
+                <el-radio-group v-model="item.type" class="date-type">
+                    <el-radio-button label="range">时间段</el-radio-button>
+                    <el-radio-button label="allday" :disabled="formData.dateRange.length !== 1">全天</el-radio-button>
+                </el-radio-group>
+                <div v-if="item.type === 'range'" class="range-item">
+                    <div class="start">
+                        <el-time-select
+                            v-model="item.startTime"
+                            align="right"
+                            class="date-picker"
+                            :picker-options="item.pickerOptions"
+                            placeholder="开始时间"
+                        />
+                    </div>
+                    <div class="concat">至</div>
+                    <div class="end">
+                        <el-switch
+                            v-model="item.isSecondDay"
+                            active-value="Y"
+                            inactive-value="N"
+                            active-text="第二天"
+                            inactive-text="当天"
+                        />
+                        <el-time-select
+                            v-model="item.endTime"
+                            align="right"
+                            class="date-picker"
+                            :picker-options="item.pickerOptions"
+                            placeholder="结束时间"
+                        />
+                    </div>
+                </div>
+                <div v-if="!readonly" class="operate-btn">
+                    <el-button
+                        v-if="index === 0 && formData.dateRange.length < 5 && item.type === 'range'"
+                        type="primary"
+                        :tabindex="-1"
+                        icon="el-icon-plus"
+                        circle
+                        @click="addOption"
+                    />
+                    <el-button
+                        v-else-if="index === formData.dateRange.length - 1 && formData.dateRange.length > 1"
+                        type="danger"
+                        :tabindex="-1"
+                        icon="el-icon-delete"
+                        circle
+                        @click="subOption"
+                    />
+                </div>
+            </el-form-item>
+            <el-form-item label="说明" prop="desc">
+                <el-input
+                    v-model="formData.desc"
+                    type="textarea"
+                    clearable
+                    show-word-limit
+                    :maxlength="256"
+                    :disabled="readonly"
+                    placeholder="请输入"
+                />
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar
+                :actions="toolbars"
+                @action-event="handleActionEvent"
+            />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import setting from '@/setting.js'
+import { validateId } from 'bpmn-js-properties-panel/lib/Utils'
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        },
+        pageData: {
+            type: Object,
+            default: () => {}
+        },
+        positionList: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data () {
+        const isCreate = this.$utils.isEmpty(this.pageData)
+        const dateRangeList = [{
+            type: 'range',
+            startTime: '',
+            endTime: '',
+            isSecondDay: 'N',
+            pickerOptions: {
+                start: '06:30',
+                step: '00:30',
+                end: '23:30'
+                // disabledDate (time) {
+                //     // 禁用当前日期之前的日期
+                //     return time.getTime() < Date.now() - 8.64e7
+                // }
+            }
+        }]
+        return {
+            dateRangeList,
+            reagentType: [],
+            dialogVisible: this.visible,
+            formLabelWidth: '100px',
+            title: isCreate ? '新增班次' : '编辑班次',
+            formData: !isCreate ? JSON.parse(JSON.stringify(this.pageData.row)) : {
+                isEnabled: 'Y',
+                positions: '',
+                color: '',
+                desc: '',
+                dateRange: { ...dateRangeList }
+            },
+            rules: {
+                name: [{ required: true, message: '请输入班次名称', trigger: 'change' }],
+                alias: [{ required: true, message: '请输入班次别名', trigger: 'change' }]
+            },
+            predefine: setting.color.predefine,
+            toolbars: [
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'primary', hidden: () => { return this.readonly } },
+                { key: 'continue', icon: 'ibps-icon-send', label: '保存并继续', type: 'success', hidden: () => { return this.readonly || !isCreate } },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ]
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = val
+            },
+            immediate: true
+        },
+        pageData: {
+            handler (val, oldVal) {
+                this.isCreate = this.$utils.isEmpty(val)
+                this.title = this.isCreate ? '新增班次' : '编辑班次'
+                this.toolbars[1] = {
+                    ...this.toolbars[1],
+                    hidden: () => { return this.readonly || !this.isCreate }
+                }
+                this.formData = !this.isCreate ? JSON.parse(JSON.stringify(val.row)) : {
+                    isEnabled: 'Y',
+                    dateRange: JSON.parse(JSON.stringify(this.dateRangeList))
+                }
+            },
+            immediate: true
+        }
+    },
+    methods: {
+        addOption () {
+            this.formData.dateRange.push({
+                type: 'range',
+                startTime: '',
+                endTime: '',
+                isSecondDay: 'N',
+                pickerOptions: {
+                    start: '06:30',
+                    step: '00:30',
+                    end: '23:30'
+                }
+            })
+        },
+        subOption () {
+            this.formData.dateRange.pop()
+        },
+        handleActionEvent ({ key }) {
+            switch (key) {
+                case 'save':
+                    this.handleSave(key)
+                    break
+                case 'continue':
+                    this.handleSave(key)
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        handleSave (key) {
+            this.$refs.form.validate((valid) => {
+                if (!valid) {
+                    return this.$message.warning('请填写所有必填项!')
+                }
+                const { color, dateRange } = this.formData
+                if (!color || dateRange.some(i => i.type === 'range' && !(i.startTime && i.endTime))) {
+                    return this.$message.warning('请填写所有必填项!')
+                }
+                this.submitForm(this.formData, key)
+            })
+        },
+        submitForm (data, key) {
+            const submitData = { ...data }
+            console.log(submitData)
+            this.$emit('callback', { data: submitData, index: this.pageData.index })
+            if (key === 'save') {
+                return this.closeDialog()
+            }
+            this.$refs.form.resetFields()
+            this.formData = {
+                isEnabled: 'Y',
+                dateRange: JSON.parse(JSON.stringify(this.dateRangeList))
+            }
+        },
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .classes-dialog {
+        .classes-form {
+            padding: 20px;
+            ::v-deep {
+                .el-form-item {
+                    &__label {
+                        position: relative;
+                        &:before {
+                            position: absolute;
+                            left: -8px;
+                        }
+                    }
+                    margin-bottom: 16px !important;
+                    &:last-child {
+                        margin-bottom: 0 !important;
+                    }
+                    &__error {
+                        padding-top: 8px !important;
+                    }
+                    .el-input {
+                        // width: calc(100% - 100px);
+                        width: 100%;
+                    }
+                    .el-select {
+                        width: 100%;
+                    }
+                }
+            }
+            .date-range {
+                ::v-deep {
+                    .el-form-item__content {
+                        display: flex;
+                        .date-type {
+                            flex-shrink: 0;
+                        }
+                        .range-item {
+                            flex-shrink: 0;
+                            display: flex;
+                            align-items: center;
+                            margin: 0 20px;
+                            .concat {
+                                margin: 0 20px;
+                            }
+                            .end {
+                                display: flex;
+                                align-items: center;
+                                .el-switch {
+                                    width: 150px;
+                                    flex-shrink: 0;
+                                }
+                            }
+                            .date-picker {
+                                max-width: 120px;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 584 - 0
src/views/business/​scheduleManage/components/adjust-edit.vue

@@ -0,0 +1,584 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        class="dialog adjust-dialog"
+        top="5vh"
+        width="60%"
+        :title="title"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <el-form
+            ref="adjustForm"
+            :label-width="formLabelWidth"
+            label-position="left"
+            :model.sync="formData"
+            :rules="rules"
+            class="adjust-form"
+            @submit.native.prevent
+        >
+            <el-form-item label="选择排班" prop="scheduleId">
+                <el-select
+                    v-model="formData.scheduleId"
+                    :disabled="readonly"
+                    filterable
+                    clearable
+                    placeholder="请选择排班"
+                    @change="handleScheduleChange"
+                >
+                    <el-option
+                        v-for="item in scheduleOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="调班原因" prop="reason">
+                <el-input
+                    v-model="formData.reason"
+                    type="textarea"
+                    :rows="4"
+                    clearable
+                    show-word-limit
+                    :maxlength="1024"
+                    :disabled="readonly"
+                    placeholder="请输入调班原因"
+                />
+            </el-form-item>
+            <el-form-item label="调班班次">
+                <div v-if="!readonly" class="operate-btn">
+                    <el-button
+                        v-for="btn in tableToolbars"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        plain
+                        @click="handleTableAction(btn.key)"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </div>
+                <el-table
+                    ref="adjustTable"
+                    :data="formData.adjustList"
+                    border
+                    stripe
+                    highlight-current-row
+                    style="width: 100%"
+                    :max-height="maxHeight"
+                    class="adjust-table"
+                    @selection-change="selection => handleSelectionChange(selection)"
+                >
+                    <el-table-column type="selection" width="45" header-align="center" align="center" />
+                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                    <el-table-column
+                        prop="beforeDate"
+                        label="调班日期"
+                        width="150"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <el-date-picker
+                                v-model="scope.row.beforeDate"
+                                type="date"
+                                placeholder="请选择"
+                                value-format="yyyy-MM-dd"
+                                :picker-options="pickerOptions"
+                                :disabled="readonly"
+                                @change="handleDateChange($event, scope.$index, 'beforeShiftList')"
+                            />
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        prop="beforeAdjust"
+                        label="调班班次"
+                        min-width="150"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <el-select
+                                v-model="scope.row.beforeAdjust"
+                                :disabled="readonly"
+                                multiple
+                                filterable
+                                placeholder="请选择调班班次"
+                            >
+                                <el-option
+                                    v-for="item in scope.row.beforeShiftList"
+                                    :key="item.alias"
+                                    :label="item.name"
+                                    :value="item.alias"
+                                />
+                            </el-select>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        prop="party"
+                        label="目标人员"
+                        width="130"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <el-select
+                                v-model="scope.row.party"
+                                :disabled="readonly"
+                                filterable
+                                placeholder="请选择目标人员"
+                                @change="handlePartyChange($event, scope.row, scope.$index)"
+                            >
+                                <el-option
+                                    v-for="item in scheduleInfo.scheduleStaff"
+                                    :key="item.userId"
+                                    :label="item.userName"
+                                    :value="item.userId"
+                                />
+                            </el-select>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        prop="afterDate"
+                        label="目标日期"
+                        width="150"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <el-date-picker
+                                v-model="scope.row.afterDate"
+                                type="date"
+                                placeholder="请选择"
+                                value-format="yyyy-MM-dd"
+                                :picker-options="pickerOptions"
+                                :disabled="readonly"
+                                @change="handleDateChange($event, scope.$index, 'afterShiftList')"
+                            />
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        prop="afterAdjust"
+                        label="目标班次"
+                        min-width="150"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <el-select
+                                v-model="scope.row.afterAdjust"
+                                :disabled="readonly"
+                                multiple
+                                filterable
+                                placeholder="请选择目标班次"
+                            >
+                                <el-option
+                                    v-for="item in scope.row.afterShiftList"
+                                    :key="item.alias"
+                                    :label="item.name"
+                                    :value="item.alias"
+                                />
+                            </el-select>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="el-dialog--center">
+            <ibps-toolbar :actions="toolbars" @action-event="handleFormAction" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+// import {  } from '@/views/constants/schedule'
+import { getAdjustment, saveAdjustment, queryStaffSchedule, getStaffSchedule } from '@/api/business/schedule'
+
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        params: {
+            type: Object,
+            default: () => {}
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { isSuper, userId, userList = [], deptList = [] } = this.$store.getters || {}
+        return {
+            isSuper,
+            userId,
+            userList,
+            deptList,
+            title: '调班申请',
+            maxHeight: document.body.clientHeight - 438 + 'px',
+            dialogVisible: this.visible,
+            formLabelWidth: '110px',
+            formData: {
+                scheduleId: '',
+                reason: '',
+                adjustList: []
+            },
+            rules: {},
+            loading: false,
+            scheduleList: [],
+            scheduleInfo: {},
+            currentShift: {},
+            targetShift: {},
+            scheduleOptions: [],
+            selectionIndex: [],
+            toolbars: [
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'success' },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ],
+            tableToolbars: [
+                { key: 'add', icon: 'ibps-icon-plus', label: '添加', type: 'success' },
+                { key: 'remove', icon: 'ibps-icon-trash', label: '删除', type: 'danger' }
+            ],
+            pickerOptions: {}
+        }
+    },
+    computed: {
+        beforeShiftList () {
+            return []
+        },
+        afterShiftList () {
+            return []
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+    },
+    methods: {
+        // 获取数据
+        async loadData () {
+            const { id, scheduleId } = this.params || {}
+            this.formData.scheduleId = scheduleId
+            if (this.$utils.isEmpty(scheduleId)) {
+                await this.getScheduleList()
+            } else {
+                await this.getScheduleInfo(scheduleId)
+            }
+            if (this.$utils.isEmpty(id)) {
+                return
+            }
+            // 初始化表单数据的方法
+            const initializeFormData = (data) => {
+                const { scheduleId, reason, executor, executeDate, adjustmentDetailPoList } = data || {}
+                this.formData = {
+                    scheduleId,
+                    reason,
+                    adjustList: adjustmentDetailPoList.map((i, index) => ({
+                        ...i,
+                        beforeAdjust: i.beforeAdjust ? i.beforeAdjust.split(',') : [],
+                        afterAdjust: i.afterAdjust ? i.afterAdjust.split(',') : [],
+                        beforeShiftList: this.handleDateChange(i.beforeDate, index, 'beforeShiftList'),
+                        afterShiftList: this.handleDateChange(i.afterDate, index, 'afterShiftList')
+                    }))
+                }
+                console.log(this.formData)
+            }
+            this.loading = true
+            try {
+                const res = await getAdjustment({ id: this.params.id })
+                console.log(res)
+                if (res.data) {
+                    initializeFormData(res.data)
+                }
+            } catch (error) {
+                console.error('加载排班配置失败', error)
+            } finally {
+                this.loading = false
+            }
+        },
+        getScheduleList () {
+            const params = {
+                parameters: [],
+                requestPage: {
+                    pageNo: 1,
+                    limit: 99999
+                },
+                sorts: []
+            }
+            queryStaffSchedule(params).then(res => {
+                this.scheduleList = res.data.dataResult
+                this.scheduleOptions = this.scheduleList.map(s => ({
+                    label: s.title,
+                    value: s.id
+                }))
+            })
+        },
+        getScheduleInfo (id) {
+            getStaffSchedule({ id }).then(res => {
+                const { data } = res
+                this.scheduleList = [data]
+                const config = data.config ? JSON.parse(data.config) : {}
+                this.scheduleInfo = {
+                    startDate: data.startDate,
+                    scheduleShift: config.scheduleShift,
+                    scheduleStaff: this.userList.filter(u => config.scheduleStaff.includes(u.userId) && u.userId !== this.userId),
+                    shiftList: data.staffScheduleDetailPoList
+                }
+                this.scheduleOptions = this.scheduleList.map(s => ({
+                    label: s.title,
+                    value: s.id
+                }))
+                console.log(this.scheduleInfo)
+                this.handleScheduleChange(id)
+            })
+        },
+        handleScheduleChange (val) {
+            const schedule = this.scheduleList.find(i => i.id === val)
+            if (this.$utils.isEmpty(schedule)) {
+                return this.$message.error('数据有误!所选排班不存在!')
+            }
+            const config = schedule.config ? JSON.parse(schedule.config) : {}
+            this.scheduleInfo = {
+                startDate: schedule.startDate,
+                scheduleShift: config.scheduleShift,
+                scheduleStaff: this.userList.filter(u => config.scheduleStaff.includes(u.userId) && u.userId !== this.userId),
+                shiftList: schedule.staffScheduleDetailPoList
+            }
+            this.currentShift = this.scheduleInfo.shiftList.find(s => s.userId === this.userId)
+            if (!this.currentShift) {
+                return this.$message.warning('所选排班中未含有您的信息,请重新选择!')
+            }
+            this.$set(this, 'pickerOptions', {
+                disableDate: (time) => {
+                    const startDate = new Date(schedule.startDate)
+                    const endDate = new Date(schedule.endDate)
+                    return time < startDate || time > endDate
+                }
+            })
+            this.formData.adjustList = []
+            // this.pickerOptions = {
+            //     disableDate: (time) => {
+            //         const startDate = new Date(schedule.startDate)
+            //         const endDate = new Date(schedule.endDate)
+            //         console.log('startDate:', startDate, 'endDate:', endDate, 'time:', time);
+            //         return time < startDate || time > endDate
+            //     }
+            // }
+        },
+        getDays (start, end) {
+            if (!start || !end) {
+                return 0
+            }
+            return Math.ceil((new Date(end) - new Date(start)) / (1000 * 60 * 60 * 24))
+        },
+        handleDateChange (val, index, type) {
+            console.log(val, index, type)
+            const typeMap = {
+                beforeShiftList: this.currentShift,
+                afterShiftList: this.targetShift
+            }
+            const shiftMap = {
+                beforeShiftList: 'beforeAdjust',
+                afterShiftList: 'afterAdjust'
+            }
+            if (!val) {
+                // 清除日期时,同步清除班次及option
+                this.formData.adjustList[index][type] = []
+                this.formData.adjustList[index][shiftMap[type]] = []
+                return
+            }
+            const { startDate, scheduleShift } = this.scheduleInfo
+            // 获取所选日期对应数据
+            const dayIndex = this.getDays(startDate, val)
+            const shifts = typeMap[type][`d${dayIndex}`] ? typeMap[type][`d${dayIndex}`].split(',') : []
+            const temp = scheduleShift.filter(i => shifts.includes(i.alias))
+            console.log(this.formData.adjustList)
+            this.formData.adjustList[index].recordId = this.currentShift.id
+            this.formData.adjustList[index][type] = temp
+        },
+        handlePartyChange (val, row, index) {
+            this.targetShift = this.scheduleInfo.shiftList.find(i => i.userId === val)
+            this.handleDateChange(row.startDate, index, 'afterShiftList')
+        },
+        handleFormAction ({ key }) {
+            switch (key) {
+                case 'save':
+                    this.handleSave()
+                    break
+                case 'cancel':
+                    this.handleCancel()
+                    break
+                default:
+                    break
+            }
+        },
+        handleTableAction (key, type, data) {
+            switch (key) {
+                case 'add':
+                    this.handleAddParam(type)
+                    break
+                case 'edit':
+                    this.handleAddParam(type, data)
+                    break
+                case 'remove':
+                    if (!this.selectionIndex.length) {
+                        return this.$message.warning('请选择要删除的数据')
+                    }
+                    this.handleRemove(this.selectionIndex, type)
+                    break
+                default:
+                    break
+            }
+        },
+        handleSave () {
+            const getOverview = (data) => {
+                const result = []
+                data.forEach(i => {
+                    const partyName = this.transformData(this.userList, i.party, 'userId', 'userName')
+                    const beforeAdjust = i.beforeAdjust.map(item => `【${item}】`).join('')
+                    const afterAdjust = i.afterAdjust.map(item => `【${item}】`).join('')
+                    let desc = ''
+                    if (i.party) {
+                        desc = `${i.beforeDate}班次${beforeAdjust}与${partyName}${i.afterDate}班次${afterAdjust}调换`
+                    } else {
+                        desc = `${i.beforeDate}班次${beforeAdjust}调换到${i.afterDate}`
+                    }
+                    result.push(desc)
+                })
+                return result.join('\n')
+            }
+            this.$refs.adjustForm.validate((valid) => {
+                if (!valid) {
+                    return this.$message.warning('请完善表单必填项信息!')
+                }
+                const { first, second } = this.$store.getters.level || {}
+                const { scheduleId, reason, adjustList } = this.formData || {}
+                // return
+                const submitData = {
+                    id: this.params.id,
+                    pk: this.params.id,
+                    scheduleId,
+                    reason,
+                    diDian: second || first,
+                    overview: getOverview(adjustList),
+                    status: adjustList.some(i => this.$utils.isNotEmpty(i.party)) ? '待审核' : '待审批',
+                    adjustmentDetailPoList: adjustList.map(i => ({
+                        recordId: i.recordId,
+                        beforeDate: i.beforeDate,
+                        beforeAdjust: i.beforeAdjust ? i.beforeAdjust.join(',') : '',
+                        party: i.party,
+                        status: i.party ? '待审核' : '待审批',
+                        afterDate: i.afterDate,
+                        afterAdjust: i.afterAdjust ? i.afterAdjust.join(',') : ''
+                    }))
+                }
+                this.submitForm(submitData)
+            })
+        },
+        // 提交数据
+        submitForm (data) {
+            saveAdjustment(data).then(res => {
+                this.$message.success(`${this.params.action === 'edit' ? '保存' : '申请'}成功`)
+                this.closeDialog()
+                this.$emit('refresh')
+            })
+        },
+        handleAddParam () {
+            this.formData.adjustList.push({
+                recordId: '',
+                beforeDate: '',
+                beforeAdjust: '',
+                beforeShiftList: [],
+                afterDate: '',
+                afterAdjust: '',
+                afterShiftList: [],
+                status: '',
+                party: ''
+            })
+        },
+        handleSelectionChange (v) {
+            console.log(v)
+            // this.selectionIndex = v.map(item => this.formData.indexOf(item))
+        },
+        handleRemove (removeIndex, type) {
+            let indexList = []
+            if (typeof removeIndex === 'number') {
+                indexList = [removeIndex]
+            } else {
+                indexList = removeIndex
+            }
+            indexList.sort((a, b) => b - a)
+            this.$confirm('确定要删除选中数据吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                indexList.forEach(i => {
+                    this.formData.splice(i, 1)
+                })
+            }).catch(() => {})
+        },
+        transformData (dataset, data, from, to) {
+            if (!data) {
+                return ''
+            }
+            const list = data.split(',')
+            const names = list.map(item => {
+                const temp = dataset.find(i => i[from] === item)
+                return temp ? temp[to] : ''
+            })
+            return names.filter(i => i).join(',')
+        },
+        handleCancel () {
+            this.closeDialog()
+        },
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .adjust-dialog {
+        ::v-deep {
+            .el-dialog {
+                min-width: 1024px;
+                &__header {
+                    padding: 15px 20px 16px;
+                }
+            }
+            .adjust-table {
+                .el-input, .el-select {
+                    width: 100%;
+                }
+            }
+        }
+        .adjust-form {
+            padding: 20px;
+            background: #f5f5f5;
+            border-radius: 4px;
+            overflow: hidden;
+            min-Height: 400px;
+            height: 60vh;
+            .operate-btn {
+                text-align: right;
+                margin-bottom: 5px;
+            }
+        }
+    }
+</style>

+ 255 - 0
src/views/business/​scheduleManage/components/config-list.vue

@@ -0,0 +1,255 @@
+<template>
+    <el-dialog
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="true"
+        append-to-body
+        class="dialog schedule-config-list"
+        top="5vh"
+        width="60%"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <ibps-crud
+            ref="crud"
+            :display-field="title"
+            :height="maxHeight"
+            :data="listData"
+            :toolbars="listConfig.toolbars"
+            :search-form="listConfig.searchForm"
+            :pk-key="pkKey"
+            :index-row="false"
+            :columns="listConfig.columns"
+            :row-handle="listConfig.rowHandle"
+            :header-cell-style="{ 'text-align': 'center' }"
+            :cell-style="{ 'text-align': 'center' }"
+            :pagination="pagination"
+            :loading="loading"
+            @action-event="handleAction"
+            @sort-change="handleSortChange"
+            @pagination-change="handlePaginationChange"
+            @row-dblclick="handleRowDblclick"
+        >
+            <template slot="isEffective" slot-scope="scope">
+                <el-switch
+                    v-model="scope.row.isEffective"
+                    active-value="Y"
+                    inactive-value="N"
+                    @change="handleEffective(scope.row)"
+                />
+            </template>
+        </ibps-crud>
+        <schedule-config-detail
+            v-if="showConfigDetail"
+            :visible.sync="showConfigDetail"
+            :params="params"
+            :readonly="readonly"
+            @refresh="loadData"
+            @close="() => showConfigDetail = false"
+        />
+    </el-dialog>
+</template>
+
+<script>
+import { queryScheduleConfig, removeScheduleConfig, saveScheduleConfig } from '@/api/business/schedule'
+import { scheduleType } from '@/views/constants/schedule'
+import ActionUtils from '@/utils/action'
+import FixHeight from '@/mixins/height'
+
+export default {
+    components: {
+        ScheduleConfigDetail: () => import('./config')
+    },
+    mixins: [FixHeight],
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { userList = [] } = this.$store.getters || {}
+        const userOption = userList.map(item => ({ label: item.userName, value: item.userId }))
+        return {
+            userOption,
+            scheduleType,
+            title: '排班配置项',
+            dialogVisible: this.visible,
+            pkKey: 'id', // 主键  如果主键不是pk需要传主键
+            loading: true,
+            maxHeight: 600,
+            listData: [],
+            pagination: {},
+            sorts: {},
+            showConfigDetail: false,
+            readonly: false,
+            params: {},
+            listConfig: {
+                toolbars: [
+                    { key: 'search', icon: 'ibps-icon-search', label: '查询', type: 'primary', hidden: false },
+                    { key: 'create', icon: 'ibps-icon-plus', label: '添加', type: 'success', hidden: false },
+                    { key: 'remove', icon: 'ibps-icon-close', label: '删除', type: 'danger', hidden: false }
+                ],
+                searchForm: {
+                    labelWidth: 80,
+                    itemWidth: 150,
+                    forms: [
+                        { prop: 'Q^title_^SL', label: '配置项名称' },
+                        { prop: 'Q^schedule_type_^SL', label: '排班类型', fieldType: 'select', options: scheduleType },
+                        { prop: ['Q^create_time_^DL', 'Q^create_time_^DG'], label: '创建时间', fieldType: 'daterange', itemWidth: 200 }
+                    ]
+                },
+                // 表格字段配置
+                columns: [
+                    { prop: 'name', label: '配置名称', width: 150 },
+                    { prop: 'scheduleType', label: '排班类型', tags: scheduleType, width: 100, sortable: 'custom' },
+                    { prop: 'isEffective', label: '是否生效', slotName: 'isEffective', width: 100 },
+                    { prop: 'approver', label: '调班审批人', tags: userOption, dataType: 'stringArray', minWidth: 140 },
+                    { prop: 'createBy', label: '创建人', tags: userOption, width: 90 },
+                    { prop: 'createTime', label: '创建时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 140 }
+                ],
+                rowHandle: {
+                    effect: 'display',
+                    actions: [
+                        { key: 'copy', label: '复制', type: 'primary', icon: 'ibps-icon-copy' },
+                        { key: 'edit', label: '编辑', type: 'primary', icon: 'ibps-icon-edit' }
+                        // { key: 'detail', label: '详情', type: 'primary', icon: 'ibps-icon-list-alt' }
+                    ]
+                }
+            }
+        }
+    },
+    created () {
+        this.loadData()
+    },
+    methods: {
+        // 加载数据
+        loadData () {
+            this.loading = true
+            queryScheduleConfig(this.getSearchFormData()).then(res => {
+                ActionUtils.handleListData(this, res.data)
+                this.loading = false
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        /**
+         * 获取格式化参数
+         */
+        getSearchFormData () {
+            console.log(this.pagination)
+            return ActionUtils.formatParams(
+                this.$refs['crud'] ? this.$refs['crud'].getSearcFormData() : {},
+                this.pagination,
+                this.sorts
+            )
+        },
+        /**
+         * 处理分页事件
+         */
+        handlePaginationChange (page) {
+            ActionUtils.setPagination(this.pagination, page)
+            this.loadData()
+        },
+        /**
+         * 处理排序
+         */
+        handleSortChange (sort) {
+            ActionUtils.setSorts(this.sorts, sort)
+            this.loadData()
+        },
+        /**
+         * 查询
+         */
+        search () {
+            this.loadData()
+        },
+        /**
+         * 处理按钮事件
+         */
+        handleAction (command, position, selection, data) {
+            switch (command) {
+                case 'search':
+                    ActionUtils.setFirstPagination(this.pagination)
+                    this.search()
+                    break
+                case 'create':
+                    this.handleEdit({}, command)
+                    break
+                case 'copy':
+                    this.handleEdit(data, command)
+                    break
+                case 'edit':
+                    this.handleEdit(data, command)
+                    break
+                case 'detail':
+                    this.handleEdit(data, command)
+                    break
+                case 'remove':
+                    ActionUtils.removeRecord(selection).then((ids) => {
+                        this.handleRemove(ids)
+                    }).catch(() => {})
+                    break
+                default:
+                    break
+            }
+        },
+        /**
+         * 处理编辑
+         */
+        async handleEdit (data, key) {
+            this.params = {
+                configId: data.id,
+                action: key
+            }
+            this.readonly = key === 'detail'
+            this.showConfigDetail = true
+        },
+        /**
+         * 处理删除
+         */
+        handleRemove (ids) {
+            // return this.$message.warning('避免误删测试数据,联系开发删除')
+            removeScheduleConfig({ ids }).then(() => {
+                ActionUtils.removeSuccessMessage()
+                this.search()
+            }).catch(() => {})
+        },
+        handleRowDblclick (row) {
+            this.handleEdit(row, 'detail')
+        },
+        handleEffective (row) {
+            const { id, isEffective, name } = row
+            console.log(isEffective)
+            this.$confirm(`确定要${isEffective ? '启用' : '禁用'}配置项【${name}】吗?`, '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                showClose: false,
+                closeOnClickModal: false,
+                type: 'warning'
+            }).then(() => {
+                saveScheduleConfig({
+                    ...row,
+                    id,
+                    pk: id,
+                    isEffective
+                }).then(() => {
+                    ActionUtils.success('操作成功')
+                    this.search()
+                }).catch(() => {})
+            }).catch(() => {
+                this.search()
+            })
+        },
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss">
+    .schedule-config-list {
+
+    }
+</style>

+ 655 - 0
src/views/business/​scheduleManage/components/config.vue

@@ -0,0 +1,655 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog schedule-config-dialog"
+        top="0"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div slot="title" class="config-dialog-header">
+            <div class="title">{{ title }}</div>
+            <div class="operate">
+                <template v-for="btn in toolbars">
+                    <el-button
+                        v-if="!btn.hidden"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        @click="handleActionEvent(btn.key)"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </template>
+            </div>
+        </div>
+        <el-form
+            ref="configForm"
+            :label-width="formLabelWidth"
+            label-position="left"
+            :model.sync="formData"
+            :rules="rules"
+            class="config-form"
+            @submit.native.prevent
+        >
+            <div class="config-item">
+                <div class="title">
+                    <i class="ibps-icon-star" />
+                    <span>基础配置</span>
+                </div>
+                <div class="form-container">
+                    <el-row :gutter="20" class="form-row">
+                        <el-col :span="12">
+                            <el-form-item label="配置名称" prop="name" required :show-message="false">
+                                <el-input
+                                    v-model="formData.name"
+                                    type="text"
+                                    clearable
+                                    show-word-limit
+                                    :maxlength="16"
+                                    :disabled="readonly"
+                                    placeholder="请输入"
+                                />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="20" class="form-row">
+                        <el-col :span="12">
+                            <el-form-item label="排班类型" prop="scheduleType" required :show-message="false">
+                                <el-select
+                                    v-model="formData.scheduleType"
+                                    :disabled="readonly"
+                                    clearable
+                                    filterable
+                                    placeholder="请选择排班类型"
+                                >
+                                    <el-option
+                                        v-for="item in scheduleType"
+                                        :key="item.value"
+                                        :label="item.label"
+                                        :value="item.value"
+                                    />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="是否生效" prop="isEffective" :show-message="false">
+                                <el-switch v-model="formData.isEffective" active-value="Y" inactive-value="N" />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="20" class="form-row">
+                        <el-col :span="24">
+                            <el-form-item label="排班人员" prop="scheduleStaff" :show-message="false">
+                                <!-- collapse-tags -->
+                                <el-cascader
+                                    ref="myCascader"
+                                    v-model="formData.scheduleStaff"
+                                    :options="getCascaderOptions()"
+                                    :show-all-levels="false"
+                                    clearable
+                                    :props="{
+                                        value: 'value',
+                                        label: 'label',
+                                        multiple: true,
+                                        checkStrictly: false,
+                                        emitPath: false
+                                    }"
+                                    @change="handleStaffChange"
+                                />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="20" class="form-row">
+                        <el-col :span="12">
+                            <el-form-item prop="isApproval" required>
+                                <template slot="label">
+                                    是否审批
+                                    <el-tooltip effect="dark" content="设置调班申请是否需要额外审批,若设置为否,则调班申请仅需当事人同意即可生效" placement="top">
+                                        <i class="el-icon-question question-icon" />
+                                    </el-tooltip>
+                                </template>
+                                <el-switch v-model="formData.isApproval" active-value="Y" inactive-value="N" />
+                            </el-form-item>
+                        </el-col>
+                        <el-col v-if="formData.isApproval === 'Y'" :span="12">
+                            <el-form-item label="调班审批人" prop="approver" required :show-message="false">
+                                <el-select
+                                    v-model="formData.approver"
+                                    :disabled="readonly"
+                                    multiple
+                                    filterable
+                                    placeholder="请选择"
+                                >
+                                    <el-option
+                                        v-for="item in userList"
+                                        :key="item.userId"
+                                        :label="item.userName"
+                                        :value="item.userId"
+                                    />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+            <div class="config-item">
+                <div class="title">
+                    <i class="ibps-icon-star" />
+                    <span>班次配置</span>
+                </div>
+                <div v-if="!readonly" class="operate-btn">
+                    <el-button
+                        v-for="btn in tableToolbars"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        plain
+                        @click="handleActionEvent(btn.key, 'scheduleShift')"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </div>
+                <el-table
+                    ref="scheduleTable"
+                    :data="formData.scheduleShift"
+                    border
+                    stripe
+                    highlight-current-row
+                    style="width: 100%"
+                    :max-height="maxHeight"
+                    class="schedule-table"
+                    @selection-change="selection => handleSelectionChange(selection, 'scheduleShift')"
+                >
+                    <el-table-column type="selection" width="45" header-align="center" align="center" />
+                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                    <el-table-column
+                        v-for="(item, fIndex) in scheduleColumn"
+                        :key="fIndex"
+                        :prop="item.key"
+                        :label="item.label"
+                        :width="item.width"
+                        :min-width="item.minWidth"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <template v-if="item.key === 'dateRange'">
+                                <div v-for="(d, di) in scope.row.dateRange" :key="di">
+                                    {{ d.type === 'allday' ? '全天' : (`当天 ${d.startTime}` + ' 至 ' + `${d.isSecondDay === 'Y' ? '第二天' : '当天'} ${d.endTime}`) }}
+                                </div>
+                            </template>
+                            <div v-else-if="item.key === 'positions'">
+                                <el-tag
+                                    v-for="(p, pi) in scope.row.positions"
+                                    :key="pi"
+                                    type="primary"
+                                    style="margin-left: 5px;"
+                                >{{ p }}</el-tag>
+                            </div>
+                            <div v-else-if="item.key === 'isEnabled'">{{ scope.row.isEnabled === 'Y' ? '是' : '否' }}</div>
+                            <div v-else-if="item.key === 'color'">
+                                <div class="color-item" :style="`background-color: ${scope.row.color}; width: 20px; height: 20px; margin: 0 auto; border-radius: 2px;`" />
+                            </div>
+                            <div v-else>
+                                <span>{{ scope.row[item.key] }}</span>
+                            </div>
+                        </template>
+                    </el-table-column>
+                    <el-table-column
+                        v-if="!readonly"
+                        fixed="right"
+                        label="操作"
+                        width="50"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <a><i class="el-icon-edit" @click="handleActionEvent('edit', 'scheduleShift', { row: scope.row, index: scope.$index})" /></a>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+            <div class="config-item">
+                <div class="title">
+                    <i class="ibps-icon-star" />
+                    <span>规则配置</span>
+                </div>
+                <div v-if="!readonly" class="operate-btn">
+                    <el-button
+                        v-for="btn in tableToolbars"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        plain
+                        @click="handleActionEvent(btn.key, 'scheduleRule')"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </div>
+                <el-table
+                    ref="ruleTable"
+                    :data="formData.scheduleRule"
+                    border
+                    stripe
+                    highlight-current-row
+                    style="width: 100%"
+                    :max-height="maxHeight"
+                    class="rule-table"
+                    @selection-change="selection => handleSelectionChange(selection, 'scheduleRule')"
+                >
+                    <el-table-column type="selection" width="45" header-align="center" align="center" />
+                    <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                    <el-table-column
+                        v-for="(item, fIndex) in ruleColumn"
+                        :key="fIndex"
+                        :prop="item.key"
+                        :label="item.label"
+                        :width="item.width"
+                        :min-width="item.minWidth"
+                        header-align="center"
+                        align="center"
+                    >
+                        <template slot-scope="scope">
+                            <el-switch
+                                v-if="item.type === 'switch'"
+                                v-model="scope.row[item.key]"
+                                :disabled="readonly"
+                            />
+                            <el-input
+                                v-else
+                                v-model="scope.row[item.key]"
+                                :disabled="readonly"
+                            />
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+        </el-form>
+        <add-classes
+            :visible.sync="showSchedule"
+            :page-data="scheduleData"
+            :readonly="readonly"
+            :position-list="positionList"
+            @callback="handleRowData"
+            @close="() => showSchedule = false"
+        />
+    </el-dialog>
+</template>
+
+<script>
+import { configFormRules, scheduleColumn, ruleColumn, scheduleType } from '@/views/constants/schedule'
+import { getScheduleConfig, saveScheduleConfig } from '@/api/business/schedule'
+
+export default {
+    components: {
+        AddClasses: () => import('./add-classes.vue')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        params: {
+            type: Object,
+            default: () => {}
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { isSuper, userList = [], deptList = [] } = this.$store.getters || {}
+        return {
+            isSuper,
+            userList,
+            deptList,
+            scheduleColumn,
+            ruleColumn,
+            scheduleType,
+            title: '排班信息配置',
+            maxHeight: document.body.clientHeight - 438 + 'px',
+            dialogVisible: this.visible,
+            formLabelWidth: '110px',
+            formData: {
+                scheduleShift: [],
+                scheduleRule: [],
+                isEffective: 'Y',
+                isApproval: 'Y'
+            },
+            rules: configFormRules,
+            loading: false,
+            showSchedule: false,
+            selectionIndex: {
+                scheduleShift: [],
+                scheduleRule: []
+            },
+            scheduleData: {},
+            toolbars: [
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'success' },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ],
+            tableToolbars: [
+                { key: 'add', icon: 'ibps-icon-plus', label: '添加', type: 'success' },
+                { key: 'remove', icon: 'ibps-icon-trash', label: '删除', type: 'danger' }
+            ],
+            positionList: []
+        }
+    },
+    computed: {
+
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.dialogVisible = this.visible
+            }
+        }
+    },
+    mounted () {
+        this.loadData()
+        this.getPositionLis()
+    },
+    methods: {
+        // 获取数据
+        async loadData () {
+            if (this.$utils.isEmpty(this.params.configId)) {
+                return
+            }
+            // 初始化表单数据的方法
+            const initializeFormData = (data) => {
+                const { name, approver, isApproval, isEffective, scheduleRule, scheduleShift, scheduleStaff, scheduleType } = data || {}
+                this.formData = {
+                    name,
+                    approver: approver ? approver.split(',') : [],
+                    scheduleType,
+                    isApproval,
+                    isEffective,
+                    scheduleRule: scheduleRule ? JSON.parse(scheduleRule) : [],
+                    scheduleShift: scheduleShift ? JSON.parse(scheduleShift) : [],
+                    scheduleStaff: scheduleStaff ? JSON.parse(scheduleStaff) : []
+                }
+                this.handleStaffChange(this.formData.scheduleStaff)
+            }
+            this.loading = true
+            try {
+                const res = await getScheduleConfig({ id: this.params.configId })
+                if (res.data) {
+                    initializeFormData(res.data)
+                }
+            } catch (error) {
+                console.error('加载排班配置失败', error)
+            } finally {
+                this.loading = false
+            }
+        },
+        getPositionLis () {
+            const { first, second } = this.$store.getters.level || {}
+            const sql = `select suo_shu_bu_men_ as dept, wei_hu_gang_wei_ as positionName, id_ as positionId from t_sbwhgwpzb where di_dian_ = '${second || first}'`
+            this.$common.request('sql', sql).then(res => {
+                this.positionList = res.variables.data || []
+            })
+        },
+        handleActionEvent (key, type, data) {
+            switch (key) {
+                case 'save':
+                    this.handleSave()
+                    break
+                case 'cancel':
+                    this.handleCancel()
+                    break
+                case 'add':
+                    this.handleAddParam(type)
+                    break
+                case 'edit':
+                    this.handleAddParam(type, data)
+                    break
+                case 'remove':
+                    if (!this.selectionIndex[type].length) {
+                        return this.$message.warning('请选择要删除的数据')
+                    }
+                    this.handleRemove(this.selectionIndex[type], type)
+                    break
+                default:
+                    break
+            }
+        },
+        handleSave () {
+            this.$refs.configForm.validate((valid) => {
+                if (!valid) {
+                    return this.$message.warning('请完善表单必填项信息!')
+                }
+                const { first, second } = this.$store.getters.level || {}
+                const { scheduleStaff, scheduleRule, scheduleShift, approver, ...rest } = this.formData || {}
+                const submitData = {
+                    ...rest,
+                    id: this.params.action === 'copy' ? null : this.params.configId,
+                    diDian: second || first,
+                    approver: approver.join(','),
+                    scheduleStaff: JSON.stringify(scheduleStaff),
+                    scheduleRule: JSON.stringify(scheduleRule),
+                    scheduleShift: JSON.stringify(scheduleShift)
+                }
+                this.submitForm(submitData)
+            })
+        },
+        // 提交数据
+        submitForm (data) {
+            saveScheduleConfig(data).then(res => {
+                this.$message.success(`${this.params.action === 'edit' ? '保存' : '添加'}成功`)
+                this.closeDialog()
+                this.$emit('refresh')
+            })
+        },
+        handleAddParam (type, data = {}) {
+            if (type === 'scheduleShift') {
+                this.showSchedule = true
+                this.scheduleData = data
+                return
+            }
+            this.formData[type].push({
+                key: '',
+                label: '',
+                isEnabled: true,
+                value: ''
+            })
+        },
+        handleSelectionChange (v, type) {
+            this.selectionIndex[type] = v.map(item => this.formData[type].indexOf(item))
+        },
+        handleRemove (removeIndex, type) {
+            let indexList = []
+            if (typeof removeIndex === 'number') {
+                indexList = [removeIndex]
+            } else {
+                indexList = removeIndex
+            }
+            indexList.sort((a, b) => b - a)
+            this.$confirm('确定要删除选中数据吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                indexList.forEach(i => {
+                    this.formData[type].splice(i, 1)
+                })
+            }).catch(() => {})
+        },
+        getCascaderOptions () {
+            const depts = this.deptList.filter(i => i.depth > 2)
+            const temp = depts.map(item => {
+                item.value = item.positionId
+                item.label = item.positionName
+                item.children = this.userList.map(i => {
+                    if (i.positionId.includes(item.positionId)) {
+                        i.value = i.userId
+                        i.label = i.userName
+                        return i
+                    }
+                }).filter(i => i)
+                return item
+            })
+            const res = []
+            temp.forEach(item => {
+                const index = res.findIndex(i => item.path === `${i.path}${item.value}.`)
+                if (index !== -1) {
+                    res[index].children.unshift(item)
+                } else {
+                    res.push(item)
+                }
+            })
+            return res
+        },
+        handleStaffChange (value) {
+            this.formData.scheduleStaff = Array.from(new Set(value))
+            this.$nextTick(() => {
+                let checkedNodeList = this.$refs.myCascader.getCheckedNodes()
+                checkedNodeList = checkedNodeList.filter((item, index, self) => !item.hasChildren && index === self.findIndex(t => t.value === item.value))
+                const tagListBox = this.$refs.myCascader.$el.children[1]
+                if (!checkedNodeList.length) {
+                    tagListBox.innerHTML = ''
+                    return
+                }
+                const dom = ''
+                const selectedArr = []
+                tagListBox.innerHTML = ''
+                checkedNodeList.forEach((item, index) => {
+                    // 重复的根节点元素只呈现一次就行
+                    const spanA = document.createElement('span')
+                    spanA.className = 'el-tag el-tag--info el-tag--mini el-tag--light'
+                    const spanB = document.createElement('span')
+                    spanB.innerText = item.label
+                    selectedArr.push(item.label)
+                    const iC = document.createElement('i')
+                    iC.className = 'el-tag__close el-icon-close'
+                    iC.onclick = (e) => this.delCascaderTag(e, item, tagListBox)
+                    spanA.appendChild(spanB)
+                    spanA.appendChild(iC)
+                    tagListBox.appendChild(spanA)
+                })
+            })
+        },
+        // 删除系统配置的标签显示
+        delCascaderTag (el, info, box) {
+            // 删除tag元素
+            const child = el.target.parentNode
+            box.removeChild(child)
+            // 删除指定值
+            let arr = JSON.parse(JSON.stringify(this.formData.scheduleStaff))
+            arr = arr.filter(item => item !== info.value)
+            this.formData.scheduleStaff = arr
+        },
+        transformData (dataset, data, from, to) {
+            const list = data.split(',')
+            const names = list.map(item => {
+                const temp = dataset.find(i => i[from] === item)
+                return temp ? temp[to] : ''
+            })
+            return names.filter(i => i).join(',')
+        },
+        handleRowData (row) {
+            const temp = JSON.parse(JSON.stringify(this.formData.scheduleShift))
+            if (this.$utils.isNotEmpty(row.index)) {
+                temp[row.index] = row.data
+            } else {
+                temp.push(row.data)
+            }
+            this.formData.scheduleShift = temp
+            // Vue.set(this.formData, 'scheduleShift', temp)
+        },
+        handleCancel () {
+            this.closeDialog()
+        },
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .schedule-config-dialog {
+        ::v-deep {
+            .el-dialog__header {
+                padding: 15px 20px 16px;
+            }
+        }
+        .config-dialog-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            .title {
+                line-height: 24px;
+                font-size: 18px;
+                color: #303133;
+            }
+        }
+        .config-form {
+            padding: 20px;
+            background: #f5f5f5;
+            border-radius: 4px;
+            overflow: hidden;
+            height: calc(100vh - 100px);
+            .config-item {
+                position: relative;
+                margin-bottom: 20px;
+                .title {
+                    height: 20px;
+                    line-height: 20px;
+                    font-size: 16px;
+                    font-weight: bold;
+                    margin-bottom: 10px;
+                    .ibps-icon-star {
+                        color: #FB9600;
+                        margin-right: 5px;
+                    }
+                }
+                .operate-btn {
+                    text-align: right;
+                    margin-bottom: 5px;
+                }
+                ::v-deep {
+                    .el-form-item {
+                        margin-bottom: 10 !important;
+                        &__label {
+                            position: relative;
+                            font-size: 14px !important;
+                            color: #606266;
+                            &:before {
+                                position: absolute;
+                                left: -8px;
+                            }
+                        }
+                        &__content {
+                            .el-input, .el-select, .el-input-number {
+                                width: 100%;
+                            }
+                            .el-textarea .el-input__count {
+                                padding: 0 5px;
+                                line-height: initial;
+                            }
+                        }
+                    }
+                    .el-table th.el-table__cell > .cell, .el-table td.el-table__cell {
+                        color: #606266;
+                        font-size: 14px;
+                    }
+                    .el-dropdown-link {
+                        margin-left: 8px;
+                        cursor: pointer;
+                        font-size: 16px;
+                        color: #409EFF;
+                    }
+                }
+            }
+        }
+    }
+</style>

+ 140 - 0
src/views/business/​scheduleManage/components/context-menu.vue

@@ -0,0 +1,140 @@
+<!-- ContextMenu.vue -->
+<template>
+    <transition name="fade">
+        <div
+            v-if="visible"
+            class="context-menu"
+            :style="{ top: `${position.top}px`, left: `${position.left}px` }"
+        >
+            <el-checkbox-group v-model="selectedShift" class="shift-list">
+                <el-checkbox
+                    v-for="(shift, index) in shiftList"
+                    :key="index"
+                    :label="shift.alias"
+                    class="shift-item ibps-ellipsis"
+                >
+                    <!-- <el-popover
+                        :title="shift.name"
+                        width="300"
+                        placement="right"
+                        trigger="hover"
+                    >
+                        <div>
+                            {{ shift.desc }}
+                        </div>
+                        <div
+                            slot="reference"
+                            :style="`color: ${shift.color};`"
+                            class="shift-item"
+                        >{{ shift.alias }}</div>
+                    </el-popover> -->
+                    {{ shift.alias }}
+                </el-checkbox>
+            </el-checkbox-group>
+            <el-button
+                class="confirm-btn"
+                type="success"
+                icon="el-icon-check"
+                size="mini"
+                @click="handleSubmit"
+            />
+        </div>
+    </transition>
+</template>
+
+<script>
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        shiftList: {
+            type: Array,
+            default: () => []
+        },
+        params: {
+            type: Object,
+            default: () => {}
+        },
+        position: {
+            type: Object,
+            default: () => {}
+        },
+        itemData: {
+            type: Array,
+            default: () => []
+        }
+    },
+    data () {
+        return {
+            selectedShift: this.itemData.map(i => i.alias) || []
+        }
+    },
+    watch: {
+        itemData: {
+            handler (val) {
+                this.selectedShift = val.map(i => i.alias) || []
+            },
+            deep: true,
+            immediate: true
+        }
+    },
+    methods: {
+        handleSubmit () {
+            const selected = this.shiftList.filter(i => this.selectedShift.includes(i.alias))
+            this.$emit('select', { selected, params: this.params })
+            this.$emit('close')
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+    .context-menu {
+        position: absolute;
+        background-color: rgba(204, 204, 204, 0.9);
+        border: 1px solid #cccccc;
+        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+        text-align: center;
+        z-index: 1000;
+        padding-bottom: 10px;
+        .shift-list {
+            display: flex;
+            align-items: center;
+            flex-direction: column;
+            height: 200px;
+            overflow-y: auto;
+            .shift-item {
+                padding: 8px 12px;
+                cursor: pointer;
+                margin: 0;
+                // width: calc(100% - 24px);
+                width: 100px;
+                text-align: left;
+            }
+            .shift-actived {
+                background-color: #007BFF;
+                color: #ffffff;
+                border: 1px solid #0056b3;
+                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+                border-radius: 4px;
+                font-weight: 600;
+                transition: all 0.3s ease;
+                &:hover {
+                    transform: scale(1.05);
+                }
+            }
+            .confirm-btn {
+                margin: 20px auto;
+            }
+        }
+    }
+
+    .fade-enter-active, .fade-leave-active {
+        transition: opacity 0.3s;
+    }
+    .fade-enter, .fade-leave-to {
+        opacity: 0;
+    }
+</style>

+ 55 - 0
src/views/business/​scheduleManage/components/history.vue

@@ -0,0 +1,55 @@
+<template>
+    <el-drawer
+        :title="title"
+        :visible.sync="drawerVisible"
+        :direction="direction"
+        show-close
+        size="30%"
+        :wrapper-closable="false"
+        destroy-on-close
+        append-to-body
+        custom-class="schedule-history"
+        :before-close="handleClose"
+    >
+        <span>排班历史</span>
+    </el-drawer>
+</template>
+
+<script>
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        return {
+            drawerVisible: this.visible,
+            title: '排班修改历史',
+            direction: 'rtl',
+            test: ''
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.drawerVisible = val
+            }
+        }
+    },
+    methods: {
+        handleClose () {
+            console.log(123123)
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .schedule-history {
+        ::v-deep {
+
+        }
+    }
+</style>

+ 148 - 0
src/views/business/​scheduleManage/components/record.vue

@@ -0,0 +1,148 @@
+<template>
+    <el-drawer
+        :title="title"
+        :visible.sync="drawerVisible"
+        :direction="direction"
+        show-close
+        size="30%"
+        :wrapper-closable="false"
+        destroy-on-close
+        append-to-body
+        custom-class="schedule-record"
+        :before-close="handleClose"
+        @open="loadData"
+    >
+        <el-timeline class="history-timeline" :reverse="false">
+            <el-timeline-item
+                v-for="(item, index) in timelineDate"
+                :key="index"
+                :timestamp="item.createTime"
+                placement="top"
+            >
+                <el-card class="card">
+                    <div class="applicant">申请人:{{ item.creator }}</div>
+                    <div class="reason">原因:{{ item.reason }}</div>
+                    <div class="detail">详情:{{ item.overview }}</div>
+                    <div class="approve">通过时间:{{ item.executeDate }}</div>
+                </el-card>
+            </el-timeline-item>
+        </el-timeline>
+    </el-drawer>
+</template>
+
+<script>
+import { getAdjustment } from '@/api/business/schedule'
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        scheduleId: {
+            type: String,
+            default: ''
+        }
+    },
+    data () {
+        const { userList } = this.$store.getters || {}
+        return {
+            userList,
+            drawerVisible: this.visible,
+            title: '排班修改历史',
+            direction: 'rtl',
+            timelineDate: []
+        }
+    },
+    watch: {
+        visible: {
+            handler (val, oldVal) {
+                this.drawerVisible = val
+            }
+        }
+    },
+    methods: {
+        loadData () {
+            if (!this.scheduleId) {
+                return
+            }
+            const sql = `select a.id_ as dataId, a.parent_id_ as parentId, a.record_id_ as recordId, a.before_adjust_ as beforeAdjust, a.before_date_ as beforeDate, a.after_adjust_ as afterAdjust, a.after_date_ as afterDate, a.party_ as party, b.create_by_ as createBy, date_format(b.create_time_,'%Y-%m-%d %H:%i') AS createTime, b.di_dian_ as location, b.reason_ as reason, b.executor_ as executor, b.execute_date_ as executeDate, b.overview_ as overview, b.schedule_id_ as scheduleId from t_adjustment_detail a left join t_adjustment b on a.parent_id_ = b.id_ and b.schedule_id_ = '${this.scheduleId}'`
+            this.$common.request('sql', sql).then(res => {
+                const { data = [] } = res.variables || {}
+                if (!data.length) {
+                    return
+                }
+                this.timelineDate = this.formatData(data)
+                console.log(this.timelineDate)
+            })
+        },
+        formatData (data) {
+            const groupedData = {}
+            data.forEach(item => {
+                const parentId = item.parentId
+                if (!groupedData[parentId]) {
+                    groupedData[parentId] = {
+                        parentId,
+                        createTime: item.createTime,
+                        createBy: item.createBy,
+                        creator: this.transformData(this.userList, item.createBy, 'userId', 'userName'),
+                        reason: item.reason,
+                        overview: item.overview,
+                        executor: item.executor,
+                        executeDate: item.executeDate,
+                        items: []
+                    }
+                }
+                groupedData[parentId].items.push(item)
+            })
+            return Object.values(groupedData)
+        },
+        handleClose () {
+            console.log(123123)
+            this.$emit('close', false)
+        },
+        transformData (dataset, data, from, to) {
+            if (!data) {
+                return ''
+            }
+            const list = data.split(',')
+            const names = list.map(item => {
+                const temp = dataset.find(i => i[from] === item)
+                return temp ? temp[to] : ''
+            })
+            return names.filter(i => i).join(',')
+        }
+    }
+}
+</script>
+<style lang="scss" scoped>
+    .schedule-record {
+        ::v-deep {
+            .el-drawer__header {
+                margin-bottom: 20px;
+            }
+            .el-card {
+                &__body {
+                    padding: 12px;
+                }
+            }
+        }
+        .history-timeline {
+            padding: 0 20px;
+            color: #343434;
+            .card {
+                > div > div {
+                    margin-top: 5px;
+                    &:first_child {
+                        margin-top: 0;
+                    }
+                }
+            }
+            .applicant, .reason, .detail {
+                line-height: 1.5;
+            }
+            .detail {
+                white-space: pre-wrap;
+            }
+        }
+    }
+</style>

+ 215 - 0
src/views/business/​scheduleManage/components/statistic.vue

@@ -0,0 +1,215 @@
+<template>
+    <div class="statistic-container">
+        <div id="userChart" />
+        <div id="dateChart" />
+        <div id="shiftChart" />
+    </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import { chartOption } from '../constants/options'
+import { mapValues, mean } from 'lodash'
+
+export default {
+    props: {
+        scheduleData: {
+            type: Object,
+            required: true,
+            default: () => {}
+        },
+        dateList: {
+            type: Array,
+            required: true,
+            default: () => []
+        },
+        userList: {
+            type: Array,
+            required: true,
+            default: () => []
+        }
+    },
+    mounted () {
+        this.init()
+    },
+    methods: {
+        init () {
+            this.renderUserChart()
+            this.renderDateChart()
+            this.renderShiftChart()
+        },
+        renderUserChart () {
+            const userChart = echarts.init(document.getElementById('userChart'))
+            const categories = this.userList.map(user => user.label)
+            const seriesData = {}
+            const colors = {}
+
+            // 统计每个人各班次的数量
+            this.userList.forEach(user => {
+                const userId = user.value
+                const schedules = this.scheduleData[userId]
+                schedules.forEach(schedule => {
+                    schedule.forEach(shift => {
+                        const name = shift.name
+                        if (!seriesData[name]) {
+                            seriesData[name] = Array(this.userList.length).fill(0)
+                            colors[name] = shift.color
+                        }
+                        const userIndex = this.userList.findIndex(u => u.value === userId)
+                        seriesData[name][userIndex]++
+                    })
+                })
+            })
+
+            const series = Object.keys(seriesData).map(key => ({
+                name: key,
+                type: 'bar',
+                data: seriesData[key],
+                itemStyle: {
+                    color: colors[key]
+                },
+                barMaxWidth: '50px',
+                barMinWidth: '20px',
+                markLine: {
+                    silent: true,
+                    precision: 2,
+                    data: [
+                        {
+                            name: '平均值',
+                            type: 'average'
+                        }
+                    ]
+                }
+                // label: {
+                //     show: true,
+                //     position: 'top',
+                //     textStyle: {
+                //         color: '#333',
+                //         fontSize: 14
+                //     }
+                // }
+            }))
+
+            const avg = mapValues(seriesData, values => mean(values).toFixed(2))
+            const avgText = Object.entries(avg).map(([shift, avg]) => `${shift}【${avg}】`).join(',')
+            const userOption = JSON.parse(JSON.stringify(chartOption))
+            userOption.title.text = '人员班次统计'
+            userOption.title.subtext = `人数共计:${this.userList.length},班次共计:${series.length},人均班次为:${avgText}`
+            userOption.legend.data = Object.keys(seriesData)
+            userOption.xAxis.data = categories
+            userOption.series = series
+            console.log(userOption)
+            userChart.setOption(userOption)
+        },
+        renderDateChart () {
+            const dateChart = echarts.init(document.getElementById('dateChart'))
+            const categories = this.dateList
+            const seriesData = {}
+            const colors = {}
+
+            this.dateList.forEach((date, dateIndex) => {
+                this.userList.forEach(user => {
+                    const userId = user.value
+                    const schedules = this.scheduleData[userId][dateIndex]
+                    schedules.forEach(shift => {
+                        const name = shift.name
+                        if (!seriesData[name]) {
+                            seriesData[name] = Array(this.dateList.length).fill(0)
+                            colors[name] = shift.color
+                        }
+                        seriesData[name][dateIndex]++
+                    })
+                })
+            })
+
+            const series = Object.keys(seriesData).map(key => ({
+                name: key,
+                type: 'bar',
+                data: seriesData[key],
+                itemStyle: {
+                    color: colors[key]
+                },
+                barMaxWidth: '50px',
+                barMinWidth: '20px',
+                markLine: {
+                    silent: true,
+                    precision: 2,
+                    data: [
+                        {
+                            name: '平均值',
+                            type: 'average'
+                        }
+                    ]
+                }
+            }))
+
+            const avg = mapValues(seriesData, values => mean(values).toFixed(2))
+            const avgText = Object.entries(avg).map(([shift, avg]) => `${shift}【${avg}】`).join(',')
+            const dateOption = JSON.parse(JSON.stringify(chartOption))
+            dateOption.title.text = '日期班次统计'
+            dateOption.title.subtext = `日期共计:${this.userList.length},班次共计:${series.length},日均班次为:${avgText}`
+            dateOption.legend.data = Object.keys(seriesData)
+            dateOption.xAxis.data = categories
+            dateOption.series = series
+            dateChart.setOption(dateOption)
+        },
+        // 渲染班次人数统计图
+        renderShiftChart () {
+            const shiftChart = echarts.init(document.getElementById('shiftChart'))
+            const categories = []
+            const seriesData = {}
+            const colors = {}
+
+            this.userList.forEach(user => {
+                const userId = user.value
+                const schedules = this.scheduleData[userId]
+                schedules.forEach(schedule => {
+                    schedule.forEach(shift => {
+                        const name = shift.name
+                        if (!seriesData[name]) {
+                            seriesData[name] = 0
+                            colors[name] = shift.color
+                            categories.push(name)
+                        }
+                        seriesData[name]++
+                    })
+                })
+            })
+
+            const series = [
+                {
+                    name: '人数',
+                    type: 'bar',
+                    data: categories.map(name => seriesData[name]),
+                    itemStyle: {
+                        color: params => colors[categories[params.dataIndex]]
+                    },
+                    barMaxWidth: '50px',
+                    barMinWidth: '20px'
+                }
+            ]
+
+            const shiftOption = JSON.parse(JSON.stringify(chartOption))
+            shiftOption.title.text = '班次人数统计'
+            shiftOption.legend.data = Object.keys(seriesData)
+            shiftOption.xAxis.data = categories
+            shiftOption.series = series
+            shiftChart.setOption(shiftOption)
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.statistic-container {
+    width: 100%;
+    min-height: calc(100vh - 150px);
+    #userChart, #dateChart, #shiftChart {
+        width: 100%;
+        height: 400px;
+    }
+    #dateChart, #shiftChart {
+        margin-top: 20px;
+    }
+}
+</style>

+ 138 - 0
src/views/business/​scheduleManage/constants/options.js

@@ -0,0 +1,138 @@
+const rowLimit = (params, max) => {
+    let result = ''
+    // 一行显示几个字
+    const rowMax = max
+    const rowNumber = Math.ceil(params.length / rowMax)
+    // 超过 3 个字换行
+    if (params.length > 3) {
+        for (let p = 0; p < rowNumber; p++) {
+            let tempStr = ''
+            const start = p * rowMax
+            const end = start + rowMax
+            if (p === rowNumber - 1) {
+                tempStr = params.substring(start, params.length)
+            } else {
+                tempStr = params.substring(start, end) + '\n'
+            }
+            result += tempStr
+        }
+    } else {
+        result = params
+    }
+    return result
+}
+
+const fontColor = '#333'
+
+export const chartOption = {
+    title: {
+        show: true,
+        text: '',
+        subtext: '',
+        textStyle: {
+            color: fontColor,
+            fontSize: 24,
+            fontWeight: '600'
+        },
+        subtextStyle: {
+            color: fontColor,
+            fontSize: 14,
+            fontWeight: '400',
+            align: 'center'
+        },
+        textAlign: 'center',
+        left: '50%',
+        top: '5px'
+    },
+    grid: {
+        top: '80px',
+        bottom: '30px',
+        containLabel: true
+    },
+    legend: {
+        data: [],
+        bottom: 0
+    },
+    xAxis: {
+        type: 'category',
+        data: [],
+        axisTick: {
+            alignWithLabel: false
+        },
+        axisLabel: {
+            style: {
+                fill: fontColor
+            },
+            formatter (params) {
+                return rowLimit(params, 4)
+            }
+        },
+        axisLine: {
+            lineStyle: {
+                color: fontColor
+            }
+        }
+    },
+    yAxis: {
+        type: 'value',
+        name: '',
+        nameTextStyle: {
+            color: fontColor,
+            fontSize: 14
+        },
+        splitLine: {
+            show: false
+        },
+        axisLine: {
+            lineStyle: {
+                color: fontColor
+            }
+        }
+    },
+    series: [{
+        type: 'bar',
+        name: '',
+        data: [],
+        markLine: {
+            data: [
+                {
+                    yAxis: '',
+                    tooltip: {
+                        formatter: ''
+                    },
+                    label: {
+                        show: true, position: 'inside',
+                        color: '#83bff6',
+                        formatter: ''
+                    },
+                    lineStyle: {
+                        color: '#ff4757',
+                        type: 'dashed'
+                    }
+                }
+            ]
+        },
+        itemStyle: {
+            color: null
+        },
+        label: {
+            show: true,
+            position: 'top',
+            textStyle: {
+                color: fontColor,
+                fontSize: 14
+            },
+            formatter (params) {
+                return params.value ? params.value : ''
+            }
+        }
+    }],
+    tooltip: {
+        show: true,
+        trigger: 'axis',
+        axisPointer: {
+            type: 'shadow'
+        }
+        // formatter: ''
+    }
+}

+ 1333 - 0
src/views/business/​scheduleManage/edit.vue

@@ -0,0 +1,1333 @@
+<template>
+    <el-dialog
+        v-loading="loading"
+        :visible.sync="dialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :show-close="false"
+        append-to-body
+        fullscreen
+        class="dialog schedule-edit-dialog"
+        top="0"
+        @scroll="handleScroll"
+        @open="loadData"
+        @close="closeDialog"
+    >
+        <div slot="title" class="edit-dialog-header">
+            <div class="title">{{ title }}</div>
+            <div class="operate">
+                <template v-for="btn in toolbars">
+                    <el-button
+                        v-if="!btn.steps || btn.steps.includes(activeStep)"
+                        :key="btn.key"
+                        :type="btn.type"
+                        :icon="btn.icon"
+                        :size="btn.size || 'mini'"
+                        @click="handleAction(btn.key)"
+                    >
+                        {{ btn.label }}
+                    </el-button>
+                </template>
+            </div>
+        </div>
+        <el-steps :active="activeStep" finish-status="success" simple style="margin: 20px 0;">
+            <el-step title="基础信息配置" />
+            <el-step title="人员排班" />
+            <el-step title="排班总览" />
+        </el-steps>
+        <el-form
+            v-if="activeStep === 1"
+            ref="form"
+            :label-width="formLabelWidth"
+            label-position="left"
+            :model="formData"
+            :rules="rules"
+            class="schedule-form"
+            @submit.native.prevent
+        >
+            <el-form-item label="排班名称" prop="title" required :show-message="false">
+                <el-input
+                    v-model="formData.title"
+                    type="text"
+                    clearable
+                    show-word-limit
+                    :maxlength="64"
+                    :disabled="readonly"
+                    placeholder="请输入排班名称"
+                />
+            </el-form-item>
+            <el-row :gutter="20" class="form-row">
+                <el-col :span="12">
+                    <el-form-item prop="dateRange" required :show-message="false">
+                        <template slot="label">
+                            排班时间
+                            <el-tooltip effect="dark" content="最多可支持45天内的排班" placement="top">
+                                <i class="el-icon-question question-icon" />
+                            </el-tooltip>
+                        </template>
+                        <el-date-picker
+                            v-model="formData.dateRange"
+                            type="daterange"
+                            range-separator="至"
+                            start-placeholder="开始日期"
+                            end-placeholder="结束日期"
+                            value-format="yyyy-MM-dd"
+                            unlink-panels
+                            :picker-options="pickerOptions"
+                            @change="handleDateChange"
+                        />
+                    </el-form-item>
+                </el-col>
+                <el-col :span="12">
+                    <el-form-item label="排班配置" prop="config" required :show-message="false">
+                        <el-select
+                            v-model="formData.config"
+                            :disabled="readonly"
+                            filterable
+                            placeholder="请选择排班配置"
+                            @change="handleConfigChange"
+                        >
+                            <el-option-group
+                                v-for="group in configOptions"
+                                :key="group.label"
+                                :label="group.label"
+                            >
+                                <el-option
+                                    v-for="item in group.options"
+                                    :key="item.value"
+                                    :label="item.label"
+                                    :value="item.value"
+                                    :disabled="item.isEffective === 'N'"
+                                />
+                            </el-option-group>
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+            <el-form-item label="排班人员" prop="scheduleStaff" required :show-message="false">
+                <el-select
+                    v-model="formData.scheduleStaff"
+                    :disabled="readonly"
+                    multiple
+                    filterable
+                    placeholder="请选择排班人员"
+                >
+                    <el-option
+                        v-for="item in userList"
+                        :key="item.userId"
+                        :label="item.userName"
+                        :value="item.userId"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-row :gutter="20" class="form-row">
+                <el-col :span="24">
+                    <el-form-item label="调班审批人" prop="approver" :show-message="false">
+                        <el-select
+                            v-model="formData.approver"
+                            :disabled="readonly"
+                            multiple
+                            filterable
+                            placeholder="请选择"
+                        >
+                            <el-option
+                                v-for="item in userList"
+                                :key="item.userId"
+                                :label="item.userName"
+                                :value="item.userId"
+                            />
+                        </el-select>
+                    </el-form-item>
+                </el-col>
+            </el-row>
+            <el-table
+                ref="scheduleTable"
+                :data="formData.scheduleShift"
+                border
+                stripe
+                highlight-current-row
+                style="width: 100%"
+                :max-height="maxHeight"
+                class="schedule-table"
+            >
+                <el-table-column type="index" label="序号" width="50" header-align="center" align="center" />
+                <el-table-column
+                    v-for="(item, fIndex) in scheduleColumn"
+                    :key="fIndex"
+                    :prop="item.key"
+                    :label="item.label"
+                    :width="item.width"
+                    :min-width="item.minWidth"
+                    header-align="center"
+                    align="center"
+                >
+                    <template slot-scope="scope">
+                        <template v-if="item.key === 'dateRange'">
+                            <div v-for="(d, di) in scope.row.dateRange" :key="di">
+                                {{ d.type === 'allday' ? '全天' : (`当天 ${d.startTime}` + ' 至 ' + `${d.isSecondDay === 'Y' ? '第二天' : '当天'} ${d.endTime}`) }}
+                            </div>
+                        </template>
+                        <div v-else-if="item.key === 'positions'">
+                            <el-tag
+                                v-for="(p, pi) in scope.row.positions"
+                                :key="pi"
+                                type="primary"
+                                style="margin-left: 5px;"
+                            >{{ p }}</el-tag>
+                        </div>
+                        <div v-else-if="item.key === 'isEnabled'">{{ scope.row.isEnabled === 'Y' ? '是' : '否' }}</div>
+                        <div v-else-if="item.key === 'color'">
+                            <div class="color-item" :style="`background-color: ${scope.row.color}; width: 20px; height: 20px; margin: 0 auto; border-radius: 2px;`" />
+                        </div>
+                        <div v-else>
+                            <span>{{ scope.row[item.key] }}</span>
+                        </div>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </el-form>
+        <div v-if="activeStep === 2" ref="scheduleContainer" class="schedule-container">
+            <div ref="schedule" class="schedule-box">
+                <div ref="scrollTarget" class="abscissa" :style="{ left: leftOffset + 'px' }">
+                    <div v-for="(month, mIndex) in Object.keys(dateObj)" :key="mIndex" class="abs-type">
+                        <div class="month">{{ month }}</div>
+                        <div class="abscissa-item">
+                            <div
+                                v-for="(date, dIndex) in dateObj[month]"
+                                :ref="`day${dIndex}`"
+                                :key="dIndex"
+                                class="date"
+                                @click="showShiftSetting($event, date)"
+                            >{{ date.split('-')[2] }}</div>
+                        </div>
+                    </div>
+                </div>
+                <el-popover
+                    ref="popover"
+                    trigger="manual"
+                    placement="bottom"
+                    width="255"
+                    title=""
+                    :reference="popoverReference"
+                >
+                    <el-form
+                        ref="popover-form"
+                        label-width="80px"
+                        label-position="right"
+                        :model="shiftForm"
+                        size="mini"
+                        class="popover-form"
+                        @submit.native.prevent
+                    >
+                        <el-form-item label="选择操作">
+                            <el-radio-group v-model="shiftForm.operateType">
+                                <el-tooltip effect="dark" content="将该列排班数据复制到指定的一个或多个日期" placement="left">
+                                    <el-radio-button label="copy">复制</el-radio-button>
+                                </el-tooltip>
+                                <el-tooltip effect="dark" content="将该列排班数据按指定间隔向后覆盖" placement="right">
+                                    <el-radio-button label="cycle">循环</el-radio-button>
+                                </el-tooltip>
+                            </el-radio-group>
+                        </el-form-item>
+                        <!-- <el-form-item label="覆盖方式">
+                            <el-radio-group v-model="shiftForm.pasteType">
+                                <el-tooltip effect="dark" content="无视已有数据,完全覆盖" placement="top">
+                                    <el-radio-button label="all">全覆盖</el-radio-button>
+                                </el-tooltip>
+                                <el-tooltip effect="dark" content="仅覆盖目标列的空值" placement="top">
+                                    <el-radio-button label="empty">空值覆盖</el-radio-button>
+                                </el-tooltip>
+                            </el-radio-group>
+                        </el-form-item> -->
+                        <el-form-item v-if="shiftForm.operateType === 'copy'" label="覆盖日期">
+                            <el-select
+                                v-model="shiftForm.dates"
+                                filterable
+                                width="100%"
+                                clearable
+                                multiple
+                                collapse-tags
+                                :multiple-limit="10"
+                                placeholder="请选择需覆盖的日期"
+                            >
+                                <el-option
+                                    v-for="(item, index) in dateList"
+                                    :key="index"
+                                    :label="item"
+                                    :value="item"
+                                />
+                            </el-select>
+                        </el-form-item>
+                        <el-form-item v-else label="覆盖间隔">
+                            <el-input-number
+                                v-model="shiftForm.cycle"
+                                type="number"
+                                :min="0"
+                                :max="10"
+                            />
+                        </el-form-item>
+                        <div style="text-align: right; margin: 0">
+                            <el-tooltip effect="dark" content="仅覆盖目标列的空值" placement="bottom">
+                                <el-button type="primary" size="mini" plain @click="handleShiftSetting('empty')">覆盖空值</el-button>
+                            </el-tooltip>
+                            <el-tooltip effect="dark" content="无视目标列已有数据,完全覆盖" placement="bottom">
+                                <el-button type="warning" size="mini" plain @click="handleShiftSetting('all')">覆盖全部</el-button>
+                            </el-tooltip>
+                            <el-button size="mini" plain @click="resetShiftSetting">取消</el-button>
+                        </div>
+                    </el-form>
+                </el-popover>
+                <div ref="scheduleContent" class="schedule-content">
+                    <div ref="sidebar" class="ordinate" :style="{ top: topOffset + 'px' }">
+                        <div v-for="item in ordinateList" :key="item.value" class="ordinate-item">
+                            {{ item.label }}
+                        </div>
+                    </div>
+                    <div class="shift-content">
+                        <div v-for="(row, rIndex) in ordinateList" :key="row.value" class="shift-row">
+                            <!-- <div v-for="(column, cIndex) in dateList" :key="cIndex" class="shift-column"> -->
+                            <!-- @contextmenu.prevent="handleRightClick($event, {row, rIndex, column, cIndex})" -->
+                            <div
+                                v-for="(column, cIndex) in dateList"
+                                :key="cIndex"
+                                ref="shiftItem"
+                                class="shift-item"
+                                @mouseenter="hoveredIndex = `${row.value}-${cIndex}`"
+                                @mouseleave="hoveredIndex = null"
+                                @click.prevent="handleShiftClick($event, {row, rIndex, column, cIndex})"
+                            >
+                                <div
+                                    v-for="(shift, sIndex) in scheduleData[row.value][cIndex]"
+                                    :key="sIndex"
+                                    class="shift"
+                                    :style="{ color: `${shift.color}` }"
+                                >
+                                    {{ shift.alias }}
+                                    <!-- <div :style="{ color: `${shift.color}` }">{{ shift.alias }}</div> -->
+                                </div>
+                                <div v-if="hoveredIndex === `${row.value}-${cIndex}` && !readonly" class="overlay">
+                                    <i class="el-icon-edit" />
+                                </div>
+                            </div>
+                            <!-- </div> -->
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div v-if="activeStep === 3" class="stat">
+            <statistic
+                :schedule-data="scheduleData"
+                :date-list="dateList"
+                :user-list="ordinateList"
+                @close="handleClose"
+            />
+        </div>
+        <context-menu
+            :visible.sync="contextMenuVisible"
+            :params="shiftParams"
+            :shift-list="shiftList"
+            :position="itemPosition"
+            :item-data="selectItem"
+            :readonly="readonly"
+            @select="handleSelect"
+            @close="handleClose"
+        />
+        <history
+            :visible.sync="historyVisible"
+            @close="v => historyVisible = v"
+        />
+        <record
+            :visible.sync="recordVisible"
+            :schedule-id="pageParams.id"
+            @close="v => recordVisible = v"
+        />
+    </el-dialog>
+</template>
+
+<script>
+import { cycleOptions, scheduleType, scheduleColumn } from '../../constants/schedule'
+import { queryScheduleConfig, getStaffSchedule, saveStaffSchedule } from '@/api/business/schedule'
+import request from '@/utils/request'
+import { SYSTEM_URL } from '@/api/baseUrl'
+import { previewFile } from '@/api/platform/file/attachment'
+import { mapValues, keyBy } from 'lodash'
+import html2canvas from 'html2canvas'
+import ActionUtils from '@/utils/action'
+
+export default {
+    name: 'schedule',
+    components: {
+        ContextMenu: () => import('./components/context-menu'),
+        Statistic: () => import('./components/statistic'),
+        History: () => import('./components/history'),
+        Record: () => import('./components/record')
+    },
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        pageParams: {
+            type: Object,
+            default: () => {}
+        },
+        readonly: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        const { userList = [], deptList = [] } = this.$store.getters || {}
+        const readonly = false
+        return {
+            userList,
+            deptList,
+            cycleOptions,
+            scheduleType,
+            dialogVisible: this.visible,
+            formLabelWidth: '120px',
+            loading: false,
+            activeStep: 1,
+            formData: {
+                title: '',
+                dateRange: [],
+                config: '',
+                scheduleStaff: [],
+                isApproval: 'Y',
+                approver: [],
+                scheduleShift: [],
+                scheduleRule: []
+            },
+            rules: {},
+            scheduleColumn,
+            title: readonly ? '创建排班' : '编辑排班',
+            configList: [],
+            configOptions: [],
+            maxHeight: '250px',
+            toolbars: [
+                { key: 'prev', icon: 'el-icon-d-arrow-left', label: '上一步', type: 'primary', steps: '2,3' },
+                { key: 'next', icon: 'el-icon-d-arrow-right', label: '下一步', type: 'primary', steps: '1,2' },
+                { key: 'changeView', icon: 'el-icon-set-up', label: '切换视图', type: 'primary', steps: '2' },
+                // { key: 'history', icon: 'el-icon-time', label: '排班历史', type: 'info', steps: '2,3' },
+                { key: 'record', icon: 'el-icon-tickets', label: '修改记录', type: 'warning', steps: '2,3' },
+                { key: 'export', icon: 'el-icon-download', label: '导出', type: 'primary', steps: '2,3' },
+                { key: 'reset', icon: 'el-icon-refresh', label: '重置', type: 'warning', steps: '2' },
+                // { key: 'edit', icon: 'el-icon-edit', label: '编辑', type: 'primary', steps: '2,3' },
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'primary' },
+                { key: 'submit', icon: 'ibps-icon-send', label: '提交', type: 'success', steps: '3' },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ],
+            viewType: 'users',
+            scheduleData: {},
+            scheduleRecord: [],
+            leftOffset: '',
+            topOffset: '',
+            topFixed: false,
+            leftFixed: false,
+            dateObj: {},
+            dateList: [],
+            contextMenuVisible: false,
+            historyVisible: false,
+            recordVisible: false,
+            popoverReference: null,
+            hoveredIndex: null,
+            shiftParams: {},
+            shiftList: [],
+            selectItem: [],
+            itemPosition: {},
+            shiftForm: {
+                operateType: 'copy',
+                pasteType: 'all'
+            },
+            pickerOptions: {
+                disabledDate: (time) => {
+                    const { dateRange } = this.formData
+                    if (!dateRange.length) {
+                        return false
+                    }
+                    const startDate = dateRange[0]
+                    const endDate = dateRange[1] || time
+                    // 日期差超过45天则禁用
+                    const diffDays = (endDate - startDate) / (1000 * 60 * 60 * 24)
+                    return diffDays > 45 || diffDays < -45
+                },
+                onPick: ({ minDate, maxDate }) => {
+                    if (minDate && !maxDate) {
+                        this.pickerOptions.disabledDate = (time) => {
+                            const dayDifference = (time - minDate) / (1000 * 60 * 60 * 24)
+                            return dayDifference > 45 || dayDifference < -45
+                        }
+                    }
+                }
+            }
+        }
+    },
+    computed: {
+        ordinateList () {
+            const { scheduleStaff } = this.formData
+            return this.userList.filter(u => scheduleStaff.includes(u.userId)).map(u => ({
+                label: u.userName,
+                value: u.userId
+            }))
+        }
+        // scheduleData () {
+        //     return mapValues(keyBy(this.ordinateList, 'value'), () => Array.from({ length: this.dateList.length }, () => []))
+        // }
+    },
+    created () {
+        this.loadData()
+    },
+    mounted () {
+        this.handleListener('addEventListener')
+    },
+    beforeDestroy () {
+        this.handleListener('removeEventListener')
+    },
+    methods: {
+        handleListener (event) {
+            setTimeout(() => {
+                const scrollContainer = this.$refs.scheduleContainer
+                if (scrollContainer) {
+                    scrollContainer[event]('scroll', this.handleScroll)
+                    console.log(scrollContainer)
+                }
+            }, 10)
+        },
+        handleScroll () {
+            const scrollTarget = this.$refs.scrollTarget
+            const sidebar = this.$refs.sidebar
+            const scrollContainer = this.$refs.scheduleContainer
+            const scheduleContent = this.$refs.scheduleContent
+            let defaultTopOffset = 0
+
+            if (scrollContainer.scrollTop > 80) {
+                this.topFixed = true
+                scrollTarget.classList.add('stickyTop')
+                scheduleContent.style.paddingTop = '100px'
+                defaultTopOffset = 130
+                this.leftOffset = -scrollContainer.scrollLeft
+                // this.leftOffset = -scrollContainer.scrollLeft + (this.leftFixed ? 100 : 0)
+            } else {
+                this.topFixed = false
+                scrollTarget.classList.remove('stickyTop')
+                this.leftOffset = 0
+                defaultTopOffset = 206
+                scheduleContent.style.paddingTop = '0px'
+            }
+            // // 处理左侧元素的固定
+            // if (scrollContainer.scrollLeft > 40) {
+            //     this.leftFixed = true
+            //     sidebar.classList.add('stickyLeft')
+            //     this.topOffset = defaultTopOffset - scrollContainer.scrollTop
+            //     scheduleContent.style.paddingLeft = '100px'
+            // } else {
+            //     this.leftFixed = false
+            //     sidebar.classList.remove('stickyLeft')
+            //     scheduleContent.style.paddingLeft = '0px'
+            // }
+        },
+        async loadData () {
+            this.loading = true
+            // 获取配置数据
+            queryScheduleConfig({
+                parameters: [],
+                requestPage: {
+                    pageNo: 1,
+                    limit: 1000
+                },
+                sorts: []
+            }).then(async res => {
+                const { dataResult } = res.data || {}
+                this.configList = dataResult
+                this.configOptions = this.transformConfigData(dataResult)
+                // console.log(this.configOptions)
+                if (this.$utils.isEmpty(this.pageParams.id)) {
+                    this.loading = false
+                    return
+                }
+
+                const response = await getStaffSchedule({ id: this.pageParams.id })
+                const { staffScheduleDetailPoList: records, title, endDate, startDate, type, overview, config, status } = response.data
+                const temp = config ? JSON.parse(config) : {}
+                console.log('responseData:', response.data)
+                console.log('configData:', temp)
+                this.formData = {
+                    title,
+                    status,
+                    config: temp.id,
+                    dateRange: [startDate, endDate],
+                    approver: temp.approver || [],
+                    scheduleType: type,
+                    scheduleRule: temp.scheduleRule || [],
+                    scheduleShift: temp.scheduleShift || [],
+                    scheduleStaff: temp.scheduleStaff || []
+                }
+                this.shiftList = this.formData.scheduleShift.filter(s => s.isEnabled === 'Y').map(s => ({
+                    ...s,
+                    positions: s.positions.join(','),
+                    dateRange: s.dateRange.map(d => {
+                        return d.type === 'allday' ? '全天' : (`当天 ${d.startTime}` + ' 至 ' + `${d.isSecondDay === 'Y' ? '第二天' : '当天'} ${d.endTime}`)
+                    })
+                }))
+                console.log('formData', this.formData)
+                this.scheduleData = this.transformScheduleData(records, overview, temp)
+                console.log('scheduleData', this.scheduleData)
+                this.loading = false
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        transformConfigData (data) {
+            return data.reduce((acc, item) => {
+                const { scheduleType } = item
+                // 查找是否已存在对应的 scheduleType
+                const scheduleInfo = this.scheduleType.find(i => i.value === scheduleType)
+                const existingGroup = acc.find(group => group.labelKey === scheduleType)
+                if (existingGroup) {
+                    // 如果存在,添加当前项到 options
+                    existingGroup.options.push({
+                        ...item,
+                        value: item.id,
+                        label: `${item.name}${item.isEffective === 'N' ? '(未启用)' : ''}`
+                    })
+                } else {
+                    // 如果不存在,创建新的分组
+                    acc.push({
+                        labelKey: scheduleType,
+                        label: scheduleInfo.label,
+                        options: [{
+                            ...item,
+                            value: item.id,
+                            label: `${item.name}${item.isEffective === 'N' ? '(未启用)' : ''}`
+                        }]
+                    })
+                }
+                return acc
+            }, [])
+        },
+        transformScheduleData (records, overview, { scheduleShift }) {
+            const result = {}
+            const temp = overview ? JSON.parse(overview) : {}
+            records.forEach(({ id, userId, ...days }) => {
+                result[userId] = []
+                for (let day = 1; day <= temp.dateCount; day++) {
+                    const shifts = days[`d${day}`] ? days[`d${day}`].split(',') : []
+                    const formattedShifts = shifts.map(shift => {
+                        const temp = scheduleShift.find(s => s.alias === shift)
+                        return temp || {}
+                    })
+                    result[userId].push(formattedShifts)
+                }
+                this.scheduleRecord.push({ userId, id })
+            })
+            return result
+        },
+        handleConfigChange (val) {
+            const temp = this.configList.find(i => i.id === val)
+            this.formData = {
+                ...this.formData,
+                approver: temp.approver ? temp.approver.split(',') : [],
+                scheduleType: temp.scheduleType,
+                isApproval: temp.isApproval,
+                scheduleRule: temp.scheduleRule ? JSON.parse(temp.scheduleRule) : [],
+                scheduleShift: temp.scheduleShift ? JSON.parse(temp.scheduleShift) : [],
+                scheduleStaff: temp.scheduleStaff ? JSON.parse(temp.scheduleStaff) : []
+            }
+            this.shiftList = this.formData.scheduleShift.filter(s => s.isEnabled === 'Y').map(s => ({
+                ...s,
+                positions: s.positions.join(','),
+                dateRange: s.dateRange.map(d => {
+                    return d.type === 'allday' ? '全天' : (`当天 ${d.startTime}` + ' 至 ' + `${d.isSecondDay === 'Y' ? '第二天' : '当天'} ${d.endTime}`)
+                })
+            }))
+        },
+        handleDateChange (dates) {
+            if (this.$utils.isEmpty(dates)) {
+                this.pickerOptions.disabledDate = (time) => {
+                    return false
+                }
+            }
+        },
+        getDateList (dateRange) {
+            const [startDate, endDate] = dateRange.map(date => new Date(date))
+            const dates = {}
+            const currentDate = startDate
+            // eslint-disable-next-line no-unmodified-loop-condition
+            while (currentDate <= endDate) {
+                const yearMonth = currentDate.toISOString().slice(0, 7) // 获取 'YYYY-MM' 格式
+                const date = currentDate.toISOString().split('T')[0] // 获取 'YYYY-MM-DD' 格式
+                if (!dates[yearMonth]) {
+                    dates[yearMonth] = []
+                }
+                dates[yearMonth].push(date)
+                currentDate.setDate(currentDate.getDate() + 1)
+            }
+            return dates
+        },
+        changeView () {
+            console.log('changeView')
+        },
+        handleShiftClick (event, { row, rIndex, column, cIndex }) {
+            this.selectItem = []
+            const item = this.$refs.shiftItem[rIndex * this.dateList.length + cIndex]
+            const rect = item.getBoundingClientRect()
+            // 计算弹出菜单的位置
+            let top = rect.top + window.scrollY
+            let left = rect.left + window.scrollX + rect.width
+
+            // 获取窗口的宽度和高度
+            const windowWidth = window.innerWidth
+            const windowHeight = window.innerHeight
+            const menuWidth = 126
+            const menuHeight = 240
+
+            // 检查右侧边界,如果超出右边界,调整到左侧
+            if (left + menuWidth > windowWidth) {
+                left = rect.left + window.scrollX - menuWidth
+            }
+
+            // 检查底部边界,如果超出底部边界,调整到顶部
+            if (top + menuHeight > windowHeight) {
+                top = rect.top + window.scrollY - menuHeight
+            }
+            this.itemPosition = { top, left }
+            this.shiftParams = {
+                row,
+                rIndex,
+                column,
+                cIndex
+            }
+            this.selectItem = this.scheduleData[row.value][cIndex]
+            this.contextMenuVisible = true
+        },
+        handleSelect ({ selected, params }) {
+            this.scheduleData[params.row.value][params.cIndex] = selected
+        },
+        handleClose () {
+            this.contextMenuVisible = false
+        },
+        handleAction (action) {
+            switch (action) {
+                case 'prev':
+                    this.handleStepChange(-1)
+                    break
+                case 'next':
+                    this.handleStepChange(1)
+                    break
+                case 'save':
+                    this.handleSave()
+                    break
+                case 'submit':
+                    this.handleSave('submit')
+                    break
+                case 'changeView':
+                    this.changeView()
+                    break
+                case 'history':
+                    this.historyVisible = true
+                    break
+                case 'export':
+                    this.handleExport()
+                    break
+                case 'record':
+                    this.recordVisible = true
+                    break
+                case 'reset':
+                    this.handleReset()
+                    break
+                case 'cancel':
+                    this.closeDialog()
+                    break
+                default:
+                    break
+            }
+        },
+        handleStepChange (val) {
+            if (this.activeStep === 1 && val) {
+                const valid = this.validateForm()
+                if (!valid) {
+                    this.$message.warning('请完善必填信息后再操作!')
+                    return
+                }
+                this.dateObj = this.getDateList(this.formData.dateRange)
+                this.dateList = Object.values(this.dateObj).flat()
+                if (!this.pageParams.id) {
+                    this.scheduleData = mapValues(keyBy(this.ordinateList, 'value'), () => Array.from({ length: this.dateList.length }, () => []))
+                } else {
+                    this.updateScheduleData()
+                }
+            }
+            this.handleListener('addEventListener')
+            this.activeStep += val
+        },
+        /**
+         * 更新排班数据
+         * 1. 获取当前的 ordinateList 的 value 列表
+         * 2. 删除 this.scheduleData 中不在 newOrdinateValues 里的项
+         * 3. 确保 this.scheduleData 包含所有 newOrdinateValues 的项
+         * 4. 更新数组长度
+         */
+        updateScheduleData () {
+            const newOrdinateValues = this.ordinateList.map(ordinate => ordinate.value)
+
+            Object.keys(this.scheduleData).forEach(key => {
+                if (!newOrdinateValues.includes(key)) {
+                    delete this.scheduleData[key]
+                }
+            })
+
+            newOrdinateValues.forEach(key => {
+                if (!this.scheduleData[key]) {
+                    this.scheduleData[key] = Array.from({ length: this.dateList.length }, () => [])
+                }
+            })
+
+            Object.keys(this.scheduleData).forEach(key => {
+                const scheduleArray = this.scheduleData[key]
+                const newLength = this.dateList.length
+                if (scheduleArray.length < newLength) {
+                    // 如果当前长度小于新的长度,添加新的空数组
+                    for (let i = scheduleArray.length; i < newLength; i++) {
+                        scheduleArray.push([])
+                    }
+                } else if (scheduleArray.length > newLength) {
+                    // 如果当前长度大于新的长度,裁剪数组
+                    scheduleArray.length = newLength
+                }
+            })
+        },
+        validateForm () {
+            const { scheduleRule, approver, status, ...rest } = this.formData
+            const result = Object.keys(rest).some(k => this.$utils.isEmpty(rest[k]))
+            return !result
+        },
+        // dealData (data) {
+        //     const result = Object.entries(data).map(([userId, days]) => {
+        //         const userObj = { userId }
+        //         days.forEach((day, index) => {
+        //             // userObj[`d${index + 1}`] = day.map(({ dateRange, name, alias }) => ({ dateRange, name, alias })) || []
+        //             userObj[`d${index + 1}`] = day.map(({ alias }) => alias).join(',') || ''
+        //         })
+        //         return userObj
+        //     })
+        //     return result
+        // },
+        dealData (data) {
+            var dateCount = 0
+            const result = Object.entries(data).map(([userId, days]) => {
+                const userObj = { userId, statistics: {}}
+                const temp = this.scheduleRecord.find(i => i.userId === userId)
+                const recordId = temp ? temp.id : ''
+                dateCount = days.length
+                days.forEach((day, index) => {
+                    const shifts = day.map(({ alias }) => alias).join(',') || ''
+                    userObj[`d${index + 1}`] = shifts
+
+                    // 统计每个alias出现的次数
+                    day.forEach(({ alias }) => {
+                        if (userObj.statistics[alias]) {
+                            userObj.statistics[alias]++
+                        } else {
+                            userObj.statistics[alias] = 1
+                        }
+                    })
+                })
+
+                return {
+                    ...userObj,
+                    id: recordId,
+                    pk: recordId,
+                    statistics: JSON.stringify(userObj.statistics)
+                }
+            })
+            console.log(result)
+
+            // 统计所有userId中各个alias的平均出现次数
+            const total = {}
+            const userCount = result.length
+
+            result.forEach(user => {
+                Object.entries(JSON.parse(user.statistics)).forEach(([alias, count]) => {
+                    if (total[alias]) {
+                        total[alias] += count
+                    } else {
+                        total[alias] = count
+                    }
+                })
+            })
+            const avg = {}
+            Object.entries(total).forEach(([alias, totalCount]) => {
+                avg[alias] = totalCount / userCount
+            })
+            const overview = {
+                avg,
+                total,
+                userCount,
+                dateCount
+            }
+            return { staffScheduleDetailPoList: result, overview }
+        },
+        handleSave (type) {
+            const { staffScheduleDetailPoList, overview } = this.dealData(this.scheduleData) || {}
+            const { dateRange, title, config, approver, scheduleType, status, scheduleShift, scheduleStaff, scheduleRule } = this.formData
+            const configData = {
+                id: config,
+                approver,
+                scheduleShift,
+                scheduleStaff,
+                scheduleRule
+            }
+            const submitData = {
+                id: this.pageParams.id,
+                pk: this.pageParams.id,
+                title,
+                status: type ? '已发布' : (status || '未发布'),
+                startDate: dateRange[0],
+                endDate: dateRange[1],
+                type: scheduleType,
+                config: JSON.stringify(configData),
+                overview: JSON.stringify(overview),
+                staffScheduleDetailPoList
+            }
+            console.log(submitData)
+            this.loading = true
+            saveStaffSchedule(submitData).then(() => {
+                this.$message.success('保存成功')
+                if (type) {
+                    this.handleSaveNews()
+                } else {
+                    this.loading = false
+                    this.closeDialog()
+                    this.$emit('callback')
+                }
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        handleSaveNews () {
+            this.activeStep = 2
+            this.$nextTick(async () => {
+                const element = this.$refs.schedule
+                console.log(element)
+                const fileId = await this.captureAndUpload(element)
+                const fileUrl = await previewFile(fileId)
+                const { userId, name } = this.$store.getters
+                const { first, second } = this.$store.getters.level
+                const { title, dateRange } = this.formData
+                const news = {
+                    author: name,
+                    content: `<img src="${fileUrl}" title="${title}.png" alt="image.png"/>`,
+                    depId: '',
+                    depName: '',
+                    fileAttach: '',
+                    includeChild: 'N',
+                    // 发布时间
+                    publicDate: this.$common.getDateNow(19),
+                    // 失效时间
+                    loseDate: dateRange[1] + ' 23:59:59',
+                    public0: 'Y',
+                    publicItem: 'notices',
+                    status: 'publish',
+                    title: title,
+                    type: second || first,
+                    userId: userId,
+                    userName: name
+                }
+                this.$common.saveNews(news).then(() => {
+                    this.loading = false
+                    this.closeDialog()
+                    this.$emit('callback')
+                })
+            })
+        },
+        captureAndUpload (element) {
+            const uploadImage = (blob) => {
+                return new Promise((resolve, reject) => {
+                    const data = new FormData() // 创建form对象
+                    data.append('file', blob, `${this.formData.title}.png`)
+                    request({
+                        url: SYSTEM_URL() + '/file/upload',
+                        method: 'post',
+                        isLoading: true,
+                        gateway: true,
+                        data
+                    }).then(res => {
+                        resolve(res.data.id || '')
+                    }).catch(error => {
+                        reject(error)
+                    })
+                })
+            }
+            const canvasToBlob = (canvas) => {
+                return new Promise(resolve => {
+                    canvas.toBlob(blob => {
+                        resolve(blob)
+                    }, 'image/png', 1.0)
+                })
+            }
+            return new Promise((resolve, reject) => {
+                html2canvas(element).then(canvas => {
+                    canvasToBlob(canvas).then(blob => {
+                        uploadImage(blob).then(fileId => {
+                            resolve(fileId)
+                        })
+                    })
+                })
+            })
+        },
+        async handleExport () {
+            const element = this.$refs.schedule
+            // 使用 html2canvas 渲染 DOM 元素
+            html2canvas(element, {
+                scrollX: 0,
+                scrollY: -10,
+                width: element.scrollWidth,
+                height: element.scrollHeight
+            }).then(canvas => {
+                const link = document.createElement('a')
+                link.href = canvas.toDataURL('image/png')
+                link.download = `${this.formData.title || '排班'}.png`
+                link.click()
+            }).catch(err => {
+                console.error('导出失败', err)
+            })
+        },
+        handleReset () {
+            this.$confirm('<p style="font-size: 18px;">重置后当前排班的信息将会清空,您确定要执行该操作吗?</p>', '提示', {
+                confirmButtonText: '确认',
+                cancelButtonText: '取消',
+                type: 'warning',
+                showClose: false,
+                closeOnClickModal: false,
+                dangerouslyUseHTMLString: true
+            }).then(() => {
+                this.scheduleData = mapValues(keyBy(this.ordinateList, 'value'), () => Array.from({ length: this.dateList.length }, () => []))
+            }).catch(() => {
+                // nothing
+            })
+        },
+        showShiftSetting (e, date) {
+            this.resetShiftSetting()
+            this.shiftForm.current = date
+            this.popoverReference = e.target
+            this.$refs.popover.showPopper = true
+
+            // 手动更改弹窗位置
+            setTimeout(() => {
+                const popoverElement = document.querySelector('.el-popover')
+                if (popoverElement) {
+                    const rect = popoverElement.getBoundingClientRect()
+                    const targetRect = e.target.getBoundingClientRect()
+
+                    // 计算新的位置
+                    let left = targetRect.left + (targetRect.width / 2) - (rect.width / 2)
+                    // 使用目标元素的底部作为弹窗顶部
+                    const top = targetRect.bottom
+                    const windowWidth = window.innerWidth
+
+                    // 左右边界检查
+                    if (left < 0) {
+                        left = 0
+                    } else if (left + rect.width > windowWidth) {
+                        left = windowWidth - rect.width
+                    }
+                    // 设置弹窗位置
+                    popoverElement.style.left = `${left}px`
+                    popoverElement.style.top = `${top}px`
+                    const triangle = popoverElement.querySelector('.popper__arrow')
+                    // 设置小三角位置
+                    if (triangle) {
+                        triangle.style.left = `${(targetRect.left + (targetRect.width / 2)) - left}px`
+                    }
+                }
+            }, 10)
+        },
+        resetShiftSetting () {
+            this.popoverReference = null
+            this.shiftForm = {
+                operateType: 'copy',
+                pasteType: 'all'
+            }
+            this.$refs.popover.showPopper = false
+        },
+        getDays (start, end) {
+            if (!start || !end) {
+                return 0
+            }
+            return Math.ceil((new Date(end) - new Date(start)) / (1000 * 60 * 60 * 24))
+        },
+        handleShiftSetting (type) {
+            const { cycle, dates, operateType, current } = this.shiftForm || {}
+            if ((operateType === 'copy' && this.$utils.isEmpty(dates)) || (operateType === 'cycle' && this.$utils.isEmpty(cycle))) {
+                return this.$message.warning('请补充必填信息!')
+            }
+            const target = this.getDays(this.formData.dateRange[0], current)
+            const total = this.getDays(this.formData.dateRange[0], this.formData.dateRange[1])
+            if (operateType === 'copy') {
+                dates.forEach(d => {
+                    const index = this.getDays(this.formData.dateRange[0], d)
+                    Object.keys(this.scheduleData).forEach(key => {
+                        if (type === 'all' || this.$utils.isEmpty(this.scheduleData[key][index])) {
+                            this.scheduleData[key][index] = [...this.scheduleData[key][target]]
+                        }
+                    })
+                })
+            } else {
+                // 获取需覆盖的下标数组
+                const getIndexList = (start, end, step) => {
+                    const result = []
+                    let cur = start + step + 1
+                    while (cur <= end) {
+                        result.push(cur)
+                        cur += step + 1
+                    }
+                    return result
+                }
+                const indexList = getIndexList(target, total, cycle)
+                indexList.forEach(i => {
+                    Object.keys(this.scheduleData).forEach(key => {
+                        if (type === 'all' || this.$utils.isEmpty(this.scheduleData[key][i])) {
+                            this.scheduleData[key][i] = [...this.scheduleData[key][target]]
+                        }
+                    })
+                })
+            }
+            this.resetShiftSetting()
+        },
+        closeDialog () {
+            this.$emit('close', false)
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.schedule-edit-dialog {
+    width: 100%;
+    ::v-deep {
+        .el-dialog__header {
+            padding: 15px 20px 16px;
+        }
+        .el-dialog__footer {
+            display: none;
+        }
+    }
+    .edit-dialog-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .title {
+            line-height: 24px;
+            font-size: 18px;
+            color: #303133;
+        }
+    }
+    .schedule-form {
+        width: 1080px;
+        height: 600px;
+        margin: 20px auto;
+        ::v-deep {
+            .el-form-item {
+                margin-bottom: 16px !important;
+            }
+            .el-input, .el-select, .el-input-number, .el-range-editor {
+                width: 100%;
+            }
+        }
+    }
+    .schedule-container {
+        width: 100%;
+        max-height: calc(100vh - 146px);
+        overflow: auto;
+        ::v-deep {
+            .el-button > span {
+                margin-left: 5px;
+            }
+        }
+        .page-header {
+            padding: 20px;
+            height: 80px;
+            .toolbar {
+                text-align: right;
+            }
+            .page-title {
+                margin-top: 20px;
+                font-size: 24px;
+                font-weight: 600;
+                text-align: center;
+            }
+        }
+        .schedule-box {
+            width: fit-content;
+            overflow-x: auto;
+            font-size: 14px;
+            position: relative;
+            .abscissa {
+                // width: 100%;
+                margin: 0 20px 0 110px;
+                display: flex;
+                width: fit-content;
+                .abs-type {
+                    .month {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        text-align: center;
+                        height: 29px;
+                        border: 1px solid #ccc;
+                        border-bottom: none;
+                        border-right: none;
+                    }
+                    .abscissa-item {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        .date {
+                            width: 59px;
+                            height: 28px;
+                            line-height: 30px;
+                            text-align: center;
+                            border: 1px solid #ccc;
+                            border-right: none;
+                            cursor: pointer;
+                            color: #409eff;
+                            &:hover {
+                                background: #ecf5ff;
+                            }
+                        }
+                    }
+                    &:last-child {
+                        .month {
+                            border-right: 1px solid #ccc;
+                        }
+                        .abscissa-item > div {
+                            &:last-child {
+                                border-right: 1px solid #ccc;
+                            }
+                        }
+                    }
+                }
+            }
+            .schedule-content {
+                // max-height: calc(100vh - 245px);
+                height: fit-content;
+                margin: 10px 0 20px 10px;
+                position: relative;
+                display: flex;
+                float: left;
+                overflow-y: auto;
+                white-space: nowrap;
+                .ordinate {
+                    width: 80px;
+                    padding: 0 10px;
+                    height: 100%;
+                    // background-color: #f0f0f0;
+                    display: flex;
+                    flex-shrink: 0;
+                    flex-direction: column;
+                    .ordinate-item {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        text-align: center;
+                        height: 60px;
+                        flex-shrink: 0;
+                    }
+                }
+                .shift-content {
+                    flex: 1;
+                    margin-right: 20px;
+                    .shift-row {
+                        display: flex;
+                        .shift-item {
+                            position: relative;
+                            width: 59px;
+                            height: 59px;
+                            font-size: 14px;
+                            border: 1px solid #ccc;
+                            border-bottom: none;
+                            border-right: none;
+                            cursor: context-menu;
+
+                            display: flex;
+                            flex-direction: column;
+                            justify-content: center;
+                            align-items: center;
+                            .shift {
+                                text-align: center;
+                            }
+
+                            &:has(.shift:nth-child(2)) {
+                                flex-direction: column;
+                            }
+
+                            &:has(.shift:nth-child(3)) {
+                                flex-direction: column;
+                                display: grid;
+                                grid-template-columns: repeat(2, 1fr);
+                                gap: 1px;
+                            }
+                            &:has(.shift:nth-of-type(odd):nth-last-of-type(odd)):not(:has(.overlay)) {
+                                // background: #303133;
+                                .shift:last-of-type {
+                                    grid-column: span 2;
+                                    justify-self: center;
+                                }
+                            }
+                            &:last-child {
+                                border-right: 1px solid #ccc;
+                            }
+                            .overlay {
+                                position: absolute;
+                                top: 0;
+                                left: 0;
+                                right: 0;
+                                bottom: 0;
+                                background: rgba(0, 0, 0, 0.3); /* 半透明遮罩 */
+                                display: flex;
+                                justify-content: center;
+                                align-items: center;
+                                color: white;
+                            }
+                            // .shift-item {
+                            //     width: 100%;
+                            //     height: 100%;
+                            //     font-size: 14px;
+                            //     .shift:last-child {
+                            //         margin-left: 5px;
+                            //     }
+                            //     .shift:only-child {
+                            //         margin-left: 0;
+                            //     }
+                            // }
+                        }
+                        &:last-of-type {
+                            .shift-item {
+                                border-bottom: 1px solid #ccc;
+                            }
+                        }
+                    }
+                }
+            }
+            .stickyTop {
+                position: fixed;
+                top: 80px;
+                left: 0;
+                right: 0;
+                z-index: 1001;
+                background: #fff;
+            }
+            .stickyLeft {
+                position: fixed;
+                top: 206px;
+                left: 0;
+                right: 0;
+                z-index: 1000;
+                background: #fff;
+                box-shadow: 0 2px 20px rgba(228, 231, 237, 1);
+            }
+        }
+    }
+}
+</style>

+ 230 - 0
src/views/business/​scheduleManage/list.vue

@@ -0,0 +1,230 @@
+<template>
+    <div class="main-container">
+        <ibps-crud
+            ref="crud"
+            :display-field="title"
+            :height="height"
+            :data="listData"
+            :toolbars="listConfig.toolbars"
+            :search-form="listConfig.searchForm"
+            :pk-key="pkKey"
+            :columns="listConfig.columns"
+            :row-handle="listConfig.rowHandle"
+            :pagination="pagination"
+            :loading="loading"
+            @action-event="handleAction"
+            @sort-change="handleSortChange"
+            @pagination-change="handlePaginationChange"
+            @row-dblclick="handleRowDblclick"
+        >
+            <template slot="dateRange" slot-scope="scope">
+                <span>{{ `${scope.row.startDate} 至 ${scope.row.endDate}` }}</span>
+            </template>
+        </ibps-crud>
+        <schedule-edit
+            v-if="showScheduleEdit"
+            :visible.sync="showScheduleEdit"
+            :page-params="params"
+            :readonly="readonly"
+            @refresh="loadData"
+            @close="() => showScheduleEdit = false"
+        />
+        <schedule-config-list
+            v-if="showConfigList"
+            :visible.sync="showConfigList"
+            @refresh="loadData"
+            @close="() => showConfigList = false"
+        />
+        <adjust-edit
+            v-if="showAdjustEdit"
+            :visible.sync="showAdjustEdit"
+            :params="params"
+            @refresh="loadData"
+            @close="() => showAdjustEdit = false"
+        />
+    </div>
+</template>
+
+<script>
+import { queryStaffSchedule, removeStaffSchedule, queryScheduleConfig } from '@/api/business/schedule'
+import { scheduleType } from '@/views/constants/schedule'
+import ActionUtils from '@/utils/action'
+import FixHeight from '@/mixins/height'
+
+export default {
+    components: {
+        ScheduleEdit: () => import('./edit'),
+        ScheduleConfigList: () => import('./components/config-list'),
+        AdjustEdit: () => import('./components/adjust-edit')
+    },
+    mixins: [FixHeight],
+    data () {
+        const { userList = [] } = this.$store.getters || {}
+        const userOption = userList.map(item => ({ label: item.userName, value: item.userId }))
+        return {
+            userOption,
+            scheduleType,
+            title: '排班记录',
+            pkKey: 'id', // 主键  如果主键不是pk需要传主键
+            loading: true,
+            height: document.clientHeight,
+            listData: [],
+            pagination: {},
+            sorts: {},
+            showScheduleEdit: false,
+            showConfigList: false,
+            showAdjustEdit: false,
+            readonly: false,
+            params: {},
+            listConfig: {
+                toolbars: [
+                    { key: 'search', icon: 'ibps-icon-search', label: '查询', type: 'primary', hidden: false },
+                    { key: 'create', icon: 'ibps-icon-plus', label: '创建', type: 'success', hidden: false },
+                    { key: 'remove', icon: 'ibps-icon-close', label: '删除', type: 'danger', hidden: false },
+                    { key: 'config', icon: 'ibps-icon-cogs', label: '配置', type: 'info', hidden: false }
+                ],
+                searchForm: {
+                    labelWidth: 80,
+                    itemWidth: 180,
+                    forms: [
+                        { prop: 'Q^title_^SL', label: '排班名称' },
+                        { prop: 'Q^type_^S', label: '排班类型', fieldType: 'select', options: scheduleType, multiple: 'N' },
+                        { prop: ['Q^create_time_^DL', 'Q^create_time_^DG'], label: '创建时间', fieldType: 'daterange', itemWidth: 200 }
+                    ]
+                },
+                // 表格字段配置
+                columns: [
+                    { prop: 'title', label: '排班名称', minWidth: 150 },
+                    { prop: 'type', label: '排班类型', tags: scheduleType, width: 120 },
+                    { prop: 'dateRange', label: '排班时间范围', slotName: 'dateRange', width: 180 },
+                    { prop: 'createBy', label: '创建人', tags: userOption, width: 100 },
+                    { prop: 'createTime', label: '创建时间', dateFormat: 'yyyy-MM-dd HH:mm', sortable: 'custom', width: 140 }
+                ],
+                rowHandle: {
+                    effect: 'display',
+                    actions: [
+                        { key: 'adjust', label: '申请调班', type: 'primary', icon: 'ibps-icon-exchange' },
+                        { key: 'edit', label: '编辑', type: 'primary', icon: 'ibps-icon-edit' },
+                        { key: 'preview', label: '查看', type: 'primary', icon: 'ibps-icon-eye' }
+                        // { key: 'report', label: '实验报告', type: 'success', icon: 'ibps-icon-file-text-o' }
+                    ]
+                }
+            }
+        }
+    },
+    created () {
+        this.loadData()
+    },
+    methods: {
+        // 加载数据
+        loadData () {
+            this.loading = true
+            queryStaffSchedule(this.getSearchFormData()).then(res => {
+                ActionUtils.handleListData(this, res.data)
+                this.loading = false
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+        /**
+         * 获取格式化参数
+         */
+        getSearchFormData () {
+            return ActionUtils.formatParams(
+                this.$refs['crud'] ? this.$refs['crud'].getSearcFormData() : {},
+                this.pagination,
+                this.sorts
+            )
+        },
+        /**
+         * 处理分页事件
+         */
+        handlePaginationChange (page) {
+            ActionUtils.setPagination(this.pagination, page)
+            this.loadData()
+        },
+        /**
+         * 处理排序
+         */
+        handleSortChange (sort) {
+            ActionUtils.setSorts(this.sorts, sort)
+            this.loadData()
+        },
+        /**
+         * 查询
+         */
+        search () {
+            this.loadData()
+        },
+        /**
+         * 处理按钮事件
+         */
+        handleAction (command, position, selection, data) {
+            switch (command) {
+                case 'search':
+                    ActionUtils.setFirstPagination(this.pagination)
+                    this.search()
+                    break
+                case 'create':
+                    this.handleEdit(command, {})
+                    break
+                case 'config':
+                    this.showConfigList = true
+                    break
+                case 'edit':
+                    this.handleEdit(command, data)
+                    break
+                case 'preview':
+                    this.handleEdit(command, data)
+                    break
+                case 'adjust':
+                    this.handleAdjust(command, data)
+                    break
+                case 'remove':
+                    ActionUtils.removeRecord(selection).then((ids) => {
+                        this.handleRemove(ids)
+                    }).catch(() => {})
+                    break
+                default:
+                    break
+            }
+        },
+        /**
+         * 处理编辑
+         */
+        async handleEdit (key, { id }) {
+            this.params = {
+                id
+            }
+            this.readonly = key === 'detail'
+            this.showScheduleEdit = true
+        },
+        handleAdjust (key, { id }) {
+            this.params = {
+                scheduleId: id
+            }
+            this.showAdjustEdit = true
+        },
+        /**
+         * 处理删除
+         */
+        handleRemove (ids) {
+            // return this.$message.warning('避免误删测试数据,联系开发删除')
+            removeStaffSchedule({ ids }).then(() => {
+                ActionUtils.removeSuccessMessage()
+                this.search()
+            }).catch(() => {})
+        },
+        handleRowDblclick (row) {
+            this.handleEdit(row, 'detail')
+        }
+    }
+}
+</script>
+<style lang="scss">
+    .attachment-uploader-dialog {
+        .el-dialog__body {
+            height: calc(57vh - 100px) !important;
+        }
+    }
+</style>

+ 342 - 0
src/views/business/​scheduleManage/preview.vue

@@ -0,0 +1,342 @@
+<template>
+    <div class="schedule">
+        <div class="schedule-container">
+            <div class="page-header">
+                <div class="toolbar">
+                    <el-button
+                        v-for="btn in toolbars"
+                        :key="btn.key"
+                        :type="btn.type"
+                        size="mini"
+                        :icon="btn.icon"
+                        @click="handleAction(btn.key)"
+                    >{{ btn.label }}</el-button>
+                </div>
+                <div class="page-title">{{ `检验科8月${title}` }}</div>
+            </div>
+            <div class="schedule-box">
+                <div class="abscissa">
+                    <div v-for="(month, mIndex) in Object.keys(dateObj)" :key="mIndex" class="abs-type">
+                        <div class="month">{{ month }}</div>
+                        <div class="abscissa-item">
+                            <div v-for="(date, dIndex) in dateObj[month]" :key="dIndex">{{ date.split('-')[2] }}</div>
+                        </div>
+                    </div>
+                </div>
+                <div class="schedule-content">
+                    <div class="ordinate">
+                        <div v-for="item in ordinateList" :key="item.id" class="ordinate-item">
+                            {{ item.name }}
+                        </div>
+                    </div>
+                    <div class="item-list">
+                        <div v-for="(row, rIndex) in ordinateList" :key="row.id" class="item-row">
+                            <div v-for="(column, cIndex) in dateList" :key="cIndex" class="item">
+                                <div
+                                    ref="scheduleItem"
+                                    class="item-content"
+                                    @mouseenter="hoveredIndex = `${row.id}-${cIndex}`"
+                                    @mouseleave="hoveredIndex = null"
+                                    @contextmenu.prevent="handleRightClick($event, {row, rIndex, column, cIndex})"
+                                >
+                                    <span
+                                        v-for="(icon, iconIndex) in scheduleData[row.id][cIndex]"
+                                        :key="iconIndex"
+                                        class="icon-box"
+                                    >
+                                        <i :class="icon.icon" :style="{ color: `${icon.color}` }" />
+                                    </span>
+                                    <div v-if="hoveredIndex === `${row.id}-${cIndex}` && !readonly" class="overlay">
+                                        <i class="el-icon-edit" />
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <context-menu
+            :visible.sync="showContextMenu"
+            :params="params"
+            :position="itemPosition"
+            :item-data="selectItem"
+            :readonly="readonly"
+            @select="handleSelect"
+            @close="handleClose"
+        />
+    </div>
+</template>
+
+<script>
+import { position } from '../../constants/schedule'
+import { mapValues, keyBy } from 'lodash'
+export default {
+    name: 'schedule',
+    components: {
+        ContextMenu: () => import('./components/context-menu')
+    },
+    data () {
+        const { userList = [], deptList = [] } = this.$store.getters || {}
+        const users = userList.map(item => ({ id: item.userId, name: item.userName }))
+        const readonly = false
+        return {
+            users,
+            deptList,
+            position,
+            readonly,
+            title: '排班',
+            toolbars: [
+                { key: 'changeView', icon: 'el-icon-set-up', label: '切换视图', type: 'primary' },
+                { key: 'history', icon: 'el-icon-time', label: '排班历史', type: 'info' },
+                { key: 'record', icon: 'el-icon-tickets', label: '修改记录', type: 'warning' },
+                { key: 'export', icon: 'el-icon-download', label: '导出', type: 'primary', hidden: readonly },
+                { key: 'reset', icon: 'el-icon-refresh', label: '重置', type: 'warning', hidden: readonly },
+                { key: 'edit', icon: 'el-icon-edit', label: '编辑', type: 'primary', hidden: readonly },
+                { key: 'save', icon: 'ibps-icon-save', label: '保存', type: 'primary', hidden: readonly },
+                { key: 'submit', icon: 'ibps-icon-send', label: '提交', type: 'success', hidden: readonly },
+                { key: 'cancel', icon: 'el-icon-close', label: '关闭', type: 'danger' }
+            ],
+            dateRange: ['2024-07-29', '2024-09-01'],
+            viewType: 'users',
+            dateObj: {},
+            dateList: [],
+            showContextMenu: false,
+            hoveredIndex: null,
+            params: {},
+            selectItem: [],
+            itemPosition: {}
+        }
+    },
+    computed: {
+        ordinateList () {
+            return this[this.viewType]
+        },
+        scheduleData () {
+            return mapValues(keyBy(this.ordinateList, 'id'), () => [])
+        }
+    },
+    created () {
+        this.dateObj = this.getDateList(this.dateRange)
+        this.dateList = Object.values(this.dateObj).flat()
+    },
+    methods: {
+        getDateList (dateRange) {
+            const [startDate, endDate] = dateRange.map(date => new Date(date))
+            const dates = {}
+            const currentDate = startDate
+            while (currentDate <= endDate) {
+                const yearMonth = currentDate.toISOString().slice(0, 7) // 获取 'YYYY-MM' 格式
+                const date = currentDate.toISOString().split('T')[0] // 获取 'YYYY-MM-DD' 格式
+                if (!dates[yearMonth]) {
+                    dates[yearMonth] = []
+                }
+                dates[yearMonth].push(date)
+                currentDate.setDate(currentDate.getDate() + 1)
+            }
+            return dates
+        },
+        changeView () {
+            this.viewType = this.viewType === 'users' ? 'position' : 'users'
+        },
+        handleRightClick (event, { row, colunm, rIndex, cIndex }) {
+            this.selectItem = []
+            this.showContextMenu = true
+            const item = this.$refs.scheduleItem[rIndex * this.dateList.length + cIndex + 1]
+            const rect = item.getBoundingClientRect()
+            this.itemPosition = {
+                top: rect.top + window.scrollY,
+                left: rect.left + window.scrollX
+            }
+            this.params = {
+                row,
+                rIndex,
+                colunm,
+                cIndex
+            }
+            this.selectItem = this.scheduleData[row.id][cIndex]
+            console.log(this.selectItem)
+        },
+        handleSelect ({ selected, params }) {
+            this.scheduleData[params.row.id][params.cIndex] = selected
+        },
+        handleClose () {
+            this.showContextMenu = false
+        },
+        handleAction (action) {
+            switch (action) {
+                case 'save':
+                    this.$emit('save')
+                    break
+                case 'submit':
+                    this.$emit('submit')
+                    break
+                case 'changeView':
+                    this.changeView()
+                    break
+                case 'history':
+                    this.$emit('history')
+                    break
+                case 'record':
+                    this.$emit('record')
+                    break
+                case 'cancel':
+                    this.$router.go(-1)
+                    break
+                default:
+                    break
+            }
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.schedule {
+    width: 100%;
+    .schedule-container {
+        width: 100%;
+        ::v-deep {
+            .el-button > span {
+                margin-left: 5px;
+            }
+        }
+        .page-header {
+            padding: 20px;
+            height: 80px;
+            .toolbar {
+                text-align: right;
+            }
+            .page-title {
+                margin-top: 20px;
+                font-size: 24px;
+                font-weight: 600;
+                text-align: center;
+            }
+        }
+        .schedule-box {
+            width: auto;
+            overflow-x: auto;
+            font-size: 14px;
+            position: relative;
+            .abscissa {
+                // width: 100%;
+                margin: 0 20px 0 110px;
+                display: flex;
+                .abs-type {
+                    .month {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        text-align: center;
+                        height: 29px;
+                        border: 1px solid #ccc;
+                        border-bottom: none;
+                        border-right: none;
+                    }
+                    .abscissa-item {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        > div {
+                            width: 59px;
+                            height: 28px;
+                            line-height: 30px;
+                            text-align: center;
+                            border: 1px solid #ccc;
+                            border-right: none;
+                        }
+                    }
+                    &:last-child {
+                        .month {
+                            border-right: 1px solid #ccc;
+                        }
+                        .abscissa-item > div {
+                            &:last-child {
+                                border-right: 1px solid #ccc;
+                            }
+                        }
+                    }
+                }
+            }
+            .schedule-content {
+                height: calc(100vh - 220px);
+                margin: 10px 0 20px 10px;
+                position: relative;
+                display: flex;
+                float: left;
+                overflow-y: auto;
+                white-space: nowrap;
+                .ordinate {
+                    width: 80px;
+                    padding: 0 10px;
+                    height: 100%;
+                    position: sticky;
+                    left: 0;
+                    // background-color: #f0f0f0;
+                    display: flex;
+                    flex-shrink: 0;
+                    flex-direction: column;
+                    .ordinate-item {
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                        text-align: center;
+                        height: 30px;
+                        flex-shrink: 0;
+                    }
+                }
+                .item-list {
+                    flex: 1;
+                    margin-right: 20px;
+                    .item-row {
+                        display: flex;
+                        .item {
+                            position: relative;
+                            width: 59px;
+                            height: 29px;
+                            line-height: 30px;
+                            text-align: center;
+                            border: 1px solid #ccc;
+                            border-bottom: none;
+                            border-right: none;
+                            cursor: context-menu;
+                            &:last-child {
+                                border-right: 1px solid #ccc;
+                            }
+                            .overlay {
+                                position: absolute;
+                                top: 0;
+                                left: 0;
+                                right: 0;
+                                bottom: 0;
+                                background: rgba(0, 0, 0, 0.3); /* 半透明遮罩 */
+                                display: flex;
+                                justify-content: center;
+                                align-items: center;
+                                color: white;
+                            }
+                            .item-content {
+                                width: 100%;
+                                height: 100%;
+                                font-size: 16px;
+                                .icon-box:last-child {
+                                    margin-left: 5px;
+                                }
+                                .icon-box:only-child {
+                                    margin-left: 0;
+                                }
+                            }
+                        }
+                        &:last-of-type {
+                            .item {
+                                border-bottom: 1px solid #ccc;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 191 - 0
src/views/constants/schedule.js

@@ -0,0 +1,191 @@
+export const position = [
+    { id: '1192487979046666240', name: '手工免疫岗' },
+    { id: '1192488035841736704', name: '感染免疫岗' },
+    { id: '1192488451342073856', name: '体液岗位1(N)' },
+    { id: '1192488503548575744', name: '体液岗位3(Z2)' },
+    { id: '1192488585081651200', name: '血液岗位1(C1)' },
+    { id: '1192488648084291584', name: '血液岗位2(C2)' },
+    { id: '1192488700655697920', name: '血液岗位3(Z1)' },
+    { id: '1193222635274633216', name: '生化发光' },
+    { id: '1193855468859031552', name: '接种岗W1' },
+    { id: '1193855560487796736', name: '鉴定岗W2' },
+    { id: '1193858026730160128', name: '鉴定岗W3' },
+    { id: '1193858058485235712', name: '生化常规 X1' },
+    { id: '1193858175904776192', name: '生化常规 X2' },
+    { id: '1193858270943510528', name: '生化常规 D1' },
+    { id: '1193859728850026496', name: '生化常规 D2' },
+    { id: '1193867051173675008', name: '体液岗位2(中)' },
+    { id: '1193871365321523200', name: '血液岗位4(骨髓)' },
+    { id: '1194228795146502144', name: '标本接收岗' }
+]
+
+export const configFormRules = {
+    target: [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    targetKey: [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    sn: [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    icon: [{
+        required: false,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].name': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].methodName': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].methodType': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].sn': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].isBasic': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].isDisabled': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].isPublic': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].step': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].criterion': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }],
+    'methods[].params': [{
+        required: true,
+        message: '请输入',
+        trigger: 'change'
+    }]
+}
+
+export const scheduleColumn = [
+    { label: '班次名称', key: 'name', width: '120px' },
+    { label: '班次别名', key: 'alias', width: '120px' },
+    // { label: '班次KEY', key: 'key', width: '100px' },
+    { label: '时间段', key: 'dateRange', width: '200px', slotName: 'dateRange' },
+    { label: '对应岗位', key: 'positions', minWidth: '200px', slotName: 'positions' },
+    { label: '是否可用', key: 'isEnabled', width: '100px', slotName: 'isEnabled' },
+    { label: '对应颜色', key: 'color', width: '120px', slotName: 'color' },
+    { label: '说明', key: 'desc', width: '150px' }
+]
+
+export const ruleColumn = [
+    { label: '名称', key: 'label', width: '200px' },
+    { label: '编码', key: 'key', width: '150px' },
+    { label: '是否启用', key: 'isEnabled', width: '150px' },
+    { label: '表达式', key: 'value', minWidth: '220px' }
+]
+
+export const dateType = [
+    {
+        key: 'current',
+        value: '当天'
+    },
+    {
+        key: 'next',
+        value: '次日'
+    }
+]
+
+export const scheduleType = [
+    {
+        label: '科室排班',
+        value: 'whole'
+    },
+    {
+        label: '专业组排班',
+        value: 'group'
+    },
+    {
+        label: '规培生排班',
+        value: 'train'
+    },
+    {
+        label: '进修生排班',
+        value: 'students'
+    },
+    {
+        label: '实习生排班',
+        value: 'practice'
+    }
+]
+
+export const cycleOptions = [
+    {
+        label: '日',
+        value: 'day',
+        limit: 45
+    },
+    {
+        label: '周',
+        value: 'week',
+        limit: 6
+    },
+    {
+        label: '月',
+        value: 'month',
+        limit: 1
+    }
+]
+
+export const stateType = [
+    {
+        label: '待审核',
+        value: '待审核',
+        type: 'info'
+    },
+    {
+        label: '待审批',
+        value: '待审批',
+        type: 'primary'
+    },
+    {
+        label: '已取消',
+        value: '已取消',
+        type: 'warning'
+    },
+    {
+        label: '已作废',
+        value: '已作废',
+        type: 'danger'
+    },
+    {
+        label: '已通过',
+        value: '已通过',
+        type: 'success'
+    }
+]

+ 80 - 2
src/views/system/dashboard/components/util.js

@@ -147,7 +147,9 @@ export function buildComponent (name, column, preview, vm) {
                         axis: 'y'
                     },
                     calendarToolbar: this.fullScreen ? [{ key: 'refresh' }] : [{ key: 'refresh' }, { key: 'fullscreen' }, { key: 'collapse' }],
-                    isFirstAlert: true // 是否首次日程提醒
+                    isFirstAlert: true, // 是否首次日程提醒
+                    scheduleData: [],
+                    todaySchedule: []
                 }
             },
             computed: {
@@ -157,8 +159,9 @@ export function buildComponent (name, column, preview, vm) {
             },
             mounted () {
                 this.defaultForm = JSON.parse(JSON.stringify(this.quickNavform))
-                this.$nextTick(() => {
+                this.$nextTick(async () => {
                     this.fetchData()
+                    this.scheduleData = await this.getScheduleData()
                 })
             },
             methods: {
@@ -574,6 +577,81 @@ export function buildComponent (name, column, preview, vm) {
                         return val.slice(0, length - 2) + '...'
                     }
                     return val
+                },
+                getDays (start, end) {
+                    if (!start || !end) {
+                        return 0
+                    }
+                    return Math.ceil((new Date(end) - new Date(start)) / (1000 * 60 * 60 * 24))
+                },
+                getScheduleData () {
+                    const sql = `select a.*, b.title_, b.start_date_, b.end_date_, b.config_, b.overview_ from t_schedule_detail a, t_schedule b where a.parent_id_ = b.id_ and a.user_id_ = '${this.userId}'`
+                    return new Promise((resolve, reject) => {
+                        this.$common.request('sql', sql).then(res => {
+                            const { data = [] } = res.variables || {}
+                            const eventList = []
+                            data.forEach(item => {
+                                const days = this.getDays(item.start_date_, item.end_date_)
+                                const config = item.config_ ? JSON.parse(item.config_) : {}
+                                const { scheduleShift } = config
+                                for (let i = 1; i <= days; i++) {
+                                    const shift = item[`d${i}_`]
+                                    if (shift) {
+                                        const date = this.$common.getFormatDate('string', 10, this.$common.getDate('day', i - 1, item.start_date_))
+                                        const shiftList = shift.split(',')
+                                        shiftList.forEach(s => {
+                                            const t = scheduleShift.find(i => i.alias === s)
+                                            eventList.push({
+                                                color: t ? t.color : '',
+                                                content: t.dateRange.map(d => {
+                                                    return d.type === 'allday' ? '全天' : (`当天 ${d.startTime}` + ' 至 ' + `${d.isSecondDay === 'Y' ? '第二天' : '当天'} ${d.endTime}`)
+                                                }).join('\n'),
+                                                title: s,
+                                                start: date,
+                                                end: date,
+                                                jieShuShiJian: date,
+                                                zhuangTai: '',
+                                                id: i
+                                            })
+                                        })
+                                    }
+                                }
+                            })
+                            const today = this.$common.getDateNow()
+                            this.todaySchedule = eventList.filter(i => i.start === today).map(i => i.title)
+                            console.log(this.todaySchedule)
+                            resolve(eventList)
+                        }).catch(error => {
+                            reject(error)
+                        })
+                    })
+                },
+                showMySchedule () {
+                    const scheduleConfig = {
+                        height: '100%',
+                        locale: 'zh-cn',
+                        selectable: true,
+                        buttonText: {
+                            today: '今天',
+                            dayGridMonth: '月',
+                            listMonth: '',
+                            month: '月',
+                            week: '周视图',
+                            day: '日视图',
+                            list: '表'
+                            // prev: '<i class="icon-chevron-left">后退</i>',
+                            // next: '<i class="icon-chevron-right">前进</i>'
+                        },
+                        headerToolbar: {
+                            left: 'prev,next today',
+                            // start: '',
+                            center: 'title',
+                            right: 'dayGridMonth,timeGridWeek,timeGridDay'
+                            // end: 'prev,next,today,month,agendaWeek,agendaDay,listWeek'
+                        },
+                        events: this.scheduleData
+                    }
+                    this.$emit('action-event', 'mySchedule', scheduleConfig)
                 }
             },
             template: column.templateHtml !== '' ? `${column.templateHtml}` : `<div></div>`

+ 23 - 5
src/views/system/dashboard/templates/userInfoTab.vue

@@ -1,5 +1,5 @@
 <template>
-    <el-card class="home-card changeShadow verticalCenterFlex">
+    <el-card id="user-info" class="home-card changeShadow verticalCenterFlex">
         <div ref="body" :style="{width: '100%'}">
             <ibps-list-item-meta style="align-items: center; overflow: hidden;">
                 <el-avatar
@@ -10,12 +10,12 @@
                     :size="85"
                     style="font-size: 85px; color: #c0c4cc; background: #fff;"
                 >
-                    <img :src="getPhoto(data.photo)" class="photo-img" @error="errorAvatarHandler(data)">
+                    <img :src="getPhoto(data.photo)" class="photo-img" style="height: 100%; width: 100%;" @error="errorAvatarHandler(data)">
                 </el-avatar>
                 <div slot="title" class="ibps-item-content">
                     <div class="ibps-item-content-title" style="font-size: 16px;">
-                        <span>您好!</span>
-                        <span v-if="data && data.orgName">{{ data.orgName }}的</span>
+                        <label>您好!</label>
+                        <!-- <span v-if="data && data.orgName">{{ data.orgName }}的</span> -->
                         <label v-if="data && data.fullname">{{ data.fullname }}</label>
                         <label v-if="data && data.gender">{{ data.gender | filterStatus('gender') }}</label>
                     </div>
@@ -34,7 +34,25 @@
                                 size="mini"
                             >{{ item.name }}</el-tag>
                         </div>
-                        <!-- <span v-if="data&&data.address!==''">地址:{{ data.address }}</span> -->
+                        <div>
+                            <template v-if="todaySchedule.length">
+                                <span>今日班次:</span>
+                                <el-tag
+                                    v-for="(item, index) in todaySchedule"
+                                    :key="index"
+                                    class="dept-tag"
+                                    type="info"
+                                    size="mini"
+                                >{{ item }}</el-tag>
+                            </template>
+                            <span v-else>今日无排班,祝您休息愉快!</span>
+                            <a style="color: #409eff;" @click="showMySchedule">我的排班</a>
+                            <!-- <el-button
+                                type="primary"
+                                size="mini"
+                                @click="showMySchedule"
+                            >我的排班</el-button> -->
+                        </div>
                     </div>
                 </div>
             </ibps-list-item-meta>

+ 65 - 0
src/views/system/homepage/components/mySchedule.vue

@@ -0,0 +1,65 @@
+<template>
+    <el-dialog
+        ref="dialog"
+        :title="title"
+        :visible.sync="scheduleDialogVisible"
+        :close-on-click-modal="false"
+        :close-on-press-escape="false"
+        :fullscreen="fullscreen"
+        class="my-schedule"
+        top="10vh"
+        width="60%"
+        @open="openDialog"
+        @close="closeDialog"
+    >
+        <v-full-calendar
+            ref="calendar"
+            :options="scheduleConfig"
+            :style="{height: '100%', width: '100%'}"
+        />
+    </el-dialog>
+</template>
+
+<script>
+export default {
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        },
+        scheduleConfig: {
+            type: Object,
+            default: () => {}
+        }
+    },
+    data () {
+        return {
+            title: '我的排班',
+            fullscreen: true,
+            scheduleDialogVisible: this.visible
+        }
+    },
+    methods: {
+        openDialog () {
+            this.scheduleDialogVisible = true
+        },
+        closeDialog () {
+            this.scheduleDialogVisible = false
+        },
+        getScheduleData () {
+
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+    .my-schedule {
+        ::v-deep {
+            .el-dialog__body {
+                padding: 20px 0 !important;
+                height: calc(100vh - 100px);
+            }
+        }
+    }
+</style>

+ 17 - 2
src/views/system/homepage/index.vue

@@ -127,6 +127,11 @@
             :calendar-alert-data="calendarAlertData"
             @onCalendarAlert="onCalendarAlert"
         />
+        <my-schedule
+            ref="schedule"
+            :schedule-config="scheduleConfig"
+            @close="handleClose"
+        />
     </ibps-container>
 </template>
 
@@ -150,6 +155,7 @@ import ScheduleAdd from '@/views/system/dashboard/templates/scheduleAdd'
 import { markReadCalendar } from '@/api/detection/newHomeApi'
 // 日程提醒弹窗组件
 import CalendarAlert from '@/views/system/dashboard/components/calendar-alert.vue'
+import mySchedule from './components/mySchedule.vue'
 
 const _import = require('@/utils/util.import.' + process.env.NODE_ENV)
 export default {
@@ -162,7 +168,8 @@ export default {
         'ibps-grid-layout': GridLayout,
         'ibps-grid-item': GridItem,
         ScheduleAdd,
-        CalendarAlert
+        CalendarAlert,
+        mySchedule
     },
     data () {
         return {
@@ -217,7 +224,8 @@ export default {
             calendarDialogForm: {},
             addComponentDatas: {},
             calendarAlertData: {}, // 日程
-            calendarIds: [] // 日程 id 数组
+            calendarIds: [], // 日程 id 数组
+            scheduleConfig: {}
         }
     },
     computed: {
@@ -362,6 +370,9 @@ export default {
                 case 'calendarAlert':
                     this.handleCalendarAlert(params)
                     break
+                case 'mySchedule':
+                    this.handleMySchedule(params)
+                    break
                 default:
                     break
             }
@@ -679,6 +690,10 @@ export default {
             if (param.state === 'calendar') {
                 this.$refs.myCalendar[0].hanldeCalendardel(param)
             }
+        },
+        handleMySchedule (data) {
+            this.scheduleConfig = data
+            this.$refs.schedule.openDialog()
         }
     }
 }