spring
9 天以前 9fe3ccfea5f06dd2474480aef7058a8ca907cb29
fix: 新增协同办公、薪资功能。完成库存预警页面编写
已添加21个文件
已修改13个文件
12739 ■■■■ 文件已修改
src/api/collaborativeApproval/meeting.js 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockWarningLedger.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotation.js 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 194 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/fileList.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index5.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/attendanceManagement/index.vue 822 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/enterpriseBook/index.vue 798 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 323 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingManagement/index.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/noticeManagement/index.vue 958 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/index.vue 341 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue 394 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue 414 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue 412 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/summary/index.vue 399 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/officeSupplies/index.vue 512 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/planTemplate/index.vue 867 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/processTracking/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/purchaseApproval/index.vue 1064 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/reportGeneration/index.vue 596 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rpaManagement/index.vue 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue 584 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 801 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/warningSystem/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockWarningLedger/index.vue 360 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/payrollManagement/components/formDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/payrollManagement/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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/inventoryManagement/stockWarningLedger.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
import request from "@/utils/request";
// æŸ¥è¯¢åº“存预警台账列表
export const getStockWarningLedgerPage = (params) => {
  return request({
    url: "/stockWarningLedger/listPage",
    method: "get",
    params,
  });
};
src/api/salesManagement/salesQuotation.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
// é”€å”®æŠ¥ä»·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢æŠ¥ä»·å•列表
export function getQuotationList(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: "post",
    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/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -6,7 +6,7 @@
      width="700px"
      @close="closeDia"
    >
            <el-form :model="form" label-width="140px" label-position="top" ref="formRef">
            <el-form :model="form" :rules="rules" label-width="140px" label-position="top" ref="formRef">
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="流程编号:" prop="approveId">
@@ -32,7 +32,7 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                <el-row v-if="!isQuotationApproval">
                    <el-col :span="24">
                        <el-form-item label="审批事由:" prop="approveReason">
                            <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/>
@@ -73,6 +73,54 @@
                    </el-col>
                </el-row>
            </el-form>
      <!-- æŠ¥ä»·å®¡æ‰¹ï¼šå±•示报价详情(复用销售报价“查看详情对话框”内容结构) -->
      <div v-if="isQuotationApproval" style="margin: 10px 0 18px;">
        <el-divider content-position="left">报价详情</el-divider>
        <el-skeleton :loading="quotationLoading" animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="未查询到对应报价详情" />
            <template v-else>
              <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="报价总额" :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentQuotation.totalAmount ?? 0).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="product" label="产品名称" />
                  <el-table-column prop="specification" label="规格型号" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="unitPrice" label="单价">
                    <template #default="scope">Â¥{{ Number(scope.row.unitPrice ?? 0).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>
            </template>
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }" ref="formRef" label-position="top">
        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
          <el-step
@@ -121,33 +169,16 @@
      <template #footer v-if="operationType === 'approval'">
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm(2)">不通过</el-button>
          <el-button type="primary" @click="openSignatureDialog(1)">通过</el-button>
          <el-button type="primary" @click="submitForm(1)">通过</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- ç”µå­ç­¾åå¼¹çª—(vue3-signature-pad) -->
    <el-dialog v-model="signatureDialogVisible" title="电子签名" width="600px" append-to-body>
            <vueEsign
                ref="esign"
                class="mySign"
                :width="800"
                :height="300"
                :isCrop="isCrop"
                :lineWidth="lineWidth"
                :lineColor="lineColor"
            />
      <div style="margin-top:10px;">
        <el-button @click="clearSignature">清除</el-button>
        <el-button type="primary" @click="confirmSignature">确定</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { getCurrentInstance, reactive, ref, toRefs } from "vue";
import vueEsign from "vue-esign";
import { computed, getCurrentInstance, reactive, ref, toRefs } from "vue";
import {
    approveProcessDetails,
    getDept,
@@ -156,9 +187,16 @@
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getToken } from "@/utils/auth";
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
})
const dialogFormVisible = ref(false);
const operationType = ref('')
@@ -167,6 +205,10 @@
const userStore = useUserStore()
const productOptions = ref([]);
const userList = ref([])
const quotationLoading = ref(false)
const currentQuotation = ref({})
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const data = reactive({
    form: {
        approveTime: "",
@@ -176,23 +218,12 @@
        approveReason: "",
        checkResult: "",
    },
  rules: {
    // ä½¿ç”¨éƒ¨é—¨ID做必填校验,避免名称未同步导致误报
    approveDeptId: [{ required: true, message: "请选择申请部门", trigger: "change" }],
  },
});
const { form } = toRefs(data);
const signatureDialogVisible = ref(false);
const signatureImg = ref('');
let submitStatus = null; // ä¸´æ—¶å­˜å‚¨é€šè¿‡/不通过状态
const isCrop = ref("");
const esign = ref(null);
const lineWidth = ref(0);
const lineColor = ref("#000000");
// ä¸Šä¼ é…ç½®
const upload = reactive({
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
});
const { form, rules } = toRefs(data);
// èŠ‚ç‚¹æ ‡é¢˜
const getNodeTitle = (index, len) => {
@@ -219,11 +250,27 @@
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  currentQuotation.value = {}
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    form.value = {...row}
    getProductOptions()
  // æŠ¥ä»·å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的“报价单号”去查报价列表
  if (isQuotationApproval.value) {
    const quotationNo = row?.approveReason;
    if (quotationNo) {
      quotationLoading.value = true
      getQuotationList({ quotationNo }).then((res) => {
        const records = res?.data?.records || []
        currentQuotation.value = records[0] || {}
      }).finally(() => {
        quotationLoading.value = false
      })
    }
  }
  approveProcessDetails(row.approveId).then((res) => {
    activities.value = res.data
    // å¢žåŠ isApproval字段
@@ -248,77 +295,10 @@
        productOptions.value = res.data;
    });
};
// æ‰“开签名弹窗
const openSignatureDialog = (status) => {
  submitStatus = status;
  signatureDialogVisible.value = true;
};
// æ¸…除签名
const clearSignature = () => {
    esign.value.reset();
};
// ç¡®è®¤ç­¾å
const confirmSignature = () => {
    esign.value.generate().then((res) => {
        console.log(res);
        // å°†base64转换为二进制
        const base64Data = res.split(',')[1]; // ç§»é™¤data:image/png;base64,前缀
        const binaryString = atob(base64Data);
        const bytes = new Uint8Array(binaryString.length);
        for (let i = 0; i < binaryString.length; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }
        signatureImg.value = bytes;
        // åˆ›å»ºæ–‡ä»¶å¯¹è±¡ç”¨äºŽä¸Šä¼ 
        const blob = new Blob([bytes], { type: 'image/png' });
        const file = new File([blob], 'signature.png', { type: 'image/png' });
        // åˆ›å»ºFormData
        const formData = new FormData();
        formData.append('file', file);
        // ä¸Šä¼ ç­¾åå›¾ç‰‡
        fetch(upload.url, {
            method: 'POST',
            headers: upload.headers,
            body: formData
        })
        .then(response => response.json())
        .then(data => {
            if (data.code === 200) {
                console.log('data---', data)
                let tempFileIds = [];
                tempFileIds.push(data.data.tempId);
                signatureDialogVisible.value = false;
                clearSignature();
                // åªæœ‰é€šè¿‡æ—¶æ‰ä¼ é€’签名文件ID
                if (submitStatus === 1) {
                    submitForm(submitStatus, tempFileIds);
                } else {
                    submitForm(submitStatus);
                }
            } else {
                proxy.$modal.msgError("签名图片上传失败:" + data.msg);
            }
        })
        .catch(error => {
            console.error('上传失败:', error);
            proxy.$modal.msgError("签名图片上传失败");
        });
    }).catch((err) => {
        console.log(err);
        proxy.$modal.msgWarning("请先签名!");
    })
};
// æäº¤å®¡æ‰¹
const submitForm = (status, tempFileIds) => {
const submitForm = (status) => {
  const filteredActivities = activities.value.filter(activity => activity.isShen);
  filteredActivities[0].approveNodeStatus = status;
  // åªæœ‰é€šè¿‡æ—¶æ‰éœ€è¦ç­¾å
  if (status === 1 && tempFileIds) {
    filteredActivities[0].tempFileIds = tempFileIds;
  }
  // åˆ¤æ–­æ˜¯å¦ä¸ºæœ€åŽä¸€æ­¥
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1;
  updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
@@ -330,6 +310,8 @@
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  quotationLoading.value = false
  currentQuotation.value = {}
  emit('close')
};
defineExpose({
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -16,11 +16,13 @@
        </el-row>
        <el-row>
          <el-col :span="24">
            <!-- ç”³è¯·éƒ¨é—¨ï¼šæ ¡éªŒä½¿ç”¨éƒ¨é—¨ID,便于下拉选择后立即通过校验 -->
            <el-form-item label="申请部门:" prop="approveDeptId">
<!--              <el-input v-model="form.approveDeptName" placeholder="请输入" clearable/>-->
                            <el-select
                                disabled
                                v-model="form.approveDeptId"
                                placeholder="选择部门"
                @change="handleDeptChange"
                            >
                                <el-option
                                    v-for="user in productOptions"
@@ -34,8 +36,65 @@
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="审批事由:" prop="approveReason">
            <el-form-item :label="props.approveType == 5 ? '采购说明:' : '审批事由:'" prop="approveReason">
              <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" />
            </el-form-item>
          </el-col>
        </el-row>
        <!-- è¯·å‡æ—¶é—´ï¼ˆä»…当 approveType ä¸º 2 æ—¶æ˜¾ç¤ºï¼‰ -->
        <el-row :gutter="30" v-if="props.approveType == 2">
          <el-col :span="12">
            <el-form-item label="请假开始时间:" prop="startDate">
              <el-date-picker
                  v-model="form.startDate"
                  type="date"
                  placeholder="请选择开始日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="请假结束时间:" prop="endDate">
              <el-date-picker
                  v-model="form.endDate"
                  type="date"
                  placeholder="请选择结束日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <!-- æŠ¥é”€é‡‘额(仅当 approveType ä¸º 4 æ—¶æ˜¾ç¤ºï¼‰ -->
        <el-row v-if="props.approveType == 4">
          <el-col :span="24">
            <el-form-item label="报销金额:" prop="price">
              <el-input-number
                  v-model="form.price"
                  placeholder="请输入报销金额"
                  :min="0"
                  :precision="2"
                  :step="0.01"
                  style="width: 100%"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <!-- å‡ºå·®åœ°ç‚¹ï¼ˆä»…当 approveType ä¸º 3 æ—¶æ˜¾ç¤ºï¼‰ -->
        <el-row v-if="props.approveType == 3">
          <el-col :span="24">
            <el-form-item label="备注:" prop="location">
              <el-input
                  v-model="form.location"
                  placeholder="请输入备注"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
@@ -88,6 +147,9 @@
                            <el-select
                                v-model="form.approveUser"
                                placeholder="选择人员"
                filterable
                default-first-option
                :reserve-keyword="false"
                            >
                                <el-option
                                    v-for="user in userList"
@@ -155,6 +217,8 @@
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
import useUserStore from "@/store/modules/user";
import { getCurrentDate } from "@/utils/index.js";
import log from "@/views/monitor/job/log.vue";
const userStore = useUserStore();
const dialogFormVisible = ref(false);
@@ -171,19 +235,29 @@
    approveTime: "",
    approveId: "",
    approveUser: "",
        approveDeptId: "",
    approveDeptId: "",
    approveDeptName: "",
    approveReason: "",
    checkResult: "",
    tempFileIds: [],
    approverList: [] // æ–°å¢žå­—段,存储所有节点的审批人id
    approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
    startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
    endDate: "", // è¯·å‡ç»“束时间
    price: null, // æŠ¥é”€é‡‘额
    location: "" // å‡ºå·®åœ°ç‚¹
  },
  rules: {
    approveTime: [{ required: false, message: "请输入", trigger: "change" },],
    approveTime: [{ required: false, message: "请输入", trigger: "change" }],
    approveId: [{ required: false, message: "请输入", trigger: "blur" }],
    approveUser: [{ required: false, message: "请输入", trigger: "blur" }],
        approveDeptId: [{ required: true, message: "请输入", trigger: "blur" }],
    // ä½¿ç”¨éƒ¨é—¨ID做必填校验,避免名称未同步导致误报
    approveDeptId: [{ required: true, message: "请选择申请部门", trigger: "change" }],
    approveReason: [{ required: true, message: "请输入", trigger: "blur" }],
    checkResult: [{ required: false, message: "请输入", trigger: "blur" }],
    startDate: [{ required: true, message: "请选择请假开始时间", trigger: "change" }],
    endDate: [{ required: true, message: "请选择请假结束时间", trigger: "change" }],
    price: [{ required: true, message: "请输入报销金额", trigger: "blur" }],
    location: [{ required: true, message: "请输入出差地点", trigger: "blur" }],
  },
});
const { form, rules } = toRefs(data);
@@ -208,10 +282,19 @@
function removeApproverNode(index) {
  approverNodes.value.splice(index, 1)
}
// å¤„理部门选择变化
const handleDeptChange = (deptId) => {
  if (deptId) {
    const selectedDept = productOptions.value.find(dept => dept.deptId === deptId);
    if (selectedDept) {
      form.value.approveDeptName = selectedDept.deptName;
    }
  } else {
    form.value.approveDeptName = '';
  }
};
// æ‰“开弹框
const openDialog = (type, row) => {
  console.log('openDialog', type, row)
  operationType.value = type;
  dialogFormVisible.value = true;
    userListNoPageByTenantId().then((res) => {
@@ -278,6 +361,36 @@
    proxy.$modal.msgError("请为所有审批节点选择审批人!")
    return
  }
  // å½“ approveType ä¸º 2 æ—¶ï¼Œæ ¡éªŒè¯·å‡æ—¶é—´
  if (props.approveType == 2) {
    if (!form.value.startDate) {
      proxy.$modal.msgError("请选择请假开始时间!")
      return
    }
    if (!form.value.endDate) {
      proxy.$modal.msgError("请选择请假结束时间!")
      return
    }
    // æ ¡éªŒç»“束时间不能早于开始时间
    if (new Date(form.value.endDate) < new Date(form.value.startDate)) {
      proxy.$modal.msgError("请假结束时间不能早于开始时间!")
      return
    }
  }
  // å½“ approveType ä¸º 3 æ—¶ï¼Œæ ¡éªŒå‡ºå·®åœ°ç‚¹
  if (props.approveType == 3) {
    if (!form.value.location || form.value.location.trim() === '') {
      proxy.$modal.msgError("请输入出差地点!")
      return
    }
  }
  // å½“ approveType ä¸º 4 æ—¶ï¼Œæ ¡éªŒæŠ¥é”€é‡‘额
  if (props.approveType == 4) {
    if (!form.value.price || form.value.price <= 0) {
      proxy.$modal.msgError("请输入有效的报销金额!")
      return
    }
  }
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      if (operationType.value === "add" || currentApproveStatus.value == 3) {
@@ -301,14 +414,6 @@
  dialogFormVisible.value = false;
  emit('close')
};
// èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º 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}`;
}
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
src/views/collaborativeApproval/approvalProcess/fileList.vue
@@ -1,11 +1,12 @@
<template>
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose">
    <el-table :data="tableData" border height="40vh" stripe>
  <el-dialog v-model="dialogVisible" title="附件" width="40%" :before-close="handleClose" draggable>
    <el-table :data="tableData" border height="40vh">
      <el-table-column label="附件名称" prop="name" min-width="400" show-overflow-tooltip />
      <el-table-column fixed="right" label="操作" width="100" align="center">
      <el-table-column fixed="right" label="操作" width="150" align="center">
        <template #default="scope">
          <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">下载</el-button>
          <el-button link type="primary" size="small" @click="lookFile(scope.row)">预览</el-button>
          <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
@@ -16,6 +17,8 @@
<script setup>
import { ref } from 'vue'
import filePreview from '@/components/filePreview/index.vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { delCommonFile } from '@/api/publicApi/commonFile.js'
const dialogVisible = ref(false)
const tableData = ref([])
@@ -35,6 +38,27 @@
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
// åˆ é™¤é™„ä»¶
const handleDelete = (row) => {
  ElMessageBox.confirm(`确认删除附件"${row.name}"吗?`, '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    delCommonFile([row.id]).then(() => {
      ElMessage.success('删除成功')
      // ä»Žåˆ—表中移除已删除的附件
      const index = tableData.value.findIndex(item => item.id === row.id)
      if (index !== -1) {
        tableData.value.splice(index, 1)
      }
    }).catch(() => {
      ElMessage.error('删除失败')
    })
  }).catch(() => {
    ElMessage.info('已取消删除')
  })
}
defineExpose({
  open
})
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -1,5 +1,16 @@
<template>
  <div class="app-container">
    <!-- æ ‡ç­¾é¡µåˆ‡æ¢ä¸åŒçš„审批类型 -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="approval-tabs">
      <el-tab-pane label="协同审批" name="1"></el-tab-pane>
      <el-tab-pane label="请假管理" name="2"></el-tab-pane>
      <el-tab-pane label="销售审批" name="3"></el-tab-pane>
      <el-tab-pane label="报销审批" name="4"></el-tab-pane>
      <el-tab-pane label="采购审批" name="5"></el-tab-pane>
      <el-tab-pane label="报价审批" name="6"></el-tab-pane>
      <el-tab-pane label="出库审批" name="7"></el-tab-pane>
    </el-tabs>
    <div class="search_form">
      <div>
        <span class="search_title">流程编号:</span>
@@ -24,15 +35,15 @@
        >
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增</el-button>
<!--        <el-button @click="handleOut">导出</el-button>-->
        <el-button type="primary" @click="openForm('add')" v-if="currentApproveType !== 6">新增</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :column="tableColumnCopy"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
@@ -42,8 +53,8 @@
          :total="page.total"
      ></PIMTable>
    </div>
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="approveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia>
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <FileList ref="fileListRef" />
  </div>
</template>
@@ -51,22 +62,33 @@
<script setup>
import FileList from "./fileList.vue";
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance} from "vue";
import {ElMessageBox} from "element-plus";
import { useRoute } from 'vue-router';
import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
// å®šä¹‰ç»„件接收的props
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
const userStore = useUserStore();
const route = useRoute();
// å½“前选中的标签页,默认为公出管理
const activeTab = ref('1');
// å½“前审批类型,根据选中的标签页计算
const currentApproveType = computed(() => {
  return Number(activeTab.value);
});
const userStore = useUserStore();
// æ ‡ç­¾é¡µåˆ‡æ¢å¤„理
const handleTabChange = (tabName) => {
  // åˆ‡æ¢æ ‡ç­¾é¡µæ—¶é‡ç½®æœç´¢æ¡ä»¶å’Œåˆ†é¡µï¼Œå¹¶é‡æ–°åŠ è½½æ•°æ®
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
  page.current = 1;
  getList();
};
const data = reactive({
@@ -76,75 +98,101 @@
  },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
  {
    label: "审批状态",
    prop: "approveStatus",
    dataType: "tag",
        width: 100,
    formatData: (params) => {
      if (params == 0) {
        return "待审核";
      } else if (params == 1) {
        return "审核中";
      } else if (params == 2) {
        return "审核完成";
      } else if (params == 4) {
        return "已重新提交";
      } else {
        return '不通过';
      }
// åŠ¨æ€è¡¨æ ¼åˆ—é…ç½®ï¼Œæ ¹æ®å®¡æ‰¹ç±»åž‹ç”Ÿæˆåˆ—
const tableColumnCopy = computed(() => {
  const isLeaveType = currentApproveType.value === 2; // è¯·å‡ç®¡ç†
  const isReimburseType = currentApproveType.value === 4; // æŠ¥é”€ç®¡ç†
  const isQuotationType = currentApproveType.value === 6; // æŠ¥ä»·å®¡æ‰¹
  // åŸºç¡€åˆ—配置
  const baseColumns = [
    {
      label: "审批状态",
      prop: "approveStatus",
      dataType: "tag",
      width: 100,
      formatData: (params) => {
        if (params == 0) {
          return "待审核";
        } else if (params == 1) {
          return "审核中";
        } else if (params == 2) {
          return "审核完成";
        } else if (params == 4) {
          return "已重新提交";
        } else {
          return '不通过';
        }
      },
      formatType: (params) => {
        if (params == 0) {
          return "warning";
        } else if (params == 1) {
          return "primary";
        } else if (params == 2) {
          return "success";
        } else if (params == 4) {
          return "info";
        } else {
          return 'danger';
        }
      },
    },
    formatType: (params) => {
      if (params == 0) {
        return "warning";
      } else if (params == 1) {
        return "primary";
      } else if (params == 2) {
        return "success";
      } else if (params == 4) {
        return "";
      } else {
        return 'danger';
      }
    {
      label: "流程编号",
      prop: "approveId",
      width: 170
    },
  },
  {
    label: "流程编号",
    prop: "approveId",
    width: 170
  },
  {
    label: "申请部门",
    prop: "approveDeptName",
        width: 220
  },
  {
    label: "审批事由",
    prop: "approveReason",
        width: 200
  },
  {
    label: "申请人",
    prop: "approveUserName",
    width: 120
  },
  {
    label: "申请日期",
    prop: "approveTime",
        width: 200
  },
  {
    label: "结束日期",
    prop: "approveOverTime",
    width: 120
  },
  {
    {
      label: "申请部门",
      prop: "approveDeptName",
      width: 220
    },
    {
      label: isQuotationType ? "报价单号" : "审批事由",
      prop: "approveReason",
      width: 200
    },
    {
      label: "申请人",
      prop: "approveUserName",
      width: 120
    }
  ];
  // é‡‘额列(仅报销管理显示)
  if (isReimburseType) {
    baseColumns.push({
      label: "金额(元)",
      prop: "price",
      width: 120
    });
  }
  // æ—¥æœŸåˆ—(根据类型动态配置)
  baseColumns.push(
    {
      label: isLeaveType ? "开始日期" : "申请日期",
      prop: isLeaveType ? "startDate" : "approveTime",
      width: 200
    },
    {
      label: "结束日期",
      prop: isLeaveType ? "endDate" : "approveOverTime",
      width: 120
    }
  );
  // å½“前审批人列
  baseColumns.push({
    label: "当前审批人",
    prop: "approveUserCurrentName",
    width: 120
  },
  {
  });
  // æ“ä½œåˆ—
  baseColumns.push({
    dataType: "action",
    label: "操作",
    align: "center",
@@ -157,7 +205,7 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
                disabled: (row) => row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4
        disabled: (row) => currentApproveType.value === 6 || row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4
      },
      {
        name: "审核",
@@ -165,7 +213,7 @@
        clickFun: (row) => {
          openApprovalDia("approval", row);
        },
                disabled: (row) => row.approveUserCurrentId == null || row.approveStatus == 2 || row.approveStatus == 3 || row.approveStatus == 4 || row.approveUserCurrentId !== userStore.id
        disabled: (row) => row.approveUserCurrentId == null || row.approveStatus == 2 || row.approveStatus == 3 || row.approveStatus == 4 || row.approveUserCurrentId !== userStore.id
      },
      {
        name: "详情",
@@ -182,8 +230,10 @@
        },
      },
    ],
  },
]);
  });
  return baseColumns;
});
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
@@ -214,7 +264,7 @@
};
const getList = () => {
  tableLoading.value = true;
  approveProcessListPage({...page, ...searchForm.value,approveType:props.approveType}).then(res => {
  approveProcessListPage({...page, ...searchForm.value, approveType: currentApproveType.value}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
@@ -222,6 +272,33 @@
    tableLoading.value = false;
  })
};
// å¯¼å‡º
const handleOut = () => {
  const type = currentApproveType.value
  const urlMap = {
    0: "/approveProcess/exportZero",
    1: "/approveProcess/exportOne",
    2: "/approveProcess/exportTwo",
    3: "/approveProcess/exportThree",
    4: "/approveProcess/exportFour",
    5: "/approveProcess/exportFive",
    6: "/approveProcess/exportSix",
    7: "/approveProcess/exportSeven",
  }
  const url = urlMap[type] || urlMap[0]
  const nameMap = {
    0: "协同审批管理表",
    1: "公出管理审批表",
    2: "请假管理审批表",
    3: "出差管理审批表",
    4: "报销管理审批表",
    5: "采购申请审批表",
    6: "报价审批表",
    7: "出库审批表",
  }
  const fileName = nameMap[type] || nameMap[0]
  proxy.download(url, {}, `${fileName}.xlsx`)
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
@@ -265,8 +342,27 @@
      });
};
onMounted(() => {
  // æ ¹æ®URL参数设置标签页和查询条件
  const approveType = route.query.approveType;
  const approveId = route.query.approveId;
  if (approveType) {
    // è®¾ç½®æ ‡ç­¾é¡µï¼ˆapproveType å¯¹åº” activeTab çš„ name)
    activeTab.value = String(approveType);
  }
  if (approveId) {
    // è®¾ç½®æµç¨‹ç¼–号查询条件
    searchForm.value.approveId = String(approveId);
  }
  // æŸ¥è¯¢åˆ—表
  getList();
});
</script>
<style scoped></style>
<style scoped>
.approval-tabs {
  margin-bottom: 10px;
}
</style>
src/views/collaborativeApproval/approvalProcess/index5.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
<template>
  <div class="container">
    <!-- å¼•å…¥index.vue组件并传递参数 -->
    <ApprovalProcessIndex :approveType="5" />
  </div>
</template>
<script setup>
import ApprovalProcessIndex from './index.vue'
// å®šä¹‰ç»„件名称
defineOptions({
  name: 'ApprovalProcessIndex1'
})
</script>
<style scoped>
.container {
  width: 100%;
  height: 100%;
}
</style>
src/views/collaborativeApproval/attendanceManagement/index.vue
@@ -5,8 +5,8 @@
      <el-tab-pane label="假期设置" name="holiday">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('holiday', 'add')">新增假期</el-button>
          <el-table :data="holidayData" border style="width: 100%; margin-top: 20px;" stripe>
          <el-table :data="holidayData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="name" label="假期名称" />
            <el-table-column prop="type" label="假期类型">
              <template #default="scope">
