xiaochan 1 月之前
父節點
當前提交
0b8bcd586b

+ 58 - 0
src/api/settings/schedulingClassApi.ts

@@ -0,0 +1,58 @@
+import requestV2 from "@/utils/request-v2";
+
+export type DataValue = {
+  schedulingDate: string;
+  schedulingDateFormat: string;
+  changeClassName: string;
+  name: string;
+  falseName: string;
+  falseCode: string;
+  code: string;
+  falseCodeList: { code: string; name: string }[];
+};
+
+export function getCodes() {
+  return requestV2({
+    url: "/schedulingClass/getCodes",
+    method: "get",
+  });
+}
+
+export function getSortAndName() {
+  return requestV2({
+    url: "/schedulingClass/getSortAndName",
+    method: "get",
+  });
+}
+
+export function saveSort(data) {
+  return requestV2({
+    url: "/schedulingClass/saveSort",
+    method: "post",
+    data,
+  });
+}
+
+export function createByMonthApi(month: string) {
+  return requestV2({
+    url: "/schedulingClass/createByMonth",
+    method: "get",
+    params: { month },
+  });
+}
+
+export function getByMonth(month: string) {
+  return requestV2<DataValue[]>({
+    url: "/schedulingClass/getByMonth",
+    method: "get",
+    params: { month },
+  });
+}
+
+export function updateById(data: { id: string }) {
+  return requestV2({
+    url: "/schedulingClass/updateById",
+    method: "post",
+    data,
+  });
+}

+ 0 - 1
src/components/zhu-yuan-yi-sheng/jian-cha-shen-qing/da-ying/PrintCheckList.vue

@@ -34,7 +34,6 @@ function handleQrCode() {
         lineColor: "#333",
         width: 2,
         height: 18,
-        displayValue: false,
         margin: 0,
         fontSize: 12,
       });

+ 7 - 2
src/utils/excel.js

