maven
2025-09-18 bc278f02a34cdce5be02e42b26fe9e1bc6a0d6e6
Merge remote-tracking branch 'origin/dev' into dev
已添加16个文件
已修改9个文件
5997 ■■■■ 文件已修改
multiple/assets/favicon/CMNYico.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/CMNYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/meeting.js 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionManagement/index.js 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionUpload/index.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/publicApi/index.js 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotation.js 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/video.png 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 198 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue 398 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue 418 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue 371 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue 416 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/summary/index.vue 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inspectionManagement/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/index.vue 1047 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/index.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementReport/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 646 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 604 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/CMNYico.ico
multiple/assets/logo/CMNYLogo.png
multiple/config.json
@@ -167,6 +167,16 @@
    "logo": "logo/JSYNYLogo.png",
    "favicon": "favicon/JSYNYico.ico"
  },
  "CMNY": {
    "env": {
      "VITE_APP_TITLE": "创铭能源信息管理系统",
      "VITE_BASE_API": "http://114.132.189.42:9088",
      "VITE_JAVA_API": "http://114.132.189.42:9087"
    },
    "screen": "screen/DHDCView.png",
    "logo": "logo/CMNYLogo.png",
    "favicon": "favicon/CMNYico.ico"
  },
  "screen": "/src/assets/images/login-background.png",
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
package.json
@@ -34,11 +34,13 @@
    "jsencrypt": "3.3.2",
    "nprogress": "0.2.0",
    "pinia": "2.1.7",
    "print-js": "^1.6.0",
    "qrcode": "^1.5.4",
    "sortablejs": "^1.15.6",
    "splitpanes": "3.1.5",
    "vue": "3.4.31",
    "vue-cropper": "1.1.1",
    "vue-easy-lightbox": "^1.19.0",
    "vue-esign": "^1.1.4",
    "vue-router": "4.4.0",
    "vuedraggable": "4.1.0"
src/api/collaborativeApproval/meeting.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
import request from "@/utils/request";
export function getMeetingRoomList(data) {
    return request({
        url: "/meeting/roomList",
        method: "post",
        data: data,
    });
}
export function saveRoom(data) {
    return request({
        url: "/meeting/saveRoom",
        method: "post",
        data: data,
    });
}
export function delRoom(id) {
    return request({
        url: "/meeting/delRoom/"+id,
        method: "delete",
    });
}
export function getRoomEnum() {
    return request({
        url: "/meeting/roomEnum",
        method: "get",
    });
}
export function getDraftList(data){
    return request({
        url: "/meeting/draftList",
        method: "post",
        data: data,
    });
}
export function saveDraft(data) {
    return request({
        url: "/meeting/saveDraft",
        method: "post",
        data: data,
    });
}
export function delDraft(id) {
    return request({
        url: "/meeting/delDraft/"+id,
        method: "delete",
    });
}
export function saveMeetingApplication(data){
    return request({
        url: "/meeting/saveMeetingApplication",
        method: "post",
        data: data,
    });
}
export function getExamineList(data) {
    return request({
        url: "/meeting/applicationList",
        method: "post",
        data: data,
    });
}
export function getMeetingUseList(data){
    return request({
        url: "/meeting/meetingUseList",
        method: "post",
        data: data,
    });
}
export function getMeetingPublish(data){
    return request({
        url: "/meeting/meetingPublishList",
        method: "post",
        data: data
    });
}
export function getMeetingMinutesByMeetingId(id){
    return request({
        url: "/meeting/getMeetingMinutesByMeetingId/"+id,
        method: "get",
    });
}
export function saveMeetingMinutes(data){
    return request({
        url: "/meeting/saveMeetingMinutes",
        method: "post",
        data: data,
    });
}
export function getMeetSummary(){
    return request({
        url: "/meeting/getMeetSummary",
        method: "get",
    });
}
export function getMeetSummaryItems(){
    return request({
        url: "/meeting/getMeetSummaryItems",
        method: "get",
    });
}
src/api/inspectionManagement/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
// å·¡æ£€ç®¡ç†
import request from '@/utils/request'
// å·¡æ£€ä»»åŠ¡è¡¨è¡¨æŸ¥è¯¢
export function inspectionTaskList(query) {
    return request({
        url: '/inspectionTask/list',
        method: 'get',
        params: query
    })
}
// å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
export function addOrEditInspectionTask(query) {
    return request({
        url: '/inspectionTask/addOrEditInspectionTask',
        method: 'post',
        data: query
    })
}
// å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
export function delInspectionTask(query) {
    return request({
        url: '/inspectionTask/delInspectionTask',
        method: 'delete',
        data: query
    })
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
export function delTimingTask(query) {
    return request({
        url: '/timingTask/delTimingTask',
        method: 'delete',
        data: query
    })
}
// /inspectionTask/addOrEditInspectionTask
// å·¡æ£€ä¸Šä¼ 
export function uploadInspectionTask(query) {
    return request({
        url: '/inspectionTask/addOrEditInspectionTask',
        method: 'post',
        data: query
    })
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨æŸ¥è¯¢
export function timingTaskList(query) {
    return request({
        url: '/timingTask/list',
        method: 'get',
        params: query
    })
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
export function addOrEditTimingTask(query) {
    return request({
        url: '/timingTask/addOrEditTimingTask',
        method: 'post',
        data: query
    })
}
src/api/inspectionUpload/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
// å·¡æ£€ä¸Šä¼ 
import request from '@/utils/request'
// äºŒç»´ç ç®¡ç†è¡¨æŸ¥è¯¢
export function qrCodeList(query) {
    return request({
        url: '/qrCode/list',
        method: 'get',
        params: query
    })
}
// äºŒç»´ç æ‰«ç è®°å½•表查询
export function qrCodeScanRecordList(query) {
    return request({
        url: '/qrCodeScanRecord/list',
        method: 'get',
        params: query
    })
}
// äºŒç»´ç ç®¡ç†è¡¨æ–°å¢žä¿®æ”¹
export function addOrEditQrCode(query) {
    return request({
        url: '/qrCode/addOrEditQrCode',
        method: 'post',
        data: query
    })
}
// äºŒç»´ç æ‰«ç è®°å½•表新增修改
export function addOrEditQrCodeRecord(query) {
    return request({
        url: '/qrCodeScanRecord/addOrEditQrCodeRecord',
        method: 'post',
        data: query
    })
}
// äºŒç»´ç æ‰«ç è®°å½•表新增修改
export function delQrCode(query) {
    return request({
        url: '/qrCode/delQrCode',
        method: 'delete',
        data: query
    })
}
src/api/publicApi/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
// æ–‡æ¡£ç®¡ç†
import request from '@/utils/request'
// /system/user/listAll
// æŸ¥è¯¢æ‰€æœ‰ç”¨æˆ·åˆ—表
export function userListAll() {
  return request({
    url: '/system/user/listAll',
    method: 'get'
  })
}
// /equipmentManagement/equipmentList
// æŸ¥è¯¢è®¾å¤‡åˆ—表
export function getEquipmentList(query) {
  return request({
    url: '/equipmentManagement/equipmentList',
    method: 'get',
    params: query
  })
}
// /coalInfo/coalInfoList
// æŸ¥è¯¢ç…¤ç§åˆ—表
export function getCoalInfoList(query) {
    return request({
        url: '/coalInfo/coalInfoList',
        method: 'get',
        params: query
    })
}
// /coalField/coalFieldList
// æŸ¥è¯¢ç…¤è´¨å­—段列表
export function getCoalFieldList(query) {
    return request({
        url: '/coalField/coalFieldList',
        method: 'get',
        params: query
    })
}
src/api/salesManagement/salesQuotation.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
// é”€å”®æŠ¥ä»·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢æŠ¥ä»·å•列表
export function quotationList(query) {
  return request({
    url: "/sales/quotation/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢æŠ¥ä»·å•详情
export function getQuotationDetail(query) {
  return request({
    url: "/sales/quotation/detail",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæŠ¥ä»·å•
export function addQuotation(data) {
  return request({
    url: "/sales/quotation/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æŠ¥ä»·å•
export function updateQuotation(data) {
  return request({
    url: "/sales/quotation/update",
    method: "put",
    data: data,
  });
}
// åˆ é™¤æŠ¥ä»·å•
export function deleteQuotation(query) {
  return request({
    url: "/sales/quotation/delete",
    method: "delete",
    data: query,
  });
}
// å‘送报价单
export function sendQuotation(data) {
  return request({
    url: "/sales/quotation/send",
    method: "post",
    data: data,
  });
}
// æŠ¥ä»·å•转订单
export function convertToOrder(data) {
  return request({
    url: "/sales/quotation/convertToOrder",
    method: "post",
    data: data,
  });
}
// æŸ¥è¯¢å®¢æˆ·åˆ—表
export function getCustomerList(query) {
  return request({
    url: "/basic/customer/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢äº§å“åˆ—表
export function getProductList(query) {
  return request({
    url: "/basic/product/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢ä¸šåŠ¡å‘˜åˆ—è¡¨
export function getSalespersonList(query) {
  return request({
    url: "/system/user/salespersonList",
    method: "get",
    params: query,
  });
}
// å¯¼å‡ºæŠ¥ä»·å•
export function exportQuotation(query) {
  return request({
    url: "/sales/quotation/export",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
// æ‰“印报价单
export function printQuotation(query) {
  return request({
    url: "/sales/quotation/print",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
src/assets/images/video.png
src/router/index.js
@@ -1,6 +1,6 @@
import { createWebHistory, createRouter } from 'vue-router'
import { createWebHistory, createRouter } from "vue-router";
/* Layout */
import Layout from '@/layout'
import Layout from "@/layout";
/**
 * Note: è·¯ç”±é…ç½®é¡¹
@@ -16,85 +16,80 @@
 * roles: ['admin', 'common']       // è®¿é—®è·¯ç”±çš„角色权限
 * permissions: ['a:a:a', 'b:b:b']  // è®¿é—®è·¯ç”±çš„菜单权限
 * meta : {
    noCache: true                   // å¦‚果设置为true,则不会被 <keep-alive> ç¼“å­˜(默认 false)
    title: 'title'                  // è®¾ç½®è¯¥è·¯ç”±åœ¨ä¾§è¾¹æ å’Œé¢åŒ…屑中展示的名字
    icon: 'svg-name'                // è®¾ç½®è¯¥è·¯ç”±çš„图标,对应路径src/assets/icons/svg
    breadcrumb: false               // å¦‚果设置为false,则不会在breadcrumb面包屑中显示
    activeMenu: '/system/user'      // å½“路由设置了该属性,则会高亮相对应的侧边栏。
  }
 noCache: true                   // å¦‚果设置为true,则不会被 <keep-alive> ç¼“å­˜(默认 false)
 title: 'title'                  // è®¾ç½®è¯¥è·¯ç”±åœ¨ä¾§è¾¹æ å’Œé¢åŒ…屑中展示的名字
 icon: 'svg-name'                // è®¾ç½®è¯¥è·¯ç”±çš„图标,对应路径src/assets/icons/svg
 breadcrumb: false               // å¦‚果设置为false,则不会在breadcrumb面包屑中显示
 activeMenu: '/system/user'      // å½“路由设置了该属性,则会高亮相对应的侧边栏。
 }
 */
// å…¬å…±è·¯ç”±
export const constantRoutes = [
  {
    path: '/redirect',
    path: "/redirect",
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
        path: "/redirect/:path(.*)",
        component: () => import("@/views/redirect/index.vue"),
      },
    ],
  },
  {
    path: '/login',
    component: () => import('@/views/login'),
    hidden: true
  },
  {
    path: "/callbacklccpn",
    component: () => import("@/views/tideLogin.vue"),
    path: "/login",
    component: () => import("@/views/login"),
    hidden: true,
  },
  {
    path: '/register',
    component: () => import('@/views/register'),
    hidden: true
    path: "/register",
    component: () => import("@/views/register"),
    hidden: true,
  },
  {
    path: "/:pathMatch(.*)*",
    component: () => import('@/views/error/404'),
    hidden: true
    component: () => import("@/views/error/404"),
    hidden: true,
  },
  {
    path: '/401',
    component: () => import('@/views/error/401'),
    hidden: true
    path: "/401",
    component: () => import("@/views/error/401"),
    hidden: true,
  },
  {
    path: '',
    path: "",
    component: Layout,
    redirect: '/index',
    redirect: "/index",
    children: [
      {
        path: '/index',
        component: () => import('@/views/index'),
        name: 'Index',
        meta: { title: '首页', icon: 'dashboard', affix: true }
      }
    ]
        path: "/index",
        component: () => import("@/views/index"),
        name: "Index",
        meta: { title: "首页", icon: "dashboard", affix: true },
      },
    ],
  },
  // {
  //   path: '/main/MobileChat',
  //   component: Layout,
  //   redirect: '',
  //   hidden: true,
  //   children: [
  //     {
  //       path: '',
  //       component: () => import('@/views/chatHome/chatHomeIndex/MobileChat'),
  //       name: 'MobileChat',
  //       meta: { title: 'AI对话', icon: 'dashboard', affix: true}
  //     }
  //   ]
  // },
  {
    path: '/user',
    path: "/main/MobileChat",
    component: Layout,
    redirect: "",
    hidden: true,
    children: [
      {
        path: "",
        component: () => import("@/views/chatHome/chatHomeIndex/MobileChat"),
        name: "MobileChat",
        meta: { title: "AI对话", icon: "dashboard", affix: true },
      },
    ],
  },
  {
    path: "/user",
    component: Layout,
    hidden: true,
    redirect: 'noredirect',
    redirect: "noredirect",
    children: [
      {
        path: "profile",
@@ -111,98 +106,91 @@
    name: "DeviceInfo",
    meta: { title: "设备信息", icon: "monitor" },
  },
  {
    path: "/data-dashboard",
    component: () => import("@/views/reportAnalysis/dataDashboard/index.vue"),
    hidden: true,
    name: "DataDashboard",
    meta: { title: "数据大屏", icon: "dashboard" },
  },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
export const dynamicRoutes = [
  {
    path: '/system/user-auth',
    path: "/system/user-auth",
    component: Layout,
    hidden: true,
    permissions: ['system:user:edit'],
    permissions: ["system:user:edit"],
    children: [
      {
        path: 'role/:userId(\\d+)',
        component: () => import('@/views/system/user/authRole'),
        name: 'AuthRole',
        meta: { title: '分配角色', activeMenu: '/system/user' }
      }
    ]
        path: "role/:userId(\\d+)",
        component: () => import("@/views/system/user/authRole"),
        name: "AuthRole",
        meta: { title: "分配角色", activeMenu: "/system/user" },
      },
    ],
  },
  {
    path: '/system/role-auth',
    path: "/system/role-auth",
    component: Layout,
    hidden: true,
    permissions: ['system:role:edit'],
    permissions: ["system:role:edit"],
    children: [
      {
        path: 'user/:roleId(\\d+)',
        component: () => import('@/views/system/role/authUser'),
        name: 'AuthUser',
        meta: { title: '分配用户', activeMenu: '/system/role' }
      }
    ]
        path: "user/:roleId(\\d+)",
        component: () => import("@/views/system/role/authUser"),
        name: "AuthUser",
        meta: { title: "分配用户", activeMenu: "/system/role" },
      },
    ],
  },
  {
    path: '/system/dict-data',
    path: "/system/dict-data",
    component: Layout,
    hidden: true,
    permissions: ['system:dict:list'],
    permissions: ["system:dict:list"],
    children: [
      {
        path: 'index/:dictId(\\d+)',
        component: () => import('@/views/system/dict/data'),
        name: 'Data',
        meta: { title: '字典数据', activeMenu: '/system/dict' }
      }
    ]
        path: "index/:dictId(\\d+)",
        component: () => import("@/views/system/dict/data"),
        name: "Data",
        meta: { title: "字典数据", activeMenu: "/system/dict" },
      },
    ],
  },
  {
    path: '/monitor/job-log',
    path: "/monitor/job-log",
    component: Layout,
    hidden: true,
    permissions: ['monitor:job:list'],
    permissions: ["monitor:job:list"],
    children: [
      {
        path: 'index/:jobId(\\d+)',
        component: () => import('@/views/monitor/job/log'),
        name: 'JobLog',
        meta: { title: '调度日志', activeMenu: '/monitor/job' }
      }
    ]
        path: "index/:jobId(\\d+)",
        component: () => import("@/views/monitor/job/log"),
        name: "JobLog",
        meta: { title: "调度日志", activeMenu: "/monitor/job" },
      },
    ],
  },
  {
    path: '/tool/gen-edit',
    path: "/tool/gen-edit",
    component: Layout,
    hidden: true,
    permissions: ['tool:gen:edit'],
    permissions: ["tool:gen:edit"],
    children: [
      {
        path: 'index/:tableId(\\d+)',
        component: () => import('@/views/tool/gen/editTable'),
        name: 'GenEdit',
        meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
      }
    ]
  }
]
        path: "index/:tableId(\\d+)",
        component: () => import("@/views/tool/gen/editTable"),
        name: "GenEdit",
        meta: { title: "修改生成配置", activeMenu: "/tool/gen" },
      },
    ],
  },
];
const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
      return savedPosition;
    }
    return { top: 0 }
    return { top: 0 };
  },
})
});
export default router
export default router;
src/views/collaborativeApproval/meetingBoard/index.vue
@@ -16,7 +16,7 @@
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.ongoing }}</div>
          <div class="stat-number">{{ stats.underWay }}</div>
          <div class="stat-label">进行中</div>
        </div>
      </el-card>
@@ -28,7 +28,7 @@
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-number">{{ stats.upcoming }}</div>
          <div class="stat-number">{{ stats.toStart }}</div>
          <div class="stat-label">即将开始</div>
        </div>
      </el-card>
@@ -45,11 +45,11 @@
            </el-tag>
          </div>
          <div class="meeting-time">
            <el-icon><Clock /></el-icon>
            {{ formatTime(meeting.startTime) }} - {{ formatTime(meeting.endTime) }}
             {{dayjs(meeting.startTime).format("YYYY-MM-DD")}}<el-icon><Clock /></el-icon>
           {{ formatTime(meeting.startTime) }} - {{ formatTime(meeting.endTime) }}
          </div>
        </div>
        <div class="meeting-info">
          <div class="info-item">
            <el-icon><Location /></el-icon>
@@ -66,79 +66,18 @@
        </div>
        <div class="meeting-agenda">
          <h4>议程安排</h4>
          <h4>会议纪要</h4>
          <div class="agenda-list">
            <div
              v-for="(agenda, index) in meeting.agenda"
              :key="index"
              class="agenda-item"
              :class="{ 'active': agenda.status === 'active', 'completed': agenda.status === 'completed' }"
            >
              <span class="agenda-time">{{ agenda.time }}</span>
              <span class="agenda-content">{{ agenda.content }}</span>
              <el-tag
                :type="getAgendaStatusType(agenda.status)"
                size="small"
              >
                {{ getAgendaStatusText(agenda.status) }}
              </el-tag>
            <div class="editor-container">
              <div
                  v-html="meeting.content"
              />
            </div>
          </div>
        </div>
<!--        <div class="meeting-actions">-->
<!--          <el-button type="primary" size="small" @click="joinMeeting(meeting)">-->
<!--            åŠ å…¥ä¼šè®®-->
<!--          </el-button>-->
<!--          <el-button type="info" size="small" @click="viewDetails(meeting)">-->
<!--            æŸ¥çœ‹è¯¦æƒ…-->
<!--          </el-button>-->
<!--          <el-button type="warning" size="small" @click="editMeeting(meeting)">-->
<!--            ç¼–辑-->
<!--          </el-button>-->
<!--        </div>-->
      </el-card>
    </div>
    <!-- åˆ›å»ºä¼šè®®å¯¹è¯æ¡† -->
    <el-dialog v-model="dialogVisible" title="创建会议" width="600px">
      <el-form :model="meetingForm" label-width="100px">
        <el-form-item label="会议标题">
          <el-input v-model="meetingForm.title" placeholder="请输入会议标题" />
        </el-form-item>
        <el-form-item label="会议时间">
          <el-date-picker
            v-model="meetingForm.timeRange"
            type="datetimerange"
            range-separator="至"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DD HH:mm:ss"
          />
        </el-form-item>
        <el-form-item label="会议地点">
          <el-input v-model="meetingForm.location" placeholder="请输入会议地点" />
        </el-form-item>
        <el-form-item label="主持人">
          <el-input v-model="meetingForm.host" placeholder="请输入主持人姓名" />
        </el-form-item>
        <el-form-item label="会议描述">
          <el-input
            v-model="meetingForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入会议描述"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitMeeting">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
@@ -146,63 +85,21 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Clock, Location, User, UserFilled } from '@element-plus/icons-vue'
import Editor from "@/components/Editor/index.vue";
import {getMeetSummaryItems,getMeetSummary} from '@/api/collaborativeApproval/meeting.js'
import dayjs from "dayjs";
// ç»Ÿè®¡æ•°æ®
const stats = reactive({
  total: 12,
  ongoing: 3,
  completed: 7,
  upcoming: 2
const stats = ref({
  total: 0,
  underWay: 0,
  completed: 0,
  toStart: 0
})
// ä¼šè®®æ•°æ®
const meetings = ref([
  {
    id: 1,
    title: '产品开发周会',
    status: 'ongoing',
    startTime: '2025-01-15 09:00:00',
    endTime: '2025-01-15 10:30:00',
    location: '会议室A',
    host: '陈志强',
    participants: ['陈志强', '刘雅婷', '王建国', '赵丽华'],
    agenda: [
      { time: '09:00-09:15', content: '上周工作总结', status: 'completed' },
      { time: '09:15-09:45', content: '本周开发计划', status: 'active' },
      { time: '09:45-10:00', content: '技术难点讨论', status: 'pending' },
      { time: '10:00-10:30', content: '问题反馈与解决', status: 'pending' }
    ]
  },
  {
    id: 2,
    title: '客户需求评审会',
    status: 'upcoming',
    startTime: '2025-01-15 14:00:00',
    endTime: '2025-01-15 15:00:00',
    location: '线上会议',
    host: '陈志强',
    participants: ['陈志强', '刘雅婷', '孙明华', '客户代表'],
    agenda: [
      { time: '14:00-14:20', content: '需求背景介绍', status: 'pending' },
      { time: '14:20-14:40', content: '功能需求分析', status: 'pending' },
      { time: '14:40-15:00', content: '技术可行性评估', status: 'pending' }
    ]
  },
  {
    id: 3,
    title: '团队建设活动',
    status: 'completed',
    startTime: '2025-01-14 16:00:00',
    endTime: '2025-01-14 18:00:00',
    location: '公司大厅',
    host: '人事部',
    participants: ['全体员工'],
    agenda: [
      { time: '16:00-16:30', content: '团队游戏', status: 'completed' },
      { time: '16:30-17:00', content: '经验分享', status: 'completed' },
      { time: '17:00-18:00', content: '自由交流', status: 'completed' }
    ]
  }
])
// å¯¹è¯æ¡†ç›¸å…³
@@ -218,9 +115,9 @@
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    'ongoing': 'success',
    'upcoming': 'warning',
    'completed': 'info'
    '2': 'success',
    '1': 'warning',
    '0': 'info'
  }
  return statusMap[status] || 'info'
}
@@ -228,9 +125,9 @@
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    'ongoing': '进行中',
    'upcoming': '即将开始',
    'completed': '已完成'
    '2': '进行中',
    '1': '即将开始',
    '0': '已完成'
  }
  return statusMap[status] || '未知'
}
@@ -261,65 +158,16 @@
  return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// åˆ›å»ºä¼šè®®
