From 749cb2f4ad815c5a68c19fe961d5ca2149a84dd4 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期四, 17 九月 2026 11:28:54 +0800
Subject: [PATCH] feat(mes): 新增员工排班明细页面,修复计划排程菜单404

---
 src/views/mes/cal/scheduleDetail/data.ts                   |  252 ++++++++++++++++++
 src/views/mes/cal/scheduleDetail/modules/generate-form.vue |   56 ++++
 src/views/mes/cal/scheduleDetail/modules/check-view.vue    |  162 +++++++++++
 src/views/mes/cal/scheduleDetail/modules/form.vue          |   79 +++++
 src/api/mes/cal/scheduleDetail/index.ts                    |   86 ++++++
 src/views/mes/cal/scheduleDetail/index.vue                 |  183 +++++++++++++
 6 files changed, 818 insertions(+), 0 deletions(-)

diff --git a/src/api/mes/cal/scheduleDetail/index.ts b/src/api/mes/cal/scheduleDetail/index.ts
new file mode 100644
index 0000000..0fb68e1
--- /dev/null
+++ b/src/api/mes/cal/scheduleDetail/index.ts
@@ -0,0 +1,86 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesCalScheduleDetailApi {
+  /** MES 鍛樺伐鎺掔彮鏄庣粏 */
+  export interface Detail {
+    id?: number; // 鏄庣粏缂栧彿
+    planId?: number; // 鎺掔彮璁″垝缂栧彿
+    teamId?: number; // 鐝粍缂栧彿
+    userId?: number; // 鐝粍鎴愬憳鐢ㄦ埛缂栧彿
+    day?: Date; // 鏃ユ湡
+    shiftId?: number; // 鐝缂栧彿
+    shiftName?: string; // 鐝鍚嶇О
+    startTime?: string; // 寮�濮嬫椂闂达紙HH:mm锛�
+    endTime?: string; // 缁撴潫鏃堕棿锛圚H:mm锛�
+    source?: number; // 鏉ユ簮锛�1-鑷姩鐢熸垚 2-浜哄伐璋冩暣
+    remark?: string; // 澶囨敞
+  }
+
+  /** 鍛樺伐鎺掔彮鏄庣粏鏌ヨ鍙傛暟 */
+  export interface DetailPageParam extends PageParam {
+    planId?: number; // 鎺掔彮璁″垝缂栧彿
+    teamId?: number; // 鐝粍缂栧彿
+    userId?: number; // 鐝粍鎴愬憳鐢ㄦ埛缂栧彿
+    startDay?: string; // 寮�濮嬫棩鏈�
+    endDay?: string; // 缁撴潫鏃ユ湡
+  }
+
+  /** 鍚堣鏍¢獙缁撴灉椤� */
+  export interface CheckItem {
+    day?: string; // 鏃ユ湡
+    shiftName?: string; // 鐝鍚嶇О
+    startTime?: string; // 寮�濮嬫椂闂达紙HH:mm锛�
+    endTime?: string; // 缁撴潫鏃堕棿锛圚H:mm锛�
+    issueType?: number; // 杩濊绫诲瀷锛�1-鍗曟棩鐤戜技杩炵画宸ユ椂瓒呴暱 2-鐩搁偦鐝闂翠紤鎭笉瓒�
+    issue?: string; // 闂鎻忚堪锛屼负绌鸿〃绀鸿鐝鍚堣
+  }
+
+  /** 鍚堣鏍¢獙鍙傛暟 */
+  export interface CheckParam {
+    userId: number; // 鐝粍鎴愬憳鐢ㄦ埛缂栧彿
+    startDay?: string; // 璧峰鏃ユ湡
+    endDay?: string; // 缁撴潫鏃ユ湡
+  }
+}
+
+/** 鏌ヨ鍛樺伐鎺掔彮鏄庣粏鍒嗛〉 */
+export function getDetailPage(params: MesCalScheduleDetailApi.DetailPageParam) {
+  return requestClient.get<PageResult<MesCalScheduleDetailApi.Detail>>(
+    '/mes/cal/schedule-detail/page',
+    { params },
+  );
+}
+
+/** 鏌ヨ鍛樺伐鎺掔彮鏄庣粏鍒楄〃 */
+export function getDetailList(
+  params: MesCalScheduleDetailApi.DetailPageParam,
+): Promise<MesCalScheduleDetailApi.Detail[]> {
+  return requestClient.get<MesCalScheduleDetailApi.Detail[]>(
+    '/mes/cal/schedule-detail/list',
+    { params },
+  );
+}
+
+/** 鎸夌彮缁勬帓鐝敓鎴愬憳宸ョ骇鎺掔彮鏄庣粏 */
+export function generateDetails(planId: number) {
+  return requestClient.post(
+    `/mes/cal/schedule-detail/generate?planId=${planId}`,
+  );
+}
+
+/** 鏇存柊鍛樺伐鎺掔彮鏄庣粏锛堜汉宸ュ井璋冿級 */
+export function updateDetail(data: MesCalScheduleDetailApi.Detail) {
+  return requestClient.put('/mes/cal/schedule-detail/update', data);
+}
+
+/** 鍛樺伐鎺掔彮鍚堣鏍¢獙 */
+export function checkCompliance(
+  params: MesCalScheduleDetailApi.CheckParam,
+): Promise<MesCalScheduleDetailApi.CheckItem[]> {
+  return requestClient.get<MesCalScheduleDetailApi.CheckItem[]>(
+    '/mes/cal/schedule-detail/check',
+    { params },
+  );
+}
diff --git a/src/views/mes/cal/scheduleDetail/data.ts b/src/views/mes/cal/scheduleDetail/data.ts
new file mode 100644
index 0000000..64d47dc
--- /dev/null
+++ b/src/views/mes/cal/scheduleDetail/data.ts
@@ -0,0 +1,252 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesCalScheduleDetailApi } from '#/api/mes/cal/scheduleDetail';
+
+import { getPlanPage } from '#/api/mes/cal/plan';
+import { getTeamList } from '#/api/mes/cal/team';
+import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 涓嬫媺閫夐」 */
+export interface OptionItem {
+  label: string;
+  value: number;
+}
+
+/** 鏉ユ簮锛氳嚜鍔ㄧ敓鎴� */
+export const SOURCE_AUTO = 1;
+/** 鏉ユ簮锛氫汉宸ヨ皟鏁� */
+export const SOURCE_MANUAL = 2;
+
+/** 杩濊绫诲瀷鎻忚堪 */
+export const issueTypeMap: Record<number, string> = {
+  1: '鍗曟棩鐤戜技杩炵画宸ユ椂瓒呴暱',
+  2: '鐩搁偦鐝闂翠紤鎭笉瓒�',
+};
+
+/** 鎺掔彮璁″垝閫夐」缂撳瓨锛堟悳绱㈣〃鍗曚笌鍒楄〃鍚嶇О鍥炴樉鍏辩敤锛� */
+let planOptionsCache: OptionItem[] | undefined;
+
+export async function fetchPlanOptions(): Promise<OptionItem[]> {
+  if (planOptionsCache) {
+    return planOptionsCache;
+  }
+  const page = await getPlanPage({ pageNo: 1, pageSize: 100 });
+  planOptionsCache = page.list.map((item) => ({
+    label: item.name!,
+    value: item.id!,
+  }));
+  return planOptionsCache;
+}
+
+/** 鐝粍閫夐」缂撳瓨 */
+let teamOptionsCache: OptionItem[] | undefined;
+
+export async function fetchTeamOptions(): Promise<OptionItem[]> {
+  if (teamOptionsCache) {
+    return teamOptionsCache;
+  }
+  const list = await getTeamList();
+  teamOptionsCache = list.map((item) => ({
+    label: item.name!,
+    value: item.id!,
+  }));
+  return teamOptionsCache;
+}
+
+/** 鍛樺伐閫夐」缂撳瓨 */
+let userOptionsCache: OptionItem[] | undefined;
+
+export async function fetchUserOptions(): Promise<OptionItem[]> {
+  if (userOptionsCache) {
+    return userOptionsCache;
+  }
+  const list = await getSimpleUserList();
+  userOptionsCache = list.map((item) => ({
+    label: item.nickname!,
+    value: item.id!,
+  }));
+  return userOptionsCache;
+}
+
+/** 鐢熸垚鎺掔彮鏄庣粏鐨勮〃鍗� */
+export function useGenerateFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'planId',
+      label: '鎺掔彮璁″垝',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: fetchPlanOptions,
+        optionFilterProp: 'label',
+        placeholder: '璇烽�夋嫨鎺掔彮璁″垝',
+        showSearch: true,
+      },
+      rules: 'selectRequired',
+      help: '鎸夎璁″垝涓嬬殑鐝粍鎺掔彮灞曞紑涓哄憳宸ョ骇鏄庣粏锛岄噸澶嶇敓鎴愪細鍏堟竻鐞嗘棫鏄庣粏',
+    },
+  ];
+}
+
+/** 浜哄伐璋冩暣鎺掔彮鏄庣粏鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'id',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'shiftId',
+      component: 'Input',
+      dependencies: {
+        triggerFields: [''],
+        show: () => false,
+      },
+    },
+    {
+      fieldName: 'shiftName',
+      label: '鐝鍚嶇О',
+      component: 'Input',
+      componentProps: {
+        placeholder: '璇疯緭鍏ョ彮娆″悕绉�',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'startTime',
+      label: '寮�濮嬫椂闂�',
+      component: 'TimePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'HH:mm',
+        placeholder: '璇烽�夋嫨寮�濮嬫椂闂�',
+        valueFormat: 'HH:mm',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'endTime',
+      label: '缁撴潫鏃堕棿',
+      component: 'TimePicker',
+      componentProps: {
+        class: '!w-full',
+        format: 'HH:mm',
+        placeholder: '璇烽�夋嫨缁撴潫鏃堕棿',
+        valueFormat: 'HH:mm',
+      },
+      rules: 'required',
+    },
+    {
+      fieldName: 'remark',
+      label: '澶囨敞',
+      component: 'Textarea',
+      componentProps: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        rows: 3,
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+  return [
+    {
+      fieldName: 'planId',
+      label: '鎺掔彮璁″垝',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: fetchPlanOptions,
+        optionFilterProp: 'label',
+        placeholder: '璇烽�夋嫨鎺掔彮璁″垝',
+        showSearch: true,
+      },
+    },
+    {
+      fieldName: 'teamId',
+      label: '鐝粍',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: fetchTeamOptions,
+        optionFilterProp: 'label',
+        placeholder: '璇烽�夋嫨鐝粍',
+        showSearch: true,
+      },
+    },
+    {
+      fieldName: 'userId',
+      label: '鍛樺伐',
+      component: 'ApiSelect',
+      componentProps: {
+        allowClear: true,
+        api: fetchUserOptions,
+        optionFilterProp: 'label',
+        placeholder: '璇烽�夋嫨鍛樺伐',
+        showSearch: true,
+      },
+    },
+    {
+      fieldName: 'day',
+      label: '鏃ユ湡',
+      component: 'RangePicker',
+      componentProps: {
+        ...getRangePickerDefaultProps(),
+      },
+    },
+  ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesCalScheduleDetailApi.Detail>['columns'] {
+  return [
+    {
+      field: 'day',
+      title: '鏃ユ湡',
+      minWidth: 120,
+      formatter: 'formatDate',
+    },
+    {
+      field: 'planId',
+      title: '鎺掔彮璁″垝',
+      minWidth: 150,
+      slots: { default: 'planId' },
+    },
+    {
+      field: 'teamId',
+      title: '鐝粍',
+      minWidth: 120,
+      slots: { default: 'teamId' },
+    },
+    {
+      field: 'userId',
+      title: '鍛樺伐',
+      minWidth: 100,
+      slots: { default: 'userId' },
+    },
+    { field: 'shiftName', title: '鐝鍚嶇О', minWidth: 110 },
+    { field: 'startTime', title: '寮�濮嬫椂闂�', width: 100 },
+    { field: 'endTime', title: '缁撴潫鏃堕棿', width: 100 },
+    {
+      field: 'source',
+      title: '鏉ユ簮',
+      width: 110,
+      slots: { default: 'source' },
+    },
+    { field: 'remark', title: '澶囨敞', minWidth: 140 },
+    {
+      title: '鎿嶄綔',
+      width: 180,
+      fixed: 'right',
+      slots: {
+        default: 'actions',
+      },
+    },
+  ];
+}
diff --git a/src/views/mes/cal/scheduleDetail/index.vue b/src/views/mes/cal/scheduleDetail/index.vue
new file mode 100644
index 0000000..114d10e
--- /dev/null
+++ b/src/views/mes/cal/scheduleDetail/index.vue
@@ -0,0 +1,183 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesCalScheduleDetailApi } from '#/api/mes/cal/scheduleDetail';
+
+import { onMounted, ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getDetailPage } from '#/api/mes/cal/scheduleDetail';
+
+import {
+  fetchPlanOptions,
+  fetchTeamOptions,
+  fetchUserOptions,
+  SOURCE_MANUAL,
+  useGridColumns,
+  useGridFormSchema,
+} from './data';
+import CheckView from './modules/check-view.vue';
+import Form from './modules/form.vue';
+import GenerateForm from './modules/generate-form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+  connectedComponent: Form,
+  destroyOnClose: true,
+});
+
+const [GenerateModal, generateModalApi] = useVbenModal({
+  connectedComponent: GenerateForm,
+  destroyOnClose: true,
+});
+
+const [CheckModal, checkModalApi] = useVbenModal({
+  connectedComponent: CheckView,
+  destroyOnClose: true,
+});
+
+/** 璁″垝/鐝粍/鍛樺伐缂栧彿 -> 鍚嶇О 鐨勬槧灏勶紝鐢ㄤ簬鍒楄〃鍥炴樉 */
+const planNameMap = ref<Record<number, string>>({});
+const teamNameMap = ref<Record<number, string>>({});
+const userNameMap = ref<Record<number, string>>({});
+
+onMounted(async () => {
+  const [plans, teams, users] = await Promise.all([
+    fetchPlanOptions(),
+    fetchTeamOptions(),
+    fetchUserOptions(),
+  ]);
+  planNameMap.value = Object.fromEntries(
+    plans.map((item) => [item.value, item.label]),
+  );
+  teamNameMap.value = Object.fromEntries(
+    teams.map((item) => [item.value, item.label]),
+  );
+  userNameMap.value = Object.fromEntries(
+    users.map((item) => [item.value, item.label]),
+  );
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+  gridApi.query();
+}
+
+/** 鐢熸垚鎺掔彮鏄庣粏 */
+function handleGenerate() {
+  generateModalApi.open();
+}
+
+/** 璋冩暣鎺掔彮鏄庣粏 */
+function handleEdit(row: MesCalScheduleDetailApi.Detail) {
+  formModalApi.setData({ row }).open();
+}
+
+/** 鍚堣鏍¢獙锛堥粯璁や笉甯︿汉鍛橈紝寮圭獥鍐呰嚜閫夛級 */
+function handleCheck() {
+  checkModalApi.open();
+}
+
+/** 鍚堣鏍¢獙鎸囧畾鍛樺伐 */
+function handleCheckUser(row: MesCalScheduleDetailApi.Detail) {
+  checkModalApi.setData({ userId: row.userId }).open();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+  formOptions: {
+    schema: useGridFormSchema(),
+  },
+  gridOptions: {
+    columns: useGridColumns(),
+    height: 'auto',
+    keepSource: true,
+    proxyConfig: {
+      ajax: {
+        query: async ({ page }, formValues) => {
+          const { day, ...rest } = formValues as { day?: string[] };
+          return await getDetailPage({
+            pageNo: page.currentPage,
+            pageSize: page.pageSize,
+            ...(rest as MesCalScheduleDetailApi.DetailPageParam),
+            startDay: day?.[0],
+            endDay: day?.[1],
+          });
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+      isHover: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      search: true,
+    },
+  } as VxeTableGridOptions<MesCalScheduleDetailApi.Detail>,
+});
+</script>
+
+<template>
+  <Page auto-content-height>
+    <FormModal @success="handleRefresh" />
+    <GenerateModal @success="handleRefresh" />
+    <CheckModal />
+    <Grid table-title="鍛樺伐鎺掔彮鏄庣粏鍒楄〃">
+      <template #toolbar-tools>
+        <TableAction
+          :actions="[
+            {
+              label: '鐢熸垚鎺掔彮鏄庣粏',
+              type: 'primary',
+              icon: ACTION_ICON.ADD,
+              auth: ['mes:cal-schedule-detail:generate'],
+              onClick: handleGenerate,
+            },
+            {
+              label: '鍚堣鏍¢獙',
+              type: 'primary',
+              icon: ACTION_ICON.SEARCH,
+              auth: ['mes:cal-schedule-detail:query'],
+              onClick: handleCheck,
+            },
+          ]"
+        />
+      </template>
+      <template #planId="{ row }">
+        {{ planNameMap[row.planId] ?? row.planId }}
+      </template>
+      <template #teamId="{ row }">
+        {{ teamNameMap[row.teamId] ?? row.teamId }}
+      </template>
+      <template #userId="{ row }">
+        {{ userNameMap[row.userId] ?? row.userId }}
+      </template>
+      <template #source="{ row }">
+        <Tag :color="row.source === SOURCE_MANUAL ? 'orange' : 'blue'">
+          {{ row.source === SOURCE_MANUAL ? '浜哄伐璋冩暣' : '鑷姩鐢熸垚' }}
+        </Tag>
+      </template>
+      <template #actions="{ row }">
+        <TableAction
+          :actions="[
+            {
+              label: '璋冩暣',
+              type: 'link',
+              icon: ACTION_ICON.EDIT,
+              auth: ['mes:cal-schedule-detail:update'],
+              onClick: handleEdit.bind(null, row),
+            },
+            {
+              label: '鍚堣鏍¢獙',
+              type: 'link',
+              auth: ['mes:cal-schedule-detail:query'],
+              onClick: handleCheckUser.bind(null, row),
+            },
+          ]"
+        />
+      </template>
+    </Grid>
+  </Page>
+</template>
diff --git a/src/views/mes/cal/scheduleDetail/modules/check-view.vue b/src/views/mes/cal/scheduleDetail/modules/check-view.vue
new file mode 100644
index 0000000..04a8593
--- /dev/null
+++ b/src/views/mes/cal/scheduleDetail/modules/check-view.vue
@@ -0,0 +1,162 @@
+<script lang="ts" setup>
+import type { MesCalScheduleDetailApi } from '#/api/mes/cal/scheduleDetail';
+
+import { onMounted, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import dayjs from 'dayjs';
+
+import {
+  Button,
+  Empty,
+  Form,
+  FormItem,
+  message,
+  RangePicker,
+  Select,
+  Spin,
+  Table,
+  Tag,
+} from 'ant-design-vue';
+
+import { checkCompliance } from '#/api/mes/cal/scheduleDetail';
+import { getRangePickerDefaultProps } from '#/utils';
+
+import { fetchUserOptions, issueTypeMap, type OptionItem } from '../data';
+
+const userId = ref<number>();
+const day = ref<string[]>();
+const userOptions = ref<OptionItem[]>([]);
+
+const loading = ref(false);
+const checked = ref(false);
+const items = ref<MesCalScheduleDetailApi.CheckItem[]>([]);
+
+/** 鎵ц鍚堣鏍¢獙 */
+async function doCheck() {
+  if (!userId.value) {
+    message.warning('璇烽�夋嫨鍛樺伐');
+    return;
+  }
+  loading.value = true;
+  try {
+    items.value = await checkCompliance({
+      userId: userId.value,
+      startDay: day.value?.[0],
+      endDay: day.value?.[1],
+    });
+    checked.value = true;
+  } finally {
+    loading.value = false;
+  }
+}
+
+const columns = [
+  {
+    title: '鏃ユ湡',
+    dataIndex: 'day',
+    key: 'day',
+    width: 110,
+    customRender: ({ text }: { text?: string }) =>
+      text ? dayjs(text).format('YYYY-MM-DD') : '-',
+  },
+  { title: '鐝', dataIndex: 'shiftName', key: 'shiftName', width: 100 },
+  {
+    title: '鏃堕棿',
+    key: 'time',
+    width: 120,
+    customRender: ({ record }: { record: MesCalScheduleDetailApi.CheckItem }) =>
+      `${record.startTime ?? '-'} ~ ${record.endTime ?? '-'}`,
+  },
+  {
+    title: '杩濊绫诲瀷',
+    dataIndex: 'issueType',
+    key: 'issueType',
+    width: 100,
+  },
+  { title: '闂鎻忚堪', dataIndex: 'issue', key: 'issue' },
+];
+
+/** 鍚屾棩鍙兘澶氱彮娆★紝鐢ㄨ鍙蜂綔涓� key */
+function rowIndexKey(_record: MesCalScheduleDetailApi.CheckItem, index?: number) {
+  return index ?? 0;
+}
+
+const [Modal, modalApi] = useVbenModal({
+  showConfirmButton: false,
+  cancelText: '鍏抽棴',
+  onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      checked.value = false;
+      items.value = [];
+      return;
+    }
+    // 鏀寔浠庡垪琛ㄨ浼犲叆鍛樺伐缂栧彿锛岃嚜鍔ㄥ彂璧锋牎楠�
+    const data = modalApi.getData<{ userId?: number }>();
+    if (data?.userId) {
+      userId.value = data.userId;
+      doCheck();
+    }
+  },
+});
+
+onMounted(async () => {
+  userOptions.value = await fetchUserOptions();
+});
+</script>
+
+<template>
+  <Modal title="鍛樺伐鎺掔彮鍚堣鏍¢獙" class="w-3/5">
+    <Form layout="inline" class="mb-2.5">
+      <FormItem label="鍛樺伐">
+        <Select
+          v-model:value="userId"
+          allow-clear
+          show-search
+          placeholder="璇烽�夋嫨鍛樺伐"
+          class="!w-[200px]"
+          :options="userOptions"
+          :filter-option="
+            (input: string, option: OptionItem) =>
+              (option?.label ?? '').includes(input)
+          "
+        />
+      </FormItem>
+      <FormItem label="鏃ユ湡">
+        <RangePicker
+          v-model:value="day"
+          class="!w-[260px]"
+          v-bind="getRangePickerDefaultProps()"
+        />
+      </FormItem>
+      <FormItem>
+        <Button type="primary" :loading="loading" @click="doCheck">鏍¢獙</Button>
+      </FormItem>
+    </Form>
+    <Spin :spinning="loading">
+      <Table
+        v-if="!checked || items.length > 0"
+        size="small"
+        :row-key="rowIndexKey"
+        :columns="columns"
+        :data-source="items"
+        :pagination="false"
+        :scroll="{ y: 360 }"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.key === 'issueType'">
+            <Tag v-if="record.issueType" :color="record.issueType === 1 ? 'red' : 'orange'">
+              {{ issueTypeMap[record.issueType as number] }}
+            </Tag>
+            <Tag v-else color="green">鍚堣</Tag>
+          </template>
+          <template v-else-if="column.key === 'issue'">
+            {{ record.issue || '鏃犲紓甯�' }}
+          </template>
+        </template>
+      </Table>
+      <Empty v-else description="鏈彂鐜拌繚瑙勬帓鐝褰曪紝璇ュ憳宸ユ帓鐝悎瑙�" />
+    </Spin>
+  </Modal>
+</template>
diff --git a/src/views/mes/cal/scheduleDetail/modules/form.vue b/src/views/mes/cal/scheduleDetail/modules/form.vue
new file mode 100644
index 0000000..a745ab1
--- /dev/null
+++ b/src/views/mes/cal/scheduleDetail/modules/form.vue
@@ -0,0 +1,79 @@
+<script lang="ts" setup>
+import type { MesCalScheduleDetailApi } from '#/api/mes/cal/scheduleDetail';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { updateDetail } from '#/api/mes/cal/scheduleDetail';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesCalScheduleDetailApi.Detail>();
+
+const getTitle = computed(() => {
+  return formData.value
+    ? `璋冩暣鎺掔彮鏄庣粏锛�${formData.value.shiftName ?? ''}锛塦
+    : '璋冩暣鎺掔彮鏄庣粏';
+});
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 90,
+  },
+  layout: 'horizontal',
+  schema: useFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const data = (await formApi.getValues()) as MesCalScheduleDetailApi.Detail;
+      await updateDetail(data);
+      await modalApi.close();
+      emit('success');
+      message.success($t('ui.actionMessage.operationSuccess'));
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      formData.value = undefined;
+      return;
+    }
+    // 鍔犺浇鏁版嵁
+    const data = modalApi.getData<{ row: MesCalScheduleDetailApi.Detail }>();
+    formData.value = data.row;
+    await formApi.setValues({
+      id: data.row.id,
+      shiftId: data.row.shiftId,
+      shiftName: data.row.shiftName,
+      startTime: data.row.startTime,
+      endTime: data.row.endTime,
+      remark: data.row.remark,
+    });
+  },
+});
+</script>
+
+<template>
+  <Modal :title="getTitle" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>
diff --git a/src/views/mes/cal/scheduleDetail/modules/generate-form.vue b/src/views/mes/cal/scheduleDetail/modules/generate-form.vue
new file mode 100644
index 0000000..7799cc4
--- /dev/null
+++ b/src/views/mes/cal/scheduleDetail/modules/generate-form.vue
@@ -0,0 +1,56 @@
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { generateDetails } from '#/api/mes/cal/scheduleDetail';
+import { $t } from '#/locales';
+
+import { useGenerateFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+  commonConfig: {
+    componentProps: {
+      class: 'w-full',
+    },
+    formItemClass: 'col-span-1',
+    labelWidth: 90,
+  },
+  layout: 'horizontal',
+  schema: useGenerateFormSchema(),
+  showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+  async onConfirm() {
+    const { valid } = await formApi.validate();
+    if (!valid) {
+      return;
+    }
+    modalApi.lock();
+    try {
+      const { planId } = await formApi.getValues();
+      await generateDetails(planId as number);
+      await modalApi.close();
+      emit('success');
+      message.success('鐢熸垚鎴愬姛');
+    } finally {
+      modalApi.unlock();
+    }
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (!isOpen) {
+      await formApi.resetForm();
+    }
+  },
+});
+</script>
+
+<template>
+  <Modal title="鐢熸垚鍛樺伐鎺掔彮鏄庣粏" class="w-2/5">
+    <Form class="mx-4" />
+  </Modal>
+</template>

--
Gitblit v1.9.3