@@ -26,6 +26,10 @@ export function createWorkSheet(data, fields, titles) {
 
 const progressBarStore = useProgressBarStore();
 
+/**
+ * 导出 Excel
+ * @param {data:{method: "post" | "get", jdt : boolean,url:string,param : any,fileName:string}} data
+ */
 export function downloadExcel(data) {
   if (data.jdt) {
     progressBarStore.initialize({
@@ -36,10 +40,11 @@ export function downloadExcel(data) {
   } else {
     startLoading();
   }
+  const { method = "post" } = data;
   axios({
-    method: data.method || "post",
+    method: method,
     url: apiUrl + data.url,
-    data: data.param,
+    [method === "post" ? "data" : "params"]: data.param,
     responseType: "blob",
     headers: {
       token: localStorage.token,

+ 40 - 0
src/utils/useTableToExcel.tsx

@@ -0,0 +1,40 @@
+import { nextTick, renderSlot } from "vue";
+
+export function useTableToExcel({
+  css,
+  hidden = true,
+}: { css: string; hidden: boolean } = {}) {
+  const uri = "data:application/vnd.ms-excel;base64,";
+
+  const table: Ref<HTMLDivElement> = shallowRef();
+
+  async function exportFun(name = "sheet1") {
+    await nextTick();
+    const tableid = unref(table).innerHTML;
+    const template = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"xmlns="http://www.w3.org/TR/REC-html40"><head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${name}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--> <style type="text/css">${css}</style></head><body>${tableid}</body></html>`;
+    const alink = document.createElement("a");
+    const base64 = btoa(
+      new TextEncoder()
+        .encode(template)
+        .reduce((data, byte) => data + String.fromCharCode(byte), "")
+    );
+    alink.href = uri + base64;
+    alink.download = `${name}.xls`;
+    alink.click();
+  }
+
+  const Template = defineComponent({
+    setup(_, { slots }) {
+      return () => (
+        <div
+          style={hidden ? { width: 0, height: 0, overflow: "hidden" } : {}}
+          ref={el => (table.value = el)}
+        >
+          {renderSlot(slots, "default")}
+        </div>
+      );
+    },
+  });
+
+  return [Template, exportFun];
+}

+ 1 - 0
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/plugins/ca/EmrCAComp.vue

@@ -300,6 +300,7 @@ function openSignIdCard() {
     },
     dialogProps: {
       title: "维护患者身份证",
+      closeOnClickModal: false,
     },
     confirmText: "保存",
   }).then(() => {

+ 1 - 0
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/plugins/ca/SignIdCard.vue

@@ -68,6 +68,7 @@ function handleSelect(row, val) {
 }
 
 function cellClick({ row }) {
+  if (props.isEdit) return;
   emits("cyDialogConfirm", row);
 }
 

+ 1 - 1
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/plugins/ca/emr-ca.tsx

@@ -256,7 +256,7 @@ export function emrCa(): Install {
     const code = res.code;
     const msg = getSignMsg(patientInfo, emrName, view);
     const caData = await sendByCode({
-      id: isDev ? "00026" : code,
+      id: code,
       msg: msg,
       desc: msg,
     });

+ 196 - 0
src/views/settings/scheduling-class/ExportFalseExcel.vue

@@ -0,0 +1,196 @@
+<script setup lang="ts">
+import { useTableToExcel } from "@/utils/useTableToExcel";
+import { DataValue } from "@/api/settings/schedulingClassApi";
+import dayjs from "dayjs";
+import env from "@/utils/setting";
+import XEUtils from "xe-utils";
+import { stringNotBlank } from "@/utils/blank-utils";
+
+const props = defineProps<{
+  deptList: any[];
+}>();
+
+const [TableTemplate, exportFun] = useTableToExcel({
+  css: `td{text-align: center}`,
+});
+
+const store = reactive({
+  headerColSpan: 0,
+  title: "",
+  dayLength: 0,
+  month: "",
+  weekList: [],
+
+  keyDayMap: new Map(),
+  deptList: [],
+});
+
+const days = ["天", "一", "二", "三", "四", "五", "六"];
+
+function employeeHandling(month) {
+  store.deptList = [];
+  const clone: { code: string; name: string }[] = XEUtils.cloneDeep(
+    props.deptList
+  );
+
+  const tmpDeptData = [];
+
+  clone.forEach(item => {
+    const classDay = [];
+
+    for (let i = 0; i < store.dayLength; i++) {
+      let strIndex = i + 1 < 10 ? `${month}-0${i + 1}` : `${month}-${i + 1}`;
+      const week = dayjs(strIndex).day();
+      const 值班人员: DataValue = store.keyDayMap.get(
+        dayjs(strIndex).format("MM-DD")
+      );
+
+      // 最优先判断是否有给领导看的排班
+      if (值班人员.falseCodeList?.length > 0) {
+        const find = 值班人员.falseCodeList.find(falseItem => {
+          return falseItem.code === item.code;
+        });
+        if (find) {
+          classDay.push("白");
+        } else {
+          classDay.push("休");
+        }
+      } else {
+        if (week === 6) {
+          // 如果有换班的话
+          if (
+            stringNotBlank(值班人员.falseCode) &&
+            值班人员.falseCode === item.code
+          ) {
+            classDay.push("白");
+          } else if (值班人员.code === item.code) {
+            classDay.push("白");
+          } else {
+            classDay.push("白/2");
+          }
+        } else if (week === 0) {
+          classDay.push("休");
+        } else {
+          classDay.push("白");
+        }
+      }
+    }
+    tmpDeptData.push({
+      name: item.name,
+      data: classDay,
+    });
+  });
+  store.deptList = tmpDeptData;
+}
+
+function renderTable(month: string, value: DataValue[]) {
+  store.keyDayMap.clear();
+  value.forEach(item => {
+    store.keyDayMap.set(dayjs(item.schedulingDate).format("MM-DD"), item);
+  });
+
+  store.dayLength = dayjs(month).daysInMonth();
+  store.headerColSpan = store.dayLength + 13;
+  store.title = `${dayjs(month).format("YYYY年MM月")}排班表`;
+  store.weekList = Array.from({ length: store.dayLength }).map((_, i) => {
+    let strIndex = i + 1 < 10 ? `${month}-0${i + 1}` : `${month}-${i + 1}`;
+    const week = dayjs(strIndex).day();
+    return days[week];
+  });
+  employeeHandling(month);
+}
+
+defineExpose({
+  exportExcel(month: string, value: DataValue[]) {
+    renderTable(month, value);
+    exportFun(dayjs(month).format("YYYY年MM月") + "排班表.xlsx");
+  },
+});
+</script>
+
+<template>
+  <TableTemplate>
+    <table style="width: 100%" cellpadding="0" cellspacing="0" border="1">
+      <colgroup>
+        <col style="width: 15px" />
+        <col style="width: 55px" />
+        <col style="width: 35px" v-for="item in store.dayLength" />
+        <col style="width: 55px" v-for="c in 11" />
+      </colgroup>
+      <thead>
+        <tr style="text-align: center; height: 20px">
+          <th :colspan="store.headerColSpan">
+            <h1>{{ env.VITE_HOSPITAL_NAME }}{{ store.title }}</h1>
+          </th>
+        </tr>
+        <tr style="text-align: left !important; height: 15px">
+          <th :colspan="store.headerColSpan" style="text-align: left">
+            <h4>部门:信息科</h4>
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td rowspan="2">序号</td>
+          <td>日期</td>
+          <td v-for="item in store.dayLength" :key="item">
+            {{ item }}
+          </td>
+          <td colspan="11"></td>
+        </tr>
+        <tr>
+          <td>星期</td>
+          <td
+            v-for="item in store.weekList"
+            :key="item"
+            :style="['天', '六'].includes(item) ? 'background: yellow' : ''"
+          >
+            {{ item }}
+          </td>
+          <td>病假</td>
+          <td>事假</td>
+          <td>补休</td>
+          <td>晚班</td>
+          <td>夜班</td>
+          <td>婚丧假</td>
+          <td>旷工</td>
+          <td>迟到早退</td>
+          <td>应休</td>
+          <td>实排</td>
+          <td>集假</td>
+        </tr>
+        <tr v-for="(item, index) in store.deptList">
+          <td>
+            {{ index + 1 }}
+          </td>
+          <td>
+            {{ item.name }}
+          </td>
+          <td v-for="c in item.data">
+            {{ c }}
+          </td>
+          <td v-for="c in 11"></td>
+        </tr>
+        <tr>
+          <td
+            :colspan="store.headerColSpan"
+            style="height: 100px; text-align: center"
+          >
+            白班记“白”,中班记A,晚班记P,夜班记N,加班记S,休息记“休”,补休记O,病假记B,事假记C,脱产学习记D,婚丧假记G,产假记H,旷工记X,迟到记J,早退记K。若为半天,则分别记为白/2等,有其他情况请文字说明。(全勤的请在后面打勾)
+          </td>
+        </tr>
+        <tr>
+          <td
+            :colspan="store.headerColSpan"
+            style="text-align: center; height: 80px"
+          >
+            考勤制表人:杜虎
+            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;科室负责人:
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </TableTemplate>
+</template>
+
+<style lang="scss"></style>

+ 133 - 0
src/views/settings/scheduling-class/ExportTrueExcel.vue

@@ -0,0 +1,133 @@
+<script setup lang="ts">
+import XEUtils from "xe-utils";
+import dayjs from "dayjs";
+import { useTableToExcel } from "@/utils/useTableToExcel";
+import { DataValue } from "@/api/settings/schedulingClassApi";
+
+const [TableTemplate, exportFun] = useTableToExcel({
+  css: `
+    .class-true_table {
+    border: 1px solid #000;
+    margin: 0;
+    padding: 0;
+    font-size: 18px
+  }
+
+   .class-true_table td {
+      padding: 5px 0;
+      width: 120px;
+      text-align: center;
+    }
+
+      .color1{
+    background: #ddebf7
+    }
+
+       .color2{
+    background: #8ea9db
+    }
+
+    `,
+});
+
+const data = ref<DataValue[][]>([]);
+const title = ref("测试");
+
+function fillInBlankDays(value: DataValue) {
+  let week = dayjs(value.schedulingDate).day();
+  if (week == 0) {
+    week = 8;
+  }
+  week = week - 1;
+  const tmp = [];
+  for (let i = 0; i < week; i++) {
+    const tmpData = dayjs(value.schedulingDate).add(-(week + i), "day");
+    tmp.push({
+      schedulingDate: tmpData.format("YYYY-MM-DD"),
+      schedulingDateFormat: tmpData.format("MM月DD日"),
+    });
+  }
+  return tmp;
+}
+
+function exportExcel(name: string, value: DataValue[]) {
+  value.forEach(item => {
+    item.schedulingDateFormat = dayjs(item.schedulingDate).format("MM月DD日");
+  });
+  const fill = fillInBlankDays(value[0]);
+  if (fill.length > 0) {
+    value = [...fill, ...value];
+  }
+  const chunk = XEUtils.chunk(value, 7);
+  const endLength = chunk[chunk.length - 1].length;
+
+  if (endLength !== 7) {
+    const endData = chunk[chunk.length - 1][endLength - 1];
+    for (let i = endLength; i < 7; i++) {
+      const tmpData = dayjs(endData.schedulingDate).add(
+        i - endLength + 1,
+        "day"
+      );
+      chunk[chunk.length - 1].push({
+        schedulingDate: tmpData.format("YYYY-MM-DD"),
+        schedulingDateFormat: tmpData.format("MM月DD日"),
+      });
+    }
+  }
+
+  data.value = chunk;
+  title.value = name;
+
+  exportFun(name);
+}
+
+defineExpose({
+  exportExcel,
+});
+</script>
+
+<template>
+  <TableTemplate>
+    <table class="class-true_table" cellspacing="0" cellpadding="0" border="1">
+      <thead>
+        <tr style="height: 40px">
+          <th colspan="8">
+            <h1>{{ title }}</h1>
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <template v-for="item in data">
+          <tr>
+            <td class="color1">星期</td>
+            <td class="color1">星期一</td>
+            <td class="color1">星期二</td>
+            <td class="color1">星期三</td>
+            <td class="color1">星期四</td>
+            <td class="color1">星期五</td>
+            <td class="color1">星期六</td>
+            <td class="color1">星期日</td>
+          </tr>
+          <tr>
+            <td class="color1">日期</td>
+            <td v-for="classItem in item" class="color1">
+              <span v-if="classItem.schedulingDateFormat">
+                {{ classItem.schedulingDateFormat }}</span
+              >
+            </td>
+          </tr>
+          <tr>
+            <td class="color2">一线班</td>
+            <td v-for="classItem in item" class="color2">
+              {{ classItem.changeClassName || classItem.name }}
+            </td>
+          </tr>
+          <tr>
+            <td style="background: #a9d08e" class="color2">二线班</td>
+            <td style="background: #a9d08e" colspan="7">范欣</td>
+          </tr>
+        </template>
+      </tbody>
+    </table>
+  </TableTemplate>
+</template>

+ 258 - 0
src/views/settings/scheduling-class/index.vue

@@ -0,0 +1,258 @@
+<script setup lang="tsx">
+import {
+  createByMonthApi,
+  getByMonth,
+  getCodes,
+  getSortAndName,
+  saveSort,
+  updateById,
+} from "@/api/settings/schedulingClassApi";
+import XcElOption from "@/components/xiao-chan/xc-el-option/XcElOption.vue";
+import Sortable from "sortablejs";
+import dayjs from "dayjs";
+import { stringNotBlank } from "@/utils/blank-utils";
+import ExportTrueExcel from "@/views/settings/scheduling-class/ExportTrueExcel.vue";
+import { useCompShallowRef } from "@/utils/useCompRef";
+import ExportFalseExcel from "@/views/settings/scheduling-class/ExportFalseExcel.vue";
+
+const store = reactive({
+  codes: [],
+  sortName: [],
+
+  currentCalendar: new Date(),
+  currentCodes: "",
+
+  months: {} as {
+    [key: string]: { id: string; code: string; schedulingDate: string }[];
+  },
+});
+
+const trueExcelRef = useCompShallowRef(ExportTrueExcel);
+const falseExcelRef = useCompShallowRef(ExportFalseExcel);
+
+const sortRef = ref<HTMLDivElement>();
+
+const currentData = computed(() => {
+  const month = dayjs(store.currentCalendar).format("YYYY-MM");
+  if (store.months[month]) {
+    const tmp = {};
+    store.months[month].forEach(i => {
+      tmp[i.schedulingDate] = i;
+    });
+    return tmp;
+  }
+  return {};
+});
+
+const handleSort = () => {
+  const tmp = store.sortName.map(i => {
+    return {
+      name: i.name,
+      code: i.code,
+    };
+  });
+  saveSort({ data: JSON.stringify(tmp) });
+};
+
+function handleSelectChange(value) {
+  const number = store.sortName.findIndex(i => i.code === value);
+  if (number === -1) {
+    store.sortName.push({
+      code: value,
+      name: store.codes.find(i => i.code === value).name,
+    });
+  }
+}
+
+function createByMonth() {
+  const month = dayjs(store.currentCalendar).format("YYYY-MM");
+  createByMonthApi(month);
+}
+
+function handleGetByMonth() {
+  const month = dayjs(store.currentCalendar).format("YYYY-MM");
+  if (store.months[month]) {
+    return;
+  }
+  getByMonth(month).then(res => {
+    store.months[month] = res;
+  });
+}
+
+function renderTemplate(data) {
+  const user = toRef<{
+    falseCode: string;
+    id: string;
+  }>(currentData.value[data.day]);
+  const tmpCode = ref(user.value?.changeClass ?? user.value?.code);
+  const tmpFalseCode = ref<string[]>(
+    stringNotBlank(user?.value?.falseCode)
+      ? user?.value?.falseCode?.split(",") || []
+      : []
+  );
+
+  const oldData = {};
+
+  return () => (
+    <div>
+      <div>{dayjs(data.day).format("MM-DD")}</div>
+      <div>
+        {user.value?.code ? (
+          <div>
+            <div>
+              主要:
+              <el-select
+                style={{ width: "120px" }}
+                model-value={tmpCode.value}
+                onUpdate:model-value={value => {
+                  tmpCode.value = value;
+                }}
+                onFocus={() => {
+                  oldData["code"] = tmpCode.value;
+                }}
+                onBlur={() => {
+                  if (tmpCode.value !== oldData["code"]) {
+                    updateById({
+                      id: user.value.id,
+                      changeClass: tmpCode.value,
+                    }).catch(() => {
+                      tmpCode.value = oldData["code"];
+                    });
+                  }
+                }}
+              >
+                <XcElOption data={store.codes} />
+              </el-select>
+            </div>
+            <div>
+              次要:
+              <el-select
+                style={{ width: "120px" }}
+                model-value={tmpFalseCode.value}
+                onUpdate:model-value={value => {
+                  tmpFalseCode.value = value;
+                  if ((value as string[]).length === 0) {
+                    updateById({
+                      id: user.value.id,
+                      falseCode: "",
+                    }).catch(() => {
+                      tmpFalseCode.value = oldData["falseCode"];
+                    });
+                  }
+                }}
+                multiple
+                clearable
+                onFocus={() => {
+                  oldData["falseCode"] = tmpFalseCode.value;
+                }}
+                onBlur={() => {
+                  if (tmpFalseCode.value == oldData["falseCode"]) {
+                    return;
+                  }
+                  updateById({
+                    id: user.value.id,
+                    falseCode: tmpFalseCode.value.join(","),
+                  }).catch(() => {
+                    tmpFalseCode.value = oldData["falseCode"];
+                  });
+                }}
+              >
+                <XcElOption data={store.codes} />
+              </el-select>
+            </div>
+          </div>
+        ) : null}
+      </div>
+    </div>
+  );
+}
+
+function exportExcel() {
+  const month = dayjs(store.currentCalendar).format("YYYY-MM");
+
+  getByMonth(month).then(res => {
+    store.months[month] = res;
+    trueExcelRef.value.exportExcel(`${month}信息中心值班表`, res);
+    falseExcelRef.value.exportExcel(month, res);
+  });
+}
+
+onMounted(async () => {
+  getCodes().then(res => {
+    store.codes = res;
+  });
+  getSortAndName().then(res => {
+    store.sortName = res;
+  });
+
+  await nextTick();
+
+  Sortable.create(sortRef.value, {
+    handle: ".scheduling_class_sort-item",
+    onEnd({ newIndex, oldIndex }) {
+      const current = store.sortName.splice(oldIndex, 1)[0];
+      store.sortName.splice(newIndex, 0, current);
+    },
+  });
+
+  handleGetByMonth();
+});
+</script>
+
+<template>
+  <div class="layout_container">
+    <ExportTrueExcel ref="trueExcelRef" />
+    <ExportFalseExcel ref="falseExcelRef" :dept-list="store.codes" />
+    <header>
+      <el-select
+        v-model="store.currentCodes"
+        style="width: 120px"
+        @change="handleSelectChange"
+      >
+        <xc-el-option :data="store.codes" />
+      </el-select>
+
+      <el-button @click="handleSort">保存排序</el-button>
+      <el-button @click="createByMonth"
+        >生成 {{ dayjs(store.currentCalendar).format("YYYY-MM") }}排班
+      </el-button>
+
+      <el-button @click="exportExcel">导出excel</el-button>
+      <div style="margin: 5px" ref="sortRef">
+        <el-tag
+          class="scheduling_class_sort-item"
+          v-for="(item, index) in store.sortName"
+          :key="item.code"
+          closable
+          style="margin: 0 2px"
+          @close="store.sortName.splice(index, 1)"
+        >
+          {{ item.name }}
+        </el-tag>
+      </div>
+    </header>
+    <div class="layout_main">
+      <el-calendar
+        v-model="store.currentCalendar"
+        class="scheduling_class_calendar"
+      >
+        <template #date-cell="{ data }">
+          <component :is="renderTemplate(data)" />
+        </template>
+      </el-calendar>
+    </div>
+  </div>
+</template>
+
+<style lang="scss">
+.scheduling_class_calendar {
+  .el-calendar-day {
+    height: max-content !important;
+    min-height: 85px !important;
+  }
+}
+
+.scheduling_class_sort-item {
+  cursor: move;
+}
+</style>