const createMeeting = () => {
  dialogVisible.value = true
  // é‡ç½®è¡¨å•
  Object.assign(meetingForm, {
    title: '',
    timeRange: [],
    location: '',
    host: '',
    description: ''
onMounted( async () => {
  let [resp1,resp2] = await Promise.all([getMeetSummary(),getMeetSummaryItems()])
  stats.value = resp1.data
  meetings.value = resp2.data.map(item => {
    return {
      ...item,
      participants: JSON.parse(item.participants)
    }
  })
}
// æäº¤ä¼šè®®
const submitMeeting = () => {
  if (!meetingForm.title || !meetingForm.timeRange.length || !meetingForm.location || !meetingForm.host) {
    ElMessage.warning('请填写完整的会议信息')
    return
  }
  // åˆ›å»ºæ–°ä¼šè®®
  const newMeeting = {
    id: Date.now(),
    title: meetingForm.title,
    status: 'upcoming',
    startTime: meetingForm.timeRange[0],
    endTime: meetingForm.timeRange[1],
    location: meetingForm.location,
    host: meetingForm.host,
    participants: [meetingForm.host],
    agenda: [
      { time: '待定', content: '议程待定', status: 'pending' }
    ]
  }
  meetings.value.unshift(newMeeting)
  stats.total++
  stats.upcoming++
  ElMessage.success('会议创建成功')
  dialogVisible.value = false
}
// åŠ å…¥ä¼šè®®
const joinMeeting = (meeting) => {
  ElMessage.success(`已加入会议:${meeting.title}`)
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetails = (meeting) => {
  ElMessage.info(`查看会议详情:${meeting.title}`)
}
// ç¼–辑会议
const editMeeting = (meeting) => {
  ElMessage.info(`编辑会议:${meeting.title}`)
}
onMounted(() => {
  console.log('会议看板页面加载完成')
})
</script>
@@ -480,19 +328,19 @@
  .stats-cards {
    grid-template-columns: repeat(2, 1fr);
  }
  .meeting-header {
    flex-direction: column;
    gap: 10px;
  }
  .meeting-info {
    flex-direction: column;
    gap: 10px;
  }
  .meeting-actions {
    flex-direction: column;
  }
}
</style>
</style>
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,398 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议申请</h2>
    </div>
    <!-- ç”³è¯·ç±»åž‹é€‰æ‹© -->
    <el-card class="type-card">
      <div class="type-selector">
        <div
            v-for="type in applicationTypes"
            :key="type.value"
            class="type-item"
            :class="{ active: currentType === type.value }"
            @click="changeType(type.value)"
        >
          <div class="type-icon">
            <el-icon :size="24"><component :is="type.icon"/></el-icon>
          </div>
          <div class="type-info">
            <div class="type-name">{{ type.name }}</div>
            <div class="type-desc">{{ type.desc }}</div>
          </div>
        </div>
      </div>
    </el-card>
    <!-- ä¼šè®®ç”³è¯·è¡¨å• -->
    <el-card>
      <div class="form-header">
        <h3>{{ getCurrentTypeName() }}申请</h3>
      </div>
      <el-form
          ref="meetingFormRef"
          :model="meetingForm"
          :rules="rules"
          label-width="100px"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议主题" prop="title">
              <el-input v-model="meetingForm.title" placeholder="请输入会议主题"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议室" prop="roomId">
              <el-select v-model="meetingForm.roomId" placeholder="请选择会议室" style="width: 100%">
                <el-option
                    v-for="room in meetingRooms"
                    :key="room.id"
                    :label="`${room.name} (${room.location})`"
                    :value="room.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="主持人" prop="host">
              <el-input v-model="meetingForm.host" placeholder="请输入主持人姓名"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议日期" prop="meetingDate">
              <el-date-picker
                  v-model="meetingForm.meetingDate"
                  type="date"
                  placeholder="请选择会议日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  :disabled-date="disabledDate"
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <!-- ç©ºåˆ—,保持布局 -->
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-select
                  v-model="meetingForm.startTime"
                  placeholder="请选择开始时间"
                  style="width: 100%"
              >
                <el-option
                    v-for="time in timeOptions"
                    :key="time.value"
                    :label="time.label"
                    :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束时间" prop="endTime">
              <el-select
                  v-model="meetingForm.endTime"
                  placeholder="请选择结束时间"
                  style="width: 100%"
              >
                <el-option
                    v-for="time in timeOptions"
                    :key="time.value"
                    :label="time.label"
                    :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="参会人员" prop="participants">
          <el-select
              v-model="meetingForm.participants"
              multiple
              filterable
              placeholder="请选择参会人员"
              style="width: 100%"
          >
            <el-option
                v-for="person in employees"
                :key="person.id"
                :label="`${person.staffName} (${person.postJob})`"
                :value="person.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="会议说明" prop="description">
          <el-input
              v-model="meetingForm.description"
              type="textarea"
              :rows="4"
              placeholder="请输入会议说明"
          />
        </el-form-item>
      </el-form>
      <div class="form-footer">
        <el-button @click="resetForm">重置</el-button>
        <el-button type="primary" @click="submitForm">提交</el-button>
      </div>
    </el-card>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {Plus, Document, Promotion, Bell} from '@element-plus/icons-vue'
import {getRoomEnum, saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
// å½“前申请类型
const currentType = ref('department') // approval: å®¡æ‰¹æµç¨‹, department: éƒ¨é—¨çº§, notification: é€šçŸ¥å‘布
// ç”³è¯·ç±»åž‹é€‰é¡¹
const applicationTypes = ref([
  {
    value: 'approval',
    name: '审批流程会议',
    desc: '需要经过多级审批的会议申请',
    icon: Document
  },
  {
    value: 'department',
    name: '部门级会议',
    desc: '部门内部会议申请流程',
    icon: Promotion
  },
  {
    value: 'notification',
    name: '会议通知',
    desc: '无需审批直接发布的会议通知',
    icon: Bell
  }
])
// è¡¨å•数据
const meetingForm = reactive({
  title: '',
  type: '',
  roomId: '',
  host: '',
  meetingDate: '',
  startTime: '',
  endTime: '',
  participants: [],
  description: ''
})
// è¡¨å•校验规则
const rules = {
  title: [{required: true, message: '请输入会议主题', trigger: 'blur'}],
  roomId: [{required: true, message: '请选择会议室', trigger: 'change'}],
  host: [{required: true, message: '请输入主持人', trigger: 'blur'}],
  meetingDate: [{required: true, message: '请选择会议日期', trigger: 'change'}],
  startTime: [{required: true, message: '请选择开始时间', trigger: 'change'}],
  endTime: [{required: true, message: '请选择结束时间', trigger: 'change'}],
  participants: [{required: true, message: '请选择参会人员', trigger: 'change'}]
}
// è¡¨å•引用
const meetingFormRef = ref(null)
// ä¼šè®®å®¤åˆ—表
const meetingRooms = ref([])
// å‘˜å·¥åˆ—表
const employees = ref([])
// æ—¶é—´é€‰é¡¹ï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”)
const timeOptions = ref([])
// åˆå§‹åŒ–时间选项
const initTimeOptions = () => {
  const options = []
  for (let hour = 8; hour <= 18; hour++) {
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªé€‰é¡¹ï¼šæ•´ç‚¹å’ŒåŠç‚¹
    options.push({
      value: `${hour.toString().padStart(2, '0')}:00`,
      label: `${hour.toString().padStart(2, '0')}:00`
    })
    if (hour < 18) { // 18:00之后没有半点选项
      options.push({
        value: `${hour.toString().padStart(2, '0')}:30`,
        label: `${hour.toString().padStart(2, '0')}:30`
      })
    }
  }
  timeOptions.value = options
}
// ç¦ç”¨æ—¥æœŸï¼ˆç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸï¼‰
const disabledDate = (time) => {
  // ç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸ
  return time.getTime() < Date.now() - 86400000
}
// åˆ‡æ¢ç”³è¯·ç±»åž‹
const changeType = (type) => {
  currentType.value = type
}
// èŽ·å–å½“å‰ç±»åž‹åç§°
const getCurrentTypeName = () => {
  const type = applicationTypes.value.find(t => t.value === currentType.value)
  return type ? type.name : ''
}
// é‡ç½®è¡¨å•
const resetForm = () => {
  meetingFormRef.value?.resetFields()
}
// æäº¤è¡¨å•
const submitForm = () => {
  meetingFormRef.value?.validate((valid) => {
    if (valid) {
      let formData = {...meetingForm}
      formData.applicationType = currentType.value
      formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00`
      formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00`
      formData.participants = JSON.stringify(formData.participants)
      console.log(formData)
      saveMeetingApplication(formData).then(() => {
        // æ¨¡æ‹Ÿæäº¤æ“ä½œ
        ElMessage.success(`${getCurrentTypeName()}提交成功`)
        // æ ¹æ®ä¸åŒç±»åž‹æ‰§è¡Œä¸åŒæ“ä½œ
        switch (currentType.value) {
          case 'approval':
            ElMessage.info('会议已提交审批流程')
            break
          case 'department':
            ElMessage.info('部门级会议申请已提交')
            break
          case 'notification':
            ElMessage.info('会议通知已发布')
            break
        }
        resetForm()
      })
    }
  })
}
// é¡µé¢åŠ è½½æ—¶åˆå§‹åŒ–
onMounted(() => {
  initTimeOptions()
  getRoomEnum().then(res => {
    meetingRooms.value = res.data
  })
  getStaffOnJob().then(res => {
    employees.value = res.data.sort((a, b) => a.postJob.localeCompare(b.postJob))
  })
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.type-card {
  margin-bottom: 20px;
}
.type-selector {
  display: flex;
  gap: 20px;
}
.type-item {
  flex: 1;
  display: flex;
  align-items: center;
  padding: 20px;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}
.type-item:hover {
  border-color: #409eff;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.type-item.active {
  border-color: #409eff;
  background-color: #ecf5ff;
}
.type-icon {
  margin-right: 15px;
  color: #409eff;
}
.type-name {
  font-size: 16px;
  font-weight: 500;
  color: #303133;
  margin-bottom: 5px;
}
.type-desc {
  font-size: 14px;
  color: #909399;
}
.form-header {
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 1px solid #ebeef5;
}
.form-header h3 {
  margin: 0;
  color: #303133;
}
.form-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 30px;
  padding-top: 20px;
  border-top: 1px solid #ebeef5;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,495 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议草稿</h2>
      <el-button type="primary" @click="handleAdd">
        <el-icon><Plus /></el-icon>
        æ–°å»ºè‰ç¨¿
      </el-button>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" label-width="100px" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable />
        </el-form-item>
        <el-form-item label="会议日期">
          <el-date-picker
            v-model="searchForm.meetingDate"
            type="date"
            placeholder="请选择会议日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- è‰ç¨¿åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="draftList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip />
        <el-table-column prop="room" label="会议室" align="center" width="120" />
        <el-table-column prop="host" label="主持人" align="center" width="120" />
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants }}人
          </template>
        </el-table-column>
        <el-table-column prop="createTime" label="创建时间" align="center" width="180" />
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDraft(scope.row)">查看</el-button>
            <el-button type="primary" link @click="editDraft(scope.row)">编辑</el-button>
            <el-button type="danger" link @click="deleteDraft(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è‰ç¨¿è¯¦æƒ…对话框 -->
    <el-dialog
      title="会议草稿详情"
      v-model="detailDialogVisible"
      width="800px"
    >
      <div v-if="currentDraft">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentDraft.title }}</el-descriptions-item>
          <el-descriptions-item label="会议编号">{{ currentDraft.meetingId }}</el-descriptions-item>
          <el-descriptions-item label="会议室">{{ currentDraft.room }}</el-descriptions-item>
          <el-descriptions-item label="主持人">{{ currentDraft.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentDraft.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="创建时间">{{ currentDraft.createTime }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            {{ currentDraft.participantList }}
          </div>
        </div>
        <div class="content-section mt-20">
          <h4>会议说明</h4>
          <div class="meeting-description">{{ currentDraft.description }}</div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ–°å»º/编辑草稿对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="editDialogVisible"
      width="700px"
    >
      <el-form :model="meetingForm" :rules="rules" ref="meetingFormRef" label-width="100px">
        <el-form-item label="会议主题" prop="title">
          <el-input v-model="meetingForm.title" placeholder="请输入会议主题" />
        </el-form-item>
        <el-form-item label="会议室" prop="room">
          <el-select v-model="meetingForm.roomId" placeholder="请选择会议室" style="width: 100%">
            <el-option v-for="(v,k) in roomList" :label="v.name" :value="v.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="主持人" prop="host">
          <el-input v-model="meetingForm.host" placeholder="请输入主持人" />
        </el-form-item>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="会议日期" prop="meetingDate">
              <el-date-picker
                v-model="meetingForm.meetingDate"
                type="date"
                placeholder="请选择会议日期"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                :disabled-date="disabledDate"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <!-- ç©ºåˆ—,保持布局 -->
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="开始时间" prop="startTime">
              <el-select
                v-model="meetingForm.startTime"
                placeholder="请选择开始时间"
                style="width: 100%"
              >
                <el-option
                  v-for="time in timeOptions"
                  :key="time.value"
                  :label="time.label"
                  :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束时间" prop="endTime">
              <el-select
                v-model="meetingForm.endTime"
                placeholder="请选择结束时间"
                style="width: 100%"
              >
                <el-option
                  v-for="time in timeOptions"
                  :key="time.value"
                  :label="time.label"
                  :value="time.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="参会人数" prop="participants">
          <el-input
              v-model="meetingForm.participants"
              type="number"
              placeholder="请输入参会人数"
          />
        </el-form-item>
        <el-form-item label="参会人员" prop="participants">
          <el-input
            v-model="meetingForm.participantList"
            type="textarea"
            :rows="3"
            placeholder="请输入参会人员,用逗号分隔"
          />
        </el-form-item>
        <el-form-item label="会议说明">
          <el-input
            v-model="meetingForm.description"
            type="textarea"
            :rows="4"
            placeholder="请输入会议说明"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="editDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitForm">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'
import {getRoomEnum,getDraftList,saveDraft,delDraft} from '@/api/collaborativeApproval/meeting.js'
import dayjs from "dayjs";
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
// è‰ç¨¿åˆ—表数据
const draftList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  meetingDate: ''
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const editDialogVisible = ref(false)
const roomList = ref([])
// å¯¹è¯æ¡†æ ‡é¢˜
const dialogTitle = ref('')
// å½“前查看的草稿
const currentDraft = ref(null)
// è¡¨å•引用
const meetingFormRef = ref(null)
// æ—¶é—´é€‰é¡¹ï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”,工作时间8:00-18:00)
const timeOptions = ref([])
// è¡¨å•数据
const meetingForm = reactive({
  id: '',
  meetingId: '',
  title: '',
  roomId: '',
  host: '',
  meetingDate: '',
  startTime: '',
  endTime: '',
  participants: 0,
  participantList: '',
  description: '',
  createTime: ''
})
// è¡¨å•校验规则
const rules = {
  title: [{ required: true, message: '请输入会议主题', trigger: 'blur' }],
  roomId: [{ required: true, message: '请选择会议室', trigger: 'change' }],
  host: [{ required: true, message: '请输入主持人', trigger: 'blur' }],
  meetingDate: [{ required: true, message: '请选择会议日期', trigger: 'change' }],
  startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
  endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
}
// åˆå§‹åŒ–时间选项(以半小时为间隔,工作时间8:00-18:00)
const initTimeOptions = () => {
  const options = []
  for (let hour = 8; hour <= 18; hour++) {
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªé€‰é¡¹ï¼šæ•´ç‚¹å’ŒåŠç‚¹
    options.push({
      value: `${hour.toString().padStart(2, '0')}:00`,
      label: `${hour.toString().padStart(2, '0')}:00`
    })
    if (hour < 18) { // 18:00之后没有半点选项
      options.push({
        value: `${hour.toString().padStart(2, '0')}:30`,
        label: `${hour.toString().padStart(2, '0')}:30`
      })
    }
  }
  timeOptions.value = options
}
// ç¦ç”¨æ—¥æœŸï¼ˆç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸï¼‰
const disabledDate = (time) => {
  // ç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸ
  return time.getTime() < Date.now() - 86400000
}
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getDraftList({...queryParams,...searchForm})
  queryParams.current = resp.data.current
  draftList.value = resp.data.records.map(it=>{
    it.room = roomList.value.find(room=>it.roomId===room.id).name ?? ""
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format("HH:mm")} ~ ${dayjs(it.endTime).format("HH:mm")}`
    return it
  })
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.pageNum = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    createTime: []
  })
  handleSearch()
}
// æ·»åŠ æŒ‰é’®æ“ä½œ
const handleAdd = () => {
  dialogTitle.value = '新建草稿'
  resetForm()
  editDialogVisible.value = true
}
// æŸ¥çœ‹è‰ç¨¿è¯¦æƒ…
const viewDraft = (row) => {
  currentDraft.value = row
  detailDialogVisible.value = true
}
// ç¼–辑草稿
const editDraft = (row) => {
  dialogTitle.value = '编辑草稿'
  Object.assign(meetingForm, {
    id: row.id,
    meetingId: row.meetingId,
    title: row.title,
    room: row.room,
    roomId: row.id,
    host: row.host,
    meetingDate: row.meetingTime.split(' ')[0],
    startTime: row.meetingTime.split(' ')[1],
    endTime: row.meetingTime.split(' ')[3],
    participants: row.participants,
    participantList: row.participantList,
    description: row.description,
    createTime: row.createTime
  })
  editDialogVisible.value = true
}
// åˆ é™¤è‰ç¨¿
const deleteDraft = (row) => {
  ElMessageBox.confirm(
    `确认删除会议草稿 "${row.title}"?`,
    '删除草稿',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(() => {
    delDraft(row.id).then(resp=>{
      ElMessage.success('草稿删除成功')
      getList()
    })
  }).catch(() => {})
}
// é‡ç½®è¡¨å•
const resetForm = () => {
  Object.assign(meetingForm, {
    id: '',
    meetingId: '',
    title: '',
    room: '',
    host: '',
    meetingDate: '',
    startTime: '',
    endTime: '',
    participants: 0,
    participantList: '',
    description: '',
    createTime: ''
  })
}
// æäº¤è¡¨å•
const submitForm = () => {
  meetingFormRef.value.validate((valid) => {
    if (valid) {
      let formData = {...meetingForm}
      formData.startTime = dayjs(meetingForm.meetingDate + ' ' + meetingForm.startTime).format("YYYY-MM-DD HH:mm:ss")
      formData.endTime = dayjs(meetingForm.meetingDate + ' ' + meetingForm.endTime).format("YYYY-MM-DD HH:mm:ss")
      saveDraft(formData).then(()=>{
        ElMessage.success('保存成功')
        editDialogVisible.value = false
        getList()
      })
    }
  })
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  initTimeOptions()
  getList()
  getRoomEnum().then((res) => {
    roomList.value = res.data
  })
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.meeting-description {
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
  white-space: pre-wrap;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,418 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议审批</h2>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable/>
        </el-form-item>
        <el-form-item label="申请人">
          <el-input v-model="searchForm.applicant" placeholder="请输入申请人" clearable/>
        </el-form-item>
        <el-form-item label="审批状态">
          <el-select style="width: 100px" v-model="searchForm.status" placeholder="请选择审批状态" clearable>
            <el-option label="待审批" value="0"/>
            <el-option label="已通过" value="1"/>
            <el-option label="未审批" value="2"/>
            <el-option label="已取消" value="3"/>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å®¡æ‰¹åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="approvalList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip/>
        <el-table-column prop="applicant" label="申请人" align="center" width="120"/>
        <el-table-column prop="host" label="主理人" align="center" width="120"/>
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" align="center" width="150"/>
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants.length }}人
          </template>
        </el-table-column>
        <el-table-column prop="status" label="审批状态" align="center" width="120">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDetail(scope.row)">查看</el-button>
            <el-button
                v-if="scope.row.status == '0'"
                type="primary"
                link
                @click="handleApproval(scope.row)"
            >
              å®¡æ‰¹
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
          v-show="total > 0"
          :total="total"
          v-model:page="queryParams.current"
          v-model:limit="queryParams.size"
          @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
        title="会议详情"
        v-model="detailDialogVisible"
        width="800px"
    >
      <div v-if="currentMeeting">
         <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
          <el-descriptions-item label="会议主题" label-class-name="nowrap-label">{{
              currentMeeting.title
            }}</el-descriptions-item>
          <el-descriptions-item label="申请人" label-class-name="nowrap-label">{{
              currentMeeting.applicant
            }}</el-descriptions-item>
          <el-descriptions-item label="主理人" label-class-name="nowrap-label">{{
              currentMeeting.host
            }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2" label-class-name="nowrap-label">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点" label-class-name="nowrap-label">{{
              currentMeeting.location
            }}</el-descriptions-item>
          <el-descriptions-item label="参会人数" label-class-name="nowrap-label">{{
              currentMeeting.participants.length
            }}人</el-descriptions-item>
          <el-descriptions-item label="审批状态" label-class-name="nowrap-label">
            <el-tag :type="getStatusType(currentMeeting.status)">
              {{ getStatusText(currentMeeting.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请时间" label-class-name="nowrap-label">{{
              currentMeeting.createTime
            }}</el-descriptions-item>
          <el-descriptions-item style="max-height: 400px" label="会议说明" :span="2"
                                label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- ä¼šè®®å®¡æ‰¹å¯¹è¯æ¡† -->
    <el-dialog
        title="会议审批"
        v-model="approvalDialogVisible"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentMeeting.applicant }}</el-descriptions-item>
          <el-descriptions-item label="主理人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点">{{ currentMeeting.location }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants.length }}人</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
        <div v-show="false" class="approval-opinion mt-20">
          <h4>审批意见</h4>
          <el-input
              v-model="approvalOpinion"
              type="textarea"
              placeholder="请输入审批意见"
              :rows="4"
          />
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="approvalDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="danger" @click="submitApproval('2')">不通过</el-button>
          <el-button type="primary" @click="submitApproval('1')">通 è¿‡</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'
import {getRoomEnum, getExamineList,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
import dayjs from "dayjs";
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
const roomEnum = ref([])
const staffList = ref([])
// å®¡æ‰¹åˆ—表数据
const approvalList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  applicant: '',
  status: ''
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const approvalDialogVisible = ref(false)
// å½“前查看的会议
const currentMeeting = ref(null)
// å®¡æ‰¹æ„è§
const approvalOpinion = ref('')
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getExamineList({...searchForm, ...queryParams})
  approvalList.value = resp.data.records.map(it => {
    let room = roomEnum.value.find(room => it.roomId === room.id)
    it.location = `${room.name}(${room.location})`
    let staffs = JSON.parse(it.participants)
    it.staffCount = staffs.size
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
    it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
      return {
        id: staff.id,
        name: `${staff.staffName}(${staff.postJob})`
      }
    })
    return it
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.pageNum = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    applicant: '',
    status: ''
  })
  handleSearch()
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetail = (row) => {
  currentMeeting.value = row
  detailDialogVisible.value = true
}
// å¤„理审批
const handleApproval = (row) => {
  currentMeeting.value = row
  approvalOpinion.value = ''
  approvalDialogVisible.value = true
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '0': 'info',     // å¾…审批
    '1': 'success',  // å·²é€šè¿‡
    '2': 'warning',  // æœªé€šè¿‡
    '3': 'danger'   // å–消
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    '0': '待审批',
    '1': '已通过',
    '2': '未通过',
    '3': '已取消'
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// æäº¤å®¡æ‰¹
const submitApproval = (status) => {
  // if (status === 'approved' && !approvalOpinion.value.trim()) {
  //   ElMessage.warning('请填写审批意见')
  //   return
  // }
  ElMessageBox.confirm(
      `确认${status === '1' ? '通过' : '不通过'}该会议申请?`,
      '审批确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
  ).then(() => {
    saveMeetingApplication({
      id: currentMeeting.value.id,
      status: status
    }).then(resp=>{
      // æ›´æ–°ä¼šè®®çŠ¶æ€
      currentMeeting.value.status = status
      ElMessage.success('审批提交成功')
      approvalDialogVisible.value = false
      getList()
    })
  }).catch(() => {
  })
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(async () => {
  const [resp1, resp2]= await Promise.all([getRoomEnum(), getStaffOnJob()])
  roomEnum.value = resp1.data
  staffList.value = resp2.data
  await getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.approval-opinion h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.nowrap-label {
  white-space: nowrap !important;
}
.description-content {
  white-space: pre-wrap;
  word-wrap: break-word;
  line-height: 1.6;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
  min-height: 60px;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,371 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议室使用查询</h2>
    </div>
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-card class="search-card">
      <el-form :model="queryForm" label-width="80px" inline>
        <el-form-item label="查询日期">
          <el-date-picker
              v-model="queryForm.meetingDate"
              type="date"
              placeholder="请选择日期"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              :clearable="false"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">查询</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å®¤ä½¿ç”¨æƒ…况 -->
    <el-card class="table-container" :loading="loading">
      <div class="time-table">
        <!-- è¡¨å¤´ -->
        <div class="table-header">
          <div class="header-cell room-header">会议室</div>
          <div
              v-for="timeSlot in timeSlots"
              :key="timeSlot.value"
              class="header-cell time-header"
          >
            {{ timeSlot.label }}
          </div>
        </div>
        <!-- è¡¨æ ¼å†…容 -->
        <div class="table-body">
          <div
              v-for="room in roomUsage"
              :key="room.id"
              class="table-row"
          >
            <div class="cell room-cell">{{ room.name }}</div>
            <div class="cells-container">
              <template v-for="(cell, index) in generateMeetingCells(room)" :key="index">
                <div
                    class="cell content-cell"
                    :class="[cell.type, `status-${cell.meeting?.status || '0'}`]"
                    :style="{ flex: cell.span-0.2 }"
                    @click="viewMeetingDetails(cell)"
                >
                  <div v-if="cell.type === 'meeting'" class="meeting-content">
                    <div class="meeting-title">{{ cell.meeting.title }}</div>
                    <div class="meeting-time">{{ cell.startTime }}-{{ cell.endTime }}</div>
                  </div>
                  <div v-else class="free-content">
                    ç©ºé—²
                  </div>
                </div>
              </template>
            </div>
          </div>
        </div>
      </div>
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
        title="会议详情"
        v-model="detailDialogVisible"
        width="800px"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="1" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="会议室">{{ currentMeeting.room }}</el-descriptions-item>
          <el-descriptions-item label="会议时间">{{ currentMeeting.time }}</el-descriptions-item>
          <el-descriptions-item label="主持人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants }}人</el-descriptions-item>
          <el-descriptions-item label="会议说明">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getMeetingUseList} from "@/api/collaborativeApproval/meeting.js"
import dayjs from "dayjs";
// æŸ¥è¯¢è¡¨å•
const queryForm = reactive({
  meetingDate: dayjs().format('YYYY-MM-DD')
})
let loading = ref(false)
// æ—¶é—´æ®µï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”)
const timeSlots = ref([])
// ä¼šè®®å®¤ä½¿ç”¨æƒ…况
const roomUsage = ref([])
// å½“前查看的会议
const currentMeeting = ref(null)
// æ˜¯å¦æ˜¾ç¤ºè¯¦æƒ…对话框
const detailDialogVisible = ref(false)
// åˆå§‹åŒ–时间槽(以半小时为间隔,从8:00到18:00)
const initTimeSlots = () => {
  const slots = []
  for (let hour = 8; hour < 18; hour++) {
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªæ—¶é—´æ®µï¼šæ•´ç‚¹å’ŒåŠç‚¹
    slots.push({
      label: `${hour.toString().padStart(2, '0')}:00`,
      value: `${hour.toString().padStart(2, '0')}:00`
    })
    if (hour < 18) { // åˆ°17:30为止
      slots.push({
        label: `${hour.toString().padStart(2, '0')}:30`,
        value: `${hour.toString().padStart(2, '0')}:30`
      })
    }
  }
  timeSlots.value = slots
}
// ç”Ÿæˆä¼šè®®å®¤çš„æ—¶é—´å•元格
const generateMeetingCells = (room) => {
  const cells = []
  const meetings = room.meetings || []
  const occupiedSlots = new Set()
  // å¤„理每个会议
  for (const meeting of meetings) {
    const startIdx = timeSlots.value.findIndex(slot => slot.value === meeting.startTime)
    let endIdx = timeSlots.value.findIndex(slot => slot.value === meeting.endTime)
    if (endIdx === -1) {
      endIdx = timeSlots.value.length
    }
    console.log('endIdx111', endIdx)
    if (startIdx !== -1 && endIdx !== -1) {
      // æ ‡è®°è¢«å ç”¨çš„æ—¶é—´æ®µ
      for (let i = startIdx; i < endIdx; i++) {
        occupiedSlots.add(timeSlots.value[i].value)
      }
      // åˆ›å»ºä¼šè®®å•元格
      cells.push({
        type: 'meeting',
        meeting: meeting,
        span: endIdx - startIdx,
        startTime: meeting.startTime,
        endTime: meeting.endTime
      })
    }
  }
  // å¤„理空闲时间段
  for (let i = 0; i < timeSlots.value.length; i++) {
    const slot = timeSlots.value[i]
    if (!occupiedSlots.has(slot.value)) {
      // æŸ¥æ‰¾è¿žç»­çš„空闲时间段
      let span = 1
      while (i + span < timeSlots.value.length &&
      !occupiedSlots.has(timeSlots.value[i + span].value)) {
        occupiedSlots.add(timeSlots.value[i + span].value)
        span++
      }
      cells.push({
        type: 'free',
        span: span,
        time: slot.value
      })
    }
  }
  // æŒ‰æ—¶é—´æŽ’序
  cells.sort((a, b) => {
    const timeA = a.startTime || a.time
    const timeB = b.startTime || b.time
    return timeSlots.value.findIndex(s => s.value === timeA) -
        timeSlots.value.findIndex(s => s.value === timeB)
  })
  console.log('cells', cells)
  return cells
}
// æŸ¥çœ‹ä¼šè®®è¯¦æƒ…
const viewMeetingDetails = (cell) => {
  if (cell && cell.type === 'meeting') {
    currentMeeting.value = cell.meeting
    detailDialogVisible.value = true
  } else {
    ElMessage.info('该时间段会议室空闲')
  }
}
// æŸ¥è¯¢æŒ‰é’®æ“ä½œ
const handleSearch = async () => {
  loading.value = true
  let resp = await getMeetingUseList({...queryForm})
  roomUsage.value = resp.data
  loading.value = false
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  queryForm.date = dayjs().format('YYYY-MM-DD')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  // åˆå§‹åŒ–æ—¶é—´æ§½
  initTimeSlots()
  // é»˜è®¤æŸ¥è¯¢ä»Šå¤©çš„æ•°æ®
  const today = new Date()
  queryForm.date = today.toISOString().split('T')[0]
  handleSearch()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.table-container {
  padding: 0;
}
.time-table {
  width: 100%;
  border-collapse: collapse;
}
.table-header {
  display: flex;
  border: 1px solid;
}
.table-row {
  display: flex;
  border: 1px solid #ebeef5;
  border-top: none;
}
.header-cell {
  padding: 12px 5px;
  text-align: center;
  font-weight: bold;
  border-right: 1px solid;
  min-height: 20px;
}
.room-header {
  width: 120px;
}
.time-header {
  flex: 1;
}
.cell {
  padding: 15px 5px;
  text-align: center;
  border-right: 1px solid;
  min-height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  word-break: break-word;
  line-height: 1.2;
}
.room-cell {
  width: 120px;
  font-weight: bold;
}
.cells-container {
  flex: 1;
  display: flex;
}
.content-cell {
  min-height: 60px;
  cursor: pointer;
  transition: all 0.3s;
}
.content-cell:hover {
  opacity: 0.8;
}
.free {
  color: #f56c6c;
}
.meeting {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.status-1 {
  background-color: #fef0f0;
  color: #d14646;
}
.status-0 {
  background-color: #c7ddc8;
  color: rgba(230, 162, 60, 0.29);
}
.meeting-content {
  width: 100%;
}
.meeting-title {
  font-weight: bold;
  margin-bottom: 5px;
}
.meeting-time {
  font-size: 12px;
}
.free-content {
  color: #909399;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,416 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议发布</h2>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable/>
        </el-form-item>
        <el-form-item label="申请人">
          <el-input v-model="searchForm.applicant" placeholder="请输入申请人" clearable/>
        </el-form-item>
        <el-form-item label="发布状态">
          <el-select style="width: 100px" v-model="searchForm.status" placeholder="请选择发布状态" clearable>
            <el-option label="待发布" value="0"/>
            <el-option label="已发布" value="1"/>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å‘布列表 -->
    <el-card>
      <el-table v-loading="loading" :data="approvalList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip/>
        <el-table-column prop="applicant" label="申请人" align="center" width="120"/>
        <el-table-column prop="host" label="主理人" align="center" width="120"/>
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" align="center" width="150"/>
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants.length }}人
          </template>
        </el-table-column>
        <el-table-column prop="status" label="发布状态" align="center" width="120">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDetail(scope.row)">查看</el-button>
            <el-button
                v-if="scope.row.status == '0'"
                type="primary"
                link
                @click="handleApproval(scope.row)"
            >
              å‘布
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
          v-show="total > 0"
          :total="total"
          v-model:page="queryParams.current"
          v-model:limit="queryParams.size"
          @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
        title="会议详情"
        v-model="detailDialogVisible"
        width="800px"
    >
      <div v-if="currentMeeting">
         <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
          <el-descriptions-item label="会议主题" label-class-name="nowrap-label">{{
              currentMeeting.title
            }}</el-descriptions-item>
          <el-descriptions-item label="申请人" label-class-name="nowrap-label">{{
              currentMeeting.applicant
            }}</el-descriptions-item>
          <el-descriptions-item label="主理人" label-class-name="nowrap-label">{{
              currentMeeting.host
            }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2" label-class-name="nowrap-label">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点" label-class-name="nowrap-label">{{
              currentMeeting.location
            }}</el-descriptions-item>
          <el-descriptions-item label="参会人数" label-class-name="nowrap-label">{{
              currentMeeting.participants.length
            }}人</el-descriptions-item>
          <el-descriptions-item label="发布状态" label-class-name="nowrap-label">
            <el-tag :type="getStatusType(currentMeeting.status)">
              {{ getStatusText(currentMeeting.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请时间" label-class-name="nowrap-label">{{
              currentMeeting.createTime
            }}</el-descriptions-item>
          <el-descriptions-item style="max-height: 400px" label="会议说明" :span="2"
                                label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- ä¼šè®®å‘布对话框 -->
    <el-dialog
        title="会议发布"
        v-model="approvalDialogVisible"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentMeeting.applicant }}</el-descriptions-item>
          <el-descriptions-item label="主理人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点">{{ currentMeeting.location }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants.length }}人</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
                v-for="participant in currentMeeting.participants"
                :key="participant.id"
                style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
        <div class="approval-opinion mt-20">
          <h4>发布意见</h4>
          <el-input
              v-model="publishComment"
              type="textarea"
              placeholder="请输入发布意见"
              :rows="4"
          />
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="approvalDialogVisible = false">取 æ¶ˆ</el-button>
<!--          <el-button type="danger" @click="submitApproval('2')">不通过</el-button>-->
          <el-button type="primary" @click="submitApproval('1')">发 å¸ƒ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, onMounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'
import {getRoomEnum, getMeetingPublish,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
import {getStaffOnJob} from "@/api/personnelManagement/onboarding.js";
import dayjs from "dayjs";
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
const roomEnum = ref([])
const staffList = ref([])
// å‘布列表数据
const approvalList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  applicant: '',
  status: ''
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const approvalDialogVisible = ref(false)
// å½“前查看的会议
const currentMeeting = ref(null)
// å‘布意见
const publishComment = ref('')
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getMeetingPublish({...searchForm, ...queryParams})
  approvalList.value = resp.data.records.map(it => {
    let room = roomEnum.value.find(room => it.roomId === room.id)
    it.location = `${room.name}(${room.location})`
    let staffs = JSON.parse(it.participants)
    it.staffCount = staffs.size
    it.status = it.publishStatus
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
    it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
      return {
        id: staff.id,
        name: `${staff.staffName}(${staff.postJob})`
      }
    })
    return it
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.pageNum = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    applicant: '',
    status: ''
  })
  handleSearch()
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetail = (row) => {
  currentMeeting.value = row
  detailDialogVisible.value = true
}
// å¤„理发布
const handleApproval = (row) => {
  currentMeeting.value = row
  publishComment.value = ''
  approvalDialogVisible.value = true
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '0': 'info',     // å¾…发布
    '1': 'success',  // å·²é€šè¿‡
    '2': 'danger',  // æœªé€šè¿‡
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    '0': '待发布',
    '1': '已发布',
    '2': '已取消',
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// æäº¤å‘布
const submitApproval = (status) => {
  // if (status === 'approved' && !publishComment.value.trim()) {
  //   ElMessage.warning('请填写发布意见')
  //   return
  // }
  ElMessageBox.confirm(
      `确认${status === '1' ? '发布' : '取消'}该会议?`,
      '发布确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
  ).then(() => {
    saveMeetingApplication({
      id: currentMeeting.value.id,
      publishStatus: status,
      publishComment: publishComment.value
    }).then(resp=>{
      // æ›´æ–°ä¼šè®®çŠ¶æ€
      currentMeeting.value.status = status
      ElMessage.success('发布提交成功')
      approvalDialogVisible.value = false
      getList()
    })
  }).catch(() => {
  })
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(async () => {
  const [resp1, resp2]= await Promise.all([getRoomEnum(), getStaffOnJob()])
  roomEnum.value = resp1.data
  staffList.value = resp2.data
  await getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.approval-opinion h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.nowrap-label {
  white-space: nowrap !important;
}
.description-content {
  white-space: pre-wrap;
  word-wrap: break-word;
  line-height: 1.6;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
  min-height: 60px;
}
</style>
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,306 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议室设置</h2>
      <el-button type="primary" @click="handleAdd">
        <el-icon><Plus /></el-icon>
        æ–°å¢žä¼šè®®å®¤
      </el-button>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" label-width="100px" inline>
        <el-form-item label="会议室名称">
          <el-input v-model="searchForm.name" placeholder="请输入会议室名称" clearable />
        </el-form-item>
        <el-form-item label="位置">
          <el-input v-model="searchForm.location" placeholder="请输入位置" clearable />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®å®¤åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="meetingRoomList" border>
        <el-table-column prop="name" label="会议室名称" align="center" />
        <el-table-column prop="location" label="位置" align="center" />
        <el-table-column prop="capacity" label="容纳人数" align="center" />
        <el-table-column prop="equipment" label="设备配置" align="center">
          <template #default="scope">
            <el-tag v-for="item in scope.row.equipment" :key="item" style="margin-right: 5px; margin-bottom: 5px;">
              {{ item }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" align="center" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
              {{ scope.row.status === 1 ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200">
          <template #default="scope">
            <el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
            <el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </el-card>
    <!-- æ·»åŠ /编辑对话框 -->
    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" @close="cancel">
      <el-form ref="meetingRoomFormRef" :model="meetingRoomForm" :rules="rules" label-width="100px">
        <el-form-item label="会议室名称" prop="name">
          <el-input v-model="meetingRoomForm.name" placeholder="请输入会议室名称" />
        </el-form-item>
        <el-form-item label="位置" prop="location">
          <el-input v-model="meetingRoomForm.location" placeholder="请输入会议室位置" />
        </el-form-item>
        <el-form-item label="容纳人数" prop="capacity">
          <el-input-number v-model="meetingRoomForm.capacity" :min="1" placeholder="请输入容纳人数" />
        </el-form-item>
        <el-form-item label="设备配置" prop="equipment">
          <el-select v-model="meetingRoomForm.equipment" multiple placeholder="请选择设备配置" style="width: 100%">
            <el-option
              v-for="item in equipmentOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="meetingRoomForm.status">
            <el-radio :label="1">启用</el-radio>
            <el-radio :label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="meetingRoomForm.remark" type="textarea" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="cancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import Pagination from '@/components/Pagination/index.vue'
import {getMeetingRoomList,saveRoom,delRoom} from '@/api/collaborativeApproval/meeting.js'
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
// ä¼šè®®å®¤åˆ—表数据
const meetingRoomList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  name: '',
  location: ''
})
// å¯¹è¯æ¡†æ ‡é¢˜
const dialogTitle = ref('')
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const dialogVisible = ref(false)
// è®¾å¤‡é…ç½®é€‰é¡¹
const equipmentOptions = ref([
  { value: '投影仪', label: '投影仪' },
  { value: '电视', label: '电视' },
  { value: '音响', label: '音响' },
  { value: '电话', label: '电话' },
  { value: '视频会议系统', label: '视频会议系统' },
  { value: '白板', label: '白板' },
  { value: '写字板', label: '写字板' },
  { value: '无线网络', label: '无线网络' }
])
// è¡¨å•数据
const meetingRoomForm = reactive({
  id: undefined,
  name: '',
  location: '',
  capacity: 10,
  equipment: [],
  status: 1,
  remark: ''
})
// è¡¨å•校验规则
const rules = {
  name: [{ required: true, message: '会议室名称不能为空', trigger: 'blur' }],
  location: [{ required: true, message: '位置不能为空', trigger: 'blur' }],
  capacity: [{ required: true, message: '容纳人数不能为空', trigger: 'blur' }]
}
// è¡¨å•引用
const meetingRoomFormRef = ref(null)
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getMeetingRoomList({...searchForm,...queryParams})
  meetingRoomList.value = resp.data.records.map(it=>{
    it.equipment = it.equipment.split(',')
    return it;
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.current = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    name: '',
    location: ''
  })
  handleSearch()
}
// æ·»åŠ æŒ‰é’®æ“ä½œ
const handleAdd = () => {
  dialogTitle.value = '添加会议室'
  dialogVisible.value = true
}
// ä¿®æ”¹æŒ‰é’®æ“ä½œ
const handleEdit = (row) => {
  dialogTitle.value = '修改会议室'
  Object.assign(meetingRoomForm, row)
  dialogVisible.value = true
}
// åˆ é™¤æŒ‰é’®æ“ä½œ
const handleDelete = (row) => {
  ElMessageBox.confirm(
    `是否确认删除会议室 "${row.name}"?`,
    '警告',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(() => {
    // æ¨¡æ‹Ÿåˆ é™¤æ“ä½œ
    delRoom(row.id).then(resp=>{
      ElMessage.success('删除成功')
      getList()
    })
  }).catch(() => {})
}
// å–消按钮
const cancel = () => {
  dialogVisible.value = false
  reset()
}
// è¡¨å•重置
const reset = () => {
  Object.assign(meetingRoomForm, {
    id: undefined,
    name: '',
    location: '',
    capacity: 10,
    equipment: [],
    status: 1,
    remark: ''
  })
  meetingRoomFormRef.value?.resetFields()
}
// æäº¤è¡¨å•
const submitForm = () => {
  meetingRoomFormRef.value?.validate((valid) => {
    if (valid) {
      // æ¨¡æ‹Ÿæäº¤æ“ä½œ
      let formData = {...  meetingRoomForm}
      formData.equipment = formData.equipment.join(',')
      saveRoom(formData).then(resp=>{
        ElMessage.success('保存成功')
        dialogVisible.value = false
        getList()
      })
    }
  })
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/collaborativeApproval/notificationManagement/summary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,403 @@
<template>
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜ -->
    <div class="page-header">
      <h2>会议纪要</h2>
    </div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-card class="search-card">
      <el-form :model="searchForm" inline>
        <el-form-item label="会议主题">
          <el-input v-model="searchForm.title" placeholder="请输入会议主题" clearable />
        </el-form-item>
        <el-form-item label="申请人">
          <el-input v-model="searchForm.applicant" placeholder="请输入申请人" clearable />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ä¼šè®®åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="meetingList" border>
        <el-table-column prop="title" label="会议主题" align="center" min-width="200" show-overflow-tooltip />
        <el-table-column prop="applicant" label="申请人" align="center" width="120" />
        <el-table-column prop="host" label="主持人" align="center" width="120" />
        <el-table-column prop="meetingTime" label="会议时间" align="center" width="180">
          <template #default="scope">
            {{ formatDateTime(scope.row.meetingTime) }}
          </template>
        </el-table-column>
        <el-table-column prop="location" label="会议地点" align="center" width="150" />
        <el-table-column prop="participants" label="参会人数" align="center" width="100">
          <template #default="scope">
            {{ scope.row.participants.length }}人
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" width="200" fixed="right">
          <template #default="scope">
            <el-button type="primary" link @click="viewDetail(scope.row)">查看</el-button>
            <el-button
              type="primary"
              link
              @click="addMinutes(scope.row)"
            >
              æ·»åŠ çºªè¦
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </el-card>
    <!-- ä¼šè®®è¯¦æƒ…对话框 -->
    <el-dialog
      title="会议详情"
      v-model="detailDialogVisible"
      width="800px"
    >
      <div v-if="currentMeeting">
        <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
          <el-descriptions-item label="会议主题" label-class-name="nowrap-label">{{
            currentMeeting.title
          }}</el-descriptions-item>
          <el-descriptions-item label="申请人" label-class-name="nowrap-label">{{
            currentMeeting.applicant
          }}</el-descriptions-item>
          <el-descriptions-item label="主持人" label-class-name="nowrap-label">{{
            currentMeeting.host
          }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2" label-class-name="nowrap-label">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点" label-class-name="nowrap-label">{{
            currentMeeting.location
          }}</el-descriptions-item>
          <el-descriptions-item label="参会人数" label-class-name="nowrap-label">{{
            currentMeeting.participants.length
          }}人</el-descriptions-item>
          <el-descriptions-item label="审批状态" label-class-name="nowrap-label">
            <el-tag :type="getStatusType(currentMeeting.status)">
              {{ getStatusText(currentMeeting.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请时间" label-class-name="nowrap-label">{{
            currentMeeting.createTime
          }}</el-descriptions-item>
          <el-descriptions-item style="max-height: 400px" label="会议说明" :span="2"
            label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>参会人员</h4>
          <div class="participants-list">
            <el-tag
              v-for="participant in currentMeeting.participants"
              :key="participant.id"
              style="margin-right: 10px; margin-bottom: 10px;"
            >
              {{ participant.name }}
            </el-tag>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="detailDialogVisible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ·»åŠ ä¼šè®®çºªè¦å¯¹è¯æ¡† -->
    <el-dialog
      title="添加会议纪要"
      v-model="minutesDialogVisible"
      width="80%"
      @close="handleCloseMinutesDialog"
    >
      <div v-if="currentMeeting">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="会议主题">{{ currentMeeting.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentMeeting.applicant }}</el-descriptions-item>
          <el-descriptions-item label="主持人">{{ currentMeeting.host }}</el-descriptions-item>
          <el-descriptions-item label="会议时间" :span="2">
            {{ formatDateTime(currentMeeting.meetingTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="会议地点">{{ currentMeeting.location }}</el-descriptions-item>
          <el-descriptions-item label="参会人数">{{ currentMeeting.participants.length }}人</el-descriptions-item>
        </el-descriptions>
        <div class="content-section mt-20">
          <h4>会议纪要内容</h4>
          <div class="editor-container">
            <Editor
              v-model="minutesContent"
              :min-height="400"
            />
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="minutesDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitMinutes">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'
import Editor from '@/components/Editor/index.vue'
import { getRoomEnum, getMeetingPublish ,getMeetingMinutesByMeetingId,saveMeetingMinutes} from '@/api/collaborativeApproval/meeting.js'
import { getStaffOnJob } from "@/api/personnelManagement/onboarding.js"
import dayjs from "dayjs"
// æ•°æ®åˆ—表加载状态
const loading = ref(false)
// æ€»æ¡æ•°
const total = ref(0)
const roomEnum = ref([])
const staffList = ref([])
// ä¼šè®®åˆ—表数据
const meetingList = ref([])
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  title: '',
  applicant: '',
  // status: '1' // é»˜è®¤åªæ˜¾ç¤ºå·²é€šè¿‡å®¡æ‰¹çš„会议
})
// æ˜¯å¦æ˜¾ç¤ºå¯¹è¯æ¡†
const detailDialogVisible = ref(false)
const minutesDialogVisible = ref(false)
// å½“前查看的会议
const currentMeeting = ref(null)
// ä¼šè®®çºªè¦å†…容
const minutesContent = ref('')
const minutesContentId = ref('')
// æŸ¥è¯¢æ•°æ®
const getList = async () => {
  loading.value = true
  let resp = await getMeetingPublish({ ...searchForm, ...queryParams })
  meetingList.value = resp.data.records.map(it => {
    let room = roomEnum.value.find(room => it.roomId === room.id)
    it.location = `${room.name}(${room.location})`
    let staffs = JSON.parse(it.participants)
    it.staffCount = staffs.size
    it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
    it.participants = staffList.value.filter(staff => staffs.some(id => id === staff.id)).map(staff => {
      return {
        id: staff.id,
        name: `${staff.staffName}(${staff.postJob})`
      }
    })
    return it
  })
  total.value = resp.data.total
  loading.value = false
}
// æœç´¢æŒ‰é’®æ“ä½œ
const handleSearch = () => {
  queryParams.current = 1
  getList()
}
// é‡ç½®æœç´¢è¡¨å•
const resetSearch = () => {
  Object.assign(searchForm, {
    title: '',
    applicant: '',
    // status: '1'
  })
  handleSearch()
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetail = (row) => {
  currentMeeting.value = row
  detailDialogVisible.value = true
}
// æ·»åŠ ä¼šè®®çºªè¦
const addMinutes = async (row) => {
  let resp = await getMeetingMinutesByMeetingId(row.id)
  currentMeeting.value = row
  if (resp.data){
    minutesContent.value = resp.data.content
    minutesContentId.value = resp.data.id
  }else {
    minutesContent.value = `<h2>${row.title}会议纪要</h2>
<p><strong>会议时间:</strong>${row.meetingTime}</p>
<p><strong>会议地点:</strong>${row.location}</p>
<p><strong>主持人:</strong>${row.host}</p>
<p><strong>参会人员:</strong></p>
<ol>
  ${row.participants.map(p => `<li>${p.name}</li>`).join('')}
</ol>
<p><strong>会议内容:</strong></p>
<ol>
  <li>议题一:
    <ul>
      <li>讨论内容:</li>
      <li>决议事项:</li>
    </ul>
  </li>
  <li>议题二:
    <ul>
      <li>讨论内容:</li>
      <li>决议事项:</li>
    </ul>
  </li>
</ol>
<p><strong>备注:</strong></p>`
  }
  minutesDialogVisible.value = true
}
// æäº¤ä¼šè®®çºªè¦
const submitMinutes = () => {
  if (!minutesContent.value) {
    ElMessage.warning('请输入会议纪要内容')
    return
  }
  saveMeetingMinutes({
    id: minutesContentId.value,
    content: minutesContent.value,
    meetingId: currentMeeting.value.id,
    title: currentMeeting.value.title
  }).then(resp=>{
    console.log('会议纪要内容:', minutesContent.value)
    ElMessage.success('会议纪要保存成功')
    minutesDialogVisible.value = false
  })
}
// å…³é—­ä¼šè®®çºªè¦å¯¹è¯æ¡†
const handleCloseMinutesDialog = () => {
  minutesContent.value = ''
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '0': 'info',     // å¾…审批
    '1': 'success',  // å·²é€šè¿‡
    '2': 'warning',  // æœªé€šè¿‡
    '3': 'danger'   // å–消
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    '0': '待审批',
    '1': '已通过',
    '2': '未通过',
    '3': '已取消'
  }
  return statusMap[status] || '未知'
}
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(async () => {
  const [resp1, resp2] = await Promise.all([getRoomEnum(), getStaffOnJob()])
  roomEnum.value = resp1.data
  staffList.value = resp2.data
  await getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.search-card {
  margin-bottom: 20px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.content-section h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.mt-20 {
  margin-top: 20px;
}
.participants-list {
  min-height: 40px;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
}
.nowrap-label {
  white-space: nowrap !important;
}
.editor-container {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}
</style>
src/views/inspectionManagement/index.vue
@@ -44,7 +44,7 @@
      </div>
      <div>
        <div>
          <ETable :loading="tableLoading"
          <PIMTable :loading="tableLoading"
                  :table-data="tableData"
                  :columns="tableColumns"
                  @selection-change="handleSelectionChange"
@@ -75,7 +75,7 @@
              <span v-else class="no-data">--</span>
            </div>
          </template>
          </ETable>
          </PIMTable>
          <el-table ref="table" :data="tableData" height="480" v-loading="tableLoading" border v-else style="width: 100%;height: calc(100vh - 25em)">
            <el-table-column label="序号" type="index" width="60" align="center" />
            <el-table-column prop="deviceName" label="设备名称" :show-overflow-tooltip="true">
@@ -120,7 +120,7 @@
// ç»„件引入
import Pagination from "@/components/Pagination/index.vue";
import ETable from "@/components/Table/ETable.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDia from "@/views/inspectionManagement/components/formDia.vue";
import QrCodeDia from "@/views/inspectionManagement/components/qrCodeDia.vue";
import ViewFiles from "@/views/inspectionManagement/components/viewFiles.vue";
src/views/inventoryManagement/dispatchLog/index.vue
@@ -1,248 +1,249 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">供应商名称:</span>
        <el-input
          v-model="searchForm.supplierName"
          style="width: 240px"
          placeholder="请输入"
          @change="handleQuery"
          clearable
          prefix-icon="Search"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
          >搜索</el-button
        >
      </div>
      <div>
        <!-- <el-button type="primary" @click="openForm('add')">新增</el-button> -->
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys"
        :row-key="(row) => row.id"
        show-summary
        style="width: 100%"
        :summary-method="summarizeMainTable"
        height="calc(100vh - 18.5em)"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="出库日期"
          prop="createTime"
          min-width="130"
          show-overflow-tooltip
        />
        <el-table-column
          label="供应商名称"
          prop="supplierName"
          width="250"
          show-overflow-tooltip
        />
        <el-table-column
          label="产品大类"
          prop="productCategory"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="规格型号"
          prop="specificationModel"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="单位"
          prop="unit"
          width="80"
          show-overflow-tooltip
        />
        <el-table-column
          label="出库数量"
          prop="inboundNum"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="含税单价(元)"
          prop="taxInclusiveUnitPrice"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="含税总价(元)"
          prop="taxInclusiveTotalPrice"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="税率(%)"
          prop="taxRate"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="不含税总价(元)"
          prop="taxExclusiveTotalPrice"
          width="180"
          show-overflow-tooltip
        />
        <el-table-column
          label="出库人"
          prop="createBy"
          width="80"
          show-overflow-tooltip
        />
        <!-- <el-table-column
          fixed="right"
          label="操作"
          min-width="60"
          align="center"
        >
          <template #default="scope">
            <el-button
              link
              type="primary"
              size="small"
              @click="openForm('edit', scope.row)"
              >编辑</el-button
            >
          </template>
        </el-table-column> -->
      </el-table>
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
    <!-- æ‰“印预览弹窗 -->
    <el-dialog
      v-model="printPreviewVisible"
      title="打印预览"
      width="90%"
      :close-on-click-modal="false"
      class="print-preview-dialog"
    >
      <div class="print-preview-container">
                 <div class="print-preview-header">
           <el-button type="primary" @click="executePrint">执行打印</el-button>
           <el-button @click="printPreviewVisible = false">关闭预览</el-button>
         </div>
                   <div class="print-preview-content">
          <div v-if="printData.length === 0" style="text-align: center; padding: 50px; color: #999;">
            æš‚无打印数据
          </div>
          <div v-else style="text-align: center; padding: 10px; color: #666; font-size: 14px; background: #e8f4fd; margin-bottom: 10px;">
            å…± {{ printData.length }} æ¡æ•°æ®å¾…打印
          </div>
          <div v-for="(item, index) in printData" :key="index" class="print-page">
            <div class="delivery-note">
              <div class="header">
                <div class="company-name">鼎诚瑞实业有限责任公司</div>
                <div class="document-title">零售发货单</div>
              </div>
              <div class="info-section">
                <div class="info-row">
                  <div>
                    <span class="label">发货日期:</span>
                  <span class="value">{{ formatDate(item.createTime) }}</span>
                  </div>
                                     <div>
                   <span class="label">客户名称:</span>
                   <span class="value">{{ item.supplierName || '张爱有' }}</span>
                   </div>
                </div>
                <div class="info-row">
                  <span class="label">单号:</span>
                  <span class="value">{{ item.code }}</span>
                </div>
              </div>
              <div class="table-section">
                <table class="product-table">
                  <thead>
                    <tr>
                      <th>产品名称</th>
                      <th>规格型号</th>
                      <th>单位</th>
                      <th>单价</th>
                      <th>零售数量</th>
                      <th>零售金额</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td>{{ item.productCategory || '砂灰砖' }}</td>
                      <td>{{ item.specificationModel || '标准' }}</td>
                      <td>{{ item.unit || '块' }}</td>
                      <td>{{ item.taxInclusiveUnitPrice || '0' }}</td>
                      <td>{{ item.inboundNum || '2000' }}</td>
                      <td>{{ item.taxInclusiveTotalPrice || '0' }}</td>
                    </tr>
                  </tbody>
                    <tfoot>
                     <tr>
                       <td class="label">合计</td>
                       <td class="total-value"></td>
                       <td class="total-value"></td>
                       <td class="total-value"></td>
                       <td class="total-value">{{ item.inboundNum || '2000' }}</td>
                       <td class="total-value">{{ item.taxInclusiveTotalPrice || '0' }}</td>
                     </tr>
                   </tfoot>
                </table>
              </div>
              <div class="footer-section">
                <div class="footer-row">
                  <div class="footer-item">
                    <span class="label">收货电话:</span>
                    <span class="value"></span>
                  </div>
                  <div class="footer-item">
                    <span class="label">收货人:</span>
                    <span class="value"></span>
                  </div>
                                     <div class="footer-item address-item">
                     <span class="label">收货地址:</span>
                     <span class="value address-value"></span>
                   </div>
                </div>
                <div class="footer-row">
                                     <div class="footer-item">
                     <span class="label">操作员:</span>
                     <span class="value">{{ userStore.nickname || '撕开前' }}</span>
                   </div>
                  <div class="footer-item">
                    <span class="label">打印日期:</span>
                    <span class="value">{{ formatDateTime(new Date()) }}</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
  </div>
    <div class="app-container">
        <div class="search_form">
            <div>
                <span class="search_title">供应商名称:</span>
                <el-input
                    v-model="searchForm.supplierName"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search"
                />
                <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
                >搜索</el-button
                >
            </div>
            <div>
                <!-- <el-button type="primary" @click="openForm('add')">新增</el-button> -->
                <el-button @click="handleOut">导出</el-button>
                <el-button type="danger" plain @click="handleDelete">删除</el-button>
                <el-button type="primary" plain @click="handlePrint">打印</el-button>
            </div>
        </div>
        <div class="table_list">
            <el-table
                :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                :expand-row-keys="expandedRowKeys"
                :row-key="(row) => row.id"
                show-summary
                style="width: 100%"
                :summary-method="summarizeMainTable"
                height="calc(100vh - 18.5em)"
            >
                <el-table-column align="center" type="selection" width="55" />
                <el-table-column align="center" label="序号" type="index" width="60" />
                <el-table-column
                    label="出库日期"
                    prop="createTime"
                    min-width="250"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="供应商名称"
                    prop="supplierName"
                    width="250"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="产品大类"
                    prop="productCategory"
                    width="100"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="规格型号"
                    prop="specificationModel"
                    width="100"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="单位"
                    prop="unit"
                    width="80"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="出库数量"
                    prop="inboundNum"
                    width="100"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="含税单价(元)"
                    prop="taxInclusiveUnitPrice"
                    width="100"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="含税总价(元)"
                    prop="taxInclusiveTotalPrice"
                    width="100"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="税率(%)"
                    prop="taxRate"
                    width="100"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="不含税总价(元)"
                    prop="taxExclusiveTotalPrice"
                    width="180"
                    show-overflow-tooltip
                />
                <el-table-column
                    label="出库人"
                    prop="createBy"
                    width="80"
                    show-overflow-tooltip
                />
                <!-- <el-table-column
                    fixed="right"
                    label="操作"
                    min-width="60"
                    align="center"
                >
                    <template #default="scope">
                        <el-button
                            link
                            type="primary"
                            size="small"
                            @click="openForm('edit', scope.row)"
                            >编辑</el-button
                        >
                    </template>
                </el-table-column> -->
            </el-table>
            <pagination
                v-show="total > 0"
                :total="total"
                layout="total, sizes, prev, pager, next, jumper"
                :page="page.current"
                :limit="page.size"
                @pagination="paginationChange"
            />
        </div>
        <!-- æ‰“印预览弹窗 -->
        <el-dialog
            v-model="printPreviewVisible"
            title="打印预览"
            width="90%"
            :close-on-click-modal="false"
            class="print-preview-dialog"
        >
            <div class="print-preview-container">
                <div class="print-preview-header">
                    <el-button type="primary" @click="executePrint">执行打印</el-button>
                    <el-button @click="printPreviewVisible = false">关闭预览</el-button>
                </div>
                <div class="print-preview-content">
                    <div v-if="printData.length === 0" style="text-align: center; padding: 50px; color: #999;">
                        æš‚无打印数据
                    </div>
                    <div v-else style="text-align: center; padding: 10px; color: #666; font-size: 14px; background: #e8f4fd; margin-bottom: 10px;">
                        å…± {{ printData.length }} æ¡æ•°æ®å¾…打印
                    </div>
                    <div v-for="(item, index) in printData" :key="index" class="print-page">
                        <div class="delivery-note">
                            <div class="header">
                                <div class="company-name">鼎诚瑞实业有限责任公司</div>
                                <div class="document-title">零售发货单</div>
                            </div>
                            <div class="info-section">
                                <div class="info-row">
                                    <div>
                                        <span class="label">发货日期:</span>
                                        <span class="value">{{ formatDate(item.createTime) }}</span>
                                    </div>
                                    <div>
                                        <span class="label">客户名称:</span>
                                        <span class="value">{{ item.supplierName || '张爱有' }}</span>
                                    </div>
                                </div>
                                <div class="info-row">
                                    <span class="label">单号:</span>
                                    <span class="value">{{ item.code }}</span>
                                </div>
                            </div>
                            <div class="table-section">
                                <table class="product-table">
                                    <thead>
                                    <tr>
                                        <th>产品名称</th>
                                        <th>规格型号</th>
                                        <th>单位</th>
                                        <th>单价</th>
                                        <th>零售数量</th>
                                        <th>零售金额</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    <tr>
                                        <td>{{ item.productCategory || '砂灰砖' }}</td>
                                        <td>{{ item.specificationModel || '标准' }}</td>
                                        <td>{{ item.unit || '块' }}</td>
                                        <td>{{ item.taxInclusiveUnitPrice || '0' }}</td>
                                        <td>{{ item.inboundNum || '2000' }}</td>
                                        <td>{{ item.taxInclusiveTotalPrice || '0' }}</td>
                                    </tr>
                                    </tbody>
                                    <tfoot>
                                    <tr>
                                        <td class="label">合计</td>
                                        <td class="total-value"></td>
                                        <td class="total-value"></td>
                                        <td class="total-value"></td>
                                        <td class="total-value">{{ item.inboundNum || '2000' }}</td>
                                        <td class="total-value">{{ item.taxInclusiveTotalPrice || '0' }}</td>
                                    </tr>
                                    </tfoot>
                                </table>
                            </div>
                            <div class="footer-section">
                                <div class="footer-row">
                                    <div class="footer-item">
                                        <span class="label">收货电话:</span>
                                        <span class="value"></span>
                                    </div>
                                    <div class="footer-item">
                                        <span class="label">收货人:</span>
                                        <span class="value"></span>
                                    </div>
                                    <div class="footer-item address-item">
                                        <span class="label">收货地址:</span>
                                        <span class="value address-value"></span>
                                    </div>
                                </div>
                                <div class="footer-row">
                                    <div class="footer-item">
                                        <span class="label">操作员:</span>
                                        <span class="value">{{ userStore.nickName || '撕开前' }}</span>
                                    </div>
                                    <div class="footer-item">
                                        <span class="label">打印日期:</span>
                                        <span class="value">{{ formatDateTime(new Date()) }}</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </el-dialog>
    </div>
</template>
<script setup>
@@ -251,8 +252,8 @@
import { ElMessageBox } from "element-plus";
import useUserStore from "@/store/modules/user";
import {
  getStockOutPage,
  delStockOut,
    getStockOutPage,
    delStockOut,
} from "@/api/inventoryManagement/stockOut.js";
const userStore = useUserStore();
@@ -261,8 +262,8 @@
const selectedRows = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
    current: 1,
    size: 100,
});
const total = ref(0);
@@ -272,136 +273,136 @@
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const data = reactive({
  searchForm: {
    supplierName: "",
  },
  form: {
    supplierId: null,
    supplierName: '',
    productId: null,
    productName: '',
    userId: userStore.userId,
    nickname: '',
    model: '',
    productModelId: null,
    unit: '',
    productrecordId: null,
    taxInclusiveUnitPrice: '',
    taxInclusiveTotalPrice: '',
    taxRate: '',
    taxExclusiveTotalPrice: '',
    inboundTime: '',
    inboundBatch: '',
    inboundQuantity: ''
  },
    searchForm: {
        supplierName: "",
    },
    form: {
        supplierId: null,
        supplierName: '',
        productId: null,
        productName: '',
        userId: userStore.userId,
        nickName: '',
        model: '',
        productModelId: null,
        unit: '',
        productrecordId: null,
        taxInclusiveUnitPrice: '',
        taxInclusiveTotalPrice: '',
        taxRate: '',
        taxExclusiveTotalPrice: '',
        inboundTime: '',
        inboundBatch: '',
        inboundQuantity: ''
    },
});
const { searchForm } = toRefs(data);
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
    page.current = 1;
    getList();
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
  tableLoading.value = true;
  getStockOutPage({ ...searchForm.value, ...page })
    .then((res) => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      tableData.value.map((item) => {
        item.children = [];
      });
      total.value = res.data.total;
    })
    .catch(() => {
      tableLoading.value = false;
    });
    tableLoading.value = true;
    getStockOutPage({ ...searchForm.value, ...page })
        .then((res) => {
            tableLoading.value = false;
            tableData.value = res.data.records;
            tableData.value.map((item) => {
                item.children = [];
            });
            total.value = res.data.total;
        })
        .catch(() => {
            tableLoading.value = false;
        });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
  selectedRows.value = selection.filter((item) => item.id);
  console.log("selection", selectedRows.value);
    // è¿‡æ»¤æŽ‰å­æ•°æ®
    selectedRows.value = selection.filter((item) => item.id);
    console.log("selection", selectedRows.value);
};
const expandedRowKeys = ref([]);
// ä¸»è¡¨åˆè®¡æ–¹æ³•
const summarizeMainTable = (param) => {
  return proxy.summarizeTable(param, [
    "contractAmount",
    "taxInclusiveTotalPrice",
    "taxExclusiveTotalPrice",
  ]);
    return proxy.summarizeTable(param, [
        "contractAmount",
        "taxInclusiveTotalPrice",
        "taxExclusiveTotalPrice",
    ]);
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/stockmanagement/export", {}, "出库台账.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
    ElMessageBox.confirm("是否确认导出?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            proxy.download("/stockmanagement/export", {}, "出库台账.xlsx");
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      delStockOut({ids:ids}).then((res) => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
    let ids = [];
    if (selectedRows.value.length > 0) {
        ids = selectedRows.value.map((item) => item.id);
    } else {
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            delStockOut({ids:ids}).then((res) => {
                proxy.$modal.msgSuccess("删除成功");
                getList();
            });
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
// æ‰“印功能
const handlePrint = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择要打印的数据");
    return;
  }
  printData.value = [...selectedRows.value];
  console.log('打印数据:', printData.value);
  printPreviewVisible.value = true;
    if (selectedRows.value.length === 0) {
        proxy.$modal.msgWarning("请选择要打印的数据");
        return;
    }
    printData.value = [...selectedRows.value];
    console.log('打印数据:', printData.value);
    printPreviewVisible.value = true;
};
// æ‰§è¡Œæ‰“印
const executePrint = () => {
  console.log('开始执行打印,数据条数:', printData.value.length);
  console.log('打印数据:', printData.value);
  // åˆ›å»ºä¸€ä¸ªæ–°çš„æ‰“印窗口
  const printWindow = window.open('', '_blank', 'width=800,height=600');
  // æž„建打印内容
  let printContent = `
    console.log('开始执行打印,数据条数:', printData.value.length);
    console.log('打印数据:', printData.value);
    // åˆ›å»ºä¸€ä¸ªæ–°çš„æ‰“印窗口
    const printWindow = window.open('', '_blank', 'width=800,height=600');
    // æž„建打印内容
    let printContent = `
    <!DOCTYPE html>
    <html>
    <head>
@@ -535,10 +536,10 @@
    </head>
    <body>
  `;
  // ä¸ºæ¯æ¡æ•°æ®ç”Ÿæˆæ‰“印页面
  printData.value.forEach((item, index) => {
    printContent += `
    // ä¸ºæ¯æ¡æ•°æ®ç”Ÿæˆæ‰“印页面
    printData.value.forEach((item, index) => {
        printContent += `
      <div class="print-page">
        <div class="delivery-note">
          <div class="header">
@@ -616,7 +617,7 @@
            <div class="footer-row">
              <div class="footer-item">
                <span class="label">操作员:</span>
                <span class="value">${userStore.nickname || '撕开前'}</span>
                <span class="value">${userStore.nickName || '撕开前'}</span>
              </div>
              <div class="footer-item">
                <span class="label">打印日期:</span>
@@ -627,227 +628,227 @@
        </div>
      </div>
    `;
  });
  printContent += `
    });
    printContent += `
    </body>
    </html>
  `;
  // å†™å…¥å†…容到新窗口
  printWindow.document.write(printContent);
  printWindow.document.close();
  // ç­‰å¾…内容加载完成后打印
  printWindow.onload = () => {
    setTimeout(() => {
      printWindow.print();
      printWindow.close();
      printPreviewVisible.value = false;
    }, 500);
  };
    // å†™å…¥å†…容到新窗口
    printWindow.document.write(printContent);
    printWindow.document.close();
    // ç­‰å¾…内容加载完成后打印
    printWindow.onload = () => {
        setTimeout(() => {
            printWindow.print();
            printWindow.close();
            printPreviewVisible.value = false;
        }, 500);
    };
};
// æ ¼å¼åŒ–日期
const formatDate = (dateString) => {
  if (!dateString) return getCurrentDate();
  const date = new Date(dateString);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
    if (!dateString) return getCurrentDate();
    const date = new Date(dateString);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return `${year}/${month}/${day}`;
};
// æ ¼å¼åŒ–日期时间
const formatDateTime = (date) => {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  const hours = String(date.getHours()).padStart(2, "0");
  const minutes = String(date.getMinutes()).padStart(2, "0");
  const seconds = String(date.getSeconds()).padStart(2, "0");
  return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    const hours = String(date.getHours()).padStart(2, "0");
    const minutes = String(date.getMinutes()).padStart(2, "0");
    const seconds = String(date.getSeconds()).padStart(2, "0");
    return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
};
// èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
function getCurrentDate() {
  const today = new Date();
  const year = today.getFullYear();
  const month = String(today.getMonth() + 1).padStart(2, "0"); // æœˆä»½ä»Ž0开始
  const day = String(today.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, "0"); // æœˆä»½ä»Ž0开始
    const day = String(today.getDate()).padStart(2, "0");
    return `${year}-${month}-${day}`;
}
onMounted(() => {
  getList();
    getList();
});
</script>
<style scoped lang="scss">
.print-preview-dialog {
  .el-dialog__body {
    padding: 0;
    max-height: 80vh;
    overflow-y: auto;
  }
    .el-dialog__body {
        padding: 0;
        max-height: 80vh;
        overflow-y: auto;
    }
}
.print-preview-container {
  .print-preview-header {
    padding: 15px;
    border-bottom: 1px solid #e4e7ed;
    text-align: center;
    .el-button {
      margin: 0 10px;
    }
  }
  .print-preview-content {
  padding: 20px;
  background-color: #f5f5f5;
  min-height: 400px;
}
    .print-preview-header {
        padding: 15px;
        border-bottom: 1px solid #e4e7ed;
        text-align: center;
        .el-button {
            margin: 0 10px;
        }
    }
    .print-preview-content {
        padding: 20px;
        background-color: #f5f5f5;
        min-height: 400px;
    }
}
.print-page {
  width: 220mm;
  height: 90mm;
  padding: 10mm;
  margin: 0 auto;
  background: white;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  margin-bottom: 10px;
  box-sizing: border-box;
    width: 220mm;
    height: 90mm;
    padding: 10mm;
    margin: 0 auto;
    background: white;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    margin-bottom: 10px;
    box-sizing: border-box;
}
.delivery-note {
  width: 100%;
  height: 100%;
  font-family: "SimSun", serif;
  font-size: 10px;
  line-height: 1.2;
  display: flex;
  flex-direction: column;
    width: 100%;
    height: 100%;
    font-family: "SimSun", serif;
    font-size: 10px;
    line-height: 1.2;
    display: flex;
    flex-direction: column;
}
.header {
  text-align: center;
  margin-bottom: 8px;
  .company-name {
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 4px;
  }
  .document-title {
    font-size: 16px;
    font-weight: bold;
  }
    text-align: center;
    margin-bottom: 8px;
    .company-name {
        font-size: 18px;
        font-weight: bold;
        margin-bottom: 4px;
    }
    .document-title {
        font-size: 16px;
        font-weight: bold;
    }
}
.info-section {
  margin-bottom: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  .info-row {
    line-height: 20px;
    .label {
      font-weight: bold;
      width: 60px;
      font-size: 14px;
    }
    .value {
      margin-right: 20px;
      min-width: 80px;
      font-size: 14px;
    }
  }
    margin-bottom: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .info-row {
        line-height: 20px;
        .label {
            font-weight: bold;
            width: 60px;
            font-size: 14px;
        }
        .value {
            margin-right: 20px;
            min-width: 80px;
            font-size: 14px;
        }
    }
}
.table-section {
  margin-bottom: 4px;
  flex: 1;
  .product-table {
    width: 100%;
    border-collapse: collapse;
    border: 1px solid #000;
         th, td {
       border: 1px solid #000;
       padding: 6px;
       text-align: center;
       font-size: 14px;
       line-height: 1.4;
     }
    th {
      font-weight: bold;
    }
    .total-label {
      text-align: right;
      font-weight: bold;
    }
    .total-value {
      font-weight: bold;
    }
  }
    margin-bottom: 4px;
    flex: 1;
    .product-table {
        width: 100%;
        border-collapse: collapse;
        border: 1px solid #000;
        th, td {
            border: 1px solid #000;
            padding: 6px;
            text-align: center;
            font-size: 14px;
            line-height: 1.4;
        }
        th {
            font-weight: bold;
        }
        .total-label {
            text-align: right;
            font-weight: bold;
        }
        .total-value {
            font-weight: bold;
        }
    }
}
.footer-section {
  .footer-row {
    display: flex;
    margin-bottom: 3px;
    line-height: 20px;
    justify-content: space-between;
         .footer-item {
       display: flex;
       margin-right: 20px;
       .label {
         font-weight: bold;
         width: 80px;
         font-size: 14px;
       }
       .value {
         min-width: 80px;
         font-size: 14px;
       }
       &.address-item {
         .address-value {
           min-width: 200px;
         }
       }
     }
  }
    .footer-row {
        display: flex;
        margin-bottom: 3px;
        line-height: 20px;
        justify-content: space-between;
        .footer-item {
            display: flex;
            margin-right: 20px;
            .label {
                font-weight: bold;
                width: 80px;
                font-size: 14px;
            }
            .value {
                min-width: 80px;
                font-size: 14px;
            }
            &.address-item {
                .address-value {
                    min-width: 200px;
                }
            }
        }
    }
}
@media print {
  .app-container {
    display: none;
  }
           .print-page {
      box-shadow: none;
      margin: 0;
      padding: 10mm;
      padding-left: 20mm;
      page-break-inside: avoid;
      page-break-after: always;
    }
   .print-page:last-child {
     page-break-after: avoid;
   }
    .app-container {
        display: none;
    }
    .print-page {
        box-shadow: none;
        margin: 0;
        padding: 10mm;
        padding-left: 20mm;
        page-break-inside: avoid;
        page-break-after: always;
    }
    .print-page:last-child {
        page-break-after: avoid;
    }
}
</style>
src/views/procurementManagement/index.vue
@@ -101,6 +101,26 @@
      </el-col>
      <el-col :span="8">
        <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/procurementPlan')">
          <div class="card-content">
            <div class="card-icon">
              <el-icon size="48" color="#9C27B0"><Calendar /></el-icon>
            </div>
            <div class="card-info">
              <h3>采购计划</h3>
              <p>智能采购计划配置,自动计算采购需求,考虑库存和安全库存</p>
              <div class="card-stats">
                <span>活跃计划: {{ stats.activePlans }}</span>
                <span>待计算: {{ stats.pendingCalculations }}</span>
              </div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="20" class="module-cards">
      <el-col :span="8">
        <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/procurementLedger')">
          <div class="card-content">
            <div class="card-icon">
@@ -112,6 +132,24 @@
              <div class="card-stats">
                <span>总订单: {{ stats.totalOrders }}</span>
                <span>总金额: Â¥{{ stats.totalAmount.toFixed(2) }}</span>
              </div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/procurementReport')">
          <div class="card-content">
            <div class="card-icon">
              <el-icon size="48" color="#FF6B6B"><TrendCharts /></el-icon>
            </div>
            <div class="card-info">
              <h3>采购报表</h3>
              <p>采购订单执行汇总、明细分析、业务统计、供应商供货汇总</p>
              <div class="card-stats">
                <span>报表类型: 4种</span>
                <span>数据更新: å®žæ—¶</span>
              </div>
            </div>
          </div>
@@ -179,7 +217,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Document, Box, Search, RefreshLeft, Money, List } from '@element-plus/icons-vue'
import { Document, Box, Search, RefreshLeft, Money, List, Calendar, TrendCharts } from '@element-plus/icons-vue'
const router = useRouter()
@@ -195,6 +233,8 @@
  approvedReturns: 3,
  activePrices: 45,
  pendingPrices: 2,
  activePlans: 8,
  pendingCalculations: 3,
  totalOrders: 30,
  totalAmount: 125.8,
  avgDeliveryTime: 7,
src/views/procurementManagement/procurementReport/index.vue
@@ -112,9 +112,9 @@
          </div>
        </div>
        
        <el-table :data="orderSummaryData" border v-loading="loading" stripe>
        <el-table :data="orderSummaryData" border v-loading="loading" stripe style="width: 100%">
          <el-table-column label="订单编号" prop="orderNo" width="180" fixed="left" />
          <el-table-column label="供应商名称" prop="supplierName" width="150" />
          <el-table-column label="供应商名称" prop="supplierName" min-width="150" />
          <el-table-column label="订单日期" prop="orderDate" width="120" />
          <el-table-column label="计划交期" prop="plannedDelivery" width="120" />
          <el-table-column label="实际交期" prop="actualDelivery" width="120" />
@@ -160,11 +160,11 @@
          </div>
        </div>
        
        <el-table :data="orderDetailData" border v-loading="loading" stripe>
        <el-table :data="orderDetailData" border v-loading="loading" stripe style="width: 100%">
          <el-table-column label="订单编号" prop="orderNo" width="150" fixed="left" />
          <el-table-column label="商品编码" prop="productCode" width="120" />
          <el-table-column label="商品名称" prop="productName" width="200" />
          <el-table-column label="规格型号" prop="specification" width="150" />
          <el-table-column label="商品名称" prop="productName" min-width="200" />
          <el-table-column label="规格型号" prop="specification" min-width="150" />
          <el-table-column label="单位" prop="unit" width="80" />
          <el-table-column label="计划数量" prop="plannedQuantity" width="100" />
          <el-table-column label="已收货数量" prop="receivedQuantity" width="120" />
@@ -204,11 +204,11 @@
          </div>
        </div>
        
        <el-table :data="businessSummaryData" border v-loading="loading" stripe>
        <el-table :data="businessSummaryData" border v-loading="loading" stripe style="width: 100%">
          <el-table-column label="商品类别" prop="category" width="150" fixed="left" />
          <el-table-column label="商品编码" prop="productCode" width="120" />
          <el-table-column label="商品名称" prop="productName" width="200" />
          <el-table-column label="规格型号" prop="specification" width="150" />
          <el-table-column label="商品名称" prop="productName" min-width="200" />
          <el-table-column label="规格型号" prop="specification" min-width="150" />
          <el-table-column label="采购数量" prop="purchaseQuantity" width="120" />
          <el-table-column label="采购金额" prop="purchaseAmount" width="120">
            <template #default="{ row }">Â¥{{ row.purchaseAmount.toLocaleString() }}</template>
@@ -217,7 +217,7 @@
            <template #default="{ row }">Â¥{{ row.avgPrice.toFixed(2) }}</template>
          </el-table-column>
          <el-table-column label="采购次数" prop="purchaseCount" width="100" />
          <el-table-column label="主要供应商" prop="mainSupplier" width="150" />
          <el-table-column label="主要供应商" prop="mainSupplier" min-width="150" />
          <el-table-column label="最后采购日期" prop="lastPurchaseDate" width="120" />
        </el-table>
      </div>
@@ -242,9 +242,9 @@
          </div>
        </div>
        
        <el-table :data="supplierSummaryData" border v-loading="loading" stripe>
        <el-table :data="supplierSummaryData" border v-loading="loading" stripe style="width: 100%">
          <el-table-column label="供应商编码" prop="supplierCode" width="120" fixed="left" />
          <el-table-column label="供应商名称" prop="supplierName" width="200" />
          <el-table-column label="供应商名称" prop="supplierName" min-width="200" />
          <el-table-column label="联系人" prop="contactPerson" width="120" />
          <el-table-column label="联系电话" prop="phone" width="130" />
          <el-table-column label="供货订单数" prop="orderCount" width="120" />
@@ -805,6 +805,15 @@
:deep(.el-table) {
  border-radius: 8px;
  overflow: hidden;
  width: 100% !important;
}
:deep(.el-table__body-wrapper) {
  width: 100% !important;
}
:deep(.el-table__header-wrapper) {
  width: 100% !important;
}
:deep(.el-table th) {
src/views/salesManagement/salesLedger/index.vue
@@ -36,6 +36,7 @@
          </el-button>
          <el-button @click="handleOut">导出</el-button>
          <el-button type="danger" plain @click="handleDelete">删除</el-button>
          <el-button type="primary" plain @click="handlePrint">打印</el-button>
        </div>
      </div>
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
@@ -292,6 +293,120 @@
        </div>
      </template>
    </el-dialog>
        <!-- æ‰“印预览弹窗 -->
        <el-dialog
            v-model="printPreviewVisible"
            title="打印预览"
            width="90%"
            :close-on-click-modal="false"
            class="print-preview-dialog"
        >
            <div class="print-preview-container">
                <div class="print-preview-header">
                    <el-button type="primary" @click="executePrint">执行打印</el-button>
                    <el-button @click="printPreviewVisible = false">关闭预览</el-button>
                </div>
                <div class="print-preview-content">
                    <div v-if="printData.length === 0" style="text-align: center; padding: 50px; color: #999;">
                        æš‚无打印数据
                    </div>
                    <div v-else style="text-align: center; padding: 10px; color: #666; font-size: 14px; background: #e8f4fd; margin-bottom: 10px;">
                        å…± {{ printData.length }} æ¡æ•°æ®å¾…打印
                    </div>
                    <div v-for="(item, index) in printData" :key="index" class="print-page">
                        <div class="delivery-note">
                            <div class="header">
                                <div class="company-name">鼎诚瑞实业有限责任公司</div>
                                <div class="document-title">零售发货单</div>
                            </div>
                            <div class="info-section">
                                <div class="info-row">
                                    <div>
                                        <span class="label">发货日期:</span>
                                        <span class="value">{{ formatDate(item.createTime) }}</span>
                                    </div>
                                    <div>
                                        <span class="label">客户名称:</span>
                                        <span class="value">{{ item.customerName || '张爱有' }}</span>
                                    </div>
                                </div>
                                <div class="info-row">
                                    <span class="label">单号:</span>
                                    <span class="value">{{ item.salesContractNo }}</span>
                                </div>
                            </div>
                            <div class="table-section">
                                <table class="product-table">
                                    <thead>
                                    <tr>
                                        <th>产品名称</th>
                                        <th>规格型号</th>
                                        <th>单位</th>
                                        <th>单价</th>
                                        <th>零售数量</th>
                                        <th>零售金额</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    <tr v-for="product in item.products" :key="product.id">
                                        <td>{{ product.productCategory || '' }}</td>
                                        <td>{{ product.specificationModel || '' }}</td>
                                        <td>{{ product.unit || '' }}</td>
                                        <td>{{ product.taxInclusiveUnitPrice || '0' }}</td>
                                        <td>{{ product.quantity || '0' }}</td>
                                        <td>{{ product.taxInclusiveTotalPrice || '0' }}</td>
                                    </tr>
                                    <tr v-if="!item.products || item.products.length === 0">
                                        <td colspan="6" style="text-align: center; color: #999;">暂无产品数据</td>
                                    </tr>
                                    </tbody>
                                    <tfoot>
                                    <tr>
                                        <td class="label">合计</td>
                                        <td class="total-value"></td>
                                        <td class="total-value"></td>
                                        <td class="total-value"></td>
                                        <td class="total-value">{{ getTotalQuantity(item.products) }}</td>
                                        <td class="total-value">{{ getTotalAmount(item.products) }}</td>
                                    </tr>
                                    </tfoot>
                                </table>
                            </div>
                            <div class="footer-section">
                                <div class="footer-row">
                                    <div class="footer-item">
                                        <span class="label">收货电话:</span>
                                        <span class="value"></span>
                                    </div>
                                    <div class="footer-item">
                                        <span class="label">收货人:</span>
                                        <span class="value"></span>
                                    </div>
                                    <div class="footer-item address-item">
                                        <span class="label">收货地址:</span>
                                        <span class="value address-value"></span>
                                    </div>
                                </div>
                                <div class="footer-row">
                                    <div class="footer-item">
                                        <span class="label">操作员:</span>
                                        <span class="value">{{ userStore.nickName || '撕开前' }}</span>
                                    </div>
                                    <div class="footer-item">
                                        <span class="label">打印日期:</span>
                                        <span class="value">{{ formatDateTime(new Date()) }}</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </el-dialog>
    <FileList ref="fileListRef" />
  </div>
</template>
@@ -426,6 +541,9 @@
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
});
// æ‰“印相关
const printPreviewVisible = ref(false);
const printData = ref([]);
const changeDaterange = (value) => {
  if (value) {
@@ -790,6 +908,333 @@
      proxy.$modal.msg("已取消");
    });
};
// æ‰“印功能
const handlePrint = async () => {
    if (selectedRows.value.length === 0) {
        proxy.$modal.msgWarning("请选择要打印的数据");
        return;
    }
    // æ˜¾ç¤ºåŠ è½½çŠ¶æ€
    proxy.$modal.loading("正在获取产品数据,请稍候...");
    try {
        // ä¸ºæ¯ä¸ªé€‰ä¸­çš„销售台账记录查询对应的产品数据
        const printDataWithProducts = [];
        for (const row of selectedRows.value) {
            try {
                // è°ƒç”¨productList接口查询产品数据
                const productRes = await productList({ salesLedgerId: row.id, type: 1 });
                // å°†äº§å“æ•°æ®æ•´åˆåˆ°é”€å”®å°è´¦è®°å½•中
                const rowWithProducts = {
                    ...row,
                    products: productRes.data || []
                };
                printDataWithProducts.push(rowWithProducts);
            } catch (error) {
                console.error(`获取销售台账 ${row.id} çš„产品数据失败:`, error);
                // å³ä½¿æŸä¸ªè®°å½•的产品数据获取失败,也要包含该记录
                printDataWithProducts.push({
                    ...row,
                    products: []
                });
            }
        }
        printData.value = printDataWithProducts;
        console.log('打印数据(包含产品):', printData.value);
        printPreviewVisible.value = true;
    } catch (error) {
        console.error('获取产品数据失败:', error);
        proxy.$modal.msgError("获取产品数据失败,请重试");
    } finally {
        proxy.$modal.closeLoading();
    }
};
// æ‰§è¡Œæ‰“印
const executePrint = () => {
    console.log('开始执行打印,数据条数:', printData.value.length);
    console.log('打印数据:', printData.value);
    // åˆ›å»ºä¸€ä¸ªæ–°çš„æ‰“印窗口
    const printWindow = window.open('', '_blank', 'width=800,height=600');
    // æž„建打印内容
    let printContent = `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>打印预览</title>
      <style>
        body {
          margin: 0;
          padding: 0;
          font-family: "SimSun", serif;
          background: white;
        }
                                                     .print-page {
            width: 200mm;
            height: 75mm;
            padding: 10mm;
            padding-left: 20mm;
            background: white;
            box-sizing: border-box;
            page-break-after: always;
            page-break-inside: avoid;
          }
         .print-page:last-child {
           page-break-after: avoid;
         }
        .delivery-note {
          width: 100%;
          height: 100%;
          font-size: 12px;
          line-height: 1.2;
          display: flex;
          flex-direction: column;
          color: #000;
        }
        .header {
          text-align: center;
          margin-bottom: 8px;
        }
        .company-name {
          font-size: 18px;
          font-weight: bold;
          margin-bottom: 4px;
        }
        .document-title {
          font-size: 16px;
          font-weight: bold;
        }
        .info-section {
          margin-bottom: 8px;
          display: flex;
          justify-content: space-between;
          align-items: center;
        }
        .info-row {
          line-height: 20px;
        }
        .label {
          font-weight: bold;
          width: 60px;
          font-size: 12px;
        }
        .value {
          margin-right: 20px;
          min-width: 80px;
          font-size: 12px;
        }
                 .table-section {
                 margin-bottom: 40px;
          //  flex: 0.6;
         }
        .product-table {
          width: 100%;
          border-collapse: collapse;
          border: 1px solid #000;
        }
                 .product-table th, .product-table td {
           border: 1px solid #000;
           padding: 6px;
           text-align: center;
           font-size: 12px;
           line-height: 1.4;
         }
        .product-table th {
          font-weight: bold;
        }
        .total-value {
          font-weight: bold;
        }
        .footer-section {
          margin-top: auto;
        }
        .footer-row {
          display: flex;
          margin-bottom: 3px;
          line-height: 22px;
          justify-content: space-between;
        }
        .footer-item {
          display: flex;
          margin-right: 20px;
        }
        .footer-item .label {
          font-weight: bold;
          width: 80px;
          font-size: 12px;
        }
        .footer-item .value {
          min-width: 80px;
          font-size: 12px;
        }
        .address-item .address-value {
          min-width: 200px;
        }
        @media print {
          body {
            margin: 0;
            padding: 0;
          }
                     .print-page {
             margin: 0;
             padding: 10mm;
             /* padding-left: 20mm; */
             page-break-inside: avoid;
             page-break-after: always;
           }
           .print-page:last-child {
             page-break-after: avoid;
           }
        }
      </style>
    </head>
    <body>
  `;
    // ä¸ºæ¯æ¡æ•°æ®ç”Ÿæˆæ‰“印页面
    printData.value.forEach((item, index) => {
        printContent += `
      <div class="print-page">
        <div class="delivery-note">
          <div class="header">
            <div class="company-name">鼎诚瑞实业有限责任公司</div>
            <div class="document-title">零售发货单</div>
          </div>
          <div class="info-section">
            <div class="info-row">
              <div>
                <span class="label">发货日期:</span>
                <span class="value">${formatDate(item.createTime)}</span>
              </div>
              <div>
                <span class="label">客户名称:</span>
                <span class="value">${item.customerName || '张爱有'}</span>
              </div>
            </div>
            <div class="info-row">
              <span class="label">单号:</span>
              <span class="value">${item.salesContractNo || ''}</span>
            </div>
          </div>
          <div class="table-section">
            <table class="product-table">
              <thead>
                <tr>
                  <th>产品名称</th>
                  <th>规格型号</th>
                  <th>单位</th>
                  <th>单价</th>
                  <th>零售数量</th>
                  <th>零售金额</th>
                </tr>
              </thead>
              <tbody>
                ${item.products && item.products.length > 0 ?
                  item.products.map(product => `
                    <tr>
                      <td>${product.productCategory || ''}</td>
                      <td>${product.specificationModel || ''}</td>
                      <td>${product.unit || ''}</td>
                      <td>${product.taxInclusiveUnitPrice || '0'}</td>
                      <td>${product.quantity || '0'}</td>
                      <td>${product.taxInclusiveTotalPrice || '0'}</td>
                    </tr>
                  `).join('') :
                  '<tr><td colspan="6" style="text-align: center; color: #999;">暂无产品数据</td></tr>'
                }
              </tbody>
              <tfoot>
                <tr>
                  <td class="label">合计</td>
                  <td class="total-value"></td>
                  <td class="total-value"></td>
                  <td class="total-value"></td>
                  <td class="total-value">${getTotalQuantityForPrint(item.products)}</td>
                  <td class="total-value">${getTotalAmountForPrint(item.products)}</td>
                </tr>
              </tfoot>
            </table>
          </div>
          <div class="footer-section">
            <div class="footer-row">
              <div class="footer-item">
                <span class="label">收货电话:</span>
                <span class="value"></span>
              </div>
              <div class="footer-item">
                <span class="label">收货人:</span>
                <span class="value"></span>
              </div>
              <div class="footer-item address-item">
                <span class="label">收货地址:</span>
                <span class="value address-value"></span>
              </div>
            </div>
            <div class="footer-row">
              <div class="footer-item">
                <span class="label">操作员:</span>
                <span class="value">${userStore.nickName || '撕开前'}</span>
              </div>
              <div class="footer-item">
                <span class="label">打印日期:</span>
                <span class="value">${formatDateTime(new Date())}</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    `;
    });
    printContent += `
    </body>
    </html>
  `;
    // å†™å…¥å†…容到新窗口
    printWindow.document.write(printContent);
    printWindow.document.close();
    // ç­‰å¾…内容加载完成后打印
    printWindow.onload = () => {
        setTimeout(() => {
            printWindow.print();
            printWindow.close();
            printPreviewVisible.value = false;
        }, 500);
    };
};
// æ ¼å¼åŒ–日期
const formatDate = (dateString) => {
    if (!dateString) return getCurrentDate();
    const date = new Date(dateString);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return `${year}/${month}/${day}`;
};
// æ ¼å¼åŒ–日期时间
const formatDateTime = (date) => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    const hours = String(date.getHours()).padStart(2, "0");
    const minutes = String(date.getMinutes()).padStart(2, "0");
    const seconds = String(date.getSeconds()).padStart(2, "0");
    return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
};
// èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
function getCurrentDate() {
  const today = new Date();
@@ -798,6 +1243,41 @@
  const day = String(today.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}
// è®¡ç®—产品总数量
const getTotalQuantity = (products) => {
  if (!products || products.length === 0) return '0';
  const total = products.reduce((sum, product) => {
    return sum + (parseFloat(product.quantity) || 0);
  }, 0);
  return total.toFixed(2);
};
// è®¡ç®—产品总金额
const getTotalAmount = (products) => {
  if (!products || products.length === 0) return '0';
  const total = products.reduce((sum, product) => {
    return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0);
  }, 0);
  return total.toFixed(2);
};
// ç”¨äºŽæ‰“印的计算函数
const getTotalQuantityForPrint = (products) => {
  if (!products || products.length === 0) return '0';
  const total = products.reduce((sum, product) => {
    return sum + (parseFloat(product.quantity) || 0);
  }, 0);
  return total.toFixed(2);
};
const getTotalAmountForPrint = (products) => {
  if (!products || products.length === 0) return '0';
  const total = products.reduce((sum, product) => {
    return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0);
  }, 0);
  return total.toFixed(2);
};
const mathNum = () => {
  console.log("productForm.value", productForm.value);
@@ -1001,4 +1481,170 @@
  justify-content: space-between;
  margin-bottom: 10px;
}
.print-preview-dialog {
    .el-dialog__body {
        padding: 0;
        max-height: 80vh;
        overflow-y: auto;
    }
}
.print-preview-container {
    .print-preview-header {
        padding: 15px;
        border-bottom: 1px solid #e4e7ed;
        text-align: center;
        .el-button {
            margin: 0 10px;
        }
    }
    .print-preview-content {
        padding: 20px;
        background-color: #f5f5f5;
        min-height: 400px;
    }
}
.print-page {
    width: 220mm;
    height: 90mm;
    padding: 10mm;
    margin: 0 auto;
    background: white;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    margin-bottom: 10px;
    box-sizing: border-box;
}
.delivery-note {
    width: 100%;
    height: 100%;
    font-family: "SimSun", serif;
    font-size: 10px;
    line-height: 1.2;
    display: flex;
    flex-direction: column;
}
.header {
    text-align: center;
    margin-bottom: 8px;
    .company-name {
        font-size: 18px;
        font-weight: bold;
        margin-bottom: 4px;
    }
    .document-title {
        font-size: 16px;
        font-weight: bold;
    }
}
.info-section {
    margin-bottom: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .info-row {
        line-height: 20px;
        .label {
            font-weight: bold;
            width: 60px;
            font-size: 14px;
        }
        .value {
            margin-right: 20px;
            min-width: 80px;
            font-size: 14px;
        }
    }
}
.table-section {
    margin-bottom: 4px;
    flex: 1;
    .product-table {
        width: 100%;
        border-collapse: collapse;
        border: 1px solid #000;
        th, td {
            border: 1px solid #000;
            padding: 6px;
            text-align: center;
            font-size: 14px;
            line-height: 1.4;
        }
        th {
            font-weight: bold;
        }
        .total-label {
            text-align: right;
            font-weight: bold;
        }
        .total-value {
            font-weight: bold;
        }
    }
}
.footer-section {
    .footer-row {
        display: flex;
        margin-bottom: 3px;
        line-height: 20px;
        justify-content: space-between;
        .footer-item {
            display: flex;
            margin-right: 20px;
            .label {
                font-weight: bold;
                width: 80px;
                font-size: 14px;
            }
            .value {
                min-width: 80px;
                font-size: 14px;
            }
            &.address-item {
                .address-value {
                    min-width: 200px;
                }
            }
        }
    }
}
@media print {
    .app-container {
        display: none;
    }
    .print-page {
        box-shadow: none;
        margin: 0;
        padding: 10mm;
        padding-left: 20mm;
        page-break-inside: avoid;
        page-break-after: always;
    }
    .print-page:last-child {
        page-break-after: avoid;
    }
}
</style>
src/views/salesManagement/salesQuotation/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,604 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <!-- æœç´¢åŒºåŸŸ -->
      <el-row :gutter="20" class="search-row">
        <el-col :span="6">
          <el-input
            v-model="searchForm.quotationNo"
            placeholder="请输入报价单号"
            clearable
            @keyup.enter="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.customer" placeholder="请选择客户" clearable>
            <el-option label="上海科技有限公司" value="上海科技有限公司"></el-option>
            <el-option label="深圳电子有限公司" value="深圳电子有限公司"></el-option>
            <el-option label="北京贸易公司" value="北京贸易公司"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-select v-model="searchForm.status" placeholder="请选择报价状态" clearable>
            <el-option label="草稿" value="草稿"></el-option>
            <el-option label="已发送" value="已发送"></el-option>
            <el-option label="客户确认" value="客户确认"></el-option>
            <el-option label="已过期" value="已过期"></el-option>
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;" type="primary" @click="handleAdd">
            æ–°å¢žæŠ¥ä»·
          </el-button>
        </el-col>
      </el-row>
      <!-- æŠ¥ä»·åˆ—表 -->
      <el-table
        :data="filteredList"
        style="width: 100%"
        v-loading="loading"
        border
        stripe
        height="calc(100vh - 22em)"
      >
        <el-table-column prop="id" label="ID" width="80" align="center"/>
        <el-table-column prop="quotationNo" label="报价单号" width="150" />
        <el-table-column prop="customer" label="客户名称" />
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
        <el-table-column prop="validDate" label="有效期至" width="120" />
        <el-table-column prop="totalAmount" label="报价金额" width="120">
          <template #default="scope">
            Â¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column prop="status" label="报价状态" width="100">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="250" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === '草稿'">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.status === '草稿'">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        :total="pagination.total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="pagination.currentPage"
        :limit="pagination.pageSize"
        @pagination="handleCurrentChange"
      />
    </el-card>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="900px" :close-on-click-modal="false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <!-- åŸºæœ¬ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <span class="card-title">基本信息</span>
          </template>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="客户名称" prop="customer">
                <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange">
                  <el-option label="上海科技有限公司" value="上海科技有限公司"></el-option>
                  <el-option label="深圳电子有限公司" value="深圳电子有限公司"></el-option>
                  <el-option label="北京贸易公司" value="北京贸易公司"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="业务员" prop="salesperson">
                <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%">
                  <el-option label="陈志强" value="陈志强"></el-option>
                  <el-option label="刘雅婷" value="刘雅婷"></el-option>
                  <el-option label="王建国" value="王建国"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="报价日期" prop="quotationDate">
                <el-date-picker
                  v-model="form.quotationDate"
                  type="date"
                  placeholder="选择报价日期"
                  style="width: 100%"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="有效期至" prop="validDate">
                <el-date-picker
                  v-model="form.validDate"
                  type="date"
                  placeholder="选择有效期"
                  style="width: 100%"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="付款方式" prop="paymentMethod">
                <el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%">
                  <el-option label="全款到付" value="全款到付"></el-option>
                  <el-option label="分期付款" value="分期付款"></el-option>
                  <el-option label="月结" value="月结"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="交货期" prop="deliveryPeriod">
                <el-input v-model="form.deliveryPeriod" placeholder="请输入交货期" />
              </el-form-item>
            </el-col>
          </el-row>
        </el-card>
        <!-- äº§å“ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <div class="card-header">
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct">添加产品</el-button>
            </div>
          </template>
          <el-table :data="form.products" border style="width: 100%">
            <el-table-column prop="productName" label="产品名称" width="200">
              <template #default="scope">
                <el-input v-model="scope.row.productName" placeholder="请输入产品名称" />
              </template>
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="150">
              <template #default="scope">
                <el-input v-model="scope.row.specification" placeholder="规格型号" />
              </template>
            </el-table-column>
            <el-table-column prop="quantity" label="数量" width="100">
              <template #default="scope">
                <el-input-number v-model="scope.row.quantity" :min="1" :precision="0" style="width: 100%" />
              </template>
            </el-table-column>
            <el-table-column prop="unit" label="单位" width="80">
              <template #default="scope">
                <el-input v-model="scope.row.unit" placeholder="单位" />
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价" width="120">
              <template #default="scope">
                <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" @change="calculateAmount(scope.row)" />
              </template>
            </el-table-column>
            <el-table-column prop="amount" label="金额" width="120">
              <template #default="scope">
                <span>Â¥{{ scope.row.amount.toFixed(2) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80" align="center">
              <template #default="scope">
                <el-button link type="danger" @click="removeProduct(scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
        <!-- è´¹ç”¨ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <span class="card-title">费用信息</span>
          </template>
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="产品小计">
                <el-input-number v-model="form.subtotal" :precision="2" :min="0" style="width: 100%" readonly />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="运费">
                <el-input-number v-model="form.freight" :precision="2" :min="0" style="width: 100%" @change="calculateTotal" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="其他费用">
                <el-input-number v-model="form.otherFee" :precision="2" :min="0" style="width: 100%" @change="calculateTotal" />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="8">
              <el-form-item label="折扣率(%)">
                <el-input-number v-model="form.discountRate" :precision="2" :min="0" :max="100" style="width: 100%" @change="calculateTotal" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="折扣金额">
                <el-input-number v-model="form.discountAmount" :precision="2" :min="0" style="width: 100%" readonly />
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="报价总额">
                <el-input-number v-model="form.totalAmount" :precision="2" :min="0" style="width: 100%" readonly />
              </el-form-item>
            </el-col>
          </el-row>
        </el-card>
        <!-- å¤‡æ³¨ä¿¡æ¯ -->
        <el-card class="form-card" shadow="never">
          <template #header>
            <span class="card-title">备注信息</span>
          </template>
          <el-form-item label="备注" prop="remark">
            <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息" rows="3"></el-input>
          </el-form-item>
        </el-card>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æŸ¥çœ‹è¯¦æƒ…对话框 -->
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
        <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
        <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
        <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
        <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
        <el-descriptions-item label="交货期">{{ currentQuotation.deliveryPeriod }}</el-descriptions-item>
        <el-descriptions-item label="报价状态">
          <el-tag :type="getStatusType(currentQuotation.status)">{{ currentQuotation.status }}</el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="报价总额" :span="2">
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div style="margin-top: 20px;">
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
          <el-table-column prop="productName" label="产品名称" />
          <el-table-column prop="specification" label="规格型号" />
          <el-table-column prop="quantity" label="数量" />
          <el-table-column prop="unit" label="单位" />
          <el-table-column prop="unitPrice" label="单价">
            <template #default="scope">
              Â¥{{ scope.row.unitPrice.toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="amount" label="金额">
            <template #default="scope">
              Â¥{{ scope.row.amount.toFixed(2) }}
            </template>
          </el-table-column>
        </el-table>
      </div>
      <div v-if="currentQuotation.remark" style="margin-top: 20px;">
        <h4>备注</h4>
        <p>{{ currentQuotation.remark }}</p>
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
// å“åº”式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customer: '',
  status: ''
})
const quotationList = ref([
  {
    id: 1,
    quotationNo: 'QT202312001',
    customer: '上海科技有限公司',
    salesperson: '陈志强',
    quotationDate: '2023-12-01',
    validDate: '2023-12-31',
    totalAmount: 50000.00,
    paymentMethod: '全款到付',
    deliveryPeriod: '30天',
    status: '已发送',
    remark: '重要客户报价',
    products: [
      { productName: '工业传感器', specification: 'SEN-001', quantity: 10, unit: '个', unitPrice: 5000, amount: 50000 }
    ]
  },
  {
    id: 2,
    quotationNo: 'QT202312002',
    customer: '深圳电子有限公司',
    salesperson: '刘雅婷',
    quotationDate: '2023-12-02',
    validDate: '2023-12-31',
    totalAmount: 35000.00,
    paymentMethod: '分期付款',
    deliveryPeriod: '20天',
    status: '客户确认',
    remark: '常规报价',
    products: [
      { productName: '控制模块', specification: 'CTL-002', quantity: 5, unit: '个', unitPrice: 7000, amount: 35000 }
    ]
  },
  {
    id: 3,
    quotationNo: 'QT202312003',
    customer: '北京贸易公司',
    salesperson: '王建国',
    quotationDate: '2023-12-03',
    validDate: '2023-12-31',
    totalAmount: 28000.00,
    paymentMethod: '月结',
    deliveryPeriod: '15天',
    status: '草稿',
    remark: '新客户报价',
    products: [
      { productName: '数据采集器', specification: 'DAQ-003', quantity: 4, unit: '个', unitPrice: 7000, amount: 28000 }
    ]
  }
])
const pagination = reactive({
  total: 3,
  currentPage: 1,
  pageSize: 10
})
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('新增报价')
const form = reactive({
  customer: '',
  salesperson: '',
  quotationDate: '',
  validDate: '',
  paymentMethod: '',
  deliveryPeriod: '',
  status: '草稿',
  remark: '',
  products: [],
  subtotal: 0,
  freight: 0,
  otherFee: 0,
  discountRate: 0,
  discountAmount: 0,
  totalAmount: 0
})
const rules = {
  customer: [{ required: true, message: '请选择客户', trigger: 'change' }],
  salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }],
  quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
  validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }],
  paymentMethod: [{ required: true, message: '请选择付款方式', trigger: 'change' }],
  deliveryPeriod: [{ required: true, message: '请输入交货期', trigger: 'blur' }]
}
const isEdit = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// è®¡ç®—属性
const filteredList = computed(() => {
  let list = quotationList.value
  if (searchForm.quotationNo) {
    list = list.filter(item => item.quotationNo.includes(searchForm.quotationNo))
  }
  if (searchForm.customer) {
    list = list.filter(item => item.customer === searchForm.customer)
  }
  if (searchForm.status) {
    list = list.filter(item => item.status === searchForm.status)
  }
  return list
})
// æ–¹æ³•
const getStatusType = (status) => {
  const statusMap = {
    '草稿': 'info',
    '已发送': 'primary',
    '客户确认': 'success',
    '已过期': 'danger'
  }
  return statusMap[status] || 'info'
}
const handleSearch = () => {
  // æœç´¢é€»è¾‘已在computed中处理
}
const resetSearch = () => {
  searchForm.quotationNo = ''
  searchForm.customer = ''
  searchForm.status = ''
}
const handleAdd = () => {
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  dialogVisible.value = true
}
const handleView = (row) => {
  currentQuotation.value = row
  viewDialogVisible.value = true
}
const handleEdit = (row) => {
  dialogTitle.value = '编辑报价'
  isEdit.value = true
  editId.value = row.id
  Object.assign(form, row)
  dialogVisible.value = true
}
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该报价单吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const index = quotationList.value.findIndex(item => item.id === row.id)
    if (index > -1) {
      quotationList.value.splice(index, 1)
      pagination.total--
      ElMessage.success('删除成功')
    }
  })
}
const resetForm = () => {
  form.customer = ''
  form.salesperson = ''
  form.quotationDate = ''
  form.validDate = ''
  form.paymentMethod = ''
  form.deliveryPeriod = ''
  form.status = '草稿'
  form.remark = ''
  form.products = []
  form.subtotal = 0
  form.freight = 0
  form.otherFee = 0
  form.discountRate = 0
  form.discountAmount = 0
  form.totalAmount = 0
}
const addProduct = () => {
  form.products.push({
    productName: '',
    specification: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
    amount: 0
  })
}
const removeProduct = (index) => {
  form.products.splice(index, 1)
  calculateSubtotal()
}
const calculateAmount = (product) => {
  product.amount = product.quantity * product.unitPrice
  calculateSubtotal()
}
const calculateSubtotal = () => {
  form.subtotal = form.products.reduce((sum, product) => sum + product.amount, 0)
  calculateTotal()
}
const calculateTotal = () => {
  form.discountAmount = form.subtotal * (form.discountRate / 100)
  form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount
}
const handleCustomerChange = () => {
  // å¯ä»¥æ ¹æ®å®¢æˆ·ä¿¡æ¯è‡ªåŠ¨å¡«å……ä¸€äº›é»˜è®¤å€¼
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (form.products.length === 0) {
        ElMessage.warning('请至少添加一个产品')
        return
      }
      if (isEdit.value) {
        // ç¼–辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
        if (index > -1) {
          quotationList.value[index] = { ...form, id: editId.value }
          ElMessage.success('编辑成功')
        }
      } else {
        // æ–°å¢ž
        const newId = Math.max(...quotationList.value.map(item => item.id)) + 1
        const quotationNo = `QT${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(newId).padStart(3, '0')}`
        quotationList.value.push({
          ...form,
          id: newId,
          quotationNo: quotationNo
        })
        pagination.total++
        ElMessage.success('新增成功')
      }
      dialogVisible.value = false
    }
  })
}
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
}
</script>
<style scoped>
.search-row {
  margin-bottom: 20px;
}
.form-card {
  margin-bottom: 20px;
}
.card-title {
  font-weight: bold;
  color: #303133;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.dialog-footer {
  text-align: right;
}
</style>