@@ -37,9 +37,13 @@
      <el-tab-pane label="年假设置" name="annual">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('annual', 'add')">新增年假规则</el-button>
          <el-table :data="annualData" border style="width: 100%; margin-top: 20px;" stripe>
            <el-table-column prop="employeeType" label="员工类型"/>
          <el-table :data="annualData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="employeeType" label="员工类型">
              <template #default="scope">
                <el-tag :type="getTagType(scope.row.employeeType)">{{ getTypeLabel(scope.row.employeeType) }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="workYears" label="工作年限" />
            <el-table-column prop="annualDays" label="年假天数" align="center" />
            <el-table-column prop="maxCarryOver" label="最大结转天数" align="center" />
@@ -64,8 +68,8 @@
      <el-tab-pane label="加班设置" name="overtime">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('overtime', 'add')">新增加班规则</el-button>
          <el-table :data="overtimeData" border style="width: 100%; margin-top: 20px;" stripe>
          <el-table :data="overtimeData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="name" label="规则名称" />
            <el-table-column prop="type" label="加班类型" >
              <template #default="scope">
@@ -96,15 +100,15 @@
      <el-tab-pane label="上班时间设置" name="worktime">
        <div class="tab-content">
          <el-button type="primary" @click="openDialog('worktime', 'add')">新增时间段</el-button>
          <el-table :data="worktimeData" border style="width: 100%; margin-top: 20px;" stripe>
          <el-table :data="worktimeData" border style="width: 100%; margin-top: 20px;">
            <el-table-column prop="name" label="时间段名称"  />
            <el-table-column prop="startTime" label="上班时间"/>
            <el-table-column prop="endTime" label="下班时间" />
            <el-table-column prop="flexibleStart" label="弹性上班">
              <template #default="scope">
                <el-tag :type="scope.row.flexibleStart ? 'success' : 'info'">
                  {{ scope.row.flexibleStart ? '是' : '否' }}
                <el-tag :type="scope.row.flexibleStart === 'true' ? 'success' : 'info'">
                  {{ scope.row.flexibleStart === 'true' ? '是' : '否' }}
                </el-tag>
              </template>
            </el-table-column>
@@ -125,6 +129,52 @@
          </el-table>
        </div>
      </el-tab-pane>
      <!-- æ‰“卡记录 -->
      <el-tab-pane label="打卡记录" name="attendance">
        <div class="tab-content">
          <div style="margin-bottom: 20px;">
            <el-date-picker
              v-model="attendanceDate"
              type="date"
              placeholder="选择日期"
              format="YYYY-MM-DD"
              value-format="YYYY-MM-DD"
              style="margin-right: 10px;"
              @change="filterAttendanceData"
            />
            <el-select
              v-model="attendanceStatus"
              placeholder="选择状态"
              style="width: 120px; margin-right: 10px;"
              @change="filterAttendanceData"
            >
              <el-option label="全部" value="" />
              <el-option label="正常" value="normal" />
              <el-option label="迟到" value="late" />
              <el-option label="早退" value="early" />
              <el-option label="缺勤" value="absent" />
            </el-select>
            <el-button type="primary" @click="exportAttendance">导出记录</el-button>
          </div>
          <el-table :data="filteredAttendanceData" border style="width: 100%;">
            <el-table-column prop="employeeName" label="员工姓名" width="120" />
            <el-table-column prop="department" label="部门" width="120" />
            <el-table-column prop="date" label="日期" width="120" />
            <el-table-column prop="clockInTime" label="上班打卡" width="120" />
            <el-table-column prop="clockOutTime" label="下班打卡" width="120" />
            <el-table-column prop="workHours" label="工作时长" width="100" align="center" />
            <el-table-column prop="status" label="状态" width="100" align="center">
              <template #default="scope">
                <el-tag :type="getAttendanceTagType(scope.row.status)">{{ getAttendanceStatusLabel(scope.row.status) }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="location" label="打卡地点" width="150" />
            <el-table-column prop="remark" label="备注" min-width="150" />
          </el-table>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- é€šç”¨å¼¹çª— -->
@@ -133,23 +183,29 @@
        <el-form-item label="名称" prop="name" v-if="currentType !== 'annual'">
          <el-input v-model="form.name" placeholder="请输入名称" />
        </el-form-item>
        <el-form-item label="类型" prop="type" v-if="currentType === 'holiday' || currentType === 'overtime'">
          <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
            <el-option
              v-for="option in getTypeOptions()"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            <el-option
              v-for="option in getTypeOptions()"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="员工类型" prop="employeeType" v-if="currentType === 'annual'">
          <el-select v-model="form.employeeType" placeholder="请选择员工类型" style="width: 100%">
            <el-option label="正式员工" value="regular" />
            <!-- <el-option label="正式员工" value="regular" />
            <el-option label="试用期员工" value="probation" />
            <el-option label="实习生" value="intern" />
            <el-option label="实习生" value="intern" /> -->
            <el-option
              v-for="option in getTypeOptions()"
              :key="option.value"
              :label="option.label"
              :value="option.value"
            />
          </el-select>
        </el-form-item>
@@ -191,7 +247,7 @@
             @change="validateTimeField('startTime')"
           />
         </el-form-item>
         <el-form-item label="结束时间" prop="endTime" v-if="currentType === 'overtime'">
           <el-time-picker
             v-model="form.endTime"
@@ -237,14 +293,14 @@
          <el-input-number v-model="form.flexibleMinutes" :min="0" :max="120" style="width: 100%" />
        </el-form-item>
                 <el-form-item label="状态" prop="status">
        <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
             <el-radio value="active">启用</el-radio>
             <el-radio value="inactive">停用</el-radio>
           </el-radio-group>
         </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
@@ -258,6 +314,7 @@
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listHolidaySettings, addHolidaySettings, updateHolidaySettings, delHolidaySettings, listAnnualLeaveSettingList, addAnnualLeaveSetting, updateAnnualLeaveSetting, delAnnualLeaveSetting, listOvertimeSettingList, addOvertimeSetting, updateOvertimeSetting, delOvertimeSetting, listWorkingHoursSettingList, addWorkingHoursSetting, updateWorkingHoursSetting, delWorkingHoursSetting } from '@/api/collaborativeApproval/attendanceManagement.js'
// å½“前激活的标签页
const activeTab = ref('holiday')
@@ -269,12 +326,29 @@
const currentAction = ref('')
const currentEditId = ref('')
const formRef = ref()
const page = {
    current: 1,
    size: 20,
    total: 0,
  }
const holidayData = ref([])
const annualData = ref([])
const overtimeData = ref([])
const worktimeData = ref([])
// æ‰“卡记录相关数据
const attendanceData = ref([])
const filteredAttendanceData = ref([])
const attendanceDate = ref('')
const attendanceStatus = ref('')
// è¡¨å•数据
const form = reactive({
  name: '',
  type: '',
  dateRange: [],
  startDate: '',
  endDate: '',
  days: 0,
  employeeType: '',
  workYears: '',
@@ -300,9 +374,9 @@
  workYears: [{ required: true, message: '请输入工作年限', trigger: 'blur' }],
  annualDays: [{ required: true, message: '请输入年假天数', trigger: 'blur' }],
  maxCarryOver: [{ required: true, message: '请输入最大结转天数', trigger: 'blur' }],
  startTime: [{
    required: true,
    message: '请选择开始时间',
  startTime: [{
    required: true,
    message: '请选择开始时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
@@ -312,9 +386,9 @@
      }
    }
  }],
  endTime: [{
    required: true,
    message: '请选择结束时间',
  endTime: [{
    required: true,
    message: '请选择结束时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
@@ -324,9 +398,9 @@
      }
    }
  }],
  workStartTime: [{
    required: true,
    message: '请选择上班时间',
  workStartTime: [{
    required: true,
    message: '请选择上班时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
@@ -336,9 +410,9 @@
      }
    }
  }],
  workEndTime: [{
    required: true,
    message: '请选择下班时间',
  workEndTime: [{
    required: true,
    message: '请选择下班时间',
    trigger: 'change',
    validator: (rule, value, callback) => {
      if (!value) {
@@ -350,37 +424,12 @@
  }],
  rate: [{ required: true, message: '请输入倍率', trigger: 'blur' }]
}
// æ¨¡æ‹Ÿæ•°æ®
const holidayData = ref([
  { id: '1', name: '春节', type: 'legal', startDate: '2024-02-10', endDate: '2024-02-17', days: 8, status: 'active' },
  { id: '2', name: '清明节', type: 'legal', startDate: '2024-04-05', endDate: '2024-04-05', days: 1, status: 'active' },
  { id: '3', name: '劳动节', type: 'legal', startDate: '2024-05-01', endDate: '2024-05-05', days: 5, status: 'active' }
])
const annualData = ref([
  { id: '1', employeeType: 'regular', workYears: '1-3å¹´', annualDays: 5, maxCarryOver: 2, status: 'active' },
  { id: '2', employeeType: 'regular', workYears: '3-5å¹´', annualDays: 10, maxCarryOver: 5, status: 'active' },
  { id: '3', employeeType: 'regular', workYears: '5年以上', annualDays: 15, maxCarryOver: 10, status: 'active' }
])
const overtimeData = ref([
  { id: '1', name: '工作日加班', type: 'weekday', startTime: '18:00', endTime: '22:00', rate: 1.5, status: 'active' },
  { id: '2', name: '周末加班', type: 'weekend', startTime: '09:00', endTime: '18:00', rate: 2.0, status: 'active' },
  { id: '3', name: '深夜加班', type: 'night', startTime: '22:00', endTime: '06:00', rate: 2.5, status: 'active' }
])
const worktimeData = ref([
  { id: '1', name: '标准工作时间', startTime: '09:00', endTime: '18:00', flexibleStart: true, flexibleMinutes: 30, status: 'active' },
  { id: '2', name: '早班时间', startTime: '08:00', endTime: '17:00', flexibleStart: false, flexibleMinutes: 0, status: 'active' },
  { id: '3', name: '晚班时间', startTime: '14:00', endTime: '23:00', flexibleStart: false, flexibleMinutes: 0, status: 'active' }
])
// å·¥å…·å‡½æ•°
const getTagType = (type) => {
  const tagMap = {
    legal: 'success', adjustment: 'warning', special: 'info', company: 'primary',
    weekday: 'primary', weekend: 'warning', holiday: 'danger', night: 'info'
    weekday: 'primary', weekend: 'warning', holiday: 'danger', night: 'info',
    regular: 'success', probation: 'info', intern: 'danger'
  }
  return tagMap[type] || 'info'
}
@@ -388,9 +437,31 @@
const getTypeLabel = (type) => {
  const labelMap = {
    legal: '法定节假日', adjustment: '调休日', special: '特殊假期', company: '公司假期',
    weekday: '工作日加班', weekend: '周末加班', holiday: '节假日加班', night: '深夜加班'
    weekday: '工作日加班', weekend: '周末加班', holiday: '节假日加班', night: '深夜加班',
    regular: '正式员工', probation: '试用期员工', intern: '实习生'
  }
  return labelMap[type] || type
}
// æ‰“卡记录相关工具函数
const getAttendanceTagType = (status) => {
  const tagMap = {
    normal: 'success',
    late: 'warning',
    early: 'warning',
    absent: 'danger'
  }
  return tagMap[status] || 'info'
}
const getAttendanceStatusLabel = (status) => {
  const labelMap = {
    normal: '正常',
    late: '迟到',
    early: '早退',
    absent: '缺勤'
  }
  return labelMap[status] || status
}
const getTypeOptions = () => {
@@ -408,6 +479,12 @@
      { label: '节假日加班', value: 'holiday' },
      { label: '深夜加班', value: 'night' }
    ]
  } else if (currentType.value === 'annual') {
    return [
      { label: '正式员工', value: 'regular' },
      { label: '试用期员工', value: 'probation' },
      { label: '实习生', value: 'intern' }
    ]
  }
  return []
}
@@ -418,12 +495,14 @@
    if (form.dateRange && form.dateRange.length === 2 && form.dateRange[0] && form.dateRange[1]) {
      const start = new Date(form.dateRange[0])
      const end = new Date(form.dateRange[1])
      form.startDate = start.toISOString().split('T')[0]
      form.endDate = end.toISOString().split('T')[0]
      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
        console.warn('无效的日期格式')
        return
      }
      const diffTime = Math.abs(end - start)
      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
      form.days = diffDays
@@ -434,14 +513,14 @@
}
// éªŒè¯æ—¶é—´æ ¼å¼
const validateTime = (time) => {
  if (!time) return ''
  if (typeof time === 'string') return time
  if (time instanceof Date) {
    return time.toTimeString().slice(0, 5)
  }
  return ''
}
// const validateTime = (time) => {
//   if (!time) return ''
//   if (typeof time === 'string') return time
//   if (time instanceof Date) {
//     return time.toTimeString().slice(0, 5)
//   }
//   return ''
// }
// éªŒè¯æ—¶é—´å­—段
const validateTimeField = (fieldName) => {
@@ -464,7 +543,7 @@
  try {
    currentType.value = type
    currentAction.value = action
    if (action === 'add') {
      dialogTitle.value = `新增${getTypeName(type)}`
      currentEditId.value = ''
@@ -474,7 +553,7 @@
      currentEditId.value = row.id
      fillForm(row)
    }
    dialogVisible.value = true
  } catch (error) {
    console.error('打开弹窗失败:', error)
@@ -497,6 +576,8 @@
    name: '',
    type: '',
    dateRange: [],
    startDate: '',
    endDate: '',
    days: 0,
    employeeType: '',
    workYears: '',
@@ -519,6 +600,8 @@
      name: row.name,
      type: row.type,
      dateRange: [new Date(row.startDate), new Date(row.endDate)],
      startDate: row.startDate,
      endDate: row.endDate,
      days: row.days,
      status: row.status
    })
@@ -558,15 +641,15 @@
      ElMessage.error('表单引用不存在')
      return
    }
    await formRef.value.validate()
    if (currentAction.value === 'add') {
      addItem()
    } else if (currentAction.value === 'edit') {
      editItem()
    }
    dialogVisible.value = false
    ElMessage.success('操作成功')
  } catch (error) {
@@ -576,85 +659,438 @@
}
const addItem = () => {
  const newItem = { ...form, id: Date.now().toString() }
  if (currentType.value === 'holiday') {
    newItem.startDate = form.dateRange[0].toISOString().split('T')[0]
    newItem.endDate = form.dateRange[1].toISOString().split('T')[0]
    holidayData.value.push(newItem)
    const params = {
      name: form.name,
      type: form.type,
      startDate: form.startDate,
      endDate: form.endDate,
      days: form.days,
      status: form.status
    }
    addHolidaySettings(params).then(res => {
      if(res.code == 200){
        ElMessage.success("添加成功");
        // dialogVisible.value = false;
        getHolidaySettingsList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
  } else if (currentType.value === 'annual') {
    annualData.value.push(newItem)
    // annualData.value.push(newItem)
    const params = {
      employeeType: form.employeeType,
      workYears: form.workYears,
      annualDays: form.annualDays,
      maxCarryOver: form.maxCarryOver,
      status: form.status
    }
    addAnnualLeaveSetting(params).then(res => {
      if(res.code == 200){
        ElMessage.success("添加成功");
        // dialogVisible.value = false;
        getAnnualLeaveSettingList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
  } else if (currentType.value === 'overtime') {
    newItem.startTime = form.startTime || ''
    newItem.endTime = form.endTime || ''
    overtimeData.value.push(newItem)
    const params = {
      name: form.name,
      type: form.type,
      startTime: form.startTime || '',
      endTime: form.endTime || '',
      rate: form.rate,
      status: form.status
    }
    addOvertimeSetting(params).then(res => {
      if(res.code == 200){
        ElMessage.success("添加成功");
        // dialogVisible.value = false;
        getOvertimeSettingList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // newItem.startTime = form.startTime || ''
    // newItem.endTime = form.endTime || ''
    // overtimeData.value.push(newItem)
  } else if (currentType.value === 'worktime') {
    newItem.startTime = form.workStartTime || ''
    newItem.endTime = form.workEndTime || ''
    worktimeData.value.push(newItem)
    const params = {
      name: form.name,
      startTime: form.workStartTime || '',
      endTime: form.workEndTime || '',
      flexibleStart: form.flexibleStart,
      flexibleMinutes: form.flexibleMinutes,
      status: form.status
    }
    addWorkingHoursSetting(params).then(res => {
      if(res.code == 200){
        ElMessage.success("添加成功");
        getWorkingHoursSettingList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // newItem.startTime = form.workStartTime || ''
    // newItem.endTime = form.workEndTime || ''
    // worktimeData.value.push(newItem)
  }
}
const editItem = () => {
  let dataArray
  let index
  if (currentType.value === 'holiday') {
    dataArray = holidayData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        type: form.type,
        startDate: form.dateRange[0].toISOString().split('T')[0],
        endDate: form.dateRange[1].toISOString().split('T')[0],
        days: form.days,
        status: form.status
      }
    const params = {
      id: currentEditId.value,
      name: form.name,
      type: form.type,
      startDate: form.dateRange[0].toISOString().split('T')[0],
      endDate: form.dateRange[1].toISOString().split('T')[0],
      days: form.days,
      status: form.status
    }
    updateHolidaySettings(params).then(res => {
      if(res.code == 200){
        ElMessage.success("更新成功");
        // dialogVisible.value = false;
        getHolidaySettingsList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
  } else if (currentType.value === 'annual') {
    dataArray = annualData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        employeeType: form.employeeType,
        workYears: form.workYears,
        annualDays: form.annualDays,
        maxCarryOver: form.maxCarryOver,
        status: form.status
      }
    const params = {
      id: currentEditId.value,
      employeeType: form.employeeType,
      workYears: form.workYears,
      annualDays: form.annualDays,
      maxCarryOver: form.maxCarryOver,
      status: form.status
    }
    updateAnnualLeaveSetting(params).then(res => {
      if(res.code == 200){
        ElMessage.success("更新成功");
        getAnnualLeaveSettingList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
  } else if (currentType.value === 'overtime') {
    dataArray = overtimeData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        type: form.type,
        startTime: form.startTime || '',
        endTime: form.endTime || '',
        rate: form.rate,
        status: form.status
      }
    const params = {
      id: currentEditId.value,
      name: form.name,
      type: form.type,
      startTime: form.startTime || '',
      endTime: form.endTime || '',
      rate: form.rate,
      status: form.status
    }
    updateOvertimeSetting(params).then(res => {
      if(res.code == 200){
        ElMessage.success("更新成功");
        getOvertimeSettingList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // dataArray = overtimeData.value
    // index = dataArray.findIndex(item => item.id === currentEditId.value)
    // if (index > -1) {
    //   dataArray[index] = {
    //     ...dataArray[index],
    //     name: form.name,
    //     type: form.type,
    //     startTime: form.startTime || '',
    //     endTime: form.endTime || '',
    //     rate: form.rate,
    //     status: form.status
    //   }
    // }
  } else if (currentType.value === 'worktime') {
    dataArray = worktimeData.value
    index = dataArray.findIndex(item => item.id === currentEditId.value)
    if (index > -1) {
      dataArray[index] = {
        ...dataArray[index],
        name: form.name,
        startTime: form.workStartTime || '',
        endTime: form.workEndTime || '',
        flexibleStart: form.flexibleStart,
        flexibleMinutes: form.flexibleMinutes,
        status: form.status
      }
    const params = {
      id: currentEditId.value,
      name: form.name,
      startTime: form.workStartTime || '',
      endTime: form.workEndTime || '',
      flexibleStart: form.flexibleStart,
      flexibleMinutes: form.flexibleMinutes,
      status: form.status
    }
    updateWorkingHoursSetting(params).then(res => {
      if(res.code == 200){
        ElMessage.success("更新成功");
        getWorkingHoursSettingList()
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // dataArray = worktimeData.value
    // index = dataArray.findIndex(item => item.id === currentEditId.value)
    // if (index > -1) {
    //   dataArray[index] = {
    //     ...dataArray[index],
    //     name: form.name,
    //     startTime: form.workStartTime || '',
    //     endTime: form.workEndTime || '',
    //     flexibleStart: form.flexibleStart,
    //     flexibleMinutes: form.flexibleMinutes,
    //     status: form.status
    //   }
    // }
  }
}
// æ‰“卡记录过滤功能
const filterAttendanceData = () => {
  let filtered = attendanceData.value
  // æŒ‰æ—¥æœŸè¿‡æ»¤
  if (attendanceDate.value) {
    filtered = filtered.filter(item => item.date === attendanceDate.value)
  }
  // æŒ‰çŠ¶æ€è¿‡æ»¤
  if (attendanceStatus.value) {
    filtered = filtered.filter(item => item.status === attendanceStatus.value)
  }
  filteredAttendanceData.value = filtered
}
// å¯¼å‡ºæ‰“卡记录
const exportAttendance = () => {
  ElMessage.success('导出功能开发中...')
}
// åˆå§‹åŒ–打卡记录假数据
const initAttendanceData = () => {
  const mockData = [
    {
      id: 1,
      employeeName: '陈志强',
      department: '技术部',
      date: '2025-08-15',
      clockInTime: '09:00:00',
      clockOutTime: '18:00:00',
      workHours: '8.0h',
      status: 'normal',
      location: '公司总部',
      remark: ''
    },
    {
      id: 2,
      employeeName: '李雪梅',
      department: '市场部',
      date: '2025-08-16',
       clockInTime: '08:58:00',
       clockOutTime: '18:05:00',
       workHours: '8.12h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 3,
       employeeName: '王建华',
       department: '人事部',
       date: '2025-08-16',
       clockInTime: '09:02:00',
       clockOutTime: '18:00:00',
       workHours: '7.97h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 4,
       employeeName: '赵晓丽',
       department: '财务部',
       date: '2025-09-02',
       clockInTime: '08:55:00',
       clockOutTime: '18:10:00',
       workHours: '8.25h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 5,
       employeeName: '张国庆',
       department: '技术部',
       date: '2025-09-02',
       clockInTime: '09:00:00',
       clockOutTime: '18:30:00',
       workHours: '8.5h',
       status: 'normal',
       location: '公司总部',
       remark: '加班'
     },
     {
       id: 6,
       employeeName: '刘明辉',
       department: '运营部',
       date: '2025-09-03',
       clockInTime: '09:05:00',
       clockOutTime: '18:00:00',
       workHours: '7.92h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 7,
       employeeName: '孙丽华',
       department: '设计部',
       date: '2025-09-03',
       clockInTime: '08:59:00',
       clockOutTime: '18:02:00',
       workHours: '8.05h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 8,
       employeeName: '周建军',
       department: '销售部',
       date: '2025-09-04',
       clockInTime: '09:15:00',
       clockOutTime: '18:00:00',
       workHours: '7.75h',
       status: 'late',
       location: '公司总部',
       remark: '交通堵塞'
     },
     {
       id: 9,
       employeeName: '吴小芳',
       department: '客服部',
       date: '2025-09-04',
       clockInTime: '09:01:00',
       clockOutTime: '18:00:00',
       workHours: '7.98h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 10,
       employeeName: '马文杰',
       department: '技术部',
       date: '2025-09-05',
       clockInTime: '08:57:00',
       clockOutTime: '17:30:00',
       workHours: '7.55h',
       status: 'early',
       location: '公司总部',
       remark: '有急事提前离开'
     },
     {
       id: 11,
       employeeName: '林晓东',
       department: '行政部',
       date: '2025-09-05',
       clockInTime: '09:03:00',
       clockOutTime: '18:08:00',
       workHours: '8.08h',
       status: 'normal',
       location: '公司总部',
       remark: ''
     },
     {
       id: 12,
       employeeName: '黄美玲',
       department: '财务部',
       date: '2025-09-06',
       clockInTime: '',
       clockOutTime: '',
       workHours: '0h',
       status: 'absent',
       location: '',
       remark: '请病假'
     },
    {
      id: 13,
      employeeName: '郑海涛',
      department: '市场部',
      date: '2025-08-14',
      clockInTime: '09:00:00',
      clockOutTime: '18:00:00',
      workHours: '8.0h',
      status: 'normal',
      location: '公司总部',
      remark: ''
    },
    {
      id: 14,
      employeeName: '谢丽娟',
      department: '人事部',
      date: '2025-08-20',
      clockInTime: '08:58:00',
      clockOutTime: '18:03:00',
      workHours: '8.08h',
      status: 'normal',
      location: '公司总部',
      remark: ''
    },
    {
      id: 15,
      employeeName: '何志伟',
      department: '技术部',
      date: '2025-08-21',
      clockInTime: '09:10:00',
      clockOutTime: '18:00:00',
      workHours: '7.83h',
      status: 'late',
      location: '公司总部',
      remark: ''
    },
    {
      id: 16,
      employeeName: '许雅芳',
      department: '设计部',
      date: '2025-08-22',
      clockInTime: '09:01:00',
      clockOutTime: '18:00:00',
      workHours: '7.98h',
      status: 'normal',
      location: '公司总部',
      remark: ''
    },
    {
      id: 17,
      employeeName: '邓建平',
      department: '运营部',
      date: '2025-09-10',
      clockInTime: '08:59:00',
      clockOutTime: '18:05:00',
      workHours: '8.1h',
      status: 'normal',
      location: '公司总部',
      remark: ''
    },
    {
      id: 18,
      employeeName: '曾小红',
      department: '客服部',
      date: '2025-09-11',
      clockInTime: '09:02:00',
      clockOutTime: '18:00:00',
      workHours: '7.97h',
      status: 'normal',
      location: '公司总部',
      remark: ''
    }
  ]
  attendanceData.value = mockData
  filteredAttendanceData.value = mockData
}
// åˆ é™¤é¡¹ç›®
@@ -664,21 +1100,115 @@
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    let ids = [];
    let dataArray
    if (type === 'holiday') dataArray = holidayData.value
    else if (type === 'annual') dataArray = annualData.value
    else if (type === 'overtime') dataArray = overtimeData.value
    else if (type === 'worktime') dataArray = worktimeData.value
    const index = dataArray.findIndex(item => item.id === row.id)
    if (index > -1) {
      dataArray.splice(index, 1)
      ElMessage.success('删除成功')
    if (type === 'holiday') {
      ids.push(row.id)
      delHolidaySettings(ids).then(res => {
        if(res.code == 200){
          ElMessage.success("删除成功");
          ids = []
          getHolidaySettingsList()
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    else if (type === 'annual') {
      ids.push(row.id)
      delAnnualLeaveSetting(ids).then(res => {
        if(res.code == 200){
          ElMessage.success("删除成功");
          ids = []
          getAnnualLeaveSettingList()
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    else if (type === 'overtime') {
      ids.push(row.id)
      delOvertimeSetting(ids).then(res => {
        if(res.code == 200){
          ElMessage.success("删除成功");
          ids = []
          getOvertimeSettingList()
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    else if (type === 'worktime') {
      ids.push(row.id)
      delWorkingHoursSetting(ids).then(res => {
        if(res.code == 200){
          ElMessage.success("删除成功");
          ids = []
          getWorkingHoursSettingList()
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    // const index = dataArray.findIndex(item => item.id === row.id)
    // if (index > -1) {
    //   dataArray.splice(index, 1)
    //   ElMessage.success('删除成功')
    // }
  })
}
// èŽ·å–å‡æœŸè®¾ç½®åˆ—è¡¨
const getHolidaySettingsList = () => {
  // tableLoading.value = true;
  listHolidaySettings({...page.value})
  .then(res => {
    // tableLoading.value = false;
    holidayData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
    // tableLoading.value = false;
  })
};
// èŽ·å–å¹´å‡è§„åˆ™åˆ—è¡¨
const getAnnualLeaveSettingList = () => {
  listAnnualLeaveSettingList({...page.value})
  .then(res => {
    // console.log(res.data)
    annualData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
  })
};
// èŽ·å–åŠ ç­è§„åˆ™åˆ—è¡¨
const getOvertimeSettingList = () => {
  listOvertimeSettingList({...page.value})
  .then(res => {
    // console.log(res.data)
    overtimeData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
  })
};
// èŽ·å–å·¥ä½œæ—¶é—´è§„åˆ™åˆ—è¡¨
const getWorkingHoursSettingList = () => {
  listWorkingHoursSettingList({...page.value})
  .then(res => {
    // console.log(res.data)
    worktimeData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
  })
};
onMounted(() => {
  getHolidaySettingsList()
  getAnnualLeaveSettingList()
  getOvertimeSettingList()
  getWorkingHoursSettingList()
  initAttendanceData()
  console.log('考勤管理页面加载完成')
})
src/views/collaborativeApproval/enterpriseBook/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,798 @@
<template>
  <div class="app-container">
    <!-- å¤´éƒ¨å¯¼èˆª -->
    <!-- <div class="header">
      <h2>企业通讯录管理</h2>
      <p>管理个人、公共和单位的联系方式</p>
    </div> -->
    <!-- æ ‡ç­¾é¡µåˆ‡æ¢ -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange" type="border-card">
      <el-tab-pane label="个人通讯录" name="personal">
        <div class="tab-content">
          <!-- æœç´¢æ¡† -->
          <el-input
            v-model="personalSearch.staffName"
            placeholder="搜索联系人"
            clearable
            prefix-icon="Search"
            class="search-input"
            @keyup.enter="getPersonalContactsList"
          />
          <el-button style="margin: 0 0 20px 20px;" type="primary" @click="showAddContactDialog=true">添加联系人</el-button>
          <!-- è”系人列表 -->
          <div class="contact-list">
            <div
              v-for="contact in personalContacts"
              :key="contact.id"
              class="contact-card"
              @click="showContactDetail(contact)"
            >
              <div class="contact-avatar">{{ contact.staffName.charAt(0) }}</div>
              <div class="contact-info">
                <h4>{{ contact.staffName }}</h4>
                <p>{{ contact.profession }} - {{ contact.postJob }}</p>
                <div class="contact-phone">{{ contact.phone }}</div>
              </div>
              <div class="contact-actions">
                <!-- <el-button
                  type="text"
                  icon="Phone"
                  @click.stop="callContact(contact)"
                ></el-button> -->
                <el-button
                  type="text"
                  icon="Message"
                  @click.stop="messageContact(contact)"
                ></el-button>
                <el-button
                  type="text"
                  icon="Delete"
                  @click.stop="removeFromPersonalContacts(contact.id)"
                ></el-button>
              </div>
            </div>
            <!-- ç©ºçŠ¶æ€
            <div v-if="personalContacts.length === 0 && !loading" class="empty-state">
              <el-empty description="暂无联系人" />
              <el-button type="primary" @click="showAddContactDialog=true">添加联系人</el-button>
            </div> -->
          </div>
        </div>
      </el-tab-pane>
      <el-tab-pane label="公共通讯录" name="public">
        <div class="tab-content">
          <!-- æœç´¢æ¡† -->
          <el-input
            v-model="publicSearch.staffName"
            placeholder="搜索公共联系人"
            clearable
            prefix-icon="Search"
            class="search-input"
            @keyup.enter="getPublicContactsList"
          />
          <!-- è”系人列表 publicContacts-->
          <div class="contact-list">
            <div
              v-for="contact in EmployeeList"
              :key="contact.id"
              class="contact-card"
              @click="showContactDetail(contact)"
            >
              <div class="contact-avatar">{{ contact.staffName.charAt(0) }}</div>
              <div class="contact-info">
                <h4>{{ contact.staffName }}</h4>
                <p>{{ contact.postJob }} - {{ contact.profession }}</p>
                <div class="contact-phone">{{ contact.phone }}</div>
              </div>
              <div class="contact-actions">
                <!-- <el-button
                  type="text"
                  icon="Phone"
                  @click.stop="callContact(contact)"
                ></el-button> -->
                <el-button
                  type="text"
                  icon="Message"
                  @click.stop="messageContact(contact)"
                ></el-button>
                <el-button
                  type="text"
                  icon="Delete"
                  :type="isInPersonalContacts(contact.id) ? 'primary' : ''"
                  @click.stop="togglePersonalContact(contact)"
                ></el-button>
              </div>
            </div>
          </div>
        </div>
      </el-tab-pane>
      <el-tab-pane label="单位通讯录" name="company">
        <div class="tab-content">
          <div class="company-contacts-layout">
            <!-- å·¦ä¾§éƒ¨é—¨æ ‘ -->
            <div class="department-tree">
              <!-- <h3>部门结构</h3>
              <el-tree
                :data="departmentTree"
                :props="{ label: 'deptName', children: 'children' }"
                node-key="deptId"
                ref="departmentTreeRef"
                highlight-current
                default-expand-all
                @node-click="handleDepartmentClick"
              /> -->
              <el-col >
                <div class="head-container">
                  <el-input
                    v-model="deptName"
                    placeholder="请输入部门名称"
                    clearable
                    prefix-icon="Search"
                    style="margin-bottom: 20px"
                  />
                </div>
                <div class="head-container">
                  <el-tree
                    :data="departmentTree"
                    :props="{ label: 'label', children: 'children' }"
                    :expand-on-click-node="false"
                    :filter-node-method="filterNode"
                    ref="deptTreeRef"
                    node-key="id"
                    highlight-current
                    default-expand-all
                    @node-click="handleDepartmentClick"
                  />
                </div>
              </el-col>
            </div>
            <!-- å³ä¾§éƒ¨é—¨æˆå‘˜ -->
            <div class="department-members">
              <h3>{{ currentDepartment?.label || '全部成员' }}</h3>
              <el-input
                v-model="companySearch.staffName"
                placeholder="搜索部门成员"
                clearable
                prefix-icon="Search"
                class="search-input"
                @keyup.enter="getCompanyContactsList"
              />
              <div class="contact-list">
                <div
                  v-for="contact in companyContacts"
                  :key="contact.id"
                  class="contact-card"
                  @click="showContactDetail(contact)"
                >
                  <div class="contact-avatar">{{ contact.staffName.charAt(0) }}</div>
                  <div class="contact-info">
                    <h4>{{ contact.staffName }}</h4>
                    <p>{{ contact.profession }}</p>
                    <div class="contact-phone">{{ contact.phone }}</div>
                  </div>
                  <div class="contact-actions">
                    <el-button
                      type="text"
                      icon="Message"
                      @click.stop="messageContact(contact)"
                    ></el-button>
                    <el-button
                      type="text"
                      icon="Delete"
                      :type="isInPersonalContacts(contact.id) ? 'primary' : ''"
                      @click.stop="togglePersonalContact(contact)"
                    ></el-button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </el-tab-pane>
    </el-tabs>
    <!-- è”系人详情弹窗 -->
    <el-dialog
      v-model="showDetailDialog"
      title="联系人详情"
      width="400px"
    >
      <div v-if="selectedContact" class="contact-detail">
        <div class="detail-avatar">{{ selectedContact.staffName?.charAt(0) }}</div>
        <h3>{{ selectedContact.staffName }}</h3>
        <p class="detail-position">{{ selectedContact.profession }} - {{ selectedContact.postJob }}</p>
        <div class="detail-info">
          <div class="info-item">
            <span class="label">编号:</span>
            <span class="value">{{ selectedContact.staffNo }}</span>
          </div>
          <div class="info-item">
            <span class="label">手机号码:</span>
            <span class="value">{{ selectedContact.phone }}</span>
          </div>
          <div class="info-item">
            <span class="label">邮箱:</span>
            <span class="value">{{ selectedContact.sex }}</span>
          </div>
          <div class="info-item">
            <span class="label">住址:</span>
            <span class="value">{{ selectedContact.adress || '暂无' }}</span>
          </div>
        </div>
      </div>
      <template #footer>
        <el-button @click="showDetailDialog = false">关闭</el-button>
        <el-button
          type="primary"
          v-if="activeTab !== 'personal'"
          @click="togglePersonalContact(selectedContact); showDetailDialog = false"
        >
          {{ isInPersonalContacts(selectedContact?.id) ? '从个人通讯录移除' : '添加到个人通讯录' }}
        </el-button>
      </template>
    </el-dialog>
    <!-- æ·»åŠ è”ç³»äººå¼¹çª— -->
    <el-dialog
      v-model="showAddContactDialog"
      title="添加联系人"
      width="500px"
    >
      <el-form :model="addContactForm" ref="addContactFormRef" label-width="80px">
        <!-- <el-form-item label="姓名" prop="name">
          <el-input v-model="addContactForm.name" placeholder="请输入姓名" />
        </el-form-item>
        <el-form-item label="手机号码" prop="phone">
          <el-input v-model="addContactForm.phone" placeholder="请输入手机号码" />
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="addContactForm.email" placeholder="请输入邮箱" />
        </el-form-item>
        <el-form-item label="部门" prop="department">
          <el-input v-model="addContactForm.department" placeholder="请输入部门" />
        </el-form-item> -->
        <el-form-item label="姓名" prop="name">
          <!-- <select v-model="addContactForm.contactId">
            <option v-for="item in EmployeeList" :key="item.id" :value="item.id">{{ item.staffName }}</option>
          </select> -->
          <el-select v-model="addContactForm.contactId" placeholder="请选择" style="width: 100%">
            <el-option
              v-for="option in EmployeeList"
              :key="option.id"
              :label="option.staffName"
              :value="option.id"
            />
          </el-select>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showAddContactDialog = false">取消</el-button>
        <el-button type="primary" @click="addContact">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, onMounted, reactive, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  getPersonalContacts,
  addPersonalContact,
  removePersonalContact,
  getPublicContacts,
  getCompanyContacts,
  getDepartmentTree,
  getEmployeeDetail
} from '@/api/collaborativeApproval/enterpriseBook.js'
import { getUserProfile } from '@/api/system/user.js'
import {staffJoinListPage} from "@/api/personnelManagement/onboarding.js";
import {
  changeUserStatus,
  listUser,
  resetUserPwd,
  delUser,
  getUser,
  updateUser,
  addUser,
  deptTreeSelect,
} from "@/api/system/user";
// æ ‡ç­¾é¡µçŠ¶æ€
const activeTab = ref('personal')
const loading = ref(false)
const EmployeeList = ref([])
const page = reactive({
  pageNum: 1,
  pageSize: 10,
  total: 0,
})
// ä¸ªäººé€šè®¯å½•数据
const personalContacts = ref([])
const personalSearch = ref({
  staffName: '',
})
// å…¬å…±é€šè®¯å½•数据
const publicContacts = ref([])
const publicSearch = ref({
  staffName: '',
  staffState: 1
})
// å•位通讯录数据
const companyContacts = ref([])
const companySearch = ref({
  staffName: '',
  staffState: 1
})
const departmentTree = ref([])
const departmentTreeRef = ref(null)
const currentDepartment = ref(null)
// å¼¹çª—状态
const showDetailDialog = ref(false)
const showAddContactDialog = ref(false)
const selectedContact = ref(null)
// æ·»åŠ è”ç³»äººè¡¨å•
const addContactForm = reactive({
  contactId: '',
  name: '',
  phone: '',
  email: '',
  department: '',
  position: ''
})
const addContactFormRef = ref(null)
// åˆå§‹åŒ–数据
onMounted(() => {
  getEmployeeList()
  getPersonalContactsList()
  if (activeTab.value === 'public') {
    getPublicContactsList()
  } else if (activeTab.value === 'company') {
    getDepartmentTreeData()
    getCompanyContactsList()
  }
})
// å¤„理标签页切换
const handleTabChange = (tabName) => {
  if (tabName === 'public') {
    getPublicContactsList()
  } else if (tabName === 'company') {
    getDepartmentTreeData()
    getCompanyContactsList()
  }
}
// èŽ·å–ä¸ªäººé€šè®¯å½•åˆ—è¡¨
const getPersonalContactsList = async () => {
  loading.value = true
  getPersonalContacts(page,personalSearch.value).then(res => {
    personalContacts.value = res.data.records
  })
  loading.value = false
}
// èŽ·å–å…¬å…±é€šè®¯å½•åˆ—è¡¨
const getPublicContactsList = async () => {
  loading.value = true
  getEmployeeList()
  // publicContacts.value = generateMockPublicContacts()
  loading.value = false
}
  //获取员工列表
const getEmployeeList = async () => {
  staffJoinListPage(publicSearch.value).then(res => {
    console.log(res.data.records)
      EmployeeList.value = res.data.records
    }).catch(err => {})
}
// èŽ·å–å•ä½é€šè®¯å½•åˆ—è¡¨
const getCompanyContactsList = async () => {
  loading.value = true
    staffJoinListPage(companySearch.value).then(res => {
    // console.log(res.data.records)
      companyContacts.value = res.data.records
    }).catch(err => {})
  loading.value = false
 loading.value = false
  // }
}
// èŽ·å–éƒ¨é—¨æ ‘ç»“æž„
const getDepartmentTreeData = async () => {
    deptTreeSelect().then((response) => {
    // console.log("Tree",response.data)
    departmentTree.value = response.data;
    // enabledDeptOptions.value = filterDisabledDept(
    //   JSON.parse(JSON.stringify(response.data))
    // );
  });
}
// /** è¿‡æ»¤ç¦ç”¨çš„部门 */
// function filterDisabledDept(deptList) {
//   return deptList.filter((dept) => {
//     if (dept.disabled) {
//       return false;
//     }
//     if (dept.children && dept.children.length) {
//       dept.children = filterDisabledDept(dept.children);
//     }
//     return true;
//   });
// }
// å¤„理部门点击
const handleDepartmentClick = (data) => {
  // console.log("点击",data)
  companySearch.value = {
    ...companySearch.value,
    deptId: data.id,
  }
  // currentDepartment.value = data.id
  // èŽ·å–è¯¥éƒ¨é—¨çš„æˆå‘˜åˆ—è¡¨
  getCompanyContactsList()
}
// æ˜¾ç¤ºè”系人详情
const showContactDetail = async (contact) => {
  selectedContact.value = contact
  showDetailDialog.value = true
}
// æ‹¨æ‰“电话
const callContact = (contact) => {
  ElMessage.info(`正在拨打 ${contact.name} çš„电话: ${contact.phone}`)
}
// å‘送消息
const messageContact = (contact) => {
  ElMessage.info(`正在发送消息给 ${contact.name}`)
}
// æ·»åŠ è”ç³»äºº
const addContact = async () => {
  try {
    // è¡¨å•验证
    // if (!addContactForm.name || !addContactForm.phone) {
    //   ElMessage.warning('请填写姓名和手机号码')
    //   return
    // }
    const res = await addPersonalContact(addContactForm)
    if (res.code === 200) {
      ElMessage.success('添加成功')
      showAddContactDialog.value = false
      getPersonalContactsList()
      // é‡ç½®è¡¨å•
      Object.keys(addContactForm).forEach(key => {
        addContactForm[key] = ''
      })
    }
  } catch (error) {
    ElMessage.error('添加失败')
    // æ¨¡æ‹Ÿæ·»åŠ æˆåŠŸ
    personalContacts.value.push({
      ...addContactForm,
      id: Date.now(),
      createTime: new Date().toISOString()
    })
    ElMessage.success('添加成功')
    showAddContactDialog.value = false
    // é‡ç½®è¡¨å•
    Object.keys(addContactForm).forEach(key => {
      addContactForm[key] = ''
    })
  }
}
// ä»Žä¸ªäººé€šè®¯å½•移除
const removeFromPersonalContacts = async (contactId) => {
  ElMessageBox.confirm(
    '确定要从个人通讯录中移除该联系人吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(async () => {
    try {
      const res = await removePersonalContact(contactId)
      if (res.code === 200) {
        ElMessage.success('移除成功')
        getPersonalContactsList()
      }
    } catch (error) {
      ElMessage.error('移除失败')
      // æ¨¡æ‹Ÿç§»é™¤æˆåŠŸ
      // personalContacts.value = personalContacts.value.filter(item => item.id !== contactId)
      ElMessage.success('移除成功')
    }
  })
}
// åˆ‡æ¢ä¸ªäººé€šè®¯å½•
const togglePersonalContact = async (contact) => {
  const isInPersonal = isInPersonalContacts(contact.id)
  const contactId = contact.id
  if (isInPersonal) {
    // ä»Žä¸ªäººé€šè®¯å½•移除
    //根据contactId查找personalContacts中对应的项,然后删除该项
    const index = personalContacts.value.findIndex(item => item.contactId === contactId)
    const personId = personalContacts.value[index].id
    // console.log(personId)
    await removeFromPersonalContacts(personId)
  } else {
    // æ·»åŠ åˆ°ä¸ªäººé€šè®¯å½•
    try {
      const res = await addPersonalContact({contactId: contactId})
      if (res.code === 200) {
        ElMessage.success('添加成功')
        getPersonalContactsList()
      }
    } catch (error) {
      ElMessage.error('添加失败')
      // æ¨¡æ‹Ÿæ·»åŠ æˆåŠŸ
      // personalContacts.value.push({
      //   ...contact,
      //   id: contact.id || Date.now(),
      //   createTime: new Date().toISOString()
      // })
      // ElMessage.success('添加成功')
    }
  }
}
// æ£€æŸ¥æ˜¯å¦åœ¨ä¸ªäººé€šè®¯å½•中
const isInPersonalContacts = (contactId) => {
  return personalContacts.value.some(item => item.contactId === contactId)
}
// ç”Ÿæˆæ¨¡æ‹Ÿéƒ¨é—¨æ ‘数据
const generateMockDepartmentTree = () => {
  return [
    {
      deptId: 1,
      deptName: '技术部',
      children: [
        {
          deptId: 101,
          deptName: '前端组'
        },
        {
          deptId: 102,
          deptName: '后端组'
        },
        {
          deptId: 103,
          deptName: '测试组'
        }
      ]
    },
    {
      deptId: 2,
      deptName: '产品部'
    },
    {
      deptId: 3,
      deptName: '人事部'
    },
    {
      deptId: 4,
      deptName: '财务部'
    }
  ]
}
// ç”Ÿæˆæ¨¡æ‹Ÿå•位通讯录数据
// const generateMockCompanyContacts = (deptName) => {
//   const allContacts = getEmployeeList()
//   if (deptName) {
//     return allContacts.filter(contact => contact.postJob === deptName)
//   }
//   return allContacts
// }
</script>
<style scoped>
.header {
  margin-bottom: 20px;
  padding: 15px;
  background: #f5f7fa;
  border-radius: 8px;
}
.header h2 {
  margin: 0 0 5px 0;
  color: #303133;
}
.header p {
  margin: 0;
  color: #909399;
  font-size: 14px;
}
.tab-content {
  padding: 15px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.search-input {
  margin-bottom: 20px;
  width: 300px;
}
.contact-list {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}
.contact-card {
  display: flex;
  align-items: center;
  padding: 15px;
  width: 500px;
  background: #f8f9fa;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}
.contact-card:hover {
  background: #e9ecef;
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.contact-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  background: #409eff;
  color: #fff;
  font-size: 20px;
  font-weight: bold;
  border-radius: 50%;
  margin-right: 15px;
}
.contact-info {
  flex: 1;
}
.contact-info h4 {
  margin: 0 0 5px 0;
  color: #303133;
  font-size: 16px;
}
.contact-info p {
  margin: 0 0 5px 0;
  color: #606266;
  font-size: 14px;
}
.contact-phone {
  color: #409eff;
  font-size: 14px;
}
.contact-actions {
  display: flex;
  gap: 5px;
}
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  color: #909399;
}
.company-contacts-layout {
  display: flex;
  gap: 20px;
}
.department-tree {
  width: 250px;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 8px;
}
.department-tree h3 {
  margin: 0 0 15px 0;
  padding-bottom: 10px;
  border-bottom: 1px solid #e4e7ed;
  color: #303133;
  font-size: 16px;
}
.department-members {
  flex: 1;
}
.department-members h3 {
  margin: 0 0 15px 0;
  color: #303133;
  font-size: 16px;
}
.contact-detail {
  text-align: center;
}
.detail-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 80px;
  height: 80px;
  background: #409eff;
  color: #fff;
  font-size: 32px;
  font-weight: bold;
  border-radius: 50%;
  margin: 0 auto 20px;
}
.contact-detail h3 {
  margin: 0 0 10px 0;
  color: #303133;
  font-size: 20px;
}
.detail-position {
  margin: 0 0 30px 0;
  color: #606266;
  font-size: 14px;
}
.detail-info {
  text-align: left;
}
.info-item {
  margin-bottom: 15px;
}
.info-item .label {
  display: inline-block;
  width: 100px;
  color: #909399;
  font-size: 14px;
}
.info-item .value {
  color: #303133;
  font-size: 14px;
}
</style>
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -13,22 +13,24 @@
        />
        <span class="search_title ml10">知识类型:</span>
        <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
          <el-option label="合同特批" :value="'contract'" />
          <el-option label="审批案例" :value="'approval'" />
          <el-option label="解决方案" :value="'solution'" />
          <el-option label="经验总结" :value="'experience'" />
          <el-option label="操作指南" :value="'guide'" />
          <el-option
              v-for="item in knowledgeTypeOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value"
          />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button @click="handleExport" style="margin-right: 10px">导出</el-button>
        <el-button type="primary" @click="openForm('add')">新增知识</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
@@ -60,11 +62,12 @@
          <el-col :span="12">
            <el-form-item label="知识类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%">
                <el-option label="合同特批" value="contract" />
                <el-option label="审批案例" value="approval" />
                <el-option label="解决方案" value="solution" />
                <el-option label="经验总结" value="experience" />
                <el-option label="操作指南" value="guide" />
                <el-option
                    v-for="item in knowledgeTypeOptions"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
@@ -221,7 +224,7 @@
        <span class="dialog-footer">
          <el-button @click="viewDialogVisible = false">关闭</el-button>
          <el-button type="primary" @click="copyKnowledge">复制知识</el-button>
          <el-button type="success" @click="markAsFavorite">收藏</el-button>
          <!-- <el-button type="success" @click="markAsFavorite">收藏@</el-button> -->
        </span>
      </template>
    </el-dialog>
@@ -230,9 +233,10 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js";
// è¡¨å•验证规则
const rules = {
@@ -259,7 +263,7 @@
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    size: 20,
    total: 0,
  },
  tableData: [],
@@ -268,7 +272,7 @@
    title: "",
    type: "",
    scenario: "",
    efficiency: "medium",
    efficiency: "",
    problem: "",
    solution: "",
    keyPoints: "",
@@ -282,11 +286,11 @@
  currentKnowledge: {}
});
const {
  searchForm,
  tableLoading,
  page,
  tableData,
const {
  searchForm,
  tableLoading,
  page,
  tableData,
  selectedIds,
  form,
  dialogVisible,
@@ -311,24 +315,10 @@
    prop: "type",
    dataType: "tag",
    formatData: (params) => {
      const typeMap = {
        contract: "合同特批",
        approval: "审批案例",
        solution: "解决方案",
        experience: "经验总结",
        guide: "操作指南"
      };
      return typeMap[params] || params;
      return getKnowledgeTypeLabel(params);
    },
    formatType: (params) => {
      const typeMap = {
        contract: "success",
        approval: "warning",
        solution: "primary",
        experience: "info",
        guide: "danger"
      };
      return typeMap[params] || "info";
      return getKnowledgeTypeTagType(params);
    }
  },
  {
@@ -399,111 +389,6 @@
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
let mockData = [
  {
    id: "1",
    title: "特殊合同审批流程优化方案",
    type: "contract",
    scenario: "大额合同快速审批",
    efficiency: "high",
    problem: "大额合同审批流程复杂,审批时间长,影响业务进展",
    solution: "建立绿色通道,对符合条件的合同采用简化审批流程,由部门负责人直接审批,平均审批时间从3天缩短至1天",
    keyPoints: "绿色通道条件,简化流程,审批权限,时间控制",
    creator: "张经理",
    usageCount: 15,
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    title: "跨部门协作审批经验总结",
    type: "experience",
    scenario: "多部门协作项目",
    efficiency: "medium",
    problem: "跨部门项目审批时,各部门意见不统一,审批进度缓慢",
    solution: "建立项目协调机制,指定项目负责人,定期召开协调会议,统一各方意见后再进行审批",
    keyPoints: "项目协调,定期会议,统一意见,负责人制度",
    creator: "李主管",
    usageCount: 8,
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    title: "紧急采购审批操作指南",
    type: "guide",
    scenario: "紧急采购需求",
    efficiency: "high",
    problem: "紧急采购时审批流程复杂,无法满足紧急需求",
    solution: "制定紧急采购审批标准,明确紧急程度分级,不同级别采用不同审批流程,确保紧急需求得到及时处理",
    keyPoints: "紧急分级,标准制定,流程简化,及时处理",
    creator: "王专员",
    usageCount: 12,
    createTime: "2024-01-13 09:15:00"
  }
];
// çŸ¥è¯†æ ‡é¢˜æ¨¡æ¿
const titleTemplates = [
  "{type}审批流程优化方案",
  "{scenario}处理经验总结",
  "{type}特殊情况处理指南",
  "{scenario}快速审批方案",
  "{type}标准化操作流程",
  "{scenario}问题解决方案",
  "{type}最佳实践总结",
  "{scenario}效率提升方案"
];
// çŸ¥è¯†ç±»åž‹é…ç½®
const knowledgeTypes = [
  { type: "contract", label: "合同特批", efficiency: "high" },
  { type: "approval", label: "审批案例", efficiency: "medium" },
  { type: "solution", label: "解决方案", efficiency: "high" },
  { type: "experience", label: "经验总结", efficiency: "medium" },
  { type: "guide", label: "操作指南", efficiency: "low" }
];
// åœºæ™¯åˆ—表
const scenarios = ["大额合同审批", "跨部门协作", "紧急采购", "特殊申请", "流程优化", "问题处理", "标准化建设", "效率提升"];
// è‡ªåŠ¨ç”Ÿæˆæ–°æ•°æ®
const generateNewData = () => {
  const newId = (mockData.length + 1).toString();
  const now = new Date();
  const randomType = knowledgeTypes[Math.floor(Math.random() * knowledgeTypes.length)];
  const randomScenario = scenarios[Math.floor(Math.random() * scenarios.length)];
  // ç”Ÿæˆéšæœºæ ‡é¢˜
  let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
  title = title
    .replace('{type}', randomType.label)
    .replace('{scenario}', randomScenario);
  const newKnowledge = {
    id: newId,
    title: title,
    type: randomType.type,
    scenario: randomScenario,
    efficiency: randomType.efficiency,
    problem: `在${randomScenario}过程中遇到的问题描述...`,
    solution: `针对${randomScenario}的解决方案和操作步骤...`,
    keyPoints: "关键要点1,关键要点2,关键要点3,关键要点4",
    creator: ["张经理", "李主管", "王专员", "刘总监"][Math.floor(Math.random() * 4)],
    usageCount: Math.floor(Math.random() * 20) + 1,
    createTime: now.toLocaleString()
  };
  // æ·»åŠ åˆ°æ•°æ®å¼€å¤´
  mockData.unshift(newKnowledge);
  // ä¿æŒæ•°æ®é‡åœ¨åˆç†èŒƒå›´å†…(最多保留30条)
  if (mockData.length > 30) {
    mockData = mockData.slice(0, 30);
  }
  console.log(`[${new Date().toLocaleString()}] è‡ªåŠ¨ç”Ÿæˆæ–°çŸ¥è¯†: ${title}`);
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  getList();
@@ -513,7 +398,6 @@
// å¼€å§‹è‡ªåŠ¨åˆ·æ–°
const startAutoRefresh = () => {
  setInterval(() => {
    generateNewData();
    getList();
  }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms)
};
@@ -526,24 +410,14 @@
const getList = () => {
  tableLoading.value = true;
  setTimeout(() => {
    let filteredData = [...mockData];
    if (searchForm.value.title) {
      filteredData = filteredData.filter(item =>
        item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
      );
    }
    if (searchForm.value.type) {
      filteredData = filteredData.filter(item => item.type === searchForm.value.type);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
  listKnowledgeBase({...page.value, ...searchForm.value})
  .then(res => {
    tableLoading.value = false;
  }, 500);
    tableData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// åˆ†é¡µå¤„理
@@ -568,7 +442,7 @@
      title: "",
      type: "",
      scenario: "",
      efficiency: "medium",
      efficiency: "",
      problem: "",
      solution: "",
      keyPoints: "",
@@ -578,6 +452,7 @@
  } else if (type === "edit" && row) {
    dialogTitle.value = "编辑知识";
    Object.assign(form.value, {
      id: row.id,
      title: row.title,
      type: row.type,
      scenario: row.scenario,
@@ -612,14 +487,7 @@
// èŽ·å–ç±»åž‹æ ‡ç­¾æ–‡æœ¬
const getTypeLabel = (type) => {
  const typeMap = {
    contract: "合同特批",
    approval: "审批案例",
    solution: "解决方案",
    experience: "经验总结",
    guide: "操作指南"
  };
  return typeMap[type] || type;
  return getKnowledgeTypeLabel(type);
};
// èŽ·å–æ•ˆçŽ‡æ ‡ç­¾ç±»åž‹
@@ -665,15 +533,15 @@
// å¤åˆ¶çŸ¥è¯†
const copyKnowledge = () => {
  const knowledgeText = `
知识标题:${currentKnowledge.value.title}
知识类型:${getTypeLabel(currentKnowledge.value.type)}
适用场景:${currentKnowledge.value.scenario}
问题描述:${currentKnowledge.value.problem}
解决方案:${currentKnowledge.value.solution}
关键要点:${currentKnowledge.value.keyPoints}
创建人:${currentKnowledge.value.creator}
    çŸ¥è¯†æ ‡é¢˜ï¼š${currentKnowledge.value.title}
    çŸ¥è¯†ç±»åž‹ï¼š${getTypeLabel(currentKnowledge.value.type)}
    é€‚用场景:${currentKnowledge.value.scenario}
    é—®é¢˜æè¿°ï¼š${currentKnowledge.value.problem}
    è§£å†³æ–¹æ¡ˆï¼š${currentKnowledge.value.solution}
    å…³é”®è¦ç‚¹ï¼š${currentKnowledge.value.keyPoints}
    åˆ›å»ºäººï¼š${currentKnowledge.value.creator}
  `.trim();
  // å¤åˆ¶åˆ°å‰ªè´´æ¿
  navigator.clipboard.writeText(knowledgeText).then(() => {
    ElMessage.success("知识内容已复制到剪贴板");
@@ -682,62 +550,32 @@
  });
};
// æ”¶è—çŸ¥è¯†
const markAsFavorite = () => {
  // å¢žåŠ ä½¿ç”¨æ¬¡æ•°
  const index = mockData.findIndex(item => item.id === currentKnowledge.value.id);
  if (index !== -1) {
    mockData[index].usageCount += 1;
    currentKnowledge.value.usageCount += 1;
  }
  ElMessage.success("已收藏,使用次数+1");
};
// æäº¤çŸ¥è¯†è¡¨å•
const submitForm = async () => {
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ–°å¢žçŸ¥è¯†
      const newKnowledge = {
        id: (mockData.length + 1).toString(),
        title: form.value.title,
        type: form.value.type,
        scenario: form.value.scenario,
        efficiency: form.value.efficiency,
        problem: form.value.problem,
        solution: form.value.solution,
        keyPoints: form.value.keyPoints,
        creator: form.value.creator,
        usageCount: form.value.usageCount,
        createTime: new Date().toLocaleString()
      };
      mockData.unshift(newKnowledge);
      ElMessage.success("知识创建成功");
      addKnowledgeBase({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("添加成功");
          dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    } else {
      // ç¼–辑知识
      const index = mockData.findIndex(item => item.id === selectedIds.value[0]);
      if (index !== -1) {
        Object.assign(mockData[index], {
          title: form.value.title,
          type: form.value.type,
          scenario: form.value.scenario,
          efficiency: form.value.efficiency,
          problem: form.value.problem,
          solution: form.value.solution,
          keyPoints: form.value.keyPoints,
          creator: form.value.creator,
          usageCount: form.value.usageCount
        });
        ElMessage.success("知识更新成功");
      }
      updateKnowledgeBase({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("更新成功");
          dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
@@ -749,27 +587,42 @@
    ElMessage.warning("请选择要删除的知识");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // ä»ŽmockData中删除选中的项
    selectedIds.value.forEach(id => {
      const index = mockData.findIndex(item => item.id === id);
      if (index !== -1) {
        mockData.splice(index, 1);
    // console.log(selectedIds.value);
    delKnowledgeBase(selectedIds.value).then(res => {
      if(res.code == 200){
        ElMessage.success("删除成功");
        selectedIds.value = [];
        getList();
      }
    });
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
    })
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
};
// å¯¼å‡º
const { proxy } = getCurrentInstance()
const { knowledge_type } = proxy.useDict("knowledge_type")
// å­—典工具
const knowledgeTypeOptions = computed(() => knowledge_type?.value || [])
const getKnowledgeTypeLabel = (val) => {
  const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val))
  return item ? item.label : val
}
const getKnowledgeTypeTagType = (val) => {
  const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val))
  return item?.elTagType || "info"
}
const handleExport = () => {
  proxy.download('/knowledgeBase/export', { ...searchForm.value }, '知识库.xlsx')
}
</script>
<style scoped>
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: '2024-01-15 09:00:00',
    endTime: '2024-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: '2024-01-15 14:00:00',
    endTime: '2024-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: '2024-01-14 16:00:00',
    endTime: '2024-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/meetingManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
<template>
  <div class="app-container">
    <div class="tabs-wrapper">
      <el-tabs
        v-model="activeTab"
        class="meeting-tabs"
        @tab-change="handleTabChange"
      >
        <el-tab-pane label="会议设置" name="setting" />
        <el-tab-pane label="会议列表" name="index" />
        <el-tab-pane label="会议申请" name="application" />
        <el-tab-pane label="会议审批" name="examine" />
        <el-tab-pane label="会议发布" name="publish" />
        <el-tab-pane label="会议总结" name="summary" />
      </el-tabs>
    </div>
    <div class="tab-content">
      <keep-alive>
        <component :is="currentComponent" />
      </keep-alive>
    </div>
  </div>
</template>
<script setup>
import { ref, computed } from 'vue'
import MeetSetting from '../notificationManagement/meetSetting/index.vue'
import MeetIndex from '../notificationManagement/meetIndex/index.vue'
import MeetApplication from '../notificationManagement/meetApplication/index.vue'
import MeetExamine from '../notificationManagement/meetExamine/index.vue'
import MeetPublish from '../notificationManagement/meetPublish/index.vue'
import MeetSummary from '../notificationManagement/summary/index.vue'
const activeTab = ref('setting')
const tabComponentMap = {
  setting: MeetSetting,
  index: MeetIndex,
  application: MeetApplication,
  examine: MeetExamine,
  publish: MeetPublish,
  summary: MeetSummary
}
const currentComponent = computed(() => tabComponentMap[activeTab.value] || MeetSetting)
function handleTabChange(name) {
  activeTab.value = name
}
</script>
<style scoped lang="scss">
.tabs-wrapper {
  margin-bottom: 10px;
}
.tab-content {
  min-height: 400px;
}
</style>
src/views/collaborativeApproval/noticeManagement/index.vue
@@ -3,159 +3,116 @@
    <!-- æœç´¢è¡¨å• -->
    <div class="search_form">
      <div>
        <span class="search_title">公告标题:</span>
        <el-input
          v-model="searchForm.noticeTitle"
          style="width: 240px"
          placeholder="请输入公告标题搜索"
          @change="handleQuery"
          clearable
          :prefix-icon="Search"
        />
        <span class="search_title ml10">公告类型:</span>
        <el-select v-model="searchForm.noticeType" clearable @change="handleQuery" style="width: 240px">
          <el-option label="放假通知" value="1" />
          <el-option label="设备维修通知" value="2" />
        </el-select>
        <span class="search_title ml10">状态:</span>
        <el-select v-model="searchForm.status" clearable @change="handleQuery" style="width: 240px">
          <el-option label="草稿" value="0" />
          <el-option label="已发布" value="1" />
          <el-option label="已下线" value="2" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
        <el-button @click="resetQuery" style="margin-left: 10px">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增公告</el-button>
        <el-button type="danger" plain @click="handleDelete" :disabled="!selectedIds.length">删除</el-button>
        <el-button type="info" @click="openNoticeTypeDialog">公告类型配置</el-button>
      </div>
    </div>
    <!-- é€šçŸ¥å…¬å‘Šæ¿ -->
    <div class="notice-board">
      <!-- æ”¾å‡é€šçŸ¥åŒºåŸŸ -->
      <div class="notice-section" v-if="holidayNotices.length > 0">
        <div class="section-header">
          <h3>📅 æ”¾å‡é€šçŸ¥</h3>
          <span class="section-count">{{ holidayNotices.length }}条</span>
        </div>
        <div class="notice-cards">
          <div
            v-for="notice in holidayNotices"
            :key="notice.id"
            class="notice-card holiday-card"
            :class="{ 'urgent': notice.priority === '3' }"
          >
            <div class="card-header">
              <div class="card-title">
                <el-icon class="holiday-icon"><Calendar /></el-icon>
                {{ notice.noticeTitle }}
              </div>
              <div class="card-actions">
                <el-button link type="primary" @click="handleEdit(notice)">编辑</el-button>
                <el-button link type="danger" @click="handleDelete(notice.id)">删除</el-button>
      <el-tabs v-model="activeNoticeTypeTab" @tab-change="handleNoticeTypeTabChange">
        <el-tab-pane
            v-for="noticeType in noticeTypeList"
            :key="noticeType.id"
            :label="noticeType.noticeType"
            :name="String(noticeType.id)"
        >
          <template #label>
            <span>{{ noticeType.noticeType }}
              <span class="tab-count" v-if="getNoticeCountByType(noticeType.id) > 0">
                ({{ getNoticeCountByType(noticeType.id) }})
              </span>
            </span>
          </template>
          <div class="notice-section">
            <div class="notice-cards">
              <div
                  v-for="notice in getNoticesByType(noticeType.id)"
                  :key="notice.id"
                  class="notice-card"
                  :class="{ 'urgent': notice.priority === '3' }"
              >
                <div class="card-header">
                  <div class="card-title">
                    <el-icon class="notice-icon">
                      <Calendar/>
                    </el-icon>
                    {{ notice.title }}
                  </div>
                  <div class="card-actions">
                    <el-button link type="primary" @click="handleEdit(notice)" :disabled="isNoticeExpired(notice)" v-if="notice.status !== 1">编辑</el-button>
                    <el-button link type="success" @click="handlePublish(notice)" v-if="notice.status === 0">发布</el-button>
                    <el-button link type="danger" @click="handleDelete(notice.id)" v-if="notice.status !== 1">删除</el-button>
                  </div>
                </div>
                <div class="card-content">
                  <p>{{ notice.content }}</p>
                </div>
                <div class="card-footer">
                  <div class="card-meta">
                    <span class="priority" :class="'priority-' + notice.priority">
                      {{ getPriorityText(notice.priority) }}
                    </span>
                    <span class="status" :class="'status-' + getNoticeStatus(notice)">
                      {{ getStatusText(getNoticeStatus(notice)) }}
                    </span>
                  </div>
                  <div class="card-info">
                    <span class="creator">{{ notice.createUserName }}</span>
                    <span class="expiration" v-if="notice.expirationDate">截止日期:{{ notice.expirationDate }}</span>
                  </div>
                </div>
                <div class="card-remark" v-if="notice.remark">
                  <el-icon>
                    <InfoFilled/>
                  </el-icon>
                  <span>{{ notice.remark }}</span>
                </div>
              </div>
            </div>
            <div class="card-content">
              <p>{{ notice.noticeContent }}</p>
            </div>
            <div class="card-footer">
              <div class="card-meta">
                <span class="priority" :class="'priority-' + notice.priority">
                  {{ getPriorityText(notice.priority) }}
                </span>
                <span class="status" :class="'status-' + notice.status">
                  {{ getStatusText(notice.status) }}
                </span>
              </div>
              <div class="card-info">
                <span class="creator">{{ notice.createBy }}</span>
                <span class="time">{{ notice.createTime }}</span>
              </div>
            </div>
            <div class="card-remark" v-if="notice.remark">
              <el-icon><InfoFilled /></el-icon>
              <span>{{ notice.remark }}</span>
            <pagination
                v-if="getNoticePageByType(noticeType.id).total > 0"
                :total="getNoticePageByType(noticeType.id).total"
                :page="getNoticePageByType(noticeType.id).current"
                :limit="getNoticePageByType(noticeType.id).size"
                @pagination="(val) => handleNoticeCurrentChange(noticeType.id, val)"
            />
            <!-- ç©ºçŠ¶æ€ -->
            <div class="empty-state" v-if="getNoticesByType(noticeType.id).length === 0">
              <el-empty description="暂无通知公告"/>
            </div>
          </div>
        </div>
      </div>
      <!-- è®¾å¤‡ç»´ä¿®é€šçŸ¥åŒºåŸŸ -->
      <div class="notice-section" v-if="maintenanceNotices.length > 0">
        <div class="section-header">
          <h3>🔧 è®¾å¤‡ç»´ä¿®é€šçŸ¥</h3>
          <span class="section-count">{{ maintenanceNotices.length }}条</span>
        </div>
        <div class="notice-cards">
          <div
            v-for="notice in maintenanceNotices"
            :key="notice.id"
            class="notice-card maintenance-card"
            :class="{ 'urgent': notice.priority === '3' }"
          >
            <div class="card-header">
              <div class="card-title">
                <el-icon class="maintenance-icon"><Tools /></el-icon>
                {{ notice.noticeTitle }}
              </div>
              <div class="card-actions">
                <el-button link type="primary" @click="handleEdit(notice)">编辑</el-button>
                <el-button link type="danger" @click="handleDelete(notice.id)">删除</el-button>
              </div>
            </div>
            <div class="card-content">
              <p>{{ notice.noticeContent }}</p>
            </div>
            <div class="card-footer">
              <div class="card-meta">
                <span class="priority" :class="'priority-' + notice.priority">
                  {{ getPriorityText(notice.priority) }}
                </span>
                <span class="status" :class="'status-' + notice.status">
                  {{ getStatusText(notice.status) }}
                </span>
              </div>
              <div class="card-info">
                <span class="creator">{{ notice.createBy }}</span>
                <span class="time">{{ notice.createTime }}</span>
              </div>
            </div>
            <div class="card-remark" v-if="notice.remark">
              <el-icon><InfoFilled /></el-icon>
              <span>{{ notice.remark }}</span>
            </div>
          </div>
        </div>
      </div>
      <!-- ç©ºçŠ¶æ€ -->
      <div class="empty-state" v-if="filteredNotices.length === 0">
        <el-empty description="暂无通知公告" />
      </div>
        </el-tab-pane>
      </el-tabs>
    </div>
    <!-- æ–°å¢ž/编辑对话框 -->
    <el-dialog
      :title="dialogTitle"
      v-model="dialogVisible"
      width="800px"
      append-to-body
      @close="resetForm"
    <el-dialog
        :title="dialogTitle"
        v-model="dialogVisible"
        width="800px"
        append-to-body
        @close="resetForm"
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="公告标题" prop="noticeTitle">
              <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
            <el-form-item label="公告标题" prop="title">
              <el-input v-model="form.title" placeholder="请输入公告标题"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="公告类型" prop="noticeType">
              <el-select v-model="form.noticeType" placeholder="请选择公告类型" style="width: 100%">
                <el-option label="放假通知" value="1" />
                <el-option label="设备维修通知" value="2" />
            <el-form-item label="公告类型" prop="type">
              <el-select v-model="form.type" placeholder="请选择公告类型" style="width: 100%">
                <el-option
                    v-for="item in noticeTypeList"
                    :key="item.id"
                    :label="item.noticeType"
                    :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
@@ -164,32 +121,39 @@
          <el-col :span="12">
            <el-form-item label="状态">
              <el-radio-group v-model="form.status">
                <el-radio value="0">草稿</el-radio>
                <el-radio value="1">已发布</el-radio>
                <el-radio value="2">已下线</el-radio>
                <el-radio :value="0">草稿</el-radio>
                <el-radio :value="1">正式发布</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="优先级">
              <el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%">
                <el-option label="普通" value="1" />
                <el-option label="重要" value="2" />
                <el-option label="紧急" value="3" />
                <el-option label="普通" :value="1"/>
                <el-option label="重要" :value="2"/>
                <el-option label="紧急" :value="3"/>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="过期时间" prop="expirationDate">
                            <el-date-picker  v-model="form.expirationDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="date"
                                                             placeholder="请选择" clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="公告内容" prop="noticeContent">
            <el-form-item label="公告内容" prop="content">
              <el-input
                v-model="form.noticeContent"
                type="textarea"
                :rows="6"
                placeholder="请输入公告内容"
                maxlength="500"
                show-word-limit
                  v-model="form.content"
                  type="textarea"
                  :rows="6"
                  placeholder="请输入公告内容"
                  maxlength="500"
                  show-word-limit
              />
            </el-form-item>
          </el-col>
@@ -198,12 +162,12 @@
          <el-col :span="24">
            <el-form-item label="备注">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="3"
                placeholder="请输入备注信息"
                maxlength="200"
                show-word-limit
                  v-model="form.remark"
                  type="textarea"
                  :rows="3"
                  placeholder="请输入备注信息"
                  maxlength="200"
                  show-word-limit
              />
            </el-form-item>
          </el-col>
@@ -216,177 +180,168 @@
        </div>
      </template>
    </el-dialog>
    <!-- å…¬å‘Šç±»åž‹é…ç½®å¼¹æ¡† -->
    <el-dialog
        v-model="noticeTypeDialogVisible"
        title="公告类型配置"
        width="800px"
        @close="handleNoticeTypeDialogClose"
    >
      <div class="notice-type-container">
        <div class="notice-type-header">
          <el-button type="primary" @click="handleAddNoticeType">新增类型</el-button>
        </div>
        <el-table :data="noticeTypeList" border style="width: 100%">
          <el-table-column prop="id" label="ID" width="80" align="center"/>
          <el-table-column prop="noticeType" label="公告类型" align="center">
            <template #default="scope">
              <el-input
                  v-if="scope.row.editing"
                  v-model="scope.row.noticeType"
                  placeholder="请输入公告类型"
              />
              <span v-else>{{ scope.row.noticeType }}</span>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="200" align="center">
            <template #default="scope">
              <el-button
                  v-if="scope.row.editing"
                  link
                  type="primary"
                  size="small"
                  @click="handleSaveNoticeType(scope.row)"
              >
                ä¿å­˜
              </el-button>
              <el-button
                  v-if="scope.row.editing"
                  link
                  type="info"
                  size="small"
                  @click="handleCancelEdit(scope.row)"
              >
                å–消
              </el-button>
              <el-button
                  v-if="!scope.row.editing"
                  link
                  type="primary"
                  size="small"
                  @click="handleEditNoticeType(scope.row)"
              >
                ç¼–辑
              </el-button>
              <el-button
                  v-if="!scope.row.editing"
                  link
                  type="danger"
                  size="small"
                  @click="handleDeleteNoticeType(scope.row)"
              >
                åˆ é™¤
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search, Calendar, Tools, InfoFilled } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {Calendar, InfoFilled} from "@element-plus/icons-vue";
import {onMounted, ref, reactive, toRefs, computed} from "vue";
import {ElMessage, ElMessageBox} from "element-plus";
import {useRoute} from "vue-router";
import useUserStore from "@/store/modules/user";
import {
  addNotice,
  delNotice,
  getCount,
  listNotice,
  updateNotice,
  listNoticeType,
  addNoticeType,
  delNoticeType
} from "../../../api/collaborativeApproval/noticeManagement.js";
import pagination from "../../../components/PIMTable/Pagination.vue";
const userStore = useUserStore();
const route = useRoute();
// å“åº”式数据
const data = reactive({
  searchForm: {
    noticeTitle: "",
    noticeType: "",
    status: "",
    title: "",
    type: undefined,
    status: undefined,
  },
  form: {
    id: undefined,
    noticeTitle: "",
    noticeType: "",
    noticeContent: "",
    status: "0",
    priority: "1",
    title: "",
    type: null,
    content: "",
    status: 0,
    priority: 1,
    remark: "",
    createBy: "",
    createTime: "",
        expirationDate: "",
  },
  rules: {
    noticeTitle: [
      { required: true, message: "公告标题不能为空", trigger: "blur" }
    title: [
      {required: true, message: "公告标题不能为空", trigger: "blur"}
    ],
    noticeType: [
      { required: true, message: "请选择公告类型", trigger: "change" }
    type: [
      {required: true, message: "请选择公告类型", trigger: "change"}
    ],
    noticeContent: [
      { required: true, message: "公告内容不能为空", trigger: "blur" }
    content: [
      {required: true, message: "公告内容不能为空", trigger: "blur"}
    ],
        expirationDate: [
      {required: true, message: "请选择日期", trigger: "change"}
    ]
  }
});
const { searchForm, form, rules } = toRefs(data);
const {searchForm, form, rules} = toRefs(data);
// é¡µé¢çŠ¶æ€
const dialogVisible = ref(false);
const dialogTitle = ref("");
const selectedIds = ref([]);
const formRef = ref();
// æ¨¡æ‹Ÿæ•°æ® - æ ¹æ®æ³•定节假日设计
const mockData = [
  {
    id: 1,
    noticeTitle: "2024年春节放假通知",
    noticeType: "1",
    priority: "2",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年春节放假安排如下:2月10日(初一)至2月17日(初八)放假调休,共8天。2月4日(星期日)、2月18日(星期日)上班。请各部门提前做好工作安排。",
    remark: "放假期间请保持手机畅通,如有紧急事务及时联系",
    createBy: "人事部",
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: 2,
    noticeTitle: "2024年清明节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年清明节放假安排如下:4月4日(星期四)至4月6日(星期六)放假调休,共3天。4月7日(星期日)上班。",
    remark: "请各部门做好值班安排,确保节日期间各项工作正常运转",
    createBy: "行政部",
    createTime: "2024-01-14 14:20:00"
  },
  {
    id: 3,
    noticeTitle: "2024年劳动节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年劳动节放假安排如下:5月1日(星期三)至5月5日(星期日)放假调休,共5天。4月28日(星期日)、5月11日(星期六)上班。",
    remark: "放假前请关闭电源,锁好门窗,注意安全",
    createBy: "行政部",
    createTime: "2024-01-13 09:15:00"
  },
  {
    id: 4,
    noticeTitle: "2024年端午节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年端午节放假安排如下:6月8日(星期六)至6月10日(星期一)放假调休,共3天。6月11日(星期二)上班。",
    remark: "祝大家端午节快乐,阖家幸福!",
    createBy: "行政部",
    createTime: "2024-01-12 16:30:00"
  },
  {
    id: 5,
    noticeTitle: "2024年中秋节放假通知",
    noticeType: "1",
    priority: "1",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年中秋节放假安排如下:9月15日(星期日)至9月17日(星期二)放假调休,共3天。9月14日(星期六)上班。",
    remark: "中秋佳节,祝大家团圆美满,幸福安康!",
    createBy: "行政部",
    createTime: "2024-01-11 11:20:00"
  },
  {
    id: 6,
    noticeTitle: "2024年国庆节放假通知",
    noticeType: "1",
    priority: "2",
    status: "1",
    noticeContent: "根据国务院办公厅通知,2024年国庆节放假安排如下:10月1日(星期二)至10月7日(星期一)放假调休,共7天。9月29日(星期日)、10月12日(星期六)上班。",
    remark: "国庆期间请各部门做好值班安排,确保安全稳定",
    createBy: "行政部",
    createTime: "2024-01-10 15:45:00"
  },
  {
    id: 7,
    noticeTitle: "A车间生产线年度检修通知",
    noticeType: "2",
    priority: "2",
    status: "1",
    noticeContent: "A车间生产线将于2024å¹´1月20日(周六)进行年度检修维护,预计停工8小时。检修内容包括:设备清洁、润滑保养、安全装置检查等。请生产部门提前调整生产计划。",
    remark: "维修期间请相关人员配合,确保检修工作安全顺利进行",
    createBy: "设备部",
    createTime: "2024-01-14 14:20:00"
  },
  {
    id: 8,
    noticeTitle: "B车间设备预防性维护通知",
    noticeType: "2",
    priority: "1",
    status: "1",
    noticeContent: "B车间关键设备将于2024å¹´1月25日进行预防性维护,预计停工4小时。维护内容包括:设备检查、零件更换、性能测试等。请相关部门配合。",
    remark: "维护完成后将进行试运行,确保设备正常运行",
    createBy: "设备部",
    createTime: "2024-01-13 09:15:00"
  }
];
// å…¬å‘Šç±»åž‹é…ç½®ç›¸å…³
const noticeTypeDialogVisible = ref(false);
const noticeTypeList = ref([]);
const activeNoticeTypeTab = ref('');
// é€šçŸ¥æ•°æ® - ä½¿ç”¨ Map å­˜å‚¨ï¼Œkey ä¸ºç±»åž‹ id
const noticesMap = ref({});
const noticePagesMap = ref({});
// è®¡ç®—属性
const filteredNotices = computed(() => {
  let filtered = [...mockData];
  if (searchForm.value.noticeTitle) {
    filtered = filtered.filter(item =>
      item.noticeTitle.includes(searchForm.value.noticeTitle)
    filtered = filtered.filter(item =>
        item.noticeTitle.includes(searchForm.value.noticeTitle)
    );
  }
  if (searchForm.value.noticeType) {
    filtered = filtered.filter(item =>
      item.noticeType === searchForm.value.noticeType
    filtered = filtered.filter(item =>
        item.noticeType === searchForm.value.noticeType
    );
  }
  if (searchForm.value.status !== "") {
    filtered = filtered.filter(item =>
      item.status === searchForm.value.status
    filtered = filtered.filter(item =>
        item.status === searchForm.value.status
    );
  }
  return filtered;
});
const holidayNotices = computed(() => {
  return filteredNotices.value.filter(notice => notice.noticeType === "1");
});
const maintenanceNotices = computed(() => {
  return filteredNotices.value.filter(notice => notice.noticeType === "2");
});
// æ–¹æ³•定义
@@ -396,20 +351,44 @@
const resetQuery = () => {
  searchForm.value = {
    noticeTitle: "",
    noticeType: "",
    title: "",
    type: "",
    status: ""
  };
};
const getPriorityText = (priority) => {
  const priorityMap = { "1": "普通", "2": "重要", "3": "紧急" };
  const priorityMap = {"1": "普通", "2": "重要", "3": "紧急"};
  return priorityMap[priority] || "普通";
};
const getStatusText = (status) => {
  const statusMap = { "0": "草稿", "1": "已发布", "2": "已下线" };
  const statusMap = {"0": "草稿", "1": "已发布", "2": "已过期"};
  return statusMap[status] || "未知";
};
const isNoticeExpired = (notice) => {
  if (!notice || !notice.expirationDate) {
    return false;
  }
  const expiration = new Date(notice.expirationDate);
  if (Number.isNaN(expiration.getTime())) {
    return false;
  }
  expiration.setHours(23, 59, 59, 999);
  return new Date() > expiration;
};
const getNoticeStatus = (notice) => {
  const normalizedStatus = notice && notice.status !== undefined && notice.status !== null
      ? String(notice.status)
      : "0";
  return isNoticeExpired(notice) ? "2" : normalizedStatus;
};
const openForm = (type) => {
@@ -417,44 +396,64 @@
    dialogTitle.value = "新增公告";
    form.value = {
      id: undefined,
      noticeTitle: "",
      noticeType: "",
      noticeContent: "",
      status: "0",
      priority: "1",
      title: "",
      type: undefined,
      content: "",
      status: 0,
      priority: 1,
      remark: "",
      createBy: userStore.name || "当前用户",
      createTime: new Date().toLocaleString()
      expirationDate: "",
    };
  }
  dialogVisible.value = true;
};
const handleEdit = (row) => {
  if (isNoticeExpired(row)) {
    ElMessage.warning("已过期的公告不可编辑");
    return;
  }
  dialogTitle.value = "编辑公告";
  form.value = { ...row };
  form.value = {...row};
  dialogVisible.value = true;
};
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
const handleDelete = (id) => {
  ElMessageBox.confirm(
    "确认删除这条公告吗?",
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
      "确认删除这条公告吗?",
      "提示",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      }
  ).then(() => {
    const index = mockData.findIndex(item => item.id === id);
    if (index > -1) {
      mockData.splice(index, 1);
    delNotice(id).then(res => {
      ElMessage.success("删除成功");
    }
      resetTable()
    })
  });
};
const handlePublish = (notice) => {
  ElMessageBox.confirm(
      "确认发布这条公告吗?",
      "提示",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "info"
      }
  ).then(() => {
    updateNotice({
      ...notice,
      status: 1
    }).then(res => {
      if (res.code === 200) {
        ElMessage.success("发布成功");
        resetTable()
      }
    })
  });
};
@@ -463,24 +462,85 @@
    if (valid) {
      if (form.value.id) {
        // ç¼–辑模式
        const index = mockData.findIndex(item => item.id === form.value.id);
        if (index > -1) {
          mockData[index] = { ...form.value };
        }
        ElMessage.success("修改成功");
        updateNotice(form.value).then(res => {
          ElMessage.success("修改成功");
          resetTable()
        })
      } else {
        // æ–°å¢žæ¨¡å¼
        const newId = Math.max(...mockData.map(item => item.id)) + 1;
        const newNotice = {
          ...form.value,
          id: newId,
          createTime: new Date().toLocaleString()
        };
        mockData.unshift(newNotice);
        ElMessage.success("新增成功");
        addNotice(form.value).then(res => {
          ElMessage.success("新增成功");
          resetTable()
        })
      }
      dialogVisible.value = false;
    }
  });
};
// åˆå§‹åŒ–某个类型的分页数据
const initNoticePage = (typeId) => {
  if (!noticePagesMap.value[typeId]) {
    noticePagesMap.value[typeId] = {
      total: 0,
      current: 1,
      size: 10
    };
  }
  if (!noticesMap.value[typeId]) {
    noticesMap.value[typeId] = [];
  }
};
// èŽ·å–æŸä¸ªç±»åž‹çš„é€šçŸ¥åˆ—è¡¨
const getNoticesByType = (typeId) => {
  return noticesMap.value[typeId] || [];
};
// èŽ·å–æŸä¸ªç±»åž‹çš„åˆ†é¡µæ•°æ®
const getNoticePageByType = (typeId) => {
  return noticePagesMap.value[typeId] || { total: 0, current: 1, size: 10 };
};
// èŽ·å–æŸä¸ªç±»åž‹çš„æ•°é‡
const getNoticeCountByType = (typeId) => {
  return getNoticePageByType(typeId).total || 0;
};
// èŽ·å–æŸä¸ªç±»åž‹çš„é€šçŸ¥æ•°æ®
const fetchNoticesByType = (typeId) => {
  initNoticePage(typeId);
  const pageData = noticePagesMap.value[typeId];
  listNotice({...pageData, type: typeId}).then(res => {
    if (res.code === 200) {
      noticesMap.value[typeId] = res.data.records || [];
      noticePagesMap.value[typeId].total = res.data.total || 0;
    }
  });
};
// å¤„理分页变化
const handleNoticeCurrentChange = (typeId, val) => {
  initNoticePage(typeId);
  noticePagesMap.value[typeId].size = val.limit;
  noticePagesMap.value[typeId].current = val.page;
  fetchNoticesByType(typeId);
};
// å¤„理 tab åˆ‡æ¢
const handleNoticeTypeTabChange = (tabName) => {
  activeNoticeTypeTab.value = tabName;
  const typeId = Number(tabName);
  fetchNoticesByType(typeId);
};
const resetTable = () => {
  // é‡ç½®æ‰€æœ‰ç±»åž‹çš„分页并重新获取数据
  noticeTypeList.value.forEach(type => {
    initNoticePage(type.id);
    noticePagesMap.value[type.id].current = 1;
    noticePagesMap.value[type.id].size = 10;
    fetchNoticesByType(type.id);
  });
};
@@ -488,9 +548,187 @@
  formRef.value?.resetFields();
};
// å…¬å‘Šç±»åž‹é…ç½®ç›¸å…³æ–¹æ³•
const openNoticeTypeDialog = () => {
  noticeTypeDialogVisible.value = true;
  fetchNoticeTypeList();
};
const fetchNoticeTypeList = () => {
  return listNoticeType().then(res => {
    if (res.code === 200) {
      noticeTypeList.value = res.data.map(item => ({
        ...item,
        editing: false
      }));
      // æ£€æŸ¥è·¯ç”±å‚数中的 type
      const routeType = route.query.type;
      let targetTypeId = null;
      if (routeType) {
        // å¦‚果路由参数中有 type,查找对应的类型
        const typeId = Number(routeType);
        const foundType = noticeTypeList.value.find(item => item.id === typeId);
        if (foundType) {
          targetTypeId = typeId;
        }
      }
      // å¦‚果有类型数据
      if (noticeTypeList.value.length > 0) {
        // å¦‚果路由参数指定了类型且存在,使用路由参数的类型
        // å¦åˆ™å¦‚果没有选中 tab,默认选中第一个
        if (targetTypeId !== null) {
          activeNoticeTypeTab.value = String(targetTypeId);
          fetchNoticesByType(targetTypeId);
        } else if (!activeNoticeTypeTab.value) {
          activeNoticeTypeTab.value = String(noticeTypeList.value[0].id);
          fetchNoticesByType(noticeTypeList.value[0].id);
        }
      }
    }
  });
};
const handleAddNoticeType = () => {
  const newItem = {
    id: undefined,
    noticeType: '',
    editing: true
  };
  noticeTypeList.value.push(newItem);
};
const handleEditNoticeType = (row) => {
  // ä¿å­˜åŽŸå§‹å€¼
  row.originalNoticeType = row.noticeType;
  row.editing = true;
};
const handleSaveNoticeType = (row) => {
  if (!row.noticeType || row.noticeType.trim() === '') {
    ElMessage.warning('公告类型不能为空');
    return;
  }
  const data = {
    noticeType: row.noticeType.trim()
  };
  if (row.id) {
    // ç¼–辑模式 - å…ˆåˆ é™¤å†æ·»åŠ ï¼ˆå› ä¸ºåªæœ‰ add å’Œ del æŽ¥å£ï¼‰
    delNoticeType(row.id).then(res => {
      if (res.code === 200) {
        addNoticeType(data).then(addRes => {
          if (addRes.code === 200) {
            ElMessage.success('编辑成功');
            row.editing = false;
            delete row.originalNoticeType;
            fetchNoticeTypeList().then(() => {
              // å¦‚果当前选中的类型被编辑,需要重新获取数据
              if (activeNoticeTypeTab.value === String(row.id)) {
                fetchNoticesByType(addRes.data?.id || row.id);
              }
            });
          }
        });
      }
    });
  } else {
    // æ–°å¢žæ¨¡å¼
    addNoticeType(data).then(res => {
      if (res.code === 200) {
        ElMessage.success('新增成功');
        row.editing = false;
        fetchNoticeTypeList();
      }
    });
  }
};
const handleDeleteNoticeType = (row) => {
  // å¦‚果没有id,说明是新增但未保存的行,直接从前端删除
  if (!row.id) {
    const index = noticeTypeList.value.indexOf(row);
    if (index > -1) {
      noticeTypeList.value.splice(index, 1);
    }
    return;
  }
  // å¦‚果有id,调用后端接口删除
  ElMessageBox.confirm(
      "确认删除这个公告类型吗?",
      "提示",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      }
  ).then(() => {
    delNoticeType(row.id).then(res => {
      if (res.code === 200) {
        ElMessage.success("删除成功");
        // å¦‚果删除的是当前选中的类型,切换到第一个类型
        if (activeNoticeTypeTab.value === String(row.id)) {
          fetchNoticeTypeList().then(() => {
            if (noticeTypeList.value.length > 0) {
              activeNoticeTypeTab.value = String(noticeTypeList.value[0].id);
              fetchNoticesByType(noticeTypeList.value[0].id);
            } else {
              activeNoticeTypeTab.value = '';
            }
          });
        } else {
          fetchNoticeTypeList();
        }
      }
    });
  });
};
const handleCancelEdit = (row) => {
  if (!row.id) {
    // å¦‚果是新增但未保存的行,移除它
    const index = noticeTypeList.value.indexOf(row);
    if (index > -1) {
      noticeTypeList.value.splice(index, 1);
    }
  } else {
    // å¦‚果是编辑中的行,取消编辑状态并恢复原值
    row.editing = false;
    if (row.originalNoticeType !== undefined) {
      row.noticeType = row.originalNoticeType;
      delete row.originalNoticeType;
    }
  }
};
const handleNoticeTypeDialogClose = () => {
  // å…³é—­å¼¹æ¡†æ—¶ï¼Œå–消所有编辑状态
  noticeTypeList.value.forEach(item => {
    if (item.editing && !item.id) {
      // å¦‚果是新增但未保存的行,移除它
      const index = noticeTypeList.value.indexOf(item);
      if (index > -1) {
        noticeTypeList.value.splice(index, 1);
      }
    } else if (item.editing) {
      // å¦‚果是编辑中的行,取消编辑状态并恢复原值
      item.editing = false;
      if (item.originalNoticeType !== undefined) {
        item.noticeType = item.originalNoticeType;
        delete item.originalNoticeType;
      }
    }
  });
};
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  // é¡µé¢åŠ è½½å®Œæˆ
  // å…ˆèŽ·å–å…¬å‘Šç±»åž‹åˆ—è¡¨ï¼Œç„¶åŽæ ¹æ®ç±»åž‹èŽ·å–é€šçŸ¥æ•°æ®
  fetchNoticeTypeList();
});
</script>
@@ -569,12 +807,16 @@
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.holiday-card {
  border-left-color: #67c23a;
.notice-icon {
  color: #409eff;
  margin-right: 8px;
  font-size: 18px;
}
.maintenance-card {
  border-left-color: #e6a23c;
.tab-count {
  color: #909399;
  font-size: 12px;
  margin-left: 4px;
}
.urgent {
@@ -598,17 +840,6 @@
  flex: 1;
}
.holiday-icon {
  color: #67c23a;
  margin-right: 8px;
  font-size: 18px;
}
.maintenance-icon {
  color: #e6a23c;
  margin-right: 8px;
  font-size: 18px;
}
.card-actions {
  display: flex;
@@ -624,6 +855,9 @@
  color: #606266;
  line-height: 1.6;
  font-size: 14px;
  word-break: break-all;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.card-footer {
@@ -645,13 +879,35 @@
  font-weight: 500;
}
.priority-1 { background: #f0f9ff; color: #0369a1; }
.priority-2 { background: #fef3c7; color: #d97706; }
.priority-3 { background: #fef2f2; color: #dc2626; }
.priority-1 {
  background: #f0f9ff;
  color: #0369a1;
}
.status-0 { background: #f3f4f6; color: #6b7280; }
.status-1 { background: #d1fae5; color: #059669; }
.status-2 { background: #fef3c7; color: #d97706; }
.priority-2 {
  background: #fef3c7;
  color: #d97706;
}
.priority-3 {
  background: #fef2f2;
  color: #dc2626;
}
.status-0 {
  background: #f3f4f6;
  color: #6b7280;
}
.status-1 {
  background: #d1fae5;
  color: #059669;
}
.status-2 {
  background: #fef3c7;
  color: #d97706;
}
.card-info {
  display: flex;
@@ -664,6 +920,10 @@
.creator {
  font-weight: 500;
  margin-bottom: 2px;
}
.expiration {
  margin-top: 2px;
}
.card-remark {
@@ -687,17 +947,27 @@
  text-align: right;
}
.notice-type-container {
  padding: 10px 0;
}
.notice-type-header {
  margin-bottom: 15px;
  display: flex;
  justify-content: flex-end;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .notice-cards {
    grid-template-columns: 1fr;
  }
  .search_form {
    flex-direction: column;
    gap: 15px;
  }
  .search_form > div {
    width: 100%;
  }
src/views/collaborativeApproval/notificationManagement/index.vue
@@ -87,6 +87,8 @@
                v-model="form.expireDate"
                type="date"
                placeholder="请选择有效期"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
@@ -152,6 +154,8 @@
              <el-date-picker
                v-model="meetingForm.startTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm:ss"
                placeholder="请选择开始时间"
                style="width: 100%"
              />
@@ -318,7 +322,9 @@
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { staffOnJobListPage } from "@/api/personnelManagement/employeeRecord.js";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import { listNotification, addNotification, updateNotification, delNotification,addOnlineMeeting,addFileSharing } from "@/api/collaborativeApproval/notificationManagement.js";
import { id } from "element-plus/es/locales.mjs";
// è¡¨å•验证规则
const rules = {
@@ -364,7 +370,7 @@
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    size: 20,
    total: 0,
  },
  tableData: [],
@@ -373,7 +379,7 @@
  form: {
    title: "",
    type: "",
    priority: "medium",
    priority: "",
    content: "",
    departments: [],
    expireDate: "",
@@ -403,11 +409,11 @@
  fileList: []
});
const {
  searchForm,
  tableLoading,
  page,
  tableData,
const {
  searchForm,
  tableLoading,
  page,
  tableData,
  selectedIds,
  form,
  dialogVisible,
@@ -543,7 +549,6 @@
        clickFun: (row) => {
          publishNotification(row);
        },
        // disabled: (row) => row.status === "published"
      },
      {
        name: "撤回",
@@ -551,52 +556,10 @@
        clickFun: (row) => {
          revokeNotification(row);
        },
        // disabled: (row) => row.status !== "published"
      }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
let mockData = [
  {
    id: "1",
    title: "2024年春节放假通知",
    type: "holiday",
    priority: "high",
    status: "published",
    content: "根据国家规定,结合公司实际情况,现将2024年春节放假安排通知如下...",
    departments: ["技术部", "销售部", "人事部", "财务部", "运营部", "市场部", "客服部"],
    expireDate: "2024-02-15",
    syncMethods: ["wechat", "dingtalk", "email"],
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    title: "技术部周例会通知",
    type: "meeting",
    priority: "medium",
    status: "published",
    content: "技术部定于每周五下午2点召开周例会,请各位同事准时参加...",
    departments: ["技术部"],
    expireDate: "2024-01-20",
    syncMethods: ["wechat", "dingtalk"],
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    title: "员工行为规范处罚通知",
    type: "penalty",
    priority: "high",
    status: "draft",
    content: "为维护公司正常秩序,规范员工行为,现对违反公司规定的行为进行处罚...",
    departments: ["人事部", "技术部", "销售部"],
    expireDate: "2024-02-13",
    syncMethods: ["wechat", "email"],
    createTime: "2024-01-13 09:15:00"
  }
];
// é€šçŸ¥æ ‡é¢˜æ¨¡æ¿
const titleTemplates = [
  "关于{year}å¹´{holiday}放假安排的通知",
@@ -631,7 +594,7 @@
    employeesLoading.value = true;
    // ä¼˜å…ˆä½¿ç”¨ç³»ç»Ÿç”¨æˆ·æŽ¥å£ï¼ˆæŒ‰ç§Ÿæˆ·èŽ·å–ï¼‰
    const userResponse = await userListNoPageByTenantId();
    if (userResponse.data) {
      employees.value = userResponse.data.map(user => ({
        label: user.nickName || user.userName || '未知姓名',
@@ -643,12 +606,12 @@
      })).filter(user => user.status === '0'); // åªæ˜¾ç¤ºæ­£å¸¸çŠ¶æ€çš„ç”¨æˆ·
    } else {
      // å¦‚果系统用户接口失败,使用员工台账接口
      const response = await staffOnJobListPage({
        pageNum: 1,
        pageSize: 1000,
      const response = await staffOnJobListPage({
        pageNum: 1,
        pageSize: 1000,
        staffState: 1 // åœ¨èŒçŠ¶æ€
      });
      if (response.data && response.data.records) {
        employees.value = response.data.records.map(employee => ({
          label: employee.staffName || employee.name || '未知姓名',
@@ -683,7 +646,7 @@
    }
    groups[dept].push(employee);
  });
  // æŒ‰éƒ¨é—¨åç§°æŽ’序,确保显示顺序一致
  return Object.keys(groups)
    .sort()
@@ -697,7 +660,7 @@
const filterEmployees = (query) => {
  if (query !== '') {
    const lowerQuery = query.toLowerCase();
    return employees.value.filter(employee =>
    return employees.value.filter(employee =>
      employee.label.toLowerCase().includes(lowerQuery) ||
      employee.dept.toLowerCase().includes(lowerQuery) ||
      (employee.phone && employee.phone.includes(query)) ||
@@ -712,18 +675,18 @@
const refreshEmployees = async () => {
  ElMessage.info("正在刷新员工列表...");
  await getEmployeesList();
  // ç»Ÿè®¡å„部门人数
  const deptStats = {};
  employees.value.forEach(emp => {
    const dept = emp.dept || '其他部门';
    deptStats[dept] = (deptStats[dept] || 0) + 1;
  });
  const deptInfo = Object.entries(deptStats)
    .map(([dept, count]) => `${dept}: ${count}人`)
    .join(', ');
  ElMessage.success(`员工列表刷新完成,共 ${employees.value.length} äºº (${deptInfo})`);
};
@@ -737,7 +700,7 @@
const getEmployeeInfo = (employeeId) => {
  const employee = employees.value.find(emp => emp.value === employeeId);
  if (!employee) return null;
  return {
    name: employee.label,
    dept: employee.dept,
@@ -776,7 +739,7 @@
  const now = new Date();
  const randomType = notificationTypes[Math.floor(Math.random() * notificationTypes.length)];
  const randomDept = departments[Math.floor(Math.random() * departments.length)];
  // ç”Ÿæˆéšæœºæ ‡é¢˜
  let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
  title = title
@@ -788,15 +751,15 @@
    .replace('{company}', ['公司', '集团', '总部'][Math.floor(Math.random() * 4)])
    .replace('{project}', ['数字化转型', '产品升级', '市场拓展', '人才培养'][Math.floor(Math.random() * 4)])
    .replace('{policy}', ['考勤', '薪酬', '福利', '晋升'][Math.floor(Math.random() * 4)]);
  // éšæœºçŠ¶æ€
  const statuses = ['draft', 'published'];
  const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
  // éšæœºä¼˜å…ˆçº§
  const priorities = ['low', 'medium', 'high'];
  const randomPriority = priorities[Math.floor(Math.random() * priorities.length)];
  const newNotification = {
    id: newId,
    title: title,
@@ -809,15 +772,15 @@
    syncMethods: ["wechat", "dingtalk"],
    createTime: now.toLocaleString()
  };
  // æ·»åŠ åˆ°æ•°æ®å¼€å¤´
  mockData.unshift(newNotification);
  // ä¿æŒæ•°æ®é‡åœ¨åˆç†èŒƒå›´å†…(最多保留20条)
  if (mockData.length > 20) {
    mockData = mockData.slice(0, 20);
  }
  console.log(`[${new Date().toLocaleString()}] è‡ªåŠ¨ç”Ÿæˆæ–°é€šçŸ¥: ${title}`);
};
@@ -844,24 +807,14 @@
const getList = () => {
  tableLoading.value = true;
  setTimeout(() => {
    let filteredData = [...mockData];
    if (searchForm.value.title) {
      filteredData = filteredData.filter(item =>
        item.title.toLowerCase().includes(searchForm.value.title.toLowerCase())
      );
    }
    if (searchForm.value.type) {
      filteredData = filteredData.filter(item => item.type === searchForm.value.type);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
  listNotification({...page.value, ...searchForm.value})
  .then(res => {
    tableLoading.value = false;
  }, 500);
    tableData.value = res.data.records
    page.value.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// åˆ†é¡µå¤„理
@@ -883,23 +836,27 @@
    dialogTitle.value = "新增通知";
    // é‡ç½®è¡¨å•
    Object.assign(form.value, {
      id: "",
      title: "",
      type: "",
      priority: "medium",
      priority: "",
      content: "",
      departments: [],
      expireDate: "",
      status: "draft",
      syncMethods: []
    });
  } else if (type === "edit" && row) {
    dialogTitle.value = "编辑通知";
    Object.assign(form.value, {
      id: row.id,
      title: row.title,
      type: row.type,
      priority: row.priority,
      content: row.content || "",
      departments: row.departments || [],
      expireDate: row.expireDate || "",
      status: row.status,
      syncMethods: row.syncMethods || []
    });
  }
@@ -944,43 +901,30 @@
const submitForm = async () => {
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ–°å¢žé€šçŸ¥
      const newNotification = {
        id: (mockData.length + 1).toString(),
        title: form.value.title,
        type: form.value.type,
        priority: form.value.priority,
        status: "draft",
        content: form.value.content,
        departments: form.value.departments,
        expireDate: form.value.expireDate,
        syncMethods: form.value.syncMethods,
        createTime: new Date().toLocaleString()
      };
      mockData.unshift(newNotification);
      ElMessage.success("通知创建成功");
      addNotification({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("添加成功");
          dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    } else {
      // ç¼–辑通知
      const index = mockData.findIndex(item => item.id === selectedIds.value[0]);
      if (index !== -1) {
        Object.assign(mockData[index], {
          title: form.value.title,
          type: form.value.type,
          priority: form.value.priority,
          content: form.value.content,
          departments: form.value.departments,
          expireDate: form.value.expireDate,
          syncMethods: form.value.syncMethods
        });
        ElMessage.success("通知更新成功");
      }
      updateNotification({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("更新成功");
          dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
@@ -990,7 +934,7 @@
const createMeeting = async () => {
  try {
    await meetingFormRef.value.validate();
    // æ¨¡æ‹Ÿåˆ›å»ºä¼šè®®
    const meetingInfo = {
      title: meetingForm.value.title,
@@ -998,22 +942,28 @@
      duration: meetingForm.value.duration,
      participants: meetingForm.value.participants,
      description: meetingForm.value.description,
      platform: meetingForm.value.platform,
      meetingId: `MTG${Date.now()}`
      platform: meetingForm.value.platform
    };
    // æ–°å¢žä¼šè®®
    addOnlineMeeting({...meetingInfo}).then(res => {
      if(res.code == 200){
        ElMessage.success("会议添加成功");
        meetingDialogVisible.value = false;
        getList();
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // æ¨¡æ‹Ÿå‘送到企业微信/钉钉
    const platformName = meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台";
    ElMessage.success(`会议创建成功!会议ID: ${meetingInfo.meetingId},将通过${platformName}发送通知`);
    meetingDialogVisible.value = false;
         // èŽ·å–å‚ä¼šäººå‘˜ä¿¡æ¯
    // const platformName = meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台";
    // ElMessage.success(`会议创建成功!会议ID: ${meetingInfo.meetingId},将通过${platformName}发送通知`);
    // èŽ·å–å‚ä¼šäººå‘˜ä¿¡æ¯
     const participantNames = meetingForm.value.participants.map(participantId => {
       const employee = employees.value.find(emp => emp.value === participantId);
       return employee ? employee.label : '未知人员';
     }).join('、');
     // èŽ·å–å‚ä¼šäººå‘˜è¯¦ç»†ä¿¡æ¯
     const participantDetails = meetingForm.value.participants.map(participantId => {
       const employee = employees.value.find(emp => emp.value === participantId);
@@ -1024,23 +974,29 @@
         email: employee.email
       } : null;
     }).filter(Boolean);
    // å°†ä¼šè®®ä¿¡æ¯æ·»åŠ åˆ°é€šçŸ¥åˆ—è¡¨
    const meetingNotification = {
      id: (mockData.length + 1).toString(),
      title: `[会议通知] ${meetingInfo.title}`,
      type: "meeting",
      priority: "high",
      status: "published",
             content: `会议时间: ${meetingInfo.startTime},时长: ${meetingInfo.duration}分钟,平台: ${meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台"},参会人员: ${participantNames},共${participantDetails.length}人`,
      content: `会议时间: ${meetingInfo.startTime},时长: ${meetingInfo.duration}分钟,平台: ${meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "未知平台"},参会人员: ${participantNames},共${participantDetails.length}人`,
      departments: [],
      expireDate: "",
      syncMethods: [meetingForm.value.platform],
      createTime: new Date().toLocaleString()
      syncMethods: [meetingForm.value.platform]
    };
    mockData.unshift(meetingNotification);
    getList();
    addNotification({...meetingNotification}).then(res => {
        if(res.code == 200){
          ElMessage.success("添加成功");
          // dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    // mockData.unshift(meetingNotification);
    // getList();
  } catch (error) {
    console.error("会议表单验证失败:", error);
  }
@@ -1053,16 +1009,16 @@
    ElMessage.error("上传文件大小不能超过 10MB!");
    return false;
  }
  const fileInfo = {
    name: file.name,
    size: file.size,
    type: file.type,
    uid: file.uid
  };
  fileList.value.push(fileInfo);
  fileShareForm.value.files.push(fileInfo);
  fileShareForm.value.files.push(fileInfo.name);
  return false; // é˜»æ­¢è‡ªåŠ¨ä¸Šä¼ 
};
@@ -1082,27 +1038,34 @@
const shareFiles = async () => {
  try {
    await fileShareFormRef.value.validate();
    if (fileShareForm.value.files.length === 0) {
      ElMessage.warning("请至少选择一个文件");
      return;
    }
    // æ¨¡æ‹Ÿæ–‡ä»¶å…±äº«
    const shareInfo = {
      title: fileShareForm.value.title,
      description: fileShareForm.value.description,
      departments: fileShareForm.value.departments,
      files: fileShareForm.value.files,
      shareId: `FILE${Date.now()}`
    };
    ElMessage.success(`文件共享成功!共享ID: ${shareInfo.shareId},已通知相关部门`);
    fileShareDialogVisible.value = false;
    addFileSharing({...shareInfo}).then(res => {
      if(res.code == 200){
        ElMessage.success("文件共享成功");
        fileShareDialogVisible.value = false;
        getList();
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // ElMessage.success(`文件共享成功!共享ID: ${shareInfo.shareId},已通知相关部门`);
    // å°†æ–‡ä»¶å…±äº«ä¿¡æ¯æ·»åŠ åˆ°é€šçŸ¥åˆ—è¡¨
    const fileShareNotification = {
      id: (mockData.length + 1).toString(),
      title: `[文件共享] ${shareInfo.title}`,
      type: "temporary",
      priority: "medium",
@@ -1111,11 +1074,19 @@
      departments: shareInfo.departments,
      expireDate: "",
      syncMethods: ["wechat", "dingtalk"],
      createTime: new Date().toLocaleString()
    };
    mockData.unshift(fileShareNotification);
    getList();
    addNotification({...fileShareNotification}).then(res => {
      if(res.code == 200){
        ElMessage.success("添加成功");
        // dialogVisible.value = false;
        getList();
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
    // mockData.unshift(fileShareNotification);
    // getList();
  } catch (error) {
    console.error("文件共享表单验证失败:", error);
  }
@@ -1123,33 +1094,75 @@
// å‘布通知
const publishNotification = (row) => {
  row.status = "published";
  ElMessage.success("通知发布成功");
  getList();
  Object.assign(form.value, {
    id: row.id,
    title: row.title,
    type: row.type,
    priority: row.priority,
    content: row.content || "",
    departments: row.departments || [],
    expireDate: row.expireDate || "",
    status: row.status,
    syncMethods: row.syncMethods || []
  });
  form.value.status = "published";
  updateNotification({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("通知发布成功");
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
};
// æ’¤å›žé€šçŸ¥
const revokeNotification = (row) => {
  row.status = "draft";
  ElMessage.success("通知已撤回");
  getList();
    Object.assign(form.value, {
    id: row.id,
    title: row.title,
    type: row.type,
    priority: row.priority,
    content: row.content || "",
    departments: row.departments || [],
    expireDate: row.expireDate || "",
    status: row.status,
    syncMethods: row.syncMethods || []
  });
  form.value.status = "draft";
  updateNotification({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("通知已撤回");
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
};
// åˆ é™¤é€šçŸ¥
const handleDelete = () => {
  if (selectedIds.value.length === 0) {
  let ids = [];
  if (selectedIds.value.length > 0) {
    ids = selectedIds.value;
  }else{
    ElMessage.warning("请选择要删除的通知");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
    delNotification(ids).then(res => {
      if(res.code == 200){
        ElMessage.success("删除成功");
        selectedIds.value = [];
        getList();
      }
    }).catch(err => {
      ElMessage.error(err.msg);
    })
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,394 @@
<template>
  <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>
    <!-- é¡µé¢æ ‡é¢˜ -->
    <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,414 @@
<template>
  <div>
    <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-table v-loading="loading" :data="approvalList" border :height="tableHeight">
        <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 tableHeight = ref(window.innerHeight - 380)
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,363 @@
<template>
  <div>
    <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 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,412 @@
<template>
  <div>
    <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-table v-loading="loading" :data="approvalList" border :height="tableHeight">
        <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 tableHeight = ref(window.innerHeight - 380)
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,320 @@
<template>
  <div>
    <!-- æœç´¢åŒºåŸŸ -->
    <el-form :model="searchForm" label-width="100px" class="search-form">
      <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-item class="search-actions">
        <el-button @click="handleExport">导出</el-button>
        <el-button type="primary" @click="handleAdd">
          <el-icon><Plus /></el-icon>
          æ–°å¢žä¼šè®®å®¤
        </el-button>
      </el-form-item>
    </el-form>
    <!-- ä¼šè®®å®¤åˆ—表 -->
    <el-card>
      <el-table v-loading="loading" :data="meetingRoomList" border :height="tableHeight">
        <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, getCurrentInstance } 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 tableHeight = ref(window.innerHeight - 380)
// ä¼šè®®å®¤åˆ—表数据
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()
      })
    }
  })
}
// å¯¼å‡º
const { proxy } = getCurrentInstance()
const handleExport = () => {
  proxy.download('/meeting/export', { ...searchForm }, '会议室设置.xlsx')
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.search-form {
  display: flex;
  /* align-items: center; */
}
.search-actions {
  margin-left: auto;
}
.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,399 @@
<template>
  <div>
    <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-table v-loading="loading" :data="meetingList" border :height="tableHeight">
        <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 tableHeight = ref(window.innerHeight - 380)
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/collaborativeApproval/officeSupplies/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,512 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <template #header>
        <div class="card-header">
          <span>办公物资申请管理</span>
          <el-button type="primary" @click="openShow()">
            <el-icon><Plus /></el-icon>
            æ–°å»ºç”³è¯·
          </el-button>
        </div>
      </template>
             <!-- æœç´¢åŒºåŸŸ -->
       <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="申请编号" prop="code">
           <el-input
             v-model="queryParams.code"
             placeholder="请输入申请编号"
             clearable
             style="width: 200px"
             @keyup.enter="handleQuery"
           />
         </el-form-item>
         <el-form-item label="申请人" prop="applicant">
           <el-input
             v-model="queryParams.applicant"
             placeholder="请输入申请人"
             clearable
             style="width: 200px"
             @keyup.enter="handleQuery"
           />
         </el-form-item>
         <el-form-item label="申请状态" prop="status">
           <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
             <el-option label="待审批" value="1" />
             <el-option label="已通过" value="3" />
             <el-option label="已拒绝" value="2" />
             <el-option label="已发放" value="4" />
           </el-select>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleQuery">
             <el-icon><Search /></el-icon>
             æœç´¢
           </el-button>
           <el-button @click="resetQuery">
             <el-icon><Refresh /></el-icon>
             é‡ç½®
           </el-button>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" @click="handleExport">
            <el-icon><Download /></el-icon>
            å¯¼å‡º
          </el-button>
         </el-form-item>
       </el-form>
      <!-- è¡¨æ ¼åŒºåŸŸ -->
      <el-table
        v-loading="loading"
        :data="officeList"
        @selection-change="handleSelectionChange"
        style="width: 100%"
      >
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="申请编号" align="center" prop="code" width="180" />
        <el-table-column label="申请人" align="center" prop="applicant" width="120" />
        <el-table-column label="部门" align="center" prop="dept" width="120" />
        <el-table-column label="物资类型" align="center" prop="materialType" width="120">
          <template #default="scope">
            <el-tag v-if="scope.row.materialType === 1" type="info">其他</el-tag>
            <el-tag v-if="scope.row.materialType === 2" type="success">清洁用品</el-tag>
            <el-tag v-if="scope.row.materialType === 3" type="warning">电子设备</el-tag>
            <el-tag v-if="scope.row.materialType === 4" type="danger">办公用品</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="申请数量" align="center" prop="applyNum" width="100" />
        <el-table-column label="申请原因" align="center" prop="reason" min-width="200" show-overflow-tooltip />
        <el-table-column label="申请状态" align="center" prop="status" width="100">
          <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" prop="applyTime" width="180" />
        <el-table-column label="审批人" align="center" prop="approval" width="120" />
        <el-table-column label="审批时间" align="center" prop="approvalTime" width="180" />
        <el-table-column label="发放时间" align="center" prop="issueTime" width="180" />
        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width" width="200">
          <template #default="scope">
            <el-button
              v-if="scope.row.status === 1"
              type="primary"
              link
              @click="handleApprove(scope.row)"
            >
              å®¡æ‰¹
            </el-button>
            <el-button
              v-if="scope.row.status === 3"
              type="success"
                            link
              @click="handleIssue(scope.row)"
            >
              å‘放
            </el-button>
            <el-button
              type="info"
                            link
              @click="handleDetail(scope.row)"
            >
              è¯¦æƒ…
            </el-button>
            <el-button
              v-if="scope.row.status === 2"
              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
      v-model="showApplyDialog"
      title="办公物资申请"
      width="600px"
      append-to-body
    >
      <el-form ref="applyFormRef" :model="applyForm" :rules="applyRules" label-width="100px">
        <el-form-item label="申请人" prop="applicant">
          <el-input v-model="applyForm.applicant" placeholder="请输入申请人名称" />
        </el-form-item>
        <el-form-item label="部门" prop="dept">
          <el-input v-model="applyForm.dept" placeholder="请输入部门名称" />
        </el-form-item>
        <el-form-item label="物资类型" prop="materialType">
          <el-select v-model="applyForm.materialType" placeholder="请选择物资类型" style="width: 100%">
            <el-option label="办公用品" value="4" />
            <el-option label="电子设备" value="3" />
            <el-option label="清洁用品" value="2" />
            <el-option label="其他" value="1" />
          </el-select>
        </el-form-item>
        <el-form-item label="具体物品" prop="itemName">
          <el-input v-model="applyForm.itemName" placeholder="请输入具体物品名称" />
        </el-form-item>
        <el-form-item label="申请数量" prop="applyNum">
          <el-input-number v-model="applyForm.applyNum" :min="1" :max="999" style="width: 100%" />
        </el-form-item>
        <el-form-item label="申请原因" prop="reason">
          <el-input
            v-model="applyForm.reason"
            type="textarea"
            :rows="3"
            placeholder="请输入申请原因"
          />
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="applyForm.urgency">
            <el-radio label="1">普通</el-radio>
            <el-radio label="2">紧急</el-radio>
            <el-radio label="3">非常紧急</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="showApplyDialog = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitApply">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- å®¡æ‰¹å¯¹è¯æ¡† -->
    <el-dialog
      v-model="showApproveDialog"
      title="审批申请"
      width="500px"
      append-to-body
    >
      <el-form ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="100px">
        <el-form-item label="审批结果" prop="approveResult">
          <el-radio-group v-model="approveForm.approveResult">
            <el-radio label="3">通过</el-radio>
            <el-radio label="2">拒绝</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="审批意见" prop="approvalOpinions">
          <el-input
            v-model="approveForm.approvalOpinions"
            type="textarea"
            :rows="3"
            placeholder="请输入审批意见"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="showApproveDialog = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitApprove">ç¡® å®š</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ…对话框 -->
    <el-dialog
      v-model="showDetailDialog"
      title="申请详情"
      width="700px"
      append-to-body
    >
      <el-descriptions :column="2" border>
        <el-descriptions-item label="申请编号">{{ currentDetail.code }}</el-descriptions-item>
        <el-descriptions-item label="申请人">{{ currentDetail.applicant }}</el-descriptions-item>
        <el-descriptions-item label="部门">{{ currentDetail.dept }}</el-descriptions-item>
        <el-descriptions-item label="物资类型">{{ currentDetail.materialType }}</el-descriptions-item>
        <el-descriptions-item label="具体物品">{{ currentDetail.itemName }}</el-descriptions-item>
        <el-descriptions-item label="申请数量">{{ currentDetail.applyNum }}</el-descriptions-item>
        <el-descriptions-item label="申请原因" :span="2">{{ currentDetail.reason }}</el-descriptions-item>
        <el-descriptions-item label="申请状态">
          <el-tag :type="getStatusType(currentDetail.status)">
            {{ getStatusText(currentDetail.status) }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="申请时间">{{ currentDetail.applyTime }}</el-descriptions-item>
        <el-descriptions-item label="审批人">{{ currentDetail.approval || '-' }}</el-descriptions-item>
        <el-descriptions-item label="审批时间">{{ currentDetail.approvalTime || '-' }}</el-descriptions-item>
        <el-descriptions-item label="审批意见" :span="2">{{ currentDetail.approvalOpinions || '-' }}</el-descriptions-item>
        <el-descriptions-item label="发放时间">{{ currentDetail.issueTime || '-' }}</el-descriptions-item>
        <el-descriptions-item label="发放人">{{ currentDetail.issueUser || '-' }}</el-descriptions-item>
      </el-descriptions>
    </el-dialog>
  </div>
</template>
<script setup>
import {listPage,add,update,deleteOff} from "@/api/collaborativeApproval/officeSupplies.js"
import {ref, reactive, onMounted, getCurrentInstance} from 'vue'
import Cookies from 'js-cookie'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh, Download, Check } from '@element-plus/icons-vue'
// å“åº”式数据
const loading = ref(false)
const showSearch = ref(true)
const showApplyDialog = ref(false)
const showApproveDialog = ref(false)
const showDetailDialog = ref(false)
const multipleSelection = ref([])
const officeList = ref([])
const total = ref(0)
const suppliesList = ref([])
const currentDetail = ref({})
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  current: 1,
  size: 10,
  code: '',
  applicant: '',
  status: ''
})
// ç”³è¯·è¡¨å•
const applyForm = reactive({
  applicant: '',
  dept: '',
  materialType: '',
  itemName: '',
  applyNum: 1,
  reason: '',
  urgency: '1'
})
// å®¡æ‰¹è¡¨å•
const approveForm = reactive({
  approveResult: '3',
  approvalOpinions: ''
})
// è¡¨å•校验规则
const applyRules = {
  applicant: [{ required: true, message: '请选择物资类型', trigger: 'blur' }],
  dept: [{ required: true, message: '请选择物资类型', trigger: 'blur' }],
  materialType: [{ required: true, message: '请选择物资类型', trigger: 'change' }],
  itemName: [{ required: true, message: '请输入具体物品名称', trigger: 'blur' }],
  applyNum: [{ required: true, message: '请输入申请数量', trigger: 'blur' }],
  reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
}
const approveRules = {
  approveResult: [{ required: true, message: '请选择审批结果', trigger: 'change' }],
  approvalOpinions: [{ required: true, message: '请输入审批意见', trigger: 'blur' }]
}
const openShow = () => {
  showApplyDialog.value = true
  resetApplyForm()
}
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  loading.value = true
  listPage(queryParams).then(res => {
    total.value = res.data.total
    loading.value = false
    officeList.value = res.data.records
  })
}
// æŸ¥è¯¢
const handleQuery = () => {
  queryParams.current = 1
  getList()
}
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  queryParams.code = ''
  queryParams.applicant = ''
  queryParams.status = ''
  handleQuery()
}
// å¤šé€‰
const handleSelectionChange = (selection) => {
  multipleSelection.value = selection
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    1: 'warning',
    3: 'success',
    2: 'danger',
    4: 'info'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    1: '待审批',
    3: '已通过',
    2: '已拒绝',
    4: '已发放'
  }
  return statusMap[status] || status
}
// æäº¤ç”³è¯·
const submitApply = () => {
  add(applyForm).then(() => {
    ElMessage.success('申请成功')
    getList()
    showApplyDialog.value = false
    resetApplyForm()
  })
}
//重置表单
const resetApplyForm = () => {
  // é‡ç½®è¡¨å•
  Object.assign(applyForm, {
    applicant: '',
    dept: '',
    materialType: '',
    itemName: '',
    applyNum: 1,
    reason: '',
    urgency: '1'
  })
}
// å®¡æ‰¹
const handleApprove = (row) => {
  currentDetail.value = row
  showApproveDialog.value = true
}
const formatDate = (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 sends = String(date.getSeconds()).padStart(2, '0')
  return `${year}-${month}-${day} ${hours}:${minutes}:${sends}`
}
// æäº¤å®¡æ‰¹
const submitApprove = () => {
  currentDetail.value.status = approveForm.approveResult
  // ä»Žcookie中获取当前登录用户名称
  currentDetail.value.approval = Cookies.get('username')
  currentDetail.value.approvalTime = formatDate(new Date())
  currentDetail.value.approvalOpinions = approveForm.approvalOpinions
  update(currentDetail.value).then((res) => {
    if(res.code === 200){
      showApproveDialog.value = false
      ElMessage.success('审批完成')
      getList()
      // é‡ç½®è¡¨å•
      Object.assign(approveForm, {
        approveResult: '3',
        approvalOpinions: ''
      })
    }
  })
}
// å‘放
const handleIssue = (row) => {
  row.status = 4
  row.issueTime = formatDate(new Date())
  row.issueUser = Cookies.get('username')
  update(row).then((res) =>{
    if(res.code === 200){
      ElMessage.success('发放完成')
      getList()
    }
  })
}
// æŸ¥çœ‹è¯¦æƒ…
const handleDetail = (row) => {
  currentDetail.value = row
  showDetailDialog.value = true
}
// åˆ é™¤
const handleDelete = (row) => {
  ElMessageBox.confirm('确认删除该申请吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    let ids = [row.id]
    deleteOff(ids).then((res) =>{
      ElMessage.success('删除成功')
      getList()
    })
  })
}
const { proxy } = getCurrentInstance();
// å¯¼å‡º
const handleExport = () => {
  ElMessageBox.confirm("所有的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        proxy.download("/officeSupplies/export", {}, "办公物资.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
onMounted(() => {
  getList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.mb8 {
  margin-bottom: 8px;
}
.dialog-footer {
  text-align: right;
}
:deep(.el-descriptions__label) {
  width: 120px;
}
</style>
src/views/collaborativeApproval/planTemplate/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,867 @@
<template>
  <div class="app-container">
    <!-- é¡¶éƒ¨æ“ä½œæ  -->
    <div class="header-actions">
      <div class="left-actions">
        <el-select v-model="currentLevel" placeholder="选择计划级别" style="width: 150px" @change="handleLevelChange">
          <el-option label="个人计划" value="personal" />
          <el-option label="小组计划" value="group" />
          <el-option label="部门计划" value="department" />
          <el-option label="公司计划" value="company" />
        </el-select>
        <el-select v-model="currentPeriod" placeholder="选择时间周期" style="width: 120px; margin-left: 10px" @change="handlePeriodChange">
          <el-option label="周计划" value="week" />
          <el-option label="月计划" value="month" />
          <el-option label="年计划" value="year" />
        </el-select>
        <el-date-picker
          v-model="currentDate"
          :type="datePickerType"
          placeholder="选择日期"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 180px; margin-left: 10px"
          @change="handleDateChange"
        />
      </div>
      <div class="right-actions">
        <el-button type="primary" @click="handleAddPlan">新增计划</el-button>
        <el-button @click="handleExport">导出计划</el-button>
        <!-- <el-button @click="handleShare">共享计划@</el-button> -->
      </div>
    </div>
    <!-- è®¡åˆ’概览卡片 -->
    <div class="overview-cards">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-card class="overview-card">
            <div class="card-content">
              <div class="card-icon personal">
                <el-icon><User /></el-icon>
              </div>
              <div class="card-info">
                <div class="card-title">个人计划</div>
                <div class="card-number">{{ overviewData.personal.total }}</div>
                <div class="card-progress">
                  <el-progress :percentage="overviewData.personal.completion" :stroke-width="6" />
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="overview-card">
            <div class="card-content">
              <div class="card-icon group">
                <el-icon><UserFilled /></el-icon>
              </div>
              <div class="card-info">
                <div class="card-title">小组计划</div>
                <div class="card-number">{{ overviewData.group.total }}</div>
                <div class="card-progress">
                  <el-progress :percentage="overviewData.group.completion" :stroke-width="6" />
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="overview-card">
            <div class="card-content">
              <div class="card-icon department">
                <el-icon><OfficeBuilding /></el-icon>
              </div>
              <div class="card-info">
                <div class="card-title">部门计划</div>
                <div class="card-number">{{ overviewData.department.total }}</div>
                <div class="card-progress">
                  <el-progress :percentage="overviewData.department.completion" :stroke-width="6" />
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="overview-card">
            <div class="card-content">
              <div class="card-icon company">
                <el-icon><House /></el-icon>
              </div>
              <div class="card-info">
                <div class="card-title">公司计划</div>
                <div class="card-number">{{ overviewData.company.total }}</div>
                <div class="card-progress">
                  <el-progress :percentage="overviewData.company.completion" :stroke-width="6" />
                </div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- è®¡åˆ’列表 -->
    <div class="plan-content">
      <el-card>
        <template #header>
          <div class="card-header">
            <span>{{ getCurrentLevelText() }} - {{ getCurrentPeriodText() }}</span>
            <div>
              <el-button size="small" @click="handleRefresh">刷新</el-button>
              <!-- <el-button size="small" @click="handleFilter">筛选@</el-button> -->
            </div>
          </div>
        </template>
        <div class="plan-list">
          <div v-for="plan in planList" :key="plan.id" class="plan-item">
            <div class="plan-header">
              <div class="plan-title">
                <el-tag :type="getPriorityType(plan.priority)" size="small">{{ getPriorityText(plan.priority) }}</el-tag>
                <span class="title-text">{{ plan.title }}</span>
              </div>
              <div class="plan-actions">
                <el-button size="small" @click="handleEditPlan(plan)">编辑</el-button>
                <el-button size="small" @click="handleViewDetail(plan)">详情</el-button>
                <el-dropdown @command="(command) => handleMoreAction(plan, command)">
                  <el-button size="small">
                    æ›´å¤š<el-icon class="el-icon--right"><ArrowDown /></el-icon>
                  </el-button>
                  <template #dropdown>
                    <el-dropdown-menu>
                      <!-- <el-dropdown-item command="share">共享@</el-dropdown-item> -->
                      <el-dropdown-item command="copy">复制</el-dropdown-item>
                      <el-dropdown-item command="delete" divided>删除</el-dropdown-item>
                    </el-dropdown-menu>
                  </template>
                </el-dropdown>
              </div>
            </div>
            <div class="plan-content">
              <div class="plan-description">{{ plan.description }}</div>
              <div class="plan-meta">
                <div class="meta-item">
                  <el-icon><Calendar /></el-icon>
                  <span>{{ plan.startDate }} - {{ plan.endDate }}</span>
                </div>
                <div class="meta-item">
                  <el-icon><User /></el-icon>
                  <span>{{ plan.assignee }}</span>
                </div>
                <div class="meta-item">
                  <el-icon><Clock /></el-icon>
                  <span>进度: {{ plan.progress }}%</span>
                </div>
                <div class="meta-item">
                  <el-icon><Flag /></el-icon>
                  <span>{{ getStatusText(plan.status) }}</span>
                </div>
              </div>
              <div class="plan-progress">
                <el-progress
                  :percentage="plan.progress"
                  :color="getProgressColor(plan.progress)"
                  :stroke-width="8"
                />
              </div>
              <div class="plan-tags">
                <el-tag v-for="tag in plan.tags" :key="tag" size="small" style="margin-right: 5px">
                  {{ tag }}
                </el-tag>
              </div>
            </div>
          </div>
        </div>
      </el-card>
    </div>
    <!-- æ–°å¢ž/编辑计划对话框 -->
    <el-dialog
      v-model="planDialogVisible"
      :title="operationType === 'add' ? '发布计划' : '编辑计划'"
      width="600px"
      @close="handleDialogClose"
    >
      <el-form :model="planForm" :rules="planRules" ref="planFormRef" label-width="100px">
        <el-form-item label="计划标题" prop="title">
          <el-input v-model="planForm.title" placeholder="请输入计划标题" />
        </el-form-item>
        <el-form-item label="计划描述" prop="description">
          <el-input
            v-model="planForm.description"
            type="textarea"
            :rows="3"
            placeholder="请输入计划描述"
          />
        </el-form-item>
        <el-form-item label="计划级别" prop="level">
          <el-select v-model="planForm.level" placeholder="选择计划级别" style="width: 100%">
            <el-option label="个人计划" value="personal" />
            <el-option label="小组计划" value="group" />
            <el-option label="部门计划" value="department" />
            <el-option label="公司计划" value="company" />
          </el-select>
        </el-form-item>
        <el-form-item label="时间周期" prop="period">
          <el-select v-model="planForm.period" placeholder="选择时间周期" style="width: 100%">
            <el-option label="周计划" value="week" />
            <el-option label="月计划" value="month" />
            <el-option label="年计划" value="year" />
          </el-select>
        </el-form-item>
        <el-form-item label="开始时间" prop="startDate">
          <el-date-picker
            v-model="planForm.startDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="选择开始时间"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="结束时间" prop="endDate">
          <el-date-picker
            v-model="planForm.endDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="选择结束时间"
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="负责人" prop="assignee">
          <el-input v-model="planForm.assignee" placeholder="请输入负责人" />
        </el-form-item>
        <el-form-item label="优先级" prop="priority">
          <el-select v-model="planForm.priority" placeholder="选择优先级" style="width: 100%">
            <el-option label="高" value="high" />
            <el-option label="中" value="medium" />
            <el-option label="低" value="low" />
          </el-select>
        </el-form-item>
        <!-- <el-form-item label="标签">
          <el-input v-model="planForm.tags" placeholder="请输入标签,用逗号分隔" />
        </el-form-item> -->
        <el-form-item label="标签" prop="tags">
          <!-- <el-checkbox-group v-model="planForm.tags">
            <el-checkbox label="all"></el-checkbox>
            <el-checkbox label="manager">管理层</el-checkbox>
            <el-checkbox label="hr">人事部门</el-checkbox>
            <el-checkbox label="finance">财务部门</el-checkbox>
            <el-checkbox label="tech">技术部门</el-checkbox>
          </el-checkbox-group> -->
          <el-select
            v-model="planForm.tags"
            multiple
            placeholder="请选择标签"
            style="width: 100%"
          >
            <el-option
              v-for="dept in departments"
              :key="dept"
              :label="dept"
              :value="dept"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-select v-model="planForm.status" placeholder="选择状态" style="width: 100%">
            <el-option label="未开始" value="not_started" />
            <el-option label="进行中" value="in_progress" />
            <el-option label="已完成" value="completed" />
            <el-option label="已暂停" value="paused" />
          </el-select>
        </el-form-item>
        <el-form-item label="进度" prop="progress">
          <el-input-number
            v-model="planForm.progress"
            min="0"
            max="100"
            step="1"
            placeholder="请输入进度"
            style="width: 100%"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="planDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSavePlan">保存</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- è®¡åˆ’详情对话框 -->
    <el-dialog v-model="showPlanDetailDialog" title="计划详情" width="700px">
      <div v-if="currentPlanDetail" class="mb10">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="计划标题">{{ currentPlanDetail.title }}</el-descriptions-item>
          <el-descriptions-item label="计划描述">{{ currentPlanDetail.description }}</el-descriptions-item>
          <el-descriptions-item label="计划级别">{{ getCurrentLevelText(currentPlanDetail.level) }}</el-descriptions-item>
          <el-descriptions-item label="时间周期">{{ getCurrentPeriodText(currentPlanDetail.period) }}</el-descriptions-item>
          <el-descriptions-item label="开始时间">{{ currentPlanDetail.startDate }}</el-descriptions-item>
          <el-descriptions-item label="结束时间">{{ currentPlanDetail.endDate }}</el-descriptions-item>
          <el-descriptions-item label="负责人">{{ currentPlanDetail.assignee }}</el-descriptions-item>
          <el-descriptions-item label="优先级">{{ getPriorityText(currentPlanDetail.priority) }}</el-descriptions-item>
          <el-descriptions-item label="标签">{{ currentPlanDetail.tags.join(', ') }}</el-descriptions-item>
          <el-descriptions-item label="状态">{{ getStatusText(currentPlanDetail.status) }}</el-descriptions-item>
          <el-descriptions-item label="进度">{{ currentPlanDetail.progress }}%</el-descriptions-item>
        </el-descriptions>
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const { proxy } = getCurrentInstance();
import {
  User,
  UserFilled,
  OfficeBuilding,
  House,
  Calendar,
  Clock,
  Flag,
  ArrowDown
} from '@element-plus/icons-vue'
import { listDutyPlan, addDutyPlan, updateDutyPlan, delDutyPlan,NumDutyPlan,exportDutyPlan } from '@/api/collaborativeApproval/planTemplate.js'
// å“åº”式数据
const operationType = ref('add')
const currentLevel = ref('personal')
const currentPeriod = ref('week')
const currentDate = ref(new Date())
const planDialogVisible = ref(false)
const dialogTitle = ref('新增计划')
const planFormRef = ref()
const showPlanDetailDialog = ref(false)
const currentPlanDetail = ref(null)
// è¡¨å•数据
const planForm = reactive({
  id: '',
  title: '',
  description: '',
  level: 'personal',
  period: 'week',
  startDate: '',
  endDate: '',
  assignee: '',
  priority: 'medium',
  tags: [],
  status: '',
  progress: 0
})
// è¡¨å•验证规则
const planRules = {
  title: [{ required: true, message: '请输入计划标题', trigger: 'blur' }],
  description: [{ required: true, message: '请输入计划描述', trigger: 'blur' }],
  level: [{ required: true, message: '请选择计划级别', trigger: 'change' }],
  period: [{ required: true, message: '请选择时间周期', trigger: 'change' }],
  startDate: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
  endDate: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
  assignee: [{ required: true, message: '请输入负责人', trigger: 'blur' }],
  priority: [{ required: true, message: '请选择优先级', trigger: 'change' }]
}
const departments = ["产品", "分析", "调研",'技术', '架构', '设计','市场', '推广', '营销'];
// æ¦‚览数据
const overviewData = reactive({
  personal: { total: 0, completion: 0 },
  group: { total: 0, completion: 0 },
  department: { total: 0, completion: 0 },
  company: { total: 0, completion: 0 }
})
// è®¡åˆ’列表数据
const planList = ref([])
// è®¡ç®—属性
const datePickerType = computed(() => {
  switch (currentPeriod.value) {
    case 'week':
      return 'week'
    case 'month':
      return 'month'
    case 'year':
      return 'year'
    default:
      return 'date'
  }
})
// æ–¹æ³•
const handleLevelChange = (value) => {
  console.log('计划级别变更:', value)
  getPlanList()
  // è¿™é‡Œå¯ä»¥æ ¹æ®çº§åˆ«ç­›é€‰æ•°æ®
}
const handlePeriodChange = (value) => {
  console.log('时间周期变更:', value)
  getPlanList()
  // è¿™é‡Œå¯ä»¥æ ¹æ®å‘¨æœŸç­›é€‰æ•°æ®
}
const handleDateChange = (value) => {
  console.log('日期变更:', value)
  getPlanList()
  // è¿™é‡Œå¯ä»¥æ ¹æ®æ—¥æœŸç­›é€‰æ•°æ®
}
const handleAddPlan = () => {
  operationType.value = 'add'
  dialogTitle.value = '新增计划'
  planDialogVisible.value = true
  // é‡ç½®è¡¨å•
  Object.keys(planForm).forEach(key => {
    planForm[key] = ''
  })
  planForm.level = 'personal'
  planForm.period = 'week'
  planForm.priority = 'medium'
  planForm.status = 'not_started'
  planForm.progress = 0
}
const handleEditPlan = (plan) => {
  operationType.value = 'edit'
  dialogTitle.value = '编辑计划'
  planDialogVisible.value = true
  Object.assign(planForm, plan)
  // // å¡«å……表单数据
  // Object.keys(planForm).forEach(key => {
  //   if (key === 'tags') {
  //     planForm[key] = plan[key].join(', ')
  //   } else {
  //     planForm[key] = plan[key]
  //   }
  // })
}
const handleViewDetail = (plan) => {
  currentPlanDetail.value = plan
  showPlanDetailDialog.value = true
  // ElMessage.info(`查看计划详情: ${plan.title}`)
}
const handleMoreAction = async(plan,command) => {
  let ids = [];
  ids.push(plan.id);
  console.log("ids",ids)
  switch (command) {
    case 'share':
      ElMessage.success('计划已共享')
      break
    case 'copy':
      const knowledgeText = `
        è®¡åˆ’标题:${plan.title}
        è®¡åˆ’描述:${plan.description}
        è®¡åˆ’级别:${getCurrentLevelText(plan.level)}
        æ—¶é—´å‘¨æœŸï¼š${getCurrentPeriodText(plan.period)}
        å¼€å§‹æ—¶é—´ï¼š${plan.startDate}
        ç»“束时间:${plan.endDate}
        è´Ÿè´£äººï¼š${plan.assignee}
        ä¼˜å…ˆçº§ï¼š${getPriorityText(plan.priority)}
        æ ‡ç­¾ï¼š${plan.tags.join(', ')}
        çŠ¶æ€ï¼š${getStatusText(plan.status)}
        è¿›åº¦ï¼š${plan.progress}%
      `.trim();
        // å¤åˆ¶åˆ°å‰ªè´´æ¿
        navigator.clipboard.writeText(knowledgeText).then(() => {
          ElMessage.success("知识内容已复制到剪贴板");
        }).catch(() => {
          ElMessage.error("复制失败,请手动复制");
        });
      // ElMessage.success('计划已复制')
      break
    case 'delete':
      ElMessageBox.confirm('确定要删除这个计划吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        delDutyPlan(ids).then(res => {
          if (res.code === 200) {
            ElMessage.success('计划已删除')
            ids.value = [];
            getPlanList()
          }
        })
      })
      break
  }
}
//
const handleSavePlan = async () => {
  try {
    await planFormRef.value.validate()
    if (operationType.value === 'add') {
      addDutyPlan(planForm).then(res => {
        if (res.code === 200) {
          ElMessage.success('计划保存成功')
          planDialogVisible.value = false
        }
        getPlanList()
      })
    } else {
      updateDutyPlan(planForm).then(res => {
        if (res.code === 200) {
          ElMessage.success('计划保存成功')
          planDialogVisible.value = false
        }
        getPlanList()
      })
    }
  } catch (error) {
    console.log('表单验证失败:', error)
  }
}
const handleDialogClose = () => {
  planFormRef.value?.resetFields()
}
const handleRefresh = () => {
  getPlanList()
  // ElMessage.success('数据已刷新')
}
const handleFilter = () => {
  ElMessage.info('打开筛选面板')
}
const handleExport = () => {
  ElMessageBox.confirm("是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      // exportDutyPlan().then(res => {
      // })
      proxy.download("/dutyPlan/export", {}, "计划管理.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
const handleShare = () => {
  ElMessage.success('计划已共享')
}
const getCurrentLevelText = () => {
  const levelMap = {
    personal: '个人计划',
    group: '小组计划',
    department: '部门计划',
    company: '公司计划'
  }
  return levelMap[currentLevel.value] || '个人计划'
}
const getCurrentPeriodText = () => {
  const periodMap = {
    week: '周计划',
    month: '月计划',
    year: '年计划'
  }
  return periodMap[currentPeriod.value] || '周计划'
}
const getPriorityType = (priority) => {
  const typeMap = {
    high: 'danger',
    medium: 'warning',
    low: 'info'
  }
  return typeMap[priority] || 'info'
}
const getPriorityText = (priority) => {
  const textMap = {
    high: '高',
    medium: '中',
    low: '低'
  }
  return textMap[priority] || '中'
}
const getStatusText = (status) => {
  const statusMap = {
    not_started: '未开始',
    in_progress: '进行中',
    completed: '已完成',
    paused: '已暂停'
  }
  return statusMap[status] || '未知'
}
const getProgressColor = (progress) => {
  if (progress >= 80) return '#67C23A'
  if (progress >= 50) return '#E6A23C'
  return '#F56C6C'
}
//获取数据列表
const getPlanList = async () => {
  const params = {
    level: currentLevel.value,
    period: currentPeriod.value,
    queryDate:currentDate.value
  }
  listDutyPlan(params).then(res => {
    if (res.code === 200) {
      planList.value = res.data.records
    }
  }).catch(err => {
    console.log(err)
  })
}
//获取数据
const getPlanNum = async () => {
  NumDutyPlan().then(res => {
    if (res.code === 200) {
      // console.log(res.data)
      //讲结果里面的数据根据level èµ‹å€¼ç»™overviewData
      res.data.forEach(item => {
        overviewData[item.level].total = item.num
        overviewData[item.level].completion = item.completion
      })
    }
  }).catch(err => {
    console.log(err)
  })
}
onMounted(() => {
  getPlanList()
  getPlanNum()
  console.log('多级计划模板页面已加载')
})
</script>
<style scoped>
.app-container {
  padding: 20px;
  background-color: #f5f5f5;
  min-height: 100vh;
}
.header-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  background: white;
  padding: 20px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.left-actions {
  display: flex;
  align-items: center;
}
.right-actions {
  display: flex;
  gap: 10px;
}
.overview-cards {
  margin-bottom: 20px;
}
.overview-card {
  height: 120px;
}
.card-content {
  display: flex;
  align-items: center;
  height: 100%;
}
.card-icon {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 15px;
  font-size: 24px;
  color: white;
}
.card-icon.personal {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.card-icon.group {
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.card-icon.department {
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.card-icon.company {
  background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.card-info {
  flex: 1;
}
.card-title {
  font-size: 14px;
  color: #666;
  margin-bottom: 5px;
}
.card-number {
  font-size: 24px;
  font-weight: bold;
  color: #333;
  margin-bottom: 10px;
}
.card-progress {
  width: 100%;
}
.plan-content {
  background: white;
  border-radius: 8px;
  overflow: hidden;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: bold;
  color: #333;
}
.header-actions {
  display: flex;
  gap: 10px;
}
.plan-list {
  padding: 20px 0;
}
.plan-item {
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  margin-bottom: 15px;
  padding: 20px;
  transition: all 0.3s ease;
}
.plan-item:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  transform: translateY(-2px);
}
.plan-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}
.plan-title {
  display: flex;
  align-items: center;
  gap: 10px;
}
.title-text {
  font-size: 16px;
  font-weight: bold;
  color: #333;
}
.plan-actions {
  display: flex;
  gap: 10px;
}
.plan-content {
  margin-bottom: 15px;
}
.plan-description {
  color: #666;
  margin-bottom: 15px;
  line-height: 1.6;
}
.plan-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  margin-bottom: 15px;
}
.meta-item {
  display: flex;
  align-items: center;
  gap: 5px;
  color: #666;
  font-size: 14px;
}
.plan-progress {
  margin-bottom: 15px;
}
.plan-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
/* å“åº”式设计 */
@media (max-width: 768px) {
  .header-actions {
    flex-direction: column;
    gap: 15px;
  }
  .left-actions {
    flex-wrap: wrap;
    gap: 10px;
  }
  .plan-meta {
    flex-direction: column;
    gap: 10px;
  }
  .plan-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
}
</style>
src/views/collaborativeApproval/processTracking/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <div class="process-tracking">
    <div class="header">
      <h2>过程追踪</h2>
      <el-button type="primary" @click="refreshData">刷新数据</el-button>
    </div>
    <!-- é¡¹ç›®çŠ¶æ€ç»Ÿè®¡ -->
    <div class="status-cards">
      <el-row :gutter="20">
        <el-col :span="6" v-for="(item, index) in statusStats" :key="index">
          <el-card class="status-card" :class="item.type">
            <div class="card-content">
              <div class="card-icon">
                <el-icon :size="24">
                  <component :is="item.icon" />
                </el-icon>
              </div>
              <div class="card-info">
                <div class="card-title">{{ item.label }}</div>
                <div class="card-count">{{ item.count }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
    <!-- é¡¹ç›®åˆ—表 -->
    <el-card class="project-list">
      <template #header>
        <div class="card-header">
          <span>项目列表</span>
          <el-button type="text" @click="toggleView">
            {{ viewMode === 'table' ? '切换到甘特图' : '切换到列表' }}
          </el-button>
        </div>
      </template>
      <!-- è¡¨æ ¼è§†å›¾ -->
      <div v-if="viewMode === 'table'">
        <el-table :data="projectList" style="width: 100%">
          <el-table-column prop="name" label="项目名称"  />
          <el-table-column prop="manager" label="负责人"/>
          <el-table-column label="状态" >
            <template #default="{ row }">
              <el-tag :type="getStatusType(row.status)">
                {{ getStatusText(row.status) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="进度" width="150">
            <template #default="{ row }">
              <el-progress :percentage="row.progress" :status="getProgressStatus(row.status)" />
            </template>
          </el-table-column>
          <el-table-column prop="startDate" label="开始时间" width="120" />
          <el-table-column prop="endDate" label="结束时间" width="120" />
          <el-table-column label="操作" width="150">
            <template #default="{ row }">
              <el-button type="text" @click="updateStatus(row)">更新状态</el-button>
              <el-button type="text" @click="viewDetails(row)">详情</el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      <!-- ç”˜ç‰¹å›¾è§†å›¾ -->
      <div v-else class="gantt-container">
        <div ref="ganttChart" style="width: 100%; height: 400px;"></div>
      </div>
    </el-card>
    <!-- çŠ¶æ€æ›´æ–°å¯¹è¯æ¡† -->
    <el-dialog v-model="statusDialogVisible" title="更新项目状态" width="400px">
      <el-form :model="statusForm" label-width="80px">
        <el-form-item label="项目名称">
          <el-input v-model="statusForm.name" disabled />
        </el-form-item>
        <el-form-item label="当前状态">
          <el-select v-model="statusForm.status" placeholder="请选择状态">
            <el-option label="未开始" value="not_started" />
            <el-option label="进行中" value="in_progress" />
            <el-option label="已完成" value="completed" />
            <el-option label="延期" value="delayed" />
          </el-select>
        </el-form-item>
        <el-form-item label="进度">
          <el-slider v-model="statusForm.progress" :max="100" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="statusDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="confirmStatusUpdate">确认</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { Clock, Loading, Check, Warning } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
// å“åº”式数据
const viewMode = ref('table')
const statusDialogVisible = ref(false)
const ganttChart = ref(null)
let chartInstance = null
// çŠ¶æ€ç»Ÿè®¡æ•°æ®
const statusStats = reactive([
  { label: '未开始', count: 3, type: 'not-started', icon: Clock },
  { label: '进行中', count: 5, type: 'in-progress', icon: Loading },
  { label: '已完成', count: 8, type: 'completed', icon: Check },
  { label: '延期', count: 2, type: 'delayed', icon: Warning }
])
// é¡¹ç›®åˆ—表数据
const projectList = reactive([
  {
    id: 1,
    name: '汇星钙业生产线扩建项目',
    manager: '陈志强',
    status: 'completed',
    progress: 100,
    startDate: '2024-01-01',
    endDate: '2024-01-15'
  },
  {
    id: 2,
    name: '新型环保钙粉工艺研发',
    manager: '林雪峰',
    status: 'in_progress',
    progress: 75,
    startDate: '2024-01-10',
    endDate: '2024-01-25'
  },
  {
    id: 3,
    name: '汇星钙业ERP系统升级',
    manager: '王雅琪',
    status: 'in_progress',
    progress: 60,
    startDate: '2024-01-20',
    endDate: '2024-02-10'
  },
  {
    id: 4,
    name: '矿山开采许可证续期申请',
    manager: '赵伟东',
    status: 'in_progress',
    progress: 45,
    startDate: '2024-01-25',
    endDate: '2024-02-15'
  },
  {
    id: 5,
    name: '环保设备升级改造',
    manager: '李佳欣',
    status: 'delayed',
    progress: 30,
    startDate: '2024-01-15',
    endDate: '2024-01-30'
  },
  {
    id: 6,
    name: '年度安全生产培训计划',
    manager: '张建国',
    status: 'not_started',
    progress: 0,
    startDate: '2024-02-01',
    endDate: '2024-02-20'
  }
])
// çŠ¶æ€æ›´æ–°è¡¨å•
const statusForm = reactive({
  id: null,
  name: '',
  status: '',
  progress: 0
})
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const typeMap = {
    not_started: 'info',
    in_progress: 'warning',
    completed: 'success',
    delayed: 'danger'
  }
  return typeMap[status] || 'info'
}
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const textMap = {
    not_started: '未开始',
    in_progress: '进行中',
    completed: '已完成',
    delayed: '延期'
  }
  return textMap[status] || '未知'
}
// èŽ·å–è¿›åº¦çŠ¶æ€
const getProgressStatus = (status) => {
  if (status === 'completed') return 'success'
  if (status === 'delayed') return 'exception'
  return null
}
// åˆ‡æ¢è§†å›¾æ¨¡å¼
const toggleView = () => {
  viewMode.value = viewMode.value === 'table' ? 'gantt' : 'table'
  if (viewMode.value === 'gantt') {
    nextTick(() => {
      initGanttChart()
    })
  }
}
// åˆå§‹åŒ–甘特图
const initGanttChart = () => {
  if (!ganttChart.value) return
  if (chartInstance) {
    chartInstance.dispose()
  }
  chartInstance = echarts.init(ganttChart.value)
  // å‡†å¤‡ç”˜ç‰¹å›¾æ•°æ®
  const data = projectList.map((project, index) => ({
    name: project.name,
    value: [
      index,
      new Date(project.startDate).getTime(),
      new Date(project.endDate).getTime(),
      project.progress
    ],
    itemStyle: {
      color: getGanttColor(project.status)
    }
  }))
  const option = {
    title: {
      text: '项目甘特图',
      left: 'center'
    },
    tooltip: {
      formatter: (params) => {
        const project = projectList[params.value[0]]
        return `
          <div>
            <strong>${project.name}</strong><br/>
            è´Ÿè´£äºº: ${project.manager}<br/>
            çŠ¶æ€: ${getStatusText(project.status)}<br/>
            è¿›åº¦: ${project.progress}%<br/>
            å¼€å§‹æ—¶é—´: ${project.startDate}<br/>
            ç»“束时间: ${project.endDate}
          </div>
        `
      }
    },
    grid: {
      left: '15%',
      right: '10%',
      top: '15%',
      bottom: '15%'
    },
    xAxis: {
      type: 'time',
      axisLabel: {
        formatter: (value) => {
          return echarts.format.formatTime('MM-dd', value)
        }
      }
    },
    yAxis: {
      type: 'category',
      data: projectList.map(p => p.name),
      inverse: true
    },
    series: [{
      type: 'custom',
      renderItem: (params, api) => {
        const categoryIndex = api.value(0)
        const start = api.coord([api.value(1), categoryIndex])
        const end = api.coord([api.value(2), categoryIndex])
        const height = api.size([0, 1])[1] * 0.6
        return {
          type: 'rect',
          shape: {
            x: start[0],
            y: start[1] - height / 2,
            width: end[0] - start[0],
            height: height
          },
          style: api.style()
        }
      },
      data: data
    }]
  }
  chartInstance.setOption(option)
}
// èŽ·å–ç”˜ç‰¹å›¾é¢œè‰²
const getGanttColor = (status) => {
  const colorMap = {
    not_started: '#909399',
    in_progress: '#E6A23C',
    completed: '#67C23A',
    delayed: '#F56C6C'
  }
  return colorMap[status] || '#909399'
}
// æ›´æ–°çŠ¶æ€
const updateStatus = (project) => {
  statusForm.id = project.id
  statusForm.name = project.name
  statusForm.status = project.status
  statusForm.progress = project.progress
  statusDialogVisible.value = true
}
// ç¡®è®¤çŠ¶æ€æ›´æ–°
const confirmStatusUpdate = () => {
  const project = projectList.find(p => p.id === statusForm.id)
  if (project) {
    project.status = statusForm.status
    project.progress = statusForm.progress
    // æ›´æ–°ç»Ÿè®¡æ•°æ®
    updateStatusStats()
    // å¦‚果是甘特图视图,重新渲染
    if (viewMode.value === 'gantt') {
      nextTick(() => {
        initGanttChart()
      })
    }
    ElMessage.success('状态更新成功')
  }
  statusDialogVisible.value = false
}
// æ›´æ–°çŠ¶æ€ç»Ÿè®¡
const updateStatusStats = () => {
  const stats = {
    not_started: 0,
    in_progress: 0,
    completed: 0,
    delayed: 0
  }
  projectList.forEach(project => {
    stats[project.status]++
  })
  statusStats[0].count = stats.not_started
  statusStats[1].count = stats.in_progress
  statusStats[2].count = stats.completed
  statusStats[3].count = stats.delayed
}
// æŸ¥çœ‹è¯¦æƒ…
const viewDetails = (project) => {
  ElMessage.info(`查看项目详情: ${project.name}`)
}
// åˆ·æ–°æ•°æ®
const refreshData = () => {
  // æ¨¡æ‹Ÿå®žæ—¶æ›´æ–°
  const randomProject = projectList[Math.floor(Math.random() * projectList.length)]
  if (randomProject.status === 'in_progress') {
    randomProject.progress = Math.min(100, randomProject.progress + Math.floor(Math.random() * 10))
    if (randomProject.progress === 100) {
      randomProject.status = 'completed'
    }
  }
  updateStatusStats()
  if (viewMode.value === 'gantt') {
    nextTick(() => {
      initGanttChart()
    })
  }
  ElMessage.success('数据已刷新')
}
onMounted(() => {
  updateStatusStats()
})
</script>
<style scoped>
.process-tracking {
  padding: 20px;
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.header h2 {
  margin: 0;
  color: #303133;
}
.status-cards {
  margin-bottom: 20px;
}
.status-card {
  cursor: pointer;
  transition: all 0.3s;
}
.status-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-content {
  display: flex;
  align-items: center;
}
.card-icon {
  margin-right: 15px;
  padding: 10px;
  border-radius: 8px;
}
.status-card.not-started .card-icon {
  background-color: #f4f4f5;
  color: #909399;
}
.status-card.in-progress .card-icon {
  background-color: #fdf6ec;
  color: #E6A23C;
}
.status-card.completed .card-icon {
  background-color: #f0f9ff;
  color: #67C23A;
}
.status-card.delayed .card-icon {
  background-color: #fef0f0;
  color: #F56C6C;
}
.card-info {
  flex: 1;
}
.card-title {
  font-size: 14px;
  color: #909399;
  margin-bottom: 5px;
}
.card-count {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
}
.project-list {
  margin-top: 20px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.gantt-container {
  min-height: 400px;
}
</style>
src/views/collaborativeApproval/purchaseApproval/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1064 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <el-form :model="searchForm" :inline="true">
          <el-form-item label="供应商名称:">
            <el-input v-model="searchForm.supplierName" placeholder="请输入" clearable prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="采购合同号:">
            <el-input
                v-model="searchForm.purchaseContractNumber"
                style="width: 240px"
                placeholder="请输入"
                @change="handleQuery"
                clearable
                :prefix-icon="Search"
            />
          </el-form-item>
          <el-form-item label="销售合同号:">
            <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item label="项目名称:">
            <el-input v-model="searchForm.projectName" placeholder="请输入" clearable prefix-icon="Search"
                      @change="handleQuery" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="handleQuery"> æœç´¢ </el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
    <div class="table_list">
      <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys"
        :row-key="(row) => row.id"
        show-summary
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        :row-class-name="tableRowClassName"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column type="expand">
          <template #default="props">
            <el-table
              :data="props.row.children"
              border
              show-summary
              :summary-method="summarizeChildrenTable"
            >
              <el-table-column
                align="center"
                label="序号"
                type="index"
                width="60"
              />
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="规格型号" prop="specificationModel" />
              <el-table-column label="单位" prop="unit" />
              <el-table-column label="数量" prop="quantity" />
              <el-table-column label="税率(%)" prop="taxRate" />
              <el-table-column
                label="含税单价(元)"
                prop="taxInclusiveUnitPrice"
                :formatter="formattedNumber"
              />
              <el-table-column
                label="含税总价(元)"
                prop="taxInclusiveTotalPrice"
                :formatter="formattedNumber"
              />
              <el-table-column
                label="不含税总价(元)"
                prop="taxExclusiveTotalPrice"
                :formatter="formattedNumber"
              />
            </el-table>
          </template>
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="采购合同号"
          prop="purchaseContractNumber"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="销售合同号"
          prop="salesContractNo"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="供应商名称"
          width="240"
          prop="supplierName"
          show-overflow-tooltip
        />
        <el-table-column label="订单状态" width="100" align="center">
          <template #default="scope">
            <el-tag v-if="scope.row.isInvalid" type="danger" size="small">失效</el-tag>
            <el-tag v-else type="success" size="small">正常</el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="项目名称"
          prop="projectName"
          width="420"
          show-overflow-tooltip
        />
        <el-table-column
            label="审批状态"
            prop="approvalStatus"
            width="200"
            show-overflow-tooltip
        >
          <template #default="scope">
            <el-tag
                size="small"
            >
              {{ approvalStatusText[scope.row.approvalStatus] || '未知状态' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="付款方式"
          width="100"
          prop="paymentMethod"
          show-overflow-tooltip
        />
        <el-table-column
          label="合同金额(元)"
          prop="contractAmount"
           width="200"
          show-overflow-tooltip
          :formatter="formattedNumber"
        />
        <el-table-column
          label="录入人"
          prop="recorderName"
           width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="录入日期"
          prop="entryDate"
           width="100"
          show-overflow-tooltip
        />
        <el-table-column
          fixed="right"
          label="操作"
          min-width="150"
          align="center"
        >
          <template #default="scope">
            <el-button
              link
              type="primary"
              size="small"
              @click="approvePurchase(scope.row)"
              :disabled="scope.row.approvalStatus !== 0"
              >审批</el-button
            >
            <el-button
                link
                type="primary"
                size="small"
                @click="rejectPurchase(scope.row)"
                :disabled="scope.row.approvalStatus !== 0"
            >拒绝审批</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>
  </div>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, onMounted, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { userListNoPage } from "@/api/system/user.js";
import {
  getSalesLedgerWithProducts,
  addOrUpdateSalesLedgerProduct,
  delProduct,
  delLedgerFile,
  getProductInfoByContractNo,
} from "@/api/salesManagement/salesLedger.js";
import {
  addOrEditPurchase,
  delPurchase,
  getSalesNo,
  purchaseListPage,
  productList,
  getPurchaseById,
  getOptions,
  createPurchaseNo, updateApprovalStatus,
} from "@/api/procurementManagement/procurementLedger.js";
import useFormData from "@/hooks/useFormData.js";
import QRCode from "qrcode";
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const productData = ref([]);
const selectedRows = ref([]);
const productSelectedRows = ref([]);
const modelOptions = ref([]);
const userList = ref([]);
const productOptions = ref([]);
const salesContractList = ref([]);
const supplierList = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
});
const total = ref(0);
const fileList = ref([]);
import useUserStore from "@/store/modules/user";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import dayjs from "dayjs";
import { getCurrentDate } from "@/utils/index.js";
const userStore = useUserStore();
// äºŒç»´ç ç›¸å…³å˜é‡
const qrCodeDialogVisible = ref(false);
const qrCodeUrl = ref("");
// è®¢å•审批状态显示文本
const approvalStatusText = {
  0: '待审批',
  1: '审批通过',
  2: '审批失败'
};
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
const data = reactive({
  searchForm: {
    supplierName: "", // ä¾›åº”商名称
    purchaseContractNumber: "", // é‡‡è´­åˆåŒç¼–号
    salesContractNo: "", // é”€å”®åˆåŒç¼–号
    projectName: "", // é¡¹ç›®åç§°
    entryDate: null, // å½•入日期
    entryDateStart: undefined,
    entryDateEnd: undefined,
  },
  form: {
    purchaseContractNumber: "",
    salesLedgerId: "",
    projectName: "",
    recorderId: "",
    entryDate: "",
    productData: [],
    supplierName: "",
    supplierId: "",
    paymentMethod: "",
        executionDate: "",
    approvalStatus: "0",
  },
  rules: {
    purchaseContractNumber: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    projectName: [{ required: true, message: "请输入", trigger: "blur" }],
    supplierId: [{ required: true, message: "请输入", trigger: "blur" }],
        entryDate: [{ required: true, message: "请选择", trigger: "change" }],
        executionDate: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
const {  form, rules } = toRefs(data);
const { form: searchForm } = useFormData(data.searchForm);
// äº§å“è¡¨å•弹框数据
const productFormVisible = ref(false);
const productOperationType = ref("");
const productOperationIndex = ref("");
const currentId = ref("");
const productFormData = reactive({
  productForm: {
    productId: "",
    productCategory: "",
    productModelId: "",
    specificationModel: "",
    unit: "",
    quantity: "",
    taxInclusiveUnitPrice: "",
    taxRate: "",
    taxInclusiveTotalPrice: "",
    taxExclusiveTotalPrice: "",
    invoiceType: "",
        warnNum: "",
  },
  productRules: {
    productId: [{ required: true, message: "请选择", trigger: "change" }],
    productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    unit: [{ required: true, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    taxInclusiveUnitPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxRate: [{ required: true, message: "请选择", trigger: "change" }],
        warnNum: [{ required: false, message: "请选择", trigger: "change" }],
    taxInclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxExclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
const { productForm, productRules } = toRefs(productFormData);
const upload = reactive({
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
});
const changeDaterange = (value) => {
  if (value) {
    searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
    searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
  } else {
    searchForm.entryDateStart = undefined;
    searchForm.entryDateEnd = undefined;
  }
  handleQuery();
};
const formattedNumber = (row, column, cellValue) => {
  return parseFloat(cellValue).toFixed(2);
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeChildrenTable = (param) => {
  return proxy.summarizeTable(
    param,
    [
      "taxInclusiveUnitPrice",
      "taxInclusiveTotalPrice",
      "taxExclusiveTotalPrice",
      "ticketsNum",
      "ticketsAmount",
      "futureTickets",
      "futureTicketsAmount",
    ],
    {
      ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
    }
  );
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  const { entryDate, ...rest } = searchForm;
  purchaseListPage({ ...rest, ...page })
    .then((res) => {
      tableLoading.value = false;
      // tableData.value = res.data.records;
      // å¤„理数据,添加失效状态标记
      tableData.value = res.data.records.map(record => ({
        ...record,
        isInvalid: record.isWhite === 1
      }));
      tableData.value.map((item) => {
        item.children = [];
      });
      total.value = res.data.total;
      expandedRowKeys.value = [];
    })
    .catch(() => {
      tableLoading.value = false;
    });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const productSelected = (selectedRows) => {
  productSelectedRows.value = selectedRows;
};
const expandedRowKeys = ref([]);
// å±•开行
const expandChange = (row, expandedRows) => {
  if (expandedRows.length > 0) {
    expandedRowKeys.value = [];
    try {
      productList({ salesLedgerId: row.id, type: 2 }).then((res) => {
        const index = tableData.value.findIndex((item) => item.id === row.id);
        if (index > -1) {
          tableData.value[index].children = res.data;
        }
        expandedRowKeys.value.push(row.id);
      });
    } catch (error) {
      console.log(error);
    }
  } else {
    expandedRowKeys.value = [];
  }
};
// ä¸»è¡¨åˆè®¡æ–¹æ³•
const summarizeMainTable = (param) => {
  return proxy.summarizeTable(param, ["contractAmount"]);
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeProTable = (param) => {
  return proxy.summarizeTable(param, [
    "taxInclusiveUnitPrice",
    "taxInclusiveTotalPrice",
    "taxExclusiveTotalPrice",
  ]);
};
// æ‰“开弹框
const openForm = (type, row) => {
  operationType.value = type;
  form.value = {};
  productData.value = [];
  fileList.value = [];
  if (operationType.value == "add") {
    createPurchaseNo().then((res) => {
      form.value.purchaseContractNumber = res.data;
    });
  }
  userListNoPage().then((res) => {
    userList.value = res.data;
  });
  getSalesNo().then((res) => {
    salesContractList.value = res;
  });
  getOptions().then((res) => {
    // ä¾›åº”商过滤出isWhite=0 çš„æ•°æ®
    supplierList.value = res.data.filter((item) => item.isWhite == 0);
  });
  form.value.recorderId = userStore.id;
  form.value.entryDate = getCurrentDate();
  if (type === "edit") {
    currentId.value = row.id;
    getPurchaseById({ id: row.id, type: 2 }).then((res) => {
      form.value = { ...res };
      productData.value = form.value.productData;
      if (form.value.salesLedgerFiles) {
        fileList.value = form.value.salesLedgerFiles;
      } else {
        fileList.value = [];
      }
    });
  }
  dialogFormVisible.value = true;
};
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  if (file.size > 1024 * 1024 * 10) {
    proxy.$modal.msgError("上传文件大小不能超过10MB!");
    return false;
  }
  proxy.$modal.loading("正在上传文件,请稍候...");
  return true;
}
// ä¸Šä¼ å¤±è´¥
function handleUploadError(err) {
  proxy.$modal.msgError("上传文件失败");
  proxy.$modal.closeLoading();
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file, uploadFiles) {
  proxy.$modal.closeLoading();
  if (res.code === 200) {
    file.tempId = res.data.tempId;
    proxy.$modal.msgSuccess("上传成功");
  } else {
    proxy.$modal.msgError(res.msg);
    proxy.$refs.fileUpload.handleRemove(file);
  }
}
// ç§»é™¤æ–‡ä»¶
function handleRemove(file) {
  console.log("handleRemove", file.id);
  if (file.size > 1024 * 1024 * 10) {
    // ä»…前端清理,不调用删除接口和提示
    return;
  }
  if (operationType.value === "edit") {
    let ids = [];
    ids.push(file.id);
    delLedgerFile(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
    });
  }
}
// æäº¤è¡¨å•
const submitForm = (n) => {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      if (productData.value.length > 0) {
        form.value.productData = proxy.HaveJson(productData.value);
      } else {
        proxy.$modal.msgWarning("请添加产品信息");
        return;
      }
      let tempFileIds = [];
      if (fileList.value.length > 0) {
        tempFileIds = fileList.value.map((item) => item.tempId);
      }
      form.value.tempFileIds = tempFileIds;
      form.value.type = 2;
      form.value.approvalStatus = n;
      addOrEditPurchase(form.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
        getList();
      });
    }
  });
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
};
// æ‰“开产品弹框
const openProductForm = (type, row, index) => {
  productOperationType.value = type;
  productOperationIndex.value = index;
  productForm.value = {};
  proxy.resetForm("productFormRef");
  if (type === "edit") {
    productForm.value = { ...row };
  }
  productFormVisible.value = true;
  getProductOptions();
};
const getProductOptions = () => {
  productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
  });
};
const getModels = (value) => {
  if (value) {
    productForm.value.productCategory = findNodeById(productOptions.value, value) || "";
    modelList({ id: value }).then((res) => {
      modelOptions.value = res;
    });
  } else {
    productForm.value.productCategory = "";
    modelOptions.value = [];
  }
};
const getProductModel = (value) => {
  const index = modelOptions.value.findIndex((item) => item.id === value);
  if (index !== -1) {
    productForm.value.specificationModel = modelOptions.value[index].model;
    productForm.value.unit = modelOptions.value[index].unit;
  } else {
    productForm.value.specificationModel = null;
    productForm.value.unit = null;
  }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
      return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹çš„label
    }
    if (nodes[i].children && nodes[i].children.length > 0) {
      const foundNode = findNodeById(nodes[i].children, productId);
      if (foundNode) {
        return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œç›´æŽ¥è¿”å›žï¼ˆå·²ç»æ˜¯label字符串)
      }
    }
  }
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
      value: id, // å°† id æ”¹ä¸º value
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
// æäº¤äº§å“è¡¨å•
const submitProduct = () => {
  proxy.$refs["productFormRef"].validate((valid) => {
    if (valid) {
      if (operationType.value === "edit") {
        submitProductEdit();
      } else {
        if (productOperationType.value === "add") {
          productData.value.push({ ...productForm.value });
          console.log("productData.value---", productData.value);
        } else {
          productData.value[productOperationIndex.value] = {
            ...productForm.value,
          };
        }
        closeProductDia();
      }
    }
  });
};
const submitProductEdit = () => {
  productForm.value.salesLedgerId = currentId.value;
  productForm.value.type = 2;
  addOrUpdateSalesLedgerProduct(productForm.value).then((res) => {
    proxy.$modal.msgSuccess("提交成功");
    closeProductDia();
    getPurchaseById({ id: currentId.value, type: 2 }).then((res) => {
      productData.value = res.productData;
    });
  });
};
// åˆ é™¤äº§å“
const deleteProduct = () => {
  if (productSelectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  if (operationType.value === "add") {
    productSelectedRows.value.forEach((selectedRow) => {
      const index = productData.value.findIndex(
        (product) => product.id === selectedRow.id
      );
      if (index !== -1) {
        productData.value.splice(index, 1);
      }
    });
  } else {
    let ids = [];
    if (productSelectedRows.value.length > 0) {
      ids = productSelectedRows.value.map((item) => item.id);
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        delProduct(ids).then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          closeProductDia();
          getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
            (res) => {
              productData.value = res.productData;
            }
          );
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  }
};
// å…³é—­äº§å“å¼¹æ¡†
const closeProductDia = () => {
  proxy.resetForm("productFormRef");
  productFormVisible.value = false;
};
// å®¡æ‰¹é€šè¿‡æ–¹æ³•
const approvePurchase = (row) => {
  ElMessageBox.confirm(`确认通过采购合同号为 ${row.purchaseContractNumber} çš„审批?`, '审批确认', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  }).then(() => {
    updateApprovalStatus({ id: row.id, approvalStatus: 1}).then((res)=>{
      proxy.$modal.msgSuccess('审批成功');
      getList();
    })
  }).catch(() => {
    proxy.$modal.msg('已取消审批');
  });
};
// å®¡æ‰¹æ‹’绝方法
const rejectPurchase = (row) => {
  ElMessageBox.confirm(`确认拒绝采购合同号为 ${row.purchaseContractNumber} çš„审批?`, '审批确认', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  }).then(() => {
    updateApprovalStatus({ id: row.id, approvalStatus: 2}).then((res)=>{
      proxy.$modal.msgSuccess('审批成功');
      getList();
    })
  }).catch(() => {
    proxy.$modal.msg('已取消审批');
  });
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/purchase/ledger/export", {}, "采购台账.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
        // æ£€æŸ¥æ˜¯å¦æœ‰ä»–人维护的数据
        const unauthorizedData = selectedRows.value.filter(item => item.recorderName !== userStore.nickName);
        if (unauthorizedData.length > 0) {
            proxy.$modal.msgWarning("不可删除他人维护的数据");
            return;
        }
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      delPurchase(ids).then((res) => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
const mathNum = () => {
    if (!productForm.value.taxRate) {
        proxy.$modal.msgWarning("请先选择税率");
        return;
    }
  if (!productForm.value.taxInclusiveUnitPrice) {
    return;
  }
  if (!productForm.value.quantity) {
    return;
  }
  // å«ç¨Žæ€»ä»·è®¡ç®—
  productForm.value.taxInclusiveTotalPrice =
    proxy.calculateTaxIncludeTotalPrice(
      productForm.value.taxInclusiveUnitPrice,
      productForm.value.quantity
    );
  if (productForm.value.taxRate) {
    // ä¸å«ç¨Žæ€»ä»·è®¡ç®—
    productForm.value.taxExclusiveTotalPrice =
      proxy.calculateTaxExclusiveTotalPrice(
        productForm.value.taxInclusiveTotalPrice,
        productForm.value.taxRate
      );
  }
};
const reverseMathNum = (field) => {
    if (!productForm.value.taxRate) {
        proxy.$modal.msgWarning("请先选择税率");
        return;
    }
  const taxRate = Number(productForm.value.taxRate);
  if (!taxRate) return;
  if (field === 'taxInclusiveTotalPrice') {
    // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œæ•°é‡ï¼Œåç®—含税单价
    if (productForm.value.quantity) {
      productForm.value.taxInclusiveUnitPrice =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)).toFixed(2);
    }
    // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œå«ç¨Žå•价,反算数量
    else if (productForm.value.taxInclusiveUnitPrice) {
      productForm.value.quantity =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice)).toFixed(2);
    }
    // åç®—不含税总价
    productForm.value.taxExclusiveTotalPrice =
      (Number(productForm.value.taxInclusiveTotalPrice) / (1 + taxRate / 100)).toFixed(2);
  } else if (field === 'taxExclusiveTotalPrice') {
    // åç®—含税总价
    productForm.value.taxInclusiveTotalPrice =
      (Number(productForm.value.taxExclusiveTotalPrice) * (1 + taxRate / 100)).toFixed(2);
    // å·²çŸ¥æ•°é‡ï¼Œåç®—含税单价
    if (productForm.value.quantity) {
      productForm.value.taxInclusiveUnitPrice =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)).toFixed(2);
    }
    // å·²çŸ¥å«ç¨Žå•价,反算数量
    else if (productForm.value.taxInclusiveUnitPrice) {
      productForm.value.quantity =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice)).toFixed(2);
    }
  }
};
// é”€å”®åˆåŒé€‰æ‹©æ”¹å˜æ–¹æ³•
const salesLedgerChange = async (row) => {
  console.log("row", row);
  var index = salesContractList.value.findIndex((item) => item.id == row);
  console.log("index", index);
  if (index > -1) {
    form.value.projectName = salesContractList.value[index].projectName;
    await querygProductInfoByContractNo();
  }
};
const querygProductInfoByContractNo = async () => {
  const { code, data } = await getProductInfoByContractNo({
    contractNo: form.value.salesLedgerId,
  });
  if (code == 200) {
    productData.value = data;
  }
};
// æ˜¾ç¤ºäºŒç»´ç 
const showQRCode = async (row) => {
  try {
    // æž„建二维码内容,只包含采购合同号(纯文本)
    const qrContent = row.purchaseContractNumber || '';
    // æ£€æŸ¥å†…容是否为空
    if (!qrContent || qrContent.trim() === '') {
      proxy.$modal.msgWarning("该行没有采购合同号,无法生成二维码");
      return;
    }
    qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
      width: 200,
      margin: 2,
      color: {
        dark: '#000000',
        light: '#FFFFFF'
      }
    });
    qrCodeDialogVisible.value = true;
  } catch (error) {
    console.error('生成二维码失败:', error);
    proxy.$modal.msgError("生成二维码失败:" + error.message);
  }
};
// ä¸‹è½½äºŒç»´ç 
const downloadQRCode = () => {
  if (!qrCodeUrl.value) {
    proxy.$modal.msgWarning("二维码未生成");
    return;
  }
  const a = document.createElement('a');
  a.href = qrCodeUrl.value;
  a.download = `采购合同号二维码_${new Date().getTime()}.png`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  proxy.$modal.msgSuccess("下载成功");
};
// æ‰«ç æ–°å¢žå¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanAddDialogVisible = ref(false);
const scanAddForm = reactive({
  scanContent: "",
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  contractAmount: "",
  paymentMethod: "",
  recorderName: "",
  scanRemark: "",
});
const scanAddRules = {
  purchaseContractNumber: [{ required: true, message: "请输入采购合同号", trigger: "blur" }],
  supplierName: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
  projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
};
// æ‰«ç ç™»è®°å¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanDialogVisible = ref(false);
const scanForm = reactive({
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  scanTime: "",
  scannerName: "",
  scanStatus: "未扫码",
  scanRemark: "",
});
const scanRules = {
  scanRemark: [{ required: true, message: "请输入扫码备注", trigger: "blur" }],
};
const scanRecords = ref([]);
// æ‰“开扫码新增对话框
const openScanAddDialog = () => {
  scanAddForm.scanContent = "";
  scanAddForm.purchaseContractNumber = "";
  scanAddForm.supplierName = "";
  scanAddForm.projectName = "";
  scanAddForm.contractAmount = "";
  scanAddForm.paymentMethod = "";
  scanAddForm.recorderName = userStore.nickName;
  scanAddForm.scanRemark = "";
  scanAddDialogVisible.value = true;
};
// è§£æžæ‰«ç å†…容(模拟解析二维码数据)
const parseScanContent = (content) => {
  if (!content) return;
  // æ¨¡æ‹Ÿè§£æžäºŒç»´ç å†…容,这里可以根据实际需求调整解析逻辑
  // å‡è®¾æ‰«ç å†…容格式为:合同号|供应商|项目|金额|付款方式
  const parts = content.split('|');
  if (parts.length >= 3) {
    scanAddForm.purchaseContractNumber = parts[0] || "";
    scanAddForm.supplierName = parts[1] || "";
    scanAddForm.projectName = parts[2] || "";
    scanAddForm.contractAmount = parts[3] || "";
    scanAddForm.paymentMethod = parts[4] || "";
  }
};
// å…³é—­æ‰«ç æ–°å¢žå¯¹è¯æ¡†
const closeScanAddDialog = () => {
  scanAddDialogVisible.value = false;
  proxy.resetForm("scanAddFormRef");
};
// æäº¤æ‰«ç æ–°å¢ž
const submitScanAdd = () => {
  proxy.$refs["scanAddFormRef"].validate((valid) => {
    if (valid) {
      // æž„建新增数据
      const newData = {
        purchaseContractNumber: scanAddForm.purchaseContractNumber,
        supplierName: scanAddForm.supplierName,
        projectName: scanAddForm.projectName,
        contractAmount: scanAddForm.contractAmount,
        paymentMethod: scanAddForm.paymentMethod,
        recorderName: scanAddForm.recorderName,
        entryDate: getCurrentDate(),
        remark: scanAddForm.scanRemark,
        type: 2
      };
      // æ¨¡æ‹Ÿæ–°å¢žæˆåŠŸ
      proxy.$modal.msgSuccess("扫码新增成功!");
      closeScanAddDialog();
      // å¯ä»¥é€‰æ‹©æ˜¯å¦åˆ·æ–°åˆ—表
      // getList();
    }
  });
};
// æ‰“开扫码登记对话框
const openScanDialog = (row) => {
  scanForm.purchaseContractNumber = row.purchaseContractNumber;
  scanForm.supplierName = row.supplierName;
  scanForm.projectName = row.projectName;
  scanForm.scanTime = getCurrentDateTime();
  scanForm.scannerName = userStore.nickName;
  scanForm.scanStatus = "未扫码";
  scanForm.scanRemark = "";
  scanRecords.value = [];
  scanDialogVisible.value = true;
};
// å…³é—­æ‰«ç ç™»è®°å¯¹è¯æ¡†
const closeScanDialog = () => {
  scanDialogVisible.value = false;
  proxy.resetForm("scanFormRef");
};
// æäº¤æ‰«ç ç™»è®°
const submitScan = () => {
  proxy.$refs["scanFormRef"].validate((valid) => {
    if (valid) {
      // æ·»åŠ æ‰«ç è®°å½•
      scanRecords.value.push({
        ...scanForm,
        id: Date.now(), // æ¨¡æ‹ŸID
        scanTime: getCurrentDateTime(),
      });
      scanForm.scanStatus = "已扫码";
      scanForm.scanRemark = scanForm.scanRemark || "无";
      proxy.$modal.msgSuccess("扫码登记成功!");
      closeScanDialog();
    }
  });
};
// èŽ·å–å½“å‰æ—¥æœŸæ—¶é—´
function getCurrentDateTime() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// æ·»åŠ è¡Œç±»åæ–¹æ³•
const tableRowClassName = ({ row }) => {
  return row.isInvalid ? 'invalid-row' : '';
};
onMounted(() => {
  getList();
});
</script>
<style scoped lang="scss">
.invalid-row {
  opacity: 0.6;
  background-color: #f5f7fa;
}
</style>
src/views/collaborativeApproval/reportGeneration/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,596 @@
<template>
  <div class="report-generation">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <div class="page-header">
      <h2>项目总结报告生成</h2>
      <div class="header-actions">
        <el-button v-if="!reportGenerated" type="primary" @click="generateReport" :loading="generating">
          <el-icon><Document /></el-icon>
          ç”ŸæˆæŠ¥å‘Š
        </el-button>
        <el-button v-if="reportGenerated" type="primary" @click="resetConfig">
          <el-icon><Refresh /></el-icon>
          ç”Ÿæˆæ–°æŠ¥å‘Š
        </el-button>
        <el-button @click="exportReport" :disabled="!reportGenerated">
          <el-icon><Download /></el-icon>
          å¯¼å‡ºæŠ¥å‘Š
        </el-button>
      </div>
    </div>
    <!-- æŠ¥å‘Šé…ç½®åŒºåŸŸ -->
    <el-card class="config-card" v-if="!reportGenerated">
      <template #header>
        <span>报告配置</span>
      </template>
      <el-form :model="reportConfig" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="项目名称">
              <el-select v-model="reportConfig.projectId" placeholder="请选择项目">
                <el-option
                  v-for="project in projectList"
                  :key="project.id"
                  :label="project.name"
                  :value="project.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="报告周期">
              <el-date-picker
                v-model="reportConfig.dateRange"
                type="daterange"
                range-separator="至"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </el-card>
    <!-- æŠ¥å‘Šå†…容展示区域 -->
    <div v-if="reportGenerated" class="report-content">
      <!-- é¡¹ç›®åŸºæœ¬ä¿¡æ¯ -->
      <el-card class="report-section">
        <template #header>
          <span>项目基本信息</span>
        </template>
        <el-descriptions :column="2" border>
          <el-descriptions-item label="项目名称">{{ reportData.projectInfo.name }}</el-descriptions-item>
          <el-descriptions-item label="项目经理">{{ reportData.projectInfo.manager }}</el-descriptions-item>
          <el-descriptions-item label="开始时间">{{ reportData.projectInfo.startDate }}</el-descriptions-item>
          <el-descriptions-item label="结束时间">{{ reportData.projectInfo.endDate }}</el-descriptions-item>
          <el-descriptions-item label="项目状态">
            <el-tag :type="getStatusType(reportData.projectInfo.status)">
              {{ reportData.projectInfo.status }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="总预算">{{ reportData.projectInfo.budget }}</el-descriptions-item>
        </el-descriptions>
      </el-card>
      <!-- ä»»åŠ¡å®ŒæˆçŽ‡ç»Ÿè®¡ -->
      <el-card class="report-section">
        <template #header>
          <span>任务完成率统计</span>
        </template>
        <el-row :gutter="20">
          <el-col :span="12">
            <div class="completion-stats">
              <div class="stat-item">
                <div class="stat-label">总任务数</div>
                <div class="stat-value">{{ reportData.taskStats.total }}</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">已完成</div>
                <div class="stat-value completed">{{ reportData.taskStats.completed }}</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">进行中</div>
                <div class="stat-value in-progress">{{ reportData.taskStats.inProgress }}</div>
              </div>
              <div class="stat-item">
                <div class="stat-label">未开始</div>
                <div class="stat-value pending">{{ reportData.taskStats.pending }}</div>
              </div>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="completion-rate">
              <el-progress
                type="circle"
                :percentage="reportData.taskStats.completionRate"
                :width="150"
                :stroke-width="8"
              >
                <template #default="{ percentage }">
                  <span class="percentage-value">{{ percentage }}%</span>
                  <div class="percentage-label">完成率</div>
                </template>
              </el-progress>
            </div>
          </el-col>
        </el-row>
      </el-card>
      <!-- é—®é¢˜è®°å½•统计 -->
      <el-card class="report-section">
        <template #header>
          <span>问题记录统计</span>
        </template>
        <el-row :gutter="20">
          <el-col :span="8">
            <div class="issue-summary">
              <div class="summary-item">
                <div class="summary-label">总问题数</div>
                <div class="summary-value">{{ reportData.issueStats.total }}</div>
              </div>
              <div class="summary-item">
                <div class="summary-label">已解决</div>
                <div class="summary-value resolved">{{ reportData.issueStats.resolved }}</div>
              </div>
              <div class="summary-item">
                <div class="summary-label">待解决</div>
                <div class="summary-value pending">{{ reportData.issueStats.pending }}</div>
              </div>
            </div>
          </el-col>
          <el-col :span="16">
            <el-table :data="reportData.issueStats.topIssues" size="small">
              <el-table-column prop="title" label="主要问题" />
              <el-table-column prop="severity" label="严重程度" width="100">
                <template #default="{ row }">
                  <el-tag :type="getSeverityType(row.severity)" size="small">
                    {{ row.severity }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="status" label="状态" width="80">
                <template #default="{ row }">
                  <el-tag :type="row.status === '已解决' ? 'success' : 'warning'" size="small">
                    {{ row.status }}
                  </el-tag>
                </template>
              </el-table-column>
            </el-table>
          </el-col>
        </el-row>
      </el-card>
      <!-- å»¶è¯¯åˆ†æž -->
      <el-card class="report-section">
        <template #header>
          <span>延误分析</span>
        </template>
        <el-row :gutter="20">
          <el-col :span="12">
            <div class="delay-stats">
              <div class="delay-item">
                <div class="delay-label">延误任务数</div>
                <div class="delay-value">{{ reportData.delayAnalysis.delayedTasks }}</div>
              </div>
              <div class="delay-item">
                <div class="delay-label">平均延误天数</div>
                <div class="delay-value">{{ reportData.delayAnalysis.avgDelayDays }}</div>
              </div>
              <div class="delay-item">
                <div class="delay-label">最大延误天数</div>
                <div class="delay-value">{{ reportData.delayAnalysis.maxDelayDays }}</div>
              </div>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="delay-reasons">
              <h4>主要延误原因</h4>
              <ul>
                <li v-for="reason in reportData.delayAnalysis.reasons" :key="reason.reason">
                  {{ reason.reason }} ({{ reason.count }}次)
                </li>
              </ul>
            </div>
          </el-col>
        </el-row>
      </el-card>
      <!-- å›¢é˜Ÿç»©æ•ˆ -->
      <el-card class="report-section">
        <template #header>
          <span>团队绩效</span>
        </template>
        <el-table :data="reportData.teamPerformance" size="small">
          <el-table-column prop="name" label="成员姓名" />
          <el-table-column prop="completedTasks" label="完成任务数" />
          <el-table-column prop="completionRate" label="完成率" width="100">
            <template #default="{ row }">
              <el-progress :percentage="row.completionRate" :show-text="false" />
              <span style="margin-left: 10px;">{{ row.completionRate }}%</span>
            </template>
          </el-table-column>
          <el-table-column prop="performance" label="绩效评级" width="100">
            <template #default="{ row }">
              <el-tag :type="getPerformanceType(row.performance)">
                {{ row.performance }}
              </el-tag>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
      <!-- æ€»ç»“与建议 -->
      <el-card class="report-section">
        <template #header>
          <span>总结与建议</span>
        </template>
        <div class="summary-content">
          <h4>项目总结</h4>
          <p>{{ reportData.summary.conclusion }}</p>
          <h4>改进建议</h4>
          <ul>
            <li v-for="suggestion in reportData.summary.suggestions" :key="suggestion">
              {{ suggestion }}
            </li>
          </ul>
        </div>
      </el-card>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { Document, Download, Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// å“åº”式数据
const generating = ref(false)
const reportGenerated = ref(false)
// æŠ¥å‘Šé…ç½®
const reportConfig = reactive({
  projectId: '',
  dateRange: []
})
// é¡¹ç›®åˆ—表
const projectList = ref([
  { id: 1, name: '产品库存管理系统' },
  { id: 2, name: '客户关系管理平台' },
  { id: 3, name: '财务管理系统升级' },
  { id: 4, name: '移动端应用开发' }
])
// æŠ¥å‘Šæ•°æ®
const reportData = ref({})
// æ¨¡æ‹ŸæŠ¥å‘Šæ•°æ®
const mockReportData = {
  projectInfo: {
    name: '产品库存管理系统',
    manager: '张三',
    startDate: '2024-01-01',
    endDate: '2024-03-31',
    status: '已完成',
    budget: 'Â¥500,000'
  },
  taskStats: {
    total: 45,
    completed: 38,
    inProgress: 5,
    pending: 2,
    completionRate: 84
  },
  issueStats: {
    total: 12,
    resolved: 9,
    pending: 3,
    topIssues: [
      { title: '数据库连接超时', severity: '高', status: '已解决' },
      { title: '前端页面响应慢', severity: '中', status: '已解决' },
      { title: '权限验证异常', severity: '高', status: '待解决' },
      { title: '报表导出功能缺失', severity: '中', status: '待解决' }
    ]
  },
  delayAnalysis: {
    delayedTasks: 8,
    avgDelayDays: 3.5,
    maxDelayDays: 12,
    reasons: [
      { reason: '需求变更', count: 3 },
      { reason: '技术难题', count: 2 },
      { reason: '资源不足', count: 2 },
      { reason: '外部依赖', count: 1 }
    ]
  },
  teamPerformance: [
    { name: '李四', completedTasks: 12, completionRate: 92, performance: '优秀' },
    { name: '王五', completedTasks: 10, completionRate: 85, performance: '良好' },
    { name: '赵六', completedTasks: 8, completionRate: 78, performance: '良好' },
    { name: '钱七', completedTasks: 8, completionRate: 72, performance: '一般' }
  ],
  summary: {
    conclusion: '本项目整体执行情况良好,任务完成率达到84%,团队协作效率较高。主要问题集中在技术实现和需求变更方面,通过及时沟通和技术攻关,大部分问题已得到解决。',
    suggestions: [
      '加强需求分析阶段的工作,减少后期需求变更',
      '建立技术难题预警机制,提前识别和解决技术风险',
      '优化团队资源配置,提高整体工作效率',
      '完善项目管理流程,加强过程监控'
    ]
  }
}
// ç”ŸæˆæŠ¥å‘Š
const generateReport = async () => {
  if (!reportConfig.projectId) {
    ElMessage.warning('请选择项目')
    return
  }
  generating.value = true
  // æ¨¡æ‹Ÿç”ŸæˆæŠ¥å‘Šçš„过程
  setTimeout(() => {
    reportData.value = mockReportData
    reportGenerated.value = true
    generating.value = false
    ElMessage.success('报告生成成功')
  }, 2000)
}
// å¯¼å‡ºæŠ¥å‘Š
const exportReport = () => {
  ElMessage.success('报告导出功能开发中...')
}
// é‡ç½®é…ç½®ï¼Œç”Ÿæˆæ–°æŠ¥å‘Š
const resetConfig = () => {
  reportGenerated.value = false
  reportData.value = {}
  // é‡ç½®é…ç½®ä¸ºé»˜è®¤å€¼
  reportConfig.projectId = 1
  const endDate = new Date()
  const startDate = new Date()
  startDate.setDate(startDate.getDate() - 30)
  reportConfig.dateRange = [
    startDate.toISOString().split('T')[0],
    endDate.toISOString().split('T')[0]
  ]
  ElMessage.success('已重置配置,可以生成新报告')
}
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    '已完成': 'success',
    '进行中': 'warning',
    '未开始': 'info',
    '已暂停': 'danger'
  }
  return statusMap[status] || 'info'
}
// èŽ·å–ä¸¥é‡ç¨‹åº¦ç±»åž‹
const getSeverityType = (severity) => {
  const severityMap = {
    '高': 'danger',
    '中': 'warning',
    '低': 'success'
  }
  return severityMap[severity] || 'info'
}
// èŽ·å–ç»©æ•ˆç±»åž‹
const getPerformanceType = (performance) => {
  const performanceMap = {
    '优秀': 'success',
    '良好': 'primary',
    '一般': 'warning',
    '待改进': 'danger'
  }
  return performanceMap[performance] || 'info'
}
// ç»„件挂载时初始化
onMounted(() => {
  // è®¾ç½®é»˜è®¤é¡¹ç›®
  reportConfig.projectId = 1
  // è®¾ç½®é»˜è®¤æ—¥æœŸèŒƒå›´ï¼ˆæœ€è¿‘30天)
  const endDate = new Date()
  const startDate = new Date()
  startDate.setDate(startDate.getDate() - 30)
  reportConfig.dateRange = [
    startDate.toISOString().split('T')[0],
    endDate.toISOString().split('T')[0]
  ]
})
</script>
<style scoped>
.report-generation {
  padding: 20px;
}
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.page-header h2 {
  margin: 0;
  color: #303133;
}
.header-actions {
  display: flex;
  gap: 10px;
}
.config-card {
  margin-bottom: 20px;
}
.report-content {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.report-section {
  margin-bottom: 20px;
}
.completion-stats {
  display: flex;
  justify-content: space-around;
  padding: 20px 0;
}
.stat-item {
  text-align: center;
}
.stat-label {
  font-size: 14px;
  color: #909399;
  margin-bottom: 8px;
}
.stat-value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
}
.stat-value.completed {
  color: #67c23a;
}
.stat-value.in-progress {
  color: #e6a23c;
}
.stat-value.pending {
  color: #909399;
}
.completion-rate {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
}
.percentage-value {
  font-size: 20px;
  font-weight: bold;
  color: #409eff;
}
.percentage-label {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
}
.issue-summary {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px 0;
}
.summary-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.summary-label {
  font-size: 14px;
  color: #606266;
}
.summary-value {
  font-size: 18px;
  font-weight: bold;
  color: #303133;
}
.summary-value.resolved {
  color: #67c23a;
}
.summary-value.pending {
  color: #e6a23c;
}
.delay-stats {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px 0;
}
.delay-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.delay-label {
  font-size: 14px;
  color: #606266;
}
.delay-value {
  font-size: 18px;
  font-weight: bold;
  color: #e6a23c;
}
.delay-reasons h4 {
  margin: 0 0 15px 0;
  color: #303133;
}
.delay-reasons ul {
  margin: 0;
  padding-left: 20px;
}
.delay-reasons li {
  margin-bottom: 8px;
  color: #606266;
}
.summary-content h4 {
  margin: 0 0 10px 0;
  color: #303133;
}
.summary-content p {
  line-height: 1.6;
  color: #606266;
  margin-bottom: 20px;
}
.summary-content ul {
  margin: 0;
  padding-left: 20px;
}
.summary-content li {
  margin-bottom: 8px;
  color: #606266;
  line-height: 1.5;
}
</style>
src/views/collaborativeApproval/rpaManagement/index.vue
@@ -22,6 +22,7 @@
        </el-button>
      </div>
      <div>
        <el-button @click="handleExport" style="margin-right: 10px">导出</el-button>
        <el-button type="primary" @click="openForm('add')">新增</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
@@ -89,10 +90,10 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs } from "vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import {listRpa, addRpa, updateRpa, delRpa, delRpaBatch} from "@/api/collaborativeApproval/rpaManagement.js";
// å“åº”式数据
const data = reactive({
  searchForm: {
@@ -100,20 +101,17 @@
    status: "",
  },
  form: {
    id: "",
    programName: "",
    status: "stopped",
    description: "",
    createTime: "",
    status: "",
    description: ""
  },
  dialogVisible: false,
  dialogTitle: "",
  dialogType: "add",
  selectedIds: [],
  tableLoading: false,
  page: {
    current: 1,
    size: 100,
    size: 20,
    total: 0,
  },
  tableData: [],
@@ -123,6 +121,8 @@
// è¡¨å•引用
const formRef = ref();
// é€‰æ‹©çš„行数据
const selectedRows = ref([]);
// è¡¨å•验证规则
const rules = {
@@ -179,7 +179,7 @@
    label: "操作",
    align: "center",
    fixed: "right",
    width: 230,
    width: 150,
    operation: [
      {
        name: "编辑",
@@ -188,50 +188,26 @@
          openForm("edit", row);
        }
      },
      {
        name: "开始",
        type: "text",
        clickFun: (row) => {
          handleStart(row);
        },
        disabled: (row) => row.status !== 'stopped'
      },
      {
        name: "停止",
        type: "text",
        clickFun: (row) => {
          handleStop(row);
        },
        disabled: (row) => row.status === 'stopped'
      }
      // {
      //   name: "开始",
      //   type: "text",
      //   clickFun: (row) => {
      //     handleStart(row);
      //   },
      //   disabled: (row) => row.status !== 'stopped'
      // },
      // {
      //   name: "停止",
      //   type: "text",
      //   clickFun: (row) => {
      //     handleStop(row);
      //   },
      //   disabled: (row) => row.status === 'stopped'
      // }
    ]
  }
]);
// æ¨¡æ‹Ÿæ•°æ®
const mockData = [
  {
    id: "1",
    programName: "订单处理RPA",
    status: "running",
    description: "自动处理客户订单,包括验证、分配和确认",
    createTime: "2024-01-15 10:30:00"
  },
  {
    id: "2",
    programName: "库存同步RPA",
    status: "stopped",
    description: "同步多个仓库的库存数据,确保数据一致性",
    createTime: "2024-01-14 15:20:00"
  },
  {
    id: "3",
    programName: "报表生成RPA",
    status: "error",
    description: "自动生成每日销售报表和库存报表",
    createTime: "2024-01-13 09:15:00"
  }
];
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
@@ -240,32 +216,20 @@
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  page.value.current = 1;
  // page.value.current = 1;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  // æ¨¡æ‹ŸAPI调用延迟
  setTimeout(() => {
    let filteredData = [...mockData];
    // æ ¹æ®æœç´¢æ¡ä»¶è¿‡æ»¤æ•°æ®
    if (searchForm.value.programName) {
      filteredData = filteredData.filter(item =>
        item.programName.toLowerCase().includes(searchForm.value.programName.toLowerCase())
      );
    }
    if (searchForm.value.status) {
      filteredData = filteredData.filter(item => item.status === searchForm.value.status);
    }
    tableData.value = filteredData;
    page.value.total = filteredData.length;
  listRpa({...page.value, ...searchForm.value})
  .then(res => {
    tableLoading.value = false;
  }, 500);
    tableData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// åˆ†é¡µå¤„理
@@ -277,23 +241,16 @@
// é€‰æ‹©å˜åŒ–处理
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
  selectedRows.value = selection;
};
// æ‰“开表单
const openForm = (type, row) => {
  dialogType.value = type;
  dialogVisible.value = true;
  if (type === "add") {
    dialogTitle.value = "添加RPA";
    form.value = {
      id: "",
      programName: "",
      status: "stopped",
      description: "",
      createTime: "",
    };
  } else {
    dialogTitle.value = "编辑RPA";
    form.value = { ...row };
@@ -303,33 +260,38 @@
// æäº¤è¡¨å•
const submitForm = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    if (dialogType.value === "add") {
      // æ·»åŠ æ–°RPA
      const newRPA = {
        id: Date.now().toString(),
        programName: form.value.programName,
        status: form.value.status,
        description: form.value.description,
        createTime: new Date().toLocaleString(),
      };
      mockData.unshift(newRPA);
      ElMessage.success("RPA添加成功");
      addRpa({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("添加成功");
            form.value = {
            programName: "",
            status: "",
            description: ""
          },
          dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    } else {
      // ç¼–辑RPA
      const index = mockData.findIndex(item => item.id === form.value.id);
      if (index !== -1) {
        mockData[index] = { ...form.value };
        ElMessage.success("RPA更新成功");
      }
      updateRpa({...form.value}).then(res => {
        if(res.code == 200){
          ElMessage.success("更新成功");
          dialogVisible.value = false;
          getList();
        }
      }).catch(err => {
        ElMessage.error(err.msg);
      })
    }
    dialogVisible.value = false;
    getList();
  } catch (error) {
    console.error("表单验证失败:", error);
  }
@@ -368,33 +330,37 @@
// åˆ é™¤RPA
const handleDelete = () => {
  let ids = [];
  if (selectedIds.value.length > 0) {
    ids = selectedIds.value.map((item) => item.id);
  } else {
    ElMessage.warning("请选择要删除的RPA");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    // ä»Žæ¨¡æ‹Ÿæ•°æ®ä¸­åˆ é™¤é€‰ä¸­çš„项
    ids.forEach(id => {
      const index = mockData.findIndex(item => item.id === id);
      if (index !== -1) {
        mockData.splice(index, 1);
      }
    });
    ElMessage.success("删除成功");
    selectedIds.value = [];
    getList();
  }).catch(() => {
    // ç”¨æˆ·å–消
  });
    if (selectedRows.value.length > 0) {
        ids = selectedRows.value.map((item) => item.id);
    } else {
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
    .then(() => {
        delRpa(ids).then((res) => {
                if(res.code == 200){
                    ElMessage.success("删除成功");
                    getList();
                }
            }).catch(err => {
                ElMessage.error(err.msg);
            })
    })
    .catch(() => {
        proxy.$modal.msg("已取消");
    });
};
// å¯¼å‡ºåŠŸèƒ½
const { proxy } = getCurrentInstance()
const handleExport = () => {
  proxy.download('/rpaProcessAutomation/export', { ...searchForm.value }, 'RPA管理.xlsx')
}
</script>
<style scoped></style>
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,584 @@
<template>
  <div class="app-container">
        <!-- è§„章制度管理-->
          <el-card class="box-card">
            <template #header>
              <div class="card-header">
                <span>规章制度发布</span>
              </div>
            </template>
            <div class="tab-content">
              <el-row :gutter="20" class="mb-20">
                <span class="ml-10">制度标题:</span>
                <el-col :span="6">
                  <el-input v-model="regulationSearchForm.title" placeholder="请输入制度标题" clearable />
                </el-col>
                <span class="search_title">制度分类:</span>
                <el-col :span="4">
                  <el-select v-model="regulationSearchForm.category" placeholder="制度分类" clearable>
                    <el-option label="人事制度" value="hr" />
                    <el-option label="财务制度" value="finance" />
                    <el-option label="安全制度" value="safety" />
                    <el-option label="技术制度" value="tech" />
                  </el-select>
                </el-col>
                <el-col :span="8">
                  <el-button type="primary" @click="searchRegulations">搜索</el-button>
                  <el-button @click="resetRegulationSearch">重置</el-button>
                  <el-button @click="handleExport">导出</el-button>
                  <el-button type="success" @click="handleAdd">
                    å‘布制度
                  </el-button>
                </el-col>
              </el-row>
              <el-table :data="regulations" border v-loading="tableLoading"  style="width: 100%">
                <el-table-column prop="regulationNum" label="制度编号" width="120" />
                <el-table-column prop="title" label="制度标题" min-width="150" />
                <el-table-column prop="category" label="分类" width="120">
                  <template #default="scope">
                    <el-tag>{{ getCategoryText(scope.row.category) }}</el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="version" label="版本" width="120" />
                <el-table-column prop="createUserName" label="发布人" width="120" />
                <el-table-column prop="createTime" label="发布时间" width="180" />
                <el-table-column prop="status" label="状态" width="100">
                  <template #default="scope">
                    <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                      {{ scope.row.status === 'active' ? '生效中' : '已废止' }}
                    </el-tag>
                  </template>
                </el-table-column>
                <el-table-column prop="readCount" label="已读人数" width="100" />
                <el-table-column label="操作" width="320" fixed="right">
                  <template #default="scope">
                    <el-button link @click="viewRegulation(scope.row)">查看</el-button>
                    <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
                    <el-button link type="danger" @click="repealEdit(scope.row)">废弃</el-button>
                    <el-button link type="success" @click="viewVersionHistory(scope.row)">版本历史</el-button>
                    <el-button link type="warning" @click="viewReadStatus(scope.row)">阅读状态</el-button>
                    <el-button link type="primary" @click="openFileDialog(scope.row)">附件</el-button>
                  </template>
                </el-table-column>
              </el-table>
            </div>
          </el-card>
    <!-- ç”¨å°ç”³è¯·å¯¹è¯æ¡†ï¼ˆå·²ç§»é™¤ï¼‰ -->
    <!-- è§„章制度发布对话框 -->
    <el-dialog v-model="showRegulationDialog" :title="operationType === 'add' ? '发布制度' : '编辑制度'" width="800px">
      <el-form :model="regulationForm" :rules="regulationRules" ref="regulationFormRef" label-width="100px">
        <el-form-item label="制度编号" prop="regulationNum">
          <el-input v-model="regulationForm.regulationNum" placeholder="请输入制度编号" />
        </el-form-item>
        <el-form-item label="制度标题" prop="title">
          <el-input v-model="regulationForm.title" placeholder="请输入制度标题" />
        </el-form-item>
        <el-form-item label="制度分类" prop="category">
          <el-select v-model="regulationForm.category" placeholder="请选择制度分类" style="width: 100%">
            <el-option label="人事制度" value="hr" />
            <el-option label="财务制度" value="finance" />
            <el-option label="安全制度" value="safety" />
            <el-option label="技术制度" value="tech" />
          </el-select>
        </el-form-item>
        <el-form-item label="制度内容" prop="content">
          <el-input v-model="regulationForm.content" type="textarea" :rows="10" placeholder="请输入制度详细内容" />
        </el-form-item>
        <el-form-item label="制度版本" prop="version">
          <el-input v-model="regulationForm.version" placeholder="请输入制度版本" />
        </el-form-item>
        <el-form-item label="生效时间" prop="effectiveTime">
          <el-date-picker v-model="regulationForm.effectiveTime" type="datetime" format="YYYY-MM-DD HH:mm:ss"
             value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择生效时间" style="width: 100%" />
        </el-form-item>
        <el-form-item label="适用范围" prop="scope">
          <el-checkbox-group v-model="regulationForm.scope">
            <el-checkbox label="all">全体员工</el-checkbox>
            <el-checkbox label="manager">管理层</el-checkbox>
            <el-checkbox label="hr">人事部门</el-checkbox>
            <el-checkbox label="finance">财务部门</el-checkbox>
            <el-checkbox label="tech">技术部门</el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="是否需要确认" prop="requireConfirm">
          <el-switch v-model="regulationForm.requireConfirm" />
          <span class="ml-10">开启后员工需要阅读确认</span>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="showRegulationDialog = false">取消</el-button>
          <el-button type="primary" @click="submitRegulation">发布制度</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- ç”¨å°è¯¦æƒ…对话框(已移除) -->
    <!-- è§„章制度详情对话框 -->
    <el-dialog v-model="showRegulationDetailDialog" title="规章制度详情" width="800px">
      <div v-if="currentRegulationDetail">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="制度编号">{{ currentRegulationDetail.id }}</el-descriptions-item>
          <el-descriptions-item label="制度标题">{{ currentRegulationDetail.title }}</el-descriptions-item>
          <el-descriptions-item label="分类">{{ getCategoryText(currentRegulationDetail.category) }}</el-descriptions-item>
          <el-descriptions-item label="版本">{{ currentRegulationDetail.version }}</el-descriptions-item>
          <el-descriptions-item label="发布人">{{ currentRegulationDetail.createUserName }}</el-descriptions-item>
          <el-descriptions-item label="发布时间">{{ currentRegulationDetail.createTime }}</el-descriptions-item>
        </el-descriptions>
        <div class="mt-20">
          <h4>制度内容</h4>
          <div class="regulation-content">{{ currentRegulationDetail.content }}</div>
        </div>
        <!-- å¦‚æžœtableData>0 æ˜¾ç¤º -->
        <div style="margin: 10px 0;" v-if="tableData && tableData.length > 0" >
          <el-button type="success" @click="resetForm(currentRegulationDetail)">确认查看</el-button>
        </div>
      </div>
    </el-dialog>
    <!-- ç‰ˆæœ¬åŽ†å²å¯¹è¯æ¡† -->
    <el-dialog v-model="showVersionHistoryDialog" title="版本历史" width="800px">
      <el-table :data="versionHistory" style="width: 100%;margin-bottom: 10px">
        <el-table-column prop="version" label="版本号" width="100" />
        <el-table-column prop="updateTime" label="更新时间" width="180" />
        <el-table-column prop="createUserName" label="更新人" width="120" />
        <el-table-column prop="changeLog" label="变更说明">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
              {{ scope.row.status === 'active' ? '生效中' : '已废止' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
    <!-- é˜…读状态对话框 -->
    <el-dialog v-model="showReadStatusDialog" title="阅读状态" width="800px">
      <el-table :data="readStatusList" style="width: 100%;margin-bottom: 10px">
        <el-table-column prop="employee" label="员工姓名" width="120" />
        <el-table-column prop="department" label="所属部门" width="150" />
        <el-table-column prop="createTime" label="阅读时间" width="180" />
        <el-table-column prop="confirmTime" label="确认时间" width="180" />
        <el-table-column prop="status" label="状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'confirmed' ? 'success' : 'warning'">
              {{ scope.row.status === 'confirmed' ? '已确认' : '未确认' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
    <FileListDialog
      ref="fileListDialogRef"
      v-model="fileDialogVisible"
      :show-upload-button="true"
      :show-delete-button="true"
      :delete-method="handleAttachmentDelete"
      :rules-regulations-management-id="currentFileRuleId"
      :name-column-label="'附件名称'"
      @upload="handleAttachmentUpload"
    />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listRuleManagement,addRuleManagement,updateRuleManagement,delRuleManagement,getReadingStatusByRuleId,addReadingStatus,updateReadingStatus  } from '@/api/collaborativeApproval/sealManagement.js'
import FileListDialog from '@/components/Dialog/FileListDialog.vue'
import { listRuleFiles, delRuleFile, addRuleFile } from '@/api/collaborativeApproval/rulesRegulationsManagementFile.js'
// å“åº”式数据
const operationType = ref('add')
const tableData = ref([])
const tableLoading = ref(false)
// åˆ†é¡µå‚æ•°
const page = reactive({
  current: 1,
  size: 10,
  total: 0
})
// é™„件弹窗
const fileDialogVisible = ref(false)
const fileListDialogRef = ref(null)
const currentFileRuleId = ref(null)
const filePage = reactive({
  current: 1,
  size: 10,
  total: 0
})
// è§„章制度相关
const showRegulationDialog = ref(false)
const showRegulationDetailDialog = ref(false)
const showVersionHistoryDialog = ref(false)
const showReadStatusDialog = ref(false)
const currentRegulationDetail = ref(null)
const regulationFormRef = ref()
const regulationForm = reactive({
  id: '',
  regulationNum: '',
  title: '',
  category: '',
  content: '',
  version: '',
  status: 'active',
  readCount: 0,
  effectiveTime: '',
  scope: [],
  requireConfirm: false
})
const readStatus = ref({
  id: '',
  ruleId: '',
  employee: '',
  department: '',
  createTime: '',
  confirmTime: '',
  status: 'unconfirmed'
})
const regulationRules = {
  title: [{ required: true, message: '请输入制度标题', trigger: 'blur' }],
  category: [{ required: true, message: '请选择制度分类', trigger: 'change' }],
  content: [{ required: true, message: '请输入制度内容', trigger: 'blur' }],
  effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
  scope: [{ required: true, message: '请选择适用范围', trigger: 'change' }]
}
const regulationSearchForm = reactive({
  title: '',
  category: ''
})
const regulations = ref([])
const versionHistory = ref([])
const readStatusList = ref([])
  // { employee: '陈志强', department: '销售部', readTime: '2025-01-11 10:30:00', confirmTime: '2025-01-11 10:35:00', status: 'confirmed' },
  // { employee: '刘雅婷', department: '技术部', readTime: '2025-01-11 14:20:00', confirmTime: '', status: 'unconfirmed' },
  // { employee: '王建国', department: '财务部', readTime: '2025-01-12 09:15:00', confirmTime: '2025-01-12 09:20:00', status: 'confirmed' }
// åˆ¶åº¦åˆ†ç±»
const getCategoryText = (category) => {
  const categoryMap = {
    hr: '人事制度',
    finance: '财务制度',
    safety: '安全制度',
    tech: '技术制度'
  }
  return categoryMap[category] || '未知'
}
// æœç´¢åˆ¶åº¦
const searchRegulations = () => {
  page.current=1
  getRegulationList()
}
// é‡ç½®åˆ¶åº¦æœç´¢
const resetRegulationSearch = () => {
  regulationSearchForm.title = ''
  regulationSearchForm.category = ''
  searchRegulations()
}
// æ–°å¢ž
const handleAdd = () => {
  operationType.value = 'add'
  resetRegulationForm()
  showRegulationDialog.value = true
}
// ç¼–辑
const handleEdit = (row) => {
  operationType.value = 'edit'
  Object.assign(regulationForm, row)
  showRegulationDialog.value = true
}
// åºŸå¼ƒ
const repealEdit = (row) => {
  operationType.value = 'edit'
  Object.assign(regulationForm, row)
  regulationForm.status = 'repealed'
  ElMessageBox.confirm('确认废弃该制度?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    updateRuleManagement(regulationForm).then(res => {
      if(res.code == 200){
        ElMessage.success('制度废弃成功')
        // showRegulationDialog.value = false
        getRegulationList()
        resetRegulationForm()
      }
    })
  }).catch(() => {
    ElMessage({
      type: 'info',
      message: '已取消废弃'
    })
  })
}
// å‘布制度
const submitRegulation = async () => {
  try {
    await regulationFormRef.value.validate()
    if(operationType.value == 'add'){
      addRuleManagement(regulationForm).then(res => {
        if(res.code == 200){
          ElMessage.success('制度发布成功')
          showRegulationDialog.value = false
          getRegulationList()
          resetRegulationForm()
        }
      })
    }else{
      updateRuleManagement(regulationForm).then(res => {
        if(res.code == 200){
          ElMessage.success('制度编辑成功')
          showRegulationDialog.value = false
          resetRegulationForm()
          getRegulationList()
      }})}
  }catch(err){
    ElMessage.error(err.msg)
  }
}
//重置制度表单
const resetRegulationForm = () => {
  Object.assign(regulationForm, {
    id: '',
    regulationNum: '',
    title: '',
    category: '',
    content: '',
    version: '',
    status: 'active',
    readCount: 0,
    effectiveTime: '',
    scope: [],
    requireConfirm: false
})
}
// æŸ¥çœ‹åˆ¶åº¦ç‰ˆæœ¬åŽ†å²
const viewVersionHistory = (row) => {
  showVersionHistoryDialog.value = true
  const params = {
    category: row.category
  }
  listRuleManagement(page,params).then(res => {
    if(res.code == 200){
      versionHistory.value = res.data.records
    }
  })
}
// æŸ¥çœ‹åˆ¶åº¦è¯¦æƒ…
const viewRegulation = (row) => {
  currentRegulationDetail.value = row
  showRegulationDetailDialog.value = true
  getReadingStatusByRuleId(row.id).then(res => {
    if(res.code == 200){
      readStatusList.value = res.data
      if(readStatusList.value.length==0 && tableData.value.length>0){
          const params = {
          ruleId: row.id,
          employee: tableData.value[0].staffName,
          department: tableData.value[0].postJob,
          status: 'unconfirmed'
        }
        addReadingStatus(params).then(res => {
          if(res.code == 200){
            ElMessage.success('制度阅读成功')
          }
        })
      }
    }
  })
}
// æŸ¥çœ‹åˆ¶åº¦é˜…读状态
const viewReadStatus = (row) => {
  showReadStatusDialog.value = true
  //查看阅读状态列表
  getReadingStatusByRuleId(row.id).then(res => {
    if(res.code == 200){
      readStatusList.value = res.data
    }
  })
}
//确认查看
const resetForm = (row) => {
  console.log("row",row)
  row.readCount = row.readCount + 1
  updateRuleManagement(row).then(res => {
    if(res.code == 200){
      ElMessage.success('查看数量修改成功')
      //修改阅读状态
      //根据制度id和当前登录的员工得到阅读状态
      // let item = readStatusList.value.filter(item => item.employee == tableData.value[0].staffName )
      // if(item.length>0){
      //   item[0].status = 'confirmed',
      //   item[0].confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
      // }
      // ç­›é€‰å½“前员工对应该制度的阅读状态记录
      let statusItem = readStatusList.value.find(item => item.employee === tableData.value[0].staffName && item.ruleId === row.id);
      if (statusItem) {
        // å¦‚果找到记录,更新状态和确认时间
        statusItem.status = 'confirmed';
        // æ ¼å¼åŒ–时间为"YYYY-MM-DD HH:mm:ss"格式
        const now = new Date();
        statusItem.confirmTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
        // statusItem.confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
        updateReadingStatus(statusItem).then(res => {
          if(res.code == 200){
            ElMessage.success('制度阅读状态修改成功')
          }
        })
      }
    }
  })
}
// å¯¼å‡ºè§„章制度
const { proxy } = getCurrentInstance()
const handleExport = () => {
  proxy.download('/rulesRegulationsManagement/export', { ...regulationSearchForm }, '规章制度.xlsx')
}
// é™„件:查询
const fetchRuleFiles = async (rulesRegulationsManagementId) => {
  const params = {
    current: filePage.current,
    size: filePage.size,
    rulesRegulationsManagementId
  }
  const res = await listRuleFiles(params)
  const records = res?.data?.records || []
  filePage.total = res?.data?.total || records.length
  const mapped = records.map(item => ({
    id: item.id,
    name: item.fileName || item.name,
    url: item.fileUrl || item.url,
    raw: item
  }))
  fileListDialogRef.value?.setList(mapped)
}
// æ‰“开附件弹窗
const openFileDialog = async (row) => {
  currentFileRuleId.value = row.id
  fileDialogVisible.value = true
  await fetchRuleFiles(row.id)
}
// åˆ·æ–°é™„件列表
const refreshFileList = async () => {
  if (!currentFileRuleId.value) return
  await fetchRuleFiles(currentFileRuleId.value)
}
// ä¸Šä¼ é™„件(由子组件触发)
const handleAttachmentUpload = async (filePayload) => {
  if (!currentFileRuleId.value) return
  const payload = {
    name: filePayload?.fileName || filePayload?.name,
    url: filePayload?.fileUrl || filePayload?.url,
    rulesRegulationsManagementId: currentFileRuleId.value
  }
  await addRuleFile(payload)
  ElMessage.success('文件上传成功')
  await refreshFileList()
}
// åˆ é™¤é™„ä»¶
const handleAttachmentDelete = async (row) => {
  if (!row?.id) return false
  try {
    await ElMessageBox.confirm('确认删除该附件?', '提示', { type: 'warning' })
  } catch {
    return false
  }
  await delRuleFile([row.id])
  ElMessage.success('删除成功')
  await refreshFileList()
}
// èŽ·å–è§„ç« åˆ¶åº¦åˆ—è¡¨æ•°æ®
const getRegulationList = async () => {
  tableLoading.value = true
  listRuleManagement(page,regulationSearchForm)
  .then(res => {
    regulations.value = res.data.records
    // è¿‡æ»¤æŽ‰å·²åºŸå¼ƒçš„制度
    // regulations.value = res.data.records.filter(item => item.status !== 'repealed')
    page.value.total = res.data.total;
    tableLoading.value = false;
  }).catch(err => {
    tableLoading.value = false;
  })
}
onMounted(() => {
  // åˆå§‹åŒ–
  getRegulationList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.tab-content {
  padding: 20px 0;
}
.mb-20 {
  margin-bottom: 20px;
}
.mt-20 {
  margin-top: 20px;
}
.ml-10 {
  margin-left: 10px;
}
.regulation-content {
  background-color: #f5f5f5;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
  white-space: pre-wrap;
  height: 200px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/collaborativeApproval/sealManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,801 @@
<template>
  <div class="app-container">
    <el-card class="box-card">
      <template #header>
        <div class="card-header">
          <span>用印管理发布</span>
        </div>
      </template>
   <!-- ç”¨å°ç”³è¯·ç®¡ç† -->
        <div class="tab-content">
            <el-row :gutter="20" class="mb-20 ">
              <span class="ml-10">用印标题:</span>
              <el-col :span="4">
                <el-input v-model="sealSearchForm.title" placeholder="请输入申请标题" clearable />
              </el-col>
              <span class="ml-10">用印编号:</span>
              <el-col :span="4">
                <el-input v-model="sealSearchForm.applicationNum" placeholder="请输入用印编号" clearable />
              </el-col>
              <span class="search_title">审批状态:</span>
              <el-col :span="4">
                <el-select v-model="sealSearchForm.status" placeholder="审批状态" clearable>
                  <el-option label="待审批" value="pending" />
                  <el-option label="已通过" value="approved" />
                  <el-option label="已拒绝" value="rejected" />
                </el-select>
              </el-col>
              <el-col :span="8">
                <el-button type="primary" @click="searchSealApplications">搜索</el-button>
                <el-button @click="resetSealSearch">重置</el-button>
                <el-button @click="handleExport">导出</el-button>
                <el-button type="primary" @click="showSealApplyDialog = true">申请用印
                </el-button>
              </el-col>
            </el-row>
            <el-table :data="sealApplications" border v-loading="tableLoading" style="width: 100%">
              <el-table-column prop="applicationNum" label="申请编号" width="120" />
              <el-table-column prop="title" label="申请标题" min-width="200" />
              <el-table-column prop="createUserName" label="申请人" width="120" />
              <el-table-column prop="department" label="所属部门" width="150" />
              <el-table-column prop="sealType" label="用印类型" width="120">
                <template #default="scope">
                  {{ getSealTypeText(scope.row.sealType) }}
                </template>
              </el-table-column>
              <el-table-column prop="createTime" label="申请时间" width="180" />
              <el-table-column prop="status" label="状态" width="100">
                <template #default="scope">
                  <el-tag :type="getStatusType(scope.row.status)">
                    {{ getStatusText(scope.row.status) }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="操作" width="200" fixed="right">
                <template #default="scope">
                  <el-button link @click="viewSealDetail(scope.row)">查看</el-button>
                  <el-button
                    v-if="scope.row.status === 'pending'"
                                        link
                    type="primary"
                    @click="approveSeal(scope.row)"
                  >
                    å®¡æ‰¹
                  </el-button>
                  <el-button
                    v-if="scope.row.status === 'pending'"
                                        link
                    type="danger"
                    @click="rejectSeal(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-card>
    <!-- ç”¨å°ç”³è¯·å¯¹è¯æ¡† -->
    <el-dialog v-model="showSealApplyDialog" title="申请用印" width="600px">
      <el-form :model="sealForm" :rules="sealRules" ref="sealFormRef" label-width="100px">
        <el-form-item label="申请编号" prop="applicationNum">
          <el-input v-model="sealForm.applicationNum" placeholder="请输入申请编号" />
        </el-form-item>
        <el-form-item label="申请标题" prop="title">
          <el-input v-model="sealForm.title" placeholder="请输入申请标题" />
        </el-form-item>
        <el-form-item label="用印类型" prop="sealType">
          <el-select v-model="sealForm.sealType" placeholder="请选择用印类型" style="width: 100%">
            <el-option label="公章" value="official" />
            <el-option label="合同专用章" value="contract" />
            <el-option label="财务专用章" value="finance" />
            <el-option label="法人章" value="legal" />
          </el-select>
        </el-form-item>
        <el-form-item label="申请原因" prop="reason">
          <el-input v-model="sealForm.reason" type="textarea" :rows="4" placeholder="请详细说明用印原因" />
        </el-form-item>
        <el-form-item label="审批人" prop="approveUserId">
          <el-select v-model="sealForm.approveUserId" placeholder="请选择审批人" style="width: 100%" filterable>
            <el-option
                v-for="user in userList"
                :key="user.userId"
                :label="user.nickName"
                :value="user.userId"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="sealForm.urgency">
            <el-radio label="normal">普通</el-radio>
            <el-radio label="urgent">紧急</el-radio>
            <el-radio label="very-urgent">特急</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="showSealApplyDialog = false">取消</el-button>
          <el-button type="primary" @click="submitSealApplication">提交申请</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- è§„章制度发布对话框 -->
    <!-- <el-dialog v-model="showRegulationDialog" :title="operationType === 'add' ? '发布制度' : '编辑制度'" width="800px">
      <el-form :model="regulationForm" :rules="regulationRules" ref="regulationFormRef" label-width="100px">
        <el-form-item label="制度编号" prop="regulationNum">
          <el-input v-model="regulationForm.regulationNum" placeholder="请输入制度编号" />
        </el-form-item>
        <el-form-item label="制度标题" prop="title">
          <el-input v-model="regulationForm.title" placeholder="请输入制度标题" />
        </el-form-item>
        <el-form-item label="制度分类" prop="category">
          <el-select v-model="regulationForm.category" placeholder="请选择制度分类" style="width: 100%">
            <el-option label="人事制度" value="hr" />
            <el-option label="财务制度" value="finance" />
            <el-option label="安全制度" value="safety" />
            <el-option label="技术制度" value="tech" />
          </el-select>
        </el-form-item>
        <el-form-item label="制度内容" prop="content">
          <el-input v-model="regulationForm.content" type="textarea" :rows="10" placeholder="请输入制度详细内容" />
        </el-form-item>
        <el-form-item label="制度版本" prop="version">
          <el-input v-model="regulationForm.version" placeholder="请输入制度版本" />
        </el-form-item>
        <el-form-item label="生效时间" prop="effectiveTime">
          <el-date-picker v-model="regulationForm.effectiveTime" type="datetime" format="YYYY-MM-DD HH:mm:ss"
             value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择生效时间" style="width: 100%" />
        </el-form-item>
        <el-form-item label="适用范围" prop="scope">
          <el-checkbox-group v-model="regulationForm.scope">
            <el-checkbox label="all">全体员工</el-checkbox>
            <el-checkbox label="manager">管理层</el-checkbox>
            <el-checkbox label="hr">人事部门</el-checkbox>
            <el-checkbox label="finance">财务部门</el-checkbox>
            <el-checkbox label="tech">技术部门</el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="是否需要确认" prop="requireConfirm">
          <el-switch v-model="regulationForm.requireConfirm" />
          <span class="ml-10">开启后员工需要阅读确认</span>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="showRegulationDialog = false">取消</el-button>
          <el-button type="primary" @click="submitRegulation">发布制度</el-button>
        </span>
      </template>
    </el-dialog> -->
    <!-- ç”¨å°è¯¦æƒ…对话框 -->
    <el-dialog v-model="showSealDetailDialog" title="用印申请详情" width="700px">
      <div v-if="currentSealDetail" class="mb10">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="申请编号">{{ currentSealDetail.id }}</el-descriptions-item>
          <el-descriptions-item label="申请标题">{{ currentSealDetail.title }}</el-descriptions-item>
          <el-descriptions-item label="申请人">{{ currentSealDetail.createUserName }}</el-descriptions-item>
          <el-descriptions-item label="所属部门">{{ currentSealDetail.department }}</el-descriptions-item>
          <el-descriptions-item label="用印类型">{{ getSealTypeText(currentSealDetail.sealType) }}</el-descriptions-item>
          <el-descriptions-item label="申请时间">{{ currentSealDetail.createTime }}</el-descriptions-item>
          <el-descriptions-item label="状态">
            <el-tag :type="getStatusType(currentSealDetail.status)">
              {{ getStatusText(currentSealDetail.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
        </el-descriptions>
      </div>
    </el-dialog>
    <!-- è§„章制度详情对话框 -->
    <el-dialog v-model="showRegulationDetailDialog" title="规章制度详情" width="800px">
      <div v-if="currentRegulationDetail">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="制度编号">{{ currentRegulationDetail.id }}</el-descriptions-item>
          <el-descriptions-item label="制度标题">{{ currentRegulationDetail.title }}</el-descriptions-item>
          <el-descriptions-item label="分类">{{ getCategoryText(currentRegulationDetail.category) }}</el-descriptions-item>
          <el-descriptions-item label="版本">{{ currentRegulationDetail.version }}</el-descriptions-item>
          <el-descriptions-item label="发布人">{{ currentRegulationDetail.createUserName }}</el-descriptions-item>
          <el-descriptions-item label="发布时间">{{ currentRegulationDetail.createTime }}</el-descriptions-item>
        </el-descriptions>
        <div class="mt-20">
          <h4>制度内容</h4>
          <div class="regulation-content">{{ currentRegulationDetail.content }}</div>
        </div>
        <!-- å¦‚æžœtableData>0 æ˜¾ç¤º -->
        <div style="margin: 10px 0;" v-if="tableData && tableData.length > 0" >
          <el-button type="success" @click="resetForm(currentRegulationDetail)">确认查看</el-button>
        </div>
      </div>
    </el-dialog>
    <!-- ç‰ˆæœ¬åŽ†å²å¯¹è¯æ¡† -->
    <el-dialog v-model="showVersionHistoryDialog" title="版本历史" width="800px">
      <el-table :data="versionHistory" style="width: 100%;margin-bottom: 10px">
        <el-table-column prop="version" label="版本号" width="100" />
        <el-table-column prop="updateTime" label="更新时间" width="180" />
        <el-table-column prop="createUserName" label="更新人" width="120" />
        <el-table-column prop="changeLog" label="变更说明">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
              {{ scope.row.status === 'active' ? '生效中' : '已废止' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
    <!-- é˜…读状态对话框 -->
    <el-dialog v-model="showReadStatusDialog" title="阅读状态" width="800px">
      <el-table :data="readStatusList" style="width: 100%;margin-bottom: 10px">
        <el-table-column prop="employee" label="员工姓名" width="120" />
        <el-table-column prop="department" label="所属部门" width="150" />
        <el-table-column prop="createTime" label="阅读时间" width="180" />
        <el-table-column prop="confirmTime" label="确认时间" width="180" />
        <el-table-column prop="status" label="状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'confirmed' ? 'success' : 'warning'">
              {{ scope.row.status === 'confirmed' ? '已确认' : '未确认' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { listSealApplication, addSealApplication, updateSealApplication,listRuleManagement,addRuleManagement,updateRuleManagement,delRuleManagement,getReadingStatusByRuleId,getReadingStatusList,addReadingStatus,updateReadingStatus  } from '@/api/collaborativeApproval/sealManagement.js'
import { el } from 'element-plus/es/locales.mjs'
import { getUserProfile, userListNoPageByTenantId } from '@/api/system/user.js'
import {staffJoinDel, staffJoinListPage} from "@/api/personnelManagement/onboarding.js";
import useUserStore from '@/store/modules/user'
import { userLoginFacotryList } from "@/api/system/user.js"
// å“åº”式数据
const currentUser = ref(null)
const activeTab = ref('seal')
const operationType = ref('add')
const tableData = ref([])
// ç”¨å°ç”³è¯·ç›¸å…³
const userStore = useUserStore()
const route = useRoute()
const showSealApplyDialog = ref(false)
const tableLoading = ref(false)
const showSealDetailDialog = ref(false)
const currentSealDetail = ref(null)
const sealFormRef = ref()
const userList = ref([])
const sealForm = reactive({
  applicationNum: '',
  title: '',
  sealType: '',
  reason: '',
  approveUserId: '',
  urgency: 'normal',
  status: 'pending'
})
const sealRules = {
  applicationNum: [{ required: true, message: '请输入申请编号', trigger: 'blur' }],
  title: [{ required: true, message: '请输入申请标题', trigger: 'blur' }],
  sealType: [{ required: true, message: '请选择用印类型', trigger: 'change' }],
  reason: [{ required: true, message: '请输入申请原因', trigger: 'blur' }],
  approveUserId: [{ required: true, message: '请选择审批人', trigger: 'change' }]
}
const sealSearchForm = reactive({
  title: '',
  status: '',
  applicationNum: ''
})
// åˆ†é¡µå‚æ•°
const page = reactive({
  current: 1,
  size: 100,
  total: 0
})
// è§„章制度相关
const showRegulationDialog = ref(false)
const showRegulationDetailDialog = ref(false)
const showVersionHistoryDialog = ref(false)
const showReadStatusDialog = ref(false)
const currentRegulationDetail = ref(null)
const regulationFormRef = ref()
const regulationForm = reactive({
  id: '',
  regulationNum: '',
  title: '',
  category: '',
  content: '',
  version: '',
  status: 'active',
  readCount: 0,
  effectiveTime: '',
  scope: [],
  requireConfirm: false
})
const readStatus = ref({
  id: '',
  ruleId: '',
  employee: '',
  department: '',
  createTime: '',
  confirmTime: '',
  status: 'unconfirmed'
})
const regulationRules = {
  title: [{ required: true, message: '请输入制度标题', trigger: 'blur' }],
  category: [{ required: true, message: '请选择制度分类', trigger: 'change' }],
  content: [{ required: true, message: '请输入制度内容', trigger: 'blur' }],
  effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
  scope: [{ required: true, message: '请选择适用范围', trigger: 'change' }]
}
const regulationSearchForm = reactive({
  title: '',
  category: ''
})
// å‡æ•°æ®
const sealApplications = ref([])
const regulations = ref([])
const versionHistory = ref([])
const readStatusList = ref([])
  // { employee: '陈志强', department: '销售部', readTime: '2025-01-11 10:30:00', confirmTime: '2025-01-11 10:35:00', status: 'confirmed' },
  // { employee: '刘雅婷', department: '技术部', readTime: '2025-01-11 14:20:00', confirmTime: '', status: 'unconfirmed' },
  // { employee: '王建国', department: '财务部', readTime: '2025-01-12 09:15:00', confirmTime: '2025-01-12 09:20:00', status: 'confirmed' }
// ç”¨å°ç”³è¯·çŠ¶æ€
const getStatusType = (status) => {
  const statusMap = {
    pending: 'warning',
    approved: 'success',
    rejected: 'danger'
  }
  return statusMap[status] || 'info'
}
// åˆ¶åº¦çŠ¶æ€
const getStatusText = (status) => {
  const statusMap = {
    pending: '待审批',
    approved: '已通过',
    rejected: '已拒绝'
  }
  return statusMap[status] || '未知'
}
// ç”¨å°ç±»åž‹
const getSealTypeText = (sealType) => {
  const sealTypeMap = {
    official: '公章',
    contract: '合同专用章',
    finance: '财务专用章',
    tegal: '技术专用章'
  }
  return sealTypeMap[sealType] || '未知'
}
// åˆ¶åº¦åˆ†ç±»
const getCategoryText = (category) => {
  const categoryMap = {
    hr: '人事制度',
    finance: '财务制度',
    safety: '安全制度',
    tech: '技术制度'
  }
  return categoryMap[category] || '未知'
}
// æœç´¢å°ç« ç”³è¯·
const searchSealApplications = () => {
  page.current=1
  getSealApplicationList()
  // ElMessage.success('搜索完成')
}
// é‡ç½®å°ç« ç”³è¯·æœç´¢
const resetSealSearch = () => {
  sealSearchForm.title = ''
  sealSearchForm.status = ''
  sealSearchForm.applicationNum = ''
  searchSealApplications()
}
// æœç´¢åˆ¶åº¦
const searchRegulations = () => {
  page.current=1
  getRegulationList()
}
// é‡ç½®åˆ¶åº¦æœç´¢
const resetRegulationSearch = () => {
  regulationSearchForm.title = ''
  regulationSearchForm.category = ''
  searchRegulations()
}
// æäº¤ç”¨å°ç”³è¯·
const submitSealApplication = async () => {
  try {
    await sealFormRef.value.validate()
    addSealApplication(sealForm).then(res => {
      if(res.code == 200){
        ElMessage.success('申请提交成功')
        showSealApplyDialog.value = false
        getSealApplicationList()
        Object.assign(sealForm, {
        applicationNum: '',
        title: '',
        sealType: '',
        reason: '',
        approveUserId: '',
        urgency: 'normal',
        status: 'pending'
      })
      }
    }).catch(err => {
      ElMessage.error(err.msg)
    })
  } catch (error) {
    ElMessage.error('请完善申请信息')
  }
}
// æ–°å¢ž
const handleAdd = () => {
  operationType.value = 'add'
  resetRegulationForm()
  showRegulationDialog.value = true
}
// ç¼–辑
const handleEdit = (row) => {
  operationType.value = 'edit'
  Object.assign(regulationForm, row)
  showRegulationDialog.value = true
}
// åºŸå¼ƒ
const repealEdit = (row) => {
  operationType.value = 'edit'
  Object.assign(regulationForm, row)
  regulationForm.status = 'repealed'
  ElMessageBox.confirm('确认废弃该制度?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    updateRuleManagement(regulationForm).then(res => {
      if(res.code == 200){
        ElMessage.success('制度废弃成功')
        // showRegulationDialog.value = false
        getRegulationList()
        resetRegulationForm()
      }
    })
  }).catch(() => {
    ElMessage({
      type: 'info',
      message: '已取消废弃'
    })
  })
}
// å‘布制度
const submitRegulation = async () => {
  try {
    await regulationFormRef.value.validate()
    if(operationType.value == 'add'){
      addRuleManagement(regulationForm).then(res => {
        if(res.code == 200){
          ElMessage.success('制度发布成功')
          showRegulationDialog.value = false
          getRegulationList()
          resetRegulationForm()
        }
      })
    }else{
      updateRuleManagement(regulationForm).then(res => {
        if(res.code == 200){
          ElMessage.success('制度编辑成功')
          showRegulationDialog.value = false
          resetRegulationForm()
          getRegulationList()
      }})}
  }catch(err){
    ElMessage.error(err.msg)
  }
}
//重置制度表单
const resetRegulationForm = () => {
  Object.assign(regulationForm, {
    id: '',
    regulationNum: '',
    title: '',
    category: '',
    content: '',
    version: '',
    status: 'active',
    readCount: 0,
    effectiveTime: '',
    scope: [],
    requireConfirm: false
})
}
// æŸ¥çœ‹ç”¨å°ç”³è¯·è¯¦æƒ…
const viewSealDetail = (row) => {
  currentSealDetail.value = row
  showSealDetailDialog.value = true
}
// å®¡æ‰¹ç”¨å°ç”³è¯·
const approveSeal = (row) => {
  console.log(row)
  ElMessageBox.confirm('确认通过该用印申请?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    row.status = 'approved'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('审批通过')
      }
    })
  })
}
// æ‹’绝用印申请
const rejectSeal = (row) => {
  ElMessageBox.prompt('请输入拒绝原因', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    inputPattern: /\S+/,
    inputErrorMessage: '拒绝原因不能为空'
  }).then(({ value }) => {
    row.status = 'rejected'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('审批拒绝')
      }
    })
    ElMessage.success('已拒绝申请')
  })
}
// èŽ·å–åœ¨èŒå‘˜å·¥åˆ—è¡¨
const getList = () => {
  tableLoading.value = true;
      //获取当前登录用户信息
  getUserProfile().then(res => {
    if(res.code == 200){
      console.log(res.data.userName)
      currentUser.value = res.data.userName
    }
  })
  staffJoinListPage({staffState: 1, ...page}).then(res => {
    tableLoading.value = false;
    // tableData.value = res.data.records
    // //筛选出和currentUser同名的人员
    tableData.value = res.data.records.filter(item => item.staffName === currentUser.value)
    page.total = res.data.total;
    if(tableData.value.length == 0){
    ElMessage.error('当前用户未加入任何部门')
    }
  }).catch(err => {
    tableLoading.value = false;
  })
};
// æŸ¥çœ‹åˆ¶åº¦ç‰ˆæœ¬åŽ†å²
const viewVersionHistory = (row) => {
  showVersionHistoryDialog.value = true
  const params = {
    category: row.category
  }
  listRuleManagement(page,params).then(res => {
    if(res.code == 200){
      versionHistory.value = res.data.records
    }
  })
}
// æŸ¥çœ‹åˆ¶åº¦è¯¦æƒ…
const viewRegulation = (row) => {
  getList()
  currentRegulationDetail.value = row
  showRegulationDetailDialog.value = true
  getReadingStatusByRuleId(row.id).then(res => {
    if(res.code == 200){
      readStatusList.value = res.data
      if(readStatusList.value.length==0 && tableData.value.length>0){
          const params = {
          ruleId: row.id,
          employee: tableData.value[0].staffName,
          department: tableData.value[0].postJob,
          status: 'unconfirmed'
        }
        addReadingStatus(params).then(res => {
          if(res.code == 200){
            ElMessage.success('制度阅读成功')
          }
        })
      }
    }
  })
}
// æŸ¥çœ‹åˆ¶åº¦é˜…读状态
const viewReadStatus = (row) => {
  showReadStatusDialog.value = true
  //查看阅读状态列表
  getReadingStatusByRuleId(row.id).then(res => {
    if(res.code == 200){
      readStatusList.value = res.data
    }
  })
}
//确认查看
const resetForm = (row) => {
  console.log("row",row)
  row.readCount = row.readCount + 1
  updateRuleManagement(row).then(res => {
    if(res.code == 200){
      ElMessage.success('查看数量修改成功')
      //修改阅读状态
      //根据制度id和当前登录的员工得到阅读状态
      // let item = readStatusList.value.filter(item => item.employee == tableData.value[0].staffName )
      // if(item.length>0){
      //   item[0].status = 'confirmed',
      //   item[0].confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
      // }
      // ç­›é€‰å½“前员工对应该制度的阅读状态记录
      let statusItem = readStatusList.value.find(item => item.employee === tableData.value[0].staffName && item.ruleId === row.id);
      if (statusItem) {
        // å¦‚果找到记录,更新状态和确认时间
        statusItem.status = 'confirmed';
        // æ ¼å¼åŒ–时间为"YYYY-MM-DD HH:mm:ss"格式
        const now = new Date();
        statusItem.confirmTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
        // statusItem.confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
        updateReadingStatus(statusItem).then(res => {
          if(res.code == 200){
            ElMessage.success('制度阅读状态修改成功')
          }
        })
      }
    }
  })
}
// å¯¼å‡ºç”¨å°ç”³è¯·
const { proxy } = getCurrentInstance()
const handleExport = () => {
  proxy.download('/sealApplicationManagement/export', { ...sealSearchForm }, '用印申请.xlsx')
}
// èŽ·å–å°ç« ç”³è¯·åˆ—è¡¨æ•°æ®
const getSealApplicationList = async () => {
  tableLoading.value = true
  listSealApplication(page,sealSearchForm)
  .then(res => {
    //获取当前登录的部门信息
// èŽ·å–å½“å‰ç™»å½•çš„éƒ¨é—¨ä¿¡æ¯å¹¶è¿‡æ»¤æ•°æ®
    const currentFactoryName = userStore.currentFactoryName
    if (currentFactoryName) {
      // æ ¹æ®currentFactoryName过滤出department相同的数据
      sealApplications.value = res.data.records.filter(item => item.department === currentFactoryName)
      // æ›´æ–°è¿‡æ»¤åŽçš„æ€»æ•°
      page.total = sealApplications.value.length
    } else {
      // å¦‚果没有currentFactoryName,则显示所有数据
      sealApplications.value = res.data.records
      page.total = res.data.total
    }
    // sealApplications.value = res.data.records
    // page.value.total = res.data.total;
    tableLoading.value = false;
  }).catch(err => {
    tableLoading.value = false;
  })
}
// èŽ·å–è§„ç« åˆ¶åº¦åˆ—è¡¨æ•°æ®
const getRegulationList = async () => {
  tableLoading.value = true
  listRuleManagement(page,regulationSearchForm)
  .then(res => {
    regulations.value = res.data.records
    // è¿‡æ»¤æŽ‰å·²åºŸå¼ƒçš„制度
    // regulations.value = res.data.records.filter(item => item.status !== 'repealed')
    page.total = res.data.total;
    tableLoading.value = false;
  }).catch(err => {
    tableLoading.value = false;
  })
}
// ç›‘听对话框打开,获取用户列表
watch(showSealApplyDialog, (newVal) => {
  if (newVal) {
    userListNoPageByTenantId().then((res) => {
      userList.value = res.data;
    });
  }
});
onMounted(() => {
  // è·¯ç”±æºå¸¦ applicationNum æ—¶ï¼Œé¢„填并查询
  if (route.query.applicationNum) {
    sealSearchForm.applicationNum = String(route.query.applicationNum)
    page.current = 1
    getSealApplicationList()
  } else {
    getSealApplicationList()
  }
  getRegulationList()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.tab-content {
  padding: 20px 0;
}
.mb-20 {
  margin-bottom: 20px;
}
.mt-20 {
  margin-top: 20px;
}
.ml-10 {
  margin-left: 10px;
}
.regulation-content {
  background-color: #f5f5f5;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
  white-space: pre-wrap;
  height: 200px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/collaborativeApproval/warningSystem/index.vue
@@ -33,7 +33,6 @@
            <th>类型</th>
            <th>等级</th>
            <th>状态</th>
            <th>责任人</th>
            <th>操作</th>
          </tr>
        </thead>
@@ -52,7 +51,6 @@
                {{ warning.statusText }}
              </span>
            </td>
            <td>{{ warning.responsible }}</td>
            <td>
              <button @click="viewDetail(warning)">查看详情</button>
            </td>
@@ -96,7 +94,7 @@
          levelText: '红色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '张经理',
          responsible: '陈志强',
          description: 'A项目预算执行率已达95%,预计将超出预算范围。',
          impact: '影响项目整体财务指标,可能导致项目亏损',
          suggestions: '暂停非必要支出,优化资源配置,申请预算调整'
@@ -148,7 +146,7 @@
          levelText: '红色预警',
          status: 'pending',
          statusText: '待处理',
          responsible: '陈总监',
          responsible: '陈志强',
          description: '产品D在客户现场出现质量问题。',
          impact: '影响客户满意度,可能造成经济损失',
          suggestions: '立即召回问题产品,分析原因,制定改进措施'
src/views/inventoryManagement/stockWarningLedger/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,360 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="产品大类:">
          <el-input
            v-model="searchForm.productCategory"
            placeholder="请输入产品大类"
            clearable
            style="width: 200px"
          />
        </el-form-item>
        <el-form-item label="规格型号:">
          <el-input
            v-model="searchForm.specificationModel"
            placeholder="请输入规格型号"
            clearable
            style="width: 200px"
          />
        </el-form-item>
        <el-form-item label="预警级别:">
          <el-select
            v-model="searchForm.warningLevel"
            placeholder="请选择预警级别"
            clearable
            style="width: 150px"
          >
            <el-option label="紧急" value="紧急" />
            <el-option label="重要" value="重要" />
            <el-option label="一般" value="一般" />
          </el-select>
        </el-form-item>
        <el-form-item label="预警状态:">
          <el-select
            v-model="searchForm.warningStatus"
            placeholder="请选择预警状态"
            clearable
            style="width: 150px"
          >
            <el-option label="已预警" value="已预警" />
            <el-option label="正常" value="正常" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery">搜索</el-button>
          <el-button @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="table_list">
      <div class="actions"></div>
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        style="width: 100%"
        height="calc(100vh - 280px)"
      >
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="批次号" prop="code" width="130" show-overflow-tooltip />
        <el-table-column label="产品大类" prop="productCategory" show-overflow-tooltip />
        <el-table-column label="规格型号" prop="specificationModel" show-overflow-tooltip />
        <el-table-column label="当前库存" prop="currentStock" width="120" show-overflow-tooltip>
          <template #default="scope">
            <span :class="getStockClass(scope.row)">{{ scope.row.currentStock || 0 }}</span>
          </template>
        </el-table-column>
        <el-table-column label="最低库存" prop="warnNum" width="120" show-overflow-tooltip />
        <el-table-column label="预警级别" prop="warningLevel" width="100" show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="getWarningLevelTag(scope.row.warningLevel)">
              {{ scope.row.warningLevel || '-' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="预警状态" prop="warningStatus" width="100" show-overflow-tooltip>
          <template #default="scope">
            <el-tag :type="scope.row.warningStatus === '已预警' ? 'danger' : 'success'">
              {{ scope.row.warningStatus || '正常' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="预警时间" prop="warningTime" width="150" show-overflow-tooltip />
        <el-table-column label="预计缺货时间" prop="expectedShortageTime" width="150" show-overflow-tooltip>
          <template #default="scope">
            <div v-if="scope.row.expectedShortageTime">
              <div v-if="getCountdown(scope.row.expectedShortageTime).isExpired" class="countdown-expired">
                <el-tag type="danger">已缺货</el-tag>
              </div>
              <div v-else class="countdown-timer">
                <span :class="getCountdownClass(scope.row.expectedShortageTime)">
                  {{ getCountdown(scope.row.expectedShortageTime).text }}
                </span>
              </div>
            </div>
            <span v-else>-</span>
          </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>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import pagination from '@/components/PIMTable/Pagination.vue'
import {
  getStockWarningLedgerPage
} from '@/api/inventoryManagement/stockWarningLedger.js'
// å“åº”式数据
const tableData = ref([])
const tableLoading = ref(false)
const total = ref(0)
// åˆ†é¡µå‚æ•°
const page = reactive({
  current: 1,
  size: 100
})
// æœç´¢è¡¨å•
const searchForm = reactive({
  productCategory: '',
  specificationModel: '',
  warningLevel: '',
  warningStatus: ''
})
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  tableLoading.value = true
  const params = {
    ...page,
    ...searchForm
  }
  getStockWarningLedgerPage(params)
    .then(res => {
      tableLoading.value = false
      if (res.code === 200) {
        tableData.value = res.data.records || []
        total.value = res.data.total || 0
        // è®¡ç®—预警级别和状态
        tableData.value = tableData.value.map(item => {
          const currentStock = parseFloat(item.inboundNum0 || item.currentStock || 0)
          const warnNum = parseFloat(item.warnNum || 0)
          const safetyStock = parseFloat(item.safetyStock || warnNum * 1.2)
          // è®¡ç®—预警级别
          if (currentStock <= 0) {
            item.warningLevel = '紧急'
            item.warningStatus = '已预警'
          } else if (currentStock < warnNum) {
            item.warningLevel = '重要'
            item.warningStatus = '已预警'
          } else if (currentStock < safetyStock) {
            item.warningLevel = '一般'
            item.warningStatus = '已预警'
          } else {
            item.warningLevel = ''
            item.warningStatus = '正常'
          }
          // è®¡ç®—预计缺货时间(基于日均消耗量,这里简化处理)
          if (item.warningStatus === '已预警' && currentStock > 0 && warnNum > 0) {
            const dailyConsumption = warnNum / 30 // å‡è®¾30天消耗完最低库存
            const daysRemaining = Math.floor(currentStock / dailyConsumption)
            if (daysRemaining > 0) {
              const date = new Date()
              date.setDate(date.getDate() + daysRemaining)
              item.expectedShortageTime = date.toISOString().split('T')[0]
            }
          }
          item.currentStock = currentStock
          item.safetyStock = safetyStock
          return item
        })
      }
    })
    .catch(err => {
      tableLoading.value = false
      ElMessage.error(err.msg || '获取数据失败')
    })
}
// æœç´¢
const handleQuery = () => {
  page.current = 1
  getList()
}
// é‡ç½®æœç´¢
const resetQuery = () => {
  Object.keys(searchForm).forEach(key => {
    searchForm[key] = ''
  })
  handleQuery()
}
// åˆ†é¡µå˜åŒ–
const paginationChange = (obj) => {
  page.current = obj.page
  page.size = obj.limit
  getList()
}
// èŽ·å–åº“å­˜æ ·å¼ç±»
const getStockClass = (row) => {
  const currentStock = parseFloat(row.currentStock || row.inboundNum0 || 0)
  const warnNum = parseFloat(row.warnNum || 0)
  if (currentStock <= 0) {
    return 'text-danger'
  } else if (currentStock < warnNum) {
    return 'text-warning'
  }
  return 'text-success'
}
// èŽ·å–é¢„è­¦çº§åˆ«æ ‡ç­¾æ ·å¼
const getWarningLevelTag = (level) => {
  const levelMap = {
    '紧急': 'danger',
    '重要': 'warning',
    '一般': 'info'
  }
  return levelMap[level] || 'info'
}
// èŽ·å–å€’è®¡æ—¶ä¿¡æ¯
const getCountdown = (expectedTime) => {
  if (!expectedTime) return { text: '-', isExpired: false }
  const now = new Date().getTime()
  const expected = new Date(expectedTime).getTime()
  const diff = expected - now
  if (diff <= 0) {
    return { text: '已缺货', isExpired: true }
  }
  const days = Math.floor(diff / (1000 * 60 * 60 * 24))
  const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
  const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
  if (days > 0) {
    return { text: `${days}天${hours}小时`, isExpired: false }
  } else if (hours > 0) {
    return { text: `${hours}小时${minutes}分钟`, isExpired: false }
  } else {
    return { text: `${minutes}分钟`, isExpired: false }
  }
}
// èŽ·å–å€’è®¡æ—¶æ ·å¼ç±»
const getCountdownClass = (expectedTime) => {
  if (!expectedTime) return ''
  const now = new Date().getTime()
  const expected = new Date(expectedTime).getTime()
  const diff = expected - now
  if (diff <= 0) {
    return 'countdown-expired'
  } else if (diff <= 24 * 60 * 60 * 1000) { // 24小时内
    return 'countdown-urgent'
  } else if (diff <= 7 * 24 * 60 * 60 * 1000) { // 7天内
    return 'countdown-warning'
  } else {
    return 'countdown-normal'
  }
}
// é¡µé¢åŠ è½½
onMounted(() => {
  getList()
})
</script>
<style scoped lang="scss">
.app-container {
  padding: 20px;
  .search_form {
    background: #fff;
    padding: 20px;
    border-radius: 4px;
    margin-bottom: 20px;
  }
  .table_list {
    background: #fff;
    border-radius: 4px;
    padding: 20px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    .actions {
      display: flex;
      justify-content: space-between;
      margin-bottom: 20px;
    }
  }
  .text-danger {
    color: #f56c6c;
    font-weight: bold;
  }
  .text-warning {
    color: #e6a23c;
    font-weight: bold;
  }
  .text-success {
    color: #67c23a;
    font-weight: bold;
  }
  .countdown-timer {
    font-weight: bold;
  }
  .countdown-normal {
    color: #67c23a;
  }
  .countdown-warning {
    color: #e6a23c;
  }
  .countdown-urgent {
    color: #f56c6c;
    animation: blink 1s infinite;
  }
  .countdown-expired {
    color: #f56c6c;
    font-weight: bold;
  }
  @keyframes blink {
    0%, 50% { opacity: 1; }
    51%, 100% { opacity: 0.5; }
  }
}
</style>
src/views/personnelManagement/payrollManagement/components/formDia.vue
@@ -2,7 +2,7 @@
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增入职' : '编辑人员'"
        :title="operationType === 'add' ? '新增薪资' : '编辑薪资'"
        width="50%"
        @close="closeDia"
    >
src/views/personnelManagement/payrollManagement/index.vue
@@ -27,8 +27,8 @@
                >
            </div>
            <div>
                <el-button @click="handleExport" style="margin-right: 10px">导出</el-button>
                <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>
@@ -51,7 +51,7 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import FormDia from "@/views/personnelManagement/payrollManagement/components/formDia.vue";
import {staffJoinDel} from "@/api/personnelManagement/onboarding.js";
import {ElMessageBox} from "element-plus";
@@ -283,6 +283,22 @@
            proxy.$modal.msg("已取消");
        });
};
// å¯¼å‡ºè–ªèµ„管理
const handleExport = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            proxy.download("/compensationPerformance/export", { ...searchForm.value, ...page }, "薪资管理.xlsx");
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
onMounted(() => {
    getList();
});