zhangwencui
9 天以前 91c0cd2e0220472f9866479b218be72924b2aefa
会议审批,会议发布,会议总结模块
已添加8个文件
已修改3个文件
2955 ■■■■■ 文件已修改
package.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/managementMeetings/meetExamine.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Editor/index.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/managementMeetings/meetExamine/approve.vue 470 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/managementMeetings/meetExamine/index.vue 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/managementMeetings/meetPublish/approve.vue 497 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/managementMeetings/meetPublish/index.vue 348 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/managementMeetings/meetSummary/approve.vue 554 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/managementMeetings/meetSummary/index.vue 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -79,7 +79,8 @@
    "tslib": "^2.7.0",
    "uview-plus": "^3.4.62",
    "vue": "3.4.21",
    "vue-i18n": "^9.14.2"
    "vue-i18n": "^9.14.2",
    "@vueup/vue-quill": "^1.2.0"
  },
  "devDependencies": {
    "@dcloudio/types": "^3.4.14",
@@ -96,4 +97,4 @@
    "vite": "5.4.10",
    "vue-tsc": "2.1.6"
  }
}
}
src/api/managementMeetings/meetExamine.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
import request from '@/utils/request';
export function getExamineList(data) {
    return request({
        url: "/meeting/applicationList",
        method: "post",
        data: data,
    });
}
export function getRoomEnum() {
    return request({
        url: "/meeting/roomEnum",
        method: "get",
    });
}
export function saveMeetingApplication(data){
    return request({
        url: "/meeting/saveMeetingApplication",
        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,
    });
}
src/components/Editor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,261 @@
<template>
  <view class="editor-container">
    <div class="editor">
      <QuillEditor v-model:content="content"
                   contentType="html"
                   @textChange="(e) => emit('update:modelValue', content)"
                   :options="options"
                   :style="styles" />
    </div>
  </view>
</template>
<script setup>
  import { ref, computed, watch } from "vue";
  import { QuillEditor } from "@vueup/vue-quill";
  import "@vueup/vue-quill/dist/vue-quill.snow.css";
  import { getToken } from "@/utils/auth";
  const props = defineProps({
    /* ç¼–辑器的内容 */
    modelValue: {
      type: String,
    },
    /* é«˜åº¦ */
    height: {
      type: Number,
      default: null,
    },
    /* æœ€å°é«˜åº¦ */
    minHeight: {
      type: Number,
      default: null,
    },
    /* åªè¯» */
    readOnly: {
      type: Boolean,
      default: false,
    },
    /* ä¸Šä¼ æ–‡ä»¶å¤§å°é™åˆ¶(MB) */
    fileSize: {
      type: Number,
      default: 5,
    },
    /* ç±»åž‹ï¼ˆbase64格式、url格式) */
    type: {
      type: String,
      default: "url",
    },
  });
  const emit = defineEmits(["update:modelValue"]);
  const styles = computed(() => {
    let style = {};
    if (props.minHeight) {
      style.minHeight = `${props.minHeight}px`;
    }
    if (props.height) {
      style.height = `${props.height}px`;
    }
    return style;
  });
  const content = ref("");
  watch(
    () => props.modelValue,
    v => {
      if (v !== content.value) {
        content.value = v == undefined ? "<p></p>" : v;
      }
    },
    { immediate: true }
  );
  const options = {
    theme: "snow",
    bounds: document.body,
    debug: "warn",
    modules: {
      // å·¥å…·æ é…ç½®
      toolbar: [
        [{ align: [] }], // å¯¹é½æ–¹å¼
        ["bold", "italic", "underline", "strike"], // åŠ ç²— æ–œä½“ ä¸‹åˆ’线 åˆ é™¤çº¿
        ["blockquote", "code-block"], // å¼•用  ä»£ç å—
        [{ list: "ordered" }, { list: "bullet" }], // æœ‰åºã€æ— åºåˆ—表
        [{ indent: "-1" }, { indent: "+1" }], // ç¼©è¿›
        [{ size: ["small", false, "large", "huge"] }], // å­—体大小
        [{ header: [1, 2, 3, 4, 5, 6, false] }], // æ ‡é¢˜
        [{ color: [] }, { background: [] }], // å­—体颜色、字体背景颜色
        ["clean"], // æ¸…除文本格式
        ["link", "image", "video"], // é“¾æŽ¥ã€å›¾ç‰‡ã€è§†é¢‘
      ],
    },
    placeholder: "请输入内容",
    readOnly: props.readOnly,
  };
</script>
<style>
  .editor-container {
    width: 100%;
  }
  .editor-img-uploader {
    display: none;
  }
  .editor {
    width: 100%;
  }
  .quill-editor {
    border: 1px solid #e8e8e8;
    border-radius: 8px;
    overflow: hidden;
  }
  /* Quill编辑器样式 */
  :deep(.ql-toolbar.ql-snow) {
    border-bottom: 1px solid #e8e8e8;
    border-radius: 8px 8px 0 0;
    padding: 8px 12px;
  }
  :deep(.ql-container.ql-snow) {
    min-height: 300px;
    border-radius: 0 0 8px 8px;
  }
  :deep(.ql-editor) {
    min-height: 300px;
    font-size: 14px;
    line-height: 1.5;
    padding: 12px;
  }
  /* ç§»åŠ¨ç«¯é€‚é… */
  @media (max-width: 768px) {
    :deep(.ql-toolbar.ql-snow) {
      padding: 6px 8px;
    }
    :deep(.ql-editor) {
      font-size: 13px;
      padding: 10px;
    }
  }
  /* å›¾ç‰‡æ ·å¼ */
  :deep(.ql-editor img) {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
    margin: 8px 0;
  }
  /* å·¥å…·æ æŒ‰é’®æ ·å¼ */
  :deep(.ql-toolbar.ql-snow .ql-button) {
    height: 28px;
    width: 28px;
    padding: 4px;
  }
  :deep(.ql-toolbar.ql-snow .ql-picker-label) {
    height: 28px;
    padding: 4px 8px;
  }
  /* æç¤ºæ¡†æ ·å¼ */
  :deep(.ql-snow .ql-tooltip[data-mode="link"])::before {
    content: "请输入链接地址:";
  }
  :deep(.ql-snow .ql-tooltip.ql-editing a.ql-action)::after {
    border-right: 0px;
    content: "保存";
    padding-right: 0px;
  }
  :deep(.ql-snow .ql-tooltip[data-mode="video"])::before {
    content: "请输入视频地址:";
  }
  /* å­—体大小选项 */
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-label)::before,
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-item)::before {
    content: "14px";
  }
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"])::before,
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"])::before {
    content: "10px";
  }
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"])::before,
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"])::before {
    content: "18px";
  }
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"])::before,
  :deep(.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"])::before {
    content: "32px";
  }
  /* æ ‡é¢˜é€‰é¡¹ */
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label)::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item)::before {
    content: "文本";
  }
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"])::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"])::before {
    content: "标题1";
  }
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"])::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"])::before {
    content: "标题2";
  }
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"])::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"])::before {
    content: "标题3";
  }
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"])::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"])::before {
    content: "标题4";
  }
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"])::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"])::before {
    content: "标题5";
  }
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"])::before,
  :deep(.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"])::before {
    content: "标题6";
  }
  /* å­—体选项 */
  :deep(.ql-snow .ql-picker.ql-font .ql-picker-label)::before,
  :deep(.ql-snow .ql-picker.ql-font .ql-picker-item)::before {
    content: "标准字体";
  }
  :deep(.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"])::before,
  :deep(.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"])::before {
    content: "衬线字体";
  }
  :deep(
      .ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]
    )::before,
  :deep(
      .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]
    )::before {
    content: "等宽字体";
  }
</style>
src/pages.json
@@ -324,6 +324,48 @@
      }
    },
    {
      "path": "pages/managementMeetings/meetExamine/index",
      "style": {
        "navigationBarTitleText": "会议审批",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/managementMeetings/meetExamine/approve",
      "style": {
        "navigationBarTitleText": "审批",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/managementMeetings/meetPublish/index",
      "style": {
        "navigationBarTitleText": "会议发布",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/managementMeetings/meetPublish/approve",
      "style": {
        "navigationBarTitleText": "发布",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/managementMeetings/meetSummary/index",
      "style": {
        "navigationBarTitleText": "会议总结",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/managementMeetings/meetSummary/approve",
      "style": {
        "navigationBarTitleText": "总结",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/cooperativeOffice/collaborativeApproval/detail",
      "style": {
        "navigationBarTitleText": "审批流程",
src/pages/index.vue
@@ -315,6 +315,18 @@
      label: "会议申请",
    },
    {
      icon: "/static/images/icon/qingjiaguanli@2x.png",
      label: "会议审批",
    },
    {
      icon: "/static/images/icon/qingjiaguanli@2x.png",
      label: "会议发布",
    },
    {
      icon: "/static/images/icon/qingjiaguanli@2x.png",
      label: "会议总结",
    },
    {
      icon: "/static/images/icon/xietongshenpi@2x.png",
      label: "协同审批",
    },
@@ -522,6 +534,21 @@
          url: "/pages/managementMeetings/meetApplication/index",
        });
        break;
      case "会议审批":
        uni.navigateTo({
          url: "/pages/managementMeetings/meetExamine/index",
        });
        break;
      case "会议发布":
        uni.navigateTo({
          url: "/pages/managementMeetings/meetPublish/index",
        });
        break;
      case "会议总结":
        uni.navigateTo({
          url: "/pages/managementMeetings/meetSummary/index",
        });
        break;
      case "协同审批":
        uni.navigateTo({
          url: "/pages/cooperativeOffice/collaborativeApproval/index",
src/pages/managementMeetings/meetExamine/approve.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,470 @@
<template>
  <view class="approve-page">
    <PageHeader title="审批"
                @back="goBack" />
    <!-- ç”³è¯·ä¿¡æ¯ -->
    <view class="application-info">
      <view class="info-header">
        <text class="info-title">会议信息</text>
      </view>
      <view class="info-content">
        <view class="info-row">
          <text class="info-label">会议主题</text>
          <text class="info-value">{{ approvalData.title }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">申请人</text>
          <text class="info-value">{{ approvalData.applicant }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">主理人</text>
          <text class="info-value">{{ approvalData.host }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">会议时间</text>
          <text class="info-value">{{ formatDateTime(approvalData.meetingTime) }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">会议地点</text>
          <text class="info-value">{{ approvalData.location }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">审批状态</text>
          <text class="info-value tag"
                :class="getTagClass(approvalData.approveNodeStatus)">
            {{ formatReceiptType(approvalData.approveNodeStatus) }}
          </text>
        </view>
        <view class="info-row">
          <text class="info-label">参会人数</text>
          <text class="info-value">{{ approvalData.participants.length }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">参会人员</text>
          <text class="info-value">{{ approvalData.participants.map(it => it.name).join("、") }}</text>
        </view>
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view v-if="isEdit"
          class="footer-actions">
      <u-button class="reject-btn"
                @click="handleReject">不通过</u-button>
      <u-button class="approve-btn"
                @click="handleApprove">通过</u-button>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { saveMeetingApplication } from "@/api/managementMeetings/meetExamine";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import PageHeader from "@/components/PageHeader.vue";
  const approvalData = ref({});
  const approvalSteps = ref([]);
  const isEdit = ref(false);
  onLoad(options => {
    console.log(options, "options");
    if (options.item) {
      approvalData.value = JSON.parse(options.item);
    }
    if (options.edit) {
      isEdit.value = options.edit === "true" ? true : false;
    }
  });
  const goBack = () => {
    uni.removeStorageSync("approveId");
    uni.navigateBack();
  };
  const formatDateTime = dateTime => {
    if (!dateTime) return "";
    return dateTime.replace(" ", "\n");
  };
  // æ ¼å¼åŒ–回款方式
  const formatReceiptType = params => {
    if (params == 0) {
      return "待审核";
    } else if (params == 1) {
      return "已通过";
    } else if (params == 2) {
      return "未通过";
    } else if (params == 3) {
      return "已取消";
    } else {
      return "未知";
    }
  };
  // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
  const getTagClass = type => {
    if (type == 0) {
      return "info";
    } else if (type == 1) {
      return "success";
    } else if (type == 2) {
      return "warning";
    } else if (type == 3) {
      return "danger";
    } else {
      return "info";
    }
  };
  const submitForm = status => {
    // è°ƒç”¨åŽç«¯
    saveMeetingApplication({ id: approvalData.value.id, status: status })
      .then(res => {
        if (res.code === 200) {
          showToast("审批提交成功");
          // æç¤ºåŽè¿”回上一个页面
          setTimeout(() => {
            goBack(); // å†…部是 uni.navigateBack()
          }, 800);
        } else {
          showToast(res.message || "审批操作失败,请重试");
        }
      })
      .catch(error => {
        console.error("审批操作失败:", error);
        showToast("审批操作失败,请重试");
      });
  };
  const handleApprove = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要通过该会议申请吗?",
      success: res => {
        if (res.confirm) submitForm(1);
      },
    });
  };
  const handleReject = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定不通过该会议申请吗?",
      success: res => {
        if (res.confirm) submitForm(2);
      },
    });
  };
  // åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
  const activities = ref([]);
</script>
<style scoped lang="scss">
  .approve-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  .header {
    display: flex;
    align-items: center;
    background: #fff;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    top: 0;
    z-index: 100;
  }
  .title {
    flex: 1;
    text-align: center;
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
  .application-info {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .info-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .info-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .info-content {
    padding: 16px;
  }
  .info-row {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .info-label {
    font-size: 14px;
    color: #666;
    width: 80px;
    flex-shrink: 0;
  }
  .info-value {
    font-size: 14px;
    color: #333;
    flex: 1;
  }
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .process-steps {
    padding: 20px;
  }
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
    &:last-child {
      margin-bottom: 0;
      .step-line {
        display: none;
      }
    }
  }
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
  .step-dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
    position: relative;
    z-index: 2;
  }
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
  .process-step.completed .step-line {
    background: #52c41a;
  }
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
  .step-info {
    margin-bottom: 8px;
  }
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .step-time {
    font-size: 12px;
    color: #999;
    display: block;
  }
  .step-opinion {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    border-left: 4px solid #52c41a;
  }
  .opinion-label {
    font-size: 12px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .opinion-content {
    font-size: 14px;
    color: #333;
    line-height: 1.5;
  }
  .approval-input {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .input-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .input-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .input-content {
    padding: 16px;
  }
  .footer-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 16px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
    z-index: 1000;
  }
  .reject-btn {
    width: 120px;
    background: #ff4d4f;
    color: #fff;
  }
  .approve-btn {
    width: 120px;
    background: #52c41a;
    color: #fff;
  }
  /* é€‚配u-button样式 */
  :deep(.u-button) {
    border-radius: 6px;
  }
  @keyframes pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
    }
    70% {
      box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
    }
    100% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
    }
  }
  .signature-section {
    background: #fff;
    padding: 12px 16px 16px;
    border-top: 1px solid #f0f0f0;
  }
  .signature-header {
    margin-bottom: 8px;
  }
  .signature-title {
    font-size: 14px;
    font-weight: 600;
    color: #333;
  }
  .signature-box {
    width: 100%;
    height: 180px;
    background: #fff;
    border: 1px dashed #d9d9d9;
    border-radius: 8px;
    overflow: hidden;
  }
  .signature-actions {
    margin-top: 8px;
    display: flex;
    justify-content: flex-end;
  }
</style>
src/pages/managementMeetings/meetExamine/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,352 @@
// å®¡æ‰¹ç®¡ç†ä¸»é¡µé¢
<template>
  <view class="sales-account">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="会议审批"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入会议主题"
                    v-model="searchForm.title"
                    clearable />
        </view>
        <view class="search-button"
              @click="getList">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å®¡æ‰¹åˆ—表 -->
    <view class="ledger-list"
          v-if="ledgerList.length > 0">
      <view v-for="(item, index) in ledgerList"
            :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.title }}</text>
            </view>
            <view class="item-tag">
              <u-tag :type="getTagClass(item.status)">{{ formatReceiptType(item.status) }}</u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">申请人</text>
              <text class="detail-value">{{ item.applicant }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">主理人</text>
              <text class="detail-value">{{ item.host }}</text>
            </view>
            <view class="detail-row-approveReason">
              <text class="detail-label">会议时间</text>
              <text class="detail-value highlightBlue">{{ formatDateTime(item.meetingTime) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">会议地点</text>
              <text class="detail-value">{{ item.location }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">参会人数</text>
              <text class="detail-value">{{ item.participants.length }}</text>
            </view>
            <up-divider></up-divider>
            <view class="actions">
              <u-button type="primary"
                        size="small"
                        class="action-btn view"
                        @click="viewDetail(item)">
                è¯¦æƒ…
              </u-button>
              <u-button type="success"
                        size="small"
                        class="action-btn approve"
                        :disabled="item.status != 0"
                        @click="approve(item)">
                å®¡æ‰¹
              </u-button>
            </view>
            <!-- <view class="detail-info"
                  style="align-items: flex-end;">
              <view class="detail-row">
              </view>
            </view> -->
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无数据</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, toRefs, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    getExamineList,
    getRoomEnum,
  } from "@/api/managementMeetings/meetExamine";
  import { getStaffOnJob } from "@/api/personnelManagement/onboarding";
  import { onShow } from "@dcloudio/uni-app";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  const userStore = useUserStore();
  // æ•°æ®
  const ledgerList = ref([]);
  const data = reactive({
    searchForm: {
      title: "",
    },
  });
  const { searchForm } = toRefs(data);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æˆ¿é—´æžšä¸¾
  const roomEnum = ref([]);
  // æˆ¿é—´æžšä¸¾æŸ¥è¯¢
  const getRoomEnumList = () => {
    return getRoomEnum()
      .then(res => {
        console.log(res.data, "res.data");
        roomEnum.value = res.data;
      })
      .catch(() => {
        closeToast();
      });
  };
  // å‘˜å·¥åˆ—表
  const staffList = ref([]);
  // å‘˜å·¥åˆ—表查询
  const getStaffOnJobList = () => {
    return getStaffOnJob()
      .then(res => {
        console.log(res.data, "res.data");
        staffList.value = res.data;
      })
      .catch(() => {
        closeToast();
      });
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    showLoadingToast("加载中...");
    const page = {
      current: -1,
      size: -1,
    };
    getExamineList({
      ...page,
      ...searchForm.value,
    })
      .then(res => {
        console.log(res.data.records, "res.data.records");
        ledgerList.value = res.data.records.map(it => {
          console.log(it, "it1");
          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})`,
              };
            });
          console.log(it, "it2");
          return it;
        });
        closeToast();
      })
      .catch(() => {
        closeToast();
      });
  };
  // æ˜¾ç¤ºåŠ è½½æç¤º
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
  const formatDateTime = dateTime => {
    if (!dateTime) return "";
    return dateTime.replace(" ", "\n");
  };
  // å…³é—­æç¤º
  const closeToast = () => {
    uni.hideLoading();
  };
  // æ ¼å¼åŒ–回款方式
  const formatReceiptType = params => {
    if (params == 0) {
      return "待审核";
    } else if (params == 1) {
      return "已通过";
    } else if (params == 2) {
      return "未通过";
    } else if (params == 3) {
      return "已取消";
    } else {
      return "未知";
    }
  };
  // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
  const getTagClass = type => {
    if (type == 0) {
      return "info";
    } else if (type == 1) {
      return "success";
    } else if (type == 2) {
      return "warning";
    } else if (type == 3) {
      return "danger";
    } else {
      return "info";
    }
  };
  // ç‚¹å‡»å®¡æ ¸
  const approve = item => {
    // uni.setStorageSync("approveId", item.approveId);
    uni.navigateTo({
      url:
        "/pages/managementMeetings/meetExamine/approve?item=" +
        JSON.stringify(item) +
        "&edit=true",
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    uni.navigateTo({
      url:
        "/pages/managementMeetings/meetExamine/approve?item=" +
        JSON.stringify(item) +
        "&edit=false",
    });
  };
  onShow(async () => {
    // é¡µé¢åŠ è½½å®ŒæˆåŽçš„åˆå§‹åŒ–é€»è¾‘
    try {
      // ç­‰å¾…两个异步方法执行完成
      await Promise.all([getRoomEnumList(), getStaffOnJobList()]);
      // ä¸¤ä¸ªæ–¹æ³•执行完成后再执行 getList()
      getList();
    } catch (error) {
      console.error("初始化数据失败:", error);
      // å³ä½¿å‡ºé”™ä¹Ÿæ‰§è¡Œ getList(),确保页面能正常加载
      getList();
    }
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .u-divider {
    margin: 0 !important;
  }
  // æ–‡æ¡£å›¾æ ‡æ ·å¼ - è¦†ç›–公共样式中的背景色
  .document-icon {
    background: #ed8d05;
  }
  // æµ®åŠ¨æŒ‰é’®æ ·å¼ - è¦†ç›–公共样式中的背景色
  .fab-button {
    background: #ed8d05;
  }
  // ç‰¹æœ‰æ ·å¼
  .detail-row-user {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .detail-row-approveReason {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
  }
  .detail-value.highlightBlue {
    color: #2979ff;
    font-weight: 500;
  }
  .detail-value.highlightYellow {
    color: #ed8d05;
    font-weight: 500;
  }
  .approver-value {
    display: flex;
    justify-content: flex-end;
  }
  .approver-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: #f0f6ff;
    color: #2b7cff;
    border: 1px solid #e0efff;
    border-radius: 999px;
    padding: 4px 10px;
    max-width: 100%;
  }
  .approver-name {
    font-size: 12px;
    color: #2b7cff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .actions {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: flex-end;
    margin-top: 18rpx;
  }
  .action-btn {
    border-radius: 16px;
    height: 28px;
    line-height: 28px;
    padding: 0 12px;
  }
</style>
src/pages/managementMeetings/meetPublish/approve.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,497 @@
<template>
  <view class="approve-page">
    <PageHeader title="发布"
                @back="goBack" />
    <!-- ç”³è¯·ä¿¡æ¯ -->
    <view class="application-info">
      <view class="info-header">
        <text class="info-title">会议信息</text>
      </view>
      <view class="info-content">
        <view class="info-row">
          <text class="info-label">会议主题</text>
          <text class="info-value">{{ approvalData.title }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">申请人</text>
          <text class="info-value">{{ approvalData.applicant }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">主理人</text>
          <text class="info-value">{{ approvalData.host }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">会议时间</text>
          <text class="info-value">{{ formatDateTime(approvalData.meetingTime) }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">会议地点</text>
          <text class="info-value">{{ approvalData.location }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">审批状态</text>
          <text class="info-value tag"
                :class="getTagClass(approvalData.approveNodeStatus)">
            {{ formatReceiptType(approvalData.approveNodeStatus) }}
          </text>
        </view>
        <view class="info-row">
          <text class="info-label">会议说明</text>
          <text class="info-value">{{ approvalData.description }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">参会人数</text>
          <text class="info-value">{{ approvalData.participants.length }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">参会人员</text>
          <text class="info-value">{{ approvalData.participants.map(it => it.name).join("、") }}</text>
        </view>
      </view>
    </view>
    <!-- å‘布意见输入 -->
    <view v-if="isEdit"
          class="approval-input">
      <view class="input-header">
        <text class="input-title">发布意见</text>
      </view>
      <view class="input-content">
        <u-textarea v-model="approvalOpinion"
                    rows="4"
                    placeholder="请输入发布意见"
                    maxlength="200"
                    count />
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view v-if="isEdit"
          class="footer-actions">
      <!-- <u-button class="reject-btn"
                @click="handleReject">不通过</u-button> -->
      <u-button class="approve-btn"
                @click="handleApprove">发布</u-button>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted, computed } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import { saveMeetingApplication } from "@/api/managementMeetings/meetExamine";
  const showToast = message => {
    uni.showToast({
      title: message,
      icon: "none",
    });
  };
  import PageHeader from "@/components/PageHeader.vue";
  const approvalData = ref({});
  const approvalSteps = ref([]);
  const approvalOpinion = ref("");
  const isEdit = ref(false);
  onLoad(options => {
    console.log(options, "options");
    if (options.item) {
      approvalData.value = JSON.parse(options.item);
    }
    // ç¼–辑模式下,默认发布意见为当前审批意见
    if (options.edit) {
      isEdit.value = options.edit === "true" ? true : false;
    }
    console.log(approvalData.value, "approvalData.value");
  });
  const goBack = () => {
    uni.removeStorageSync("approveId");
    uni.navigateBack();
  };
  const formatDateTime = dateTime => {
    if (!dateTime) return "";
    return dateTime.replace(" ", "\n");
  };
  // æ ¼å¼åŒ–回款方式
  const formatReceiptType = params => {
    if (params == 0) {
      return "待发布";
    } else if (params == 1) {
      return "已发布";
    } else if (params == 2) {
      return "已取消";
    } else {
      return "未知";
    }
  };
  // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
  const getTagClass = type => {
    if (type == 0) {
      return "info";
    } else if (type == 1) {
      return "success";
    } else if (type == 2) {
      return "danger";
    } else {
      return "info";
    }
  };
  const submitForm = status => {
    // æ ¡éªŒå‘布意见
    if (!approvalOpinion.value?.trim()) {
      showToast("请输入发布意见");
      return;
    }
    // è°ƒç”¨åŽç«¯
    saveMeetingApplication({
      id: approvalData.value.id,
      publishStatus: status,
      publishComment: approvalOpinion.value, // æ·»åŠ å‘å¸ƒæ„è§
    })
      .then(res => {
        if (res.code === 200) {
          showToast("发布成功");
          // æç¤ºåŽè¿”回上一个页面
          setTimeout(() => {
            goBack(); // å†…部是 uni.navigateBack()
          }, 800);
        } else {
          showToast(res.message || "发布操作失败,请重试");
        }
      })
      .catch(error => {
        console.error("发布操作失败:", error);
        showToast("发布操作失败,请重试");
      });
  };
  const handleApprove = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要发布该会议吗?",
      success: res => {
        if (res.confirm) submitForm(1);
      },
    });
  };
  const handleReject = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定不通过该会议申请吗?",
      success: res => {
        if (res.confirm) submitForm(2);
      },
    });
  };
  // åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
  const activities = ref([]);
</script>
<style scoped lang="scss">
  .approve-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  .header {
    display: flex;
    align-items: center;
    background: #fff;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    top: 0;
    z-index: 100;
  }
  .title {
    flex: 1;
    text-align: center;
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
  .application-info {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .info-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .info-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .info-content {
    padding: 16px;
  }
  .info-row {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .info-label {
    font-size: 14px;
    color: #666;
    width: 80px;
    flex-shrink: 0;
  }
  .info-value {
    font-size: 14px;
    color: #333;
    flex: 1;
  }
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .process-steps {
    padding: 20px;
  }
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
    &:last-child {
      margin-bottom: 0;
      .step-line {
        display: none;
      }
    }
  }
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
  .step-dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
    position: relative;
    z-index: 2;
  }
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
  .process-step.completed .step-line {
    background: #52c41a;
  }
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
  .step-info {
    margin-bottom: 8px;
  }
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .step-time {
    font-size: 12px;
    color: #999;
    display: block;
  }
  .step-opinion {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    border-left: 4px solid #52c41a;
  }
  .opinion-label {
    font-size: 12px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .opinion-content {
    font-size: 14px;
    color: #333;
    line-height: 1.5;
  }
  .approval-input {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .input-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .input-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .input-content {
    padding: 16px;
  }
  .footer-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 16px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
    z-index: 1000;
  }
  .reject-btn {
    width: 120px;
    background: #ff4d4f;
    color: #fff;
  }
  .approve-btn {
    width: 120px;
    background: #52c41a;
    color: #fff;
  }
  /* é€‚配u-button样式 */
  :deep(.u-button) {
    border-radius: 6px;
  }
  @keyframes pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
    }
    70% {
      box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
    }
    100% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
    }
  }
  .signature-section {
    background: #fff;
    padding: 12px 16px 16px;
    border-top: 1px solid #f0f0f0;
  }
  .signature-header {
    margin-bottom: 8px;
  }
  .signature-title {
    font-size: 14px;
    font-weight: 600;
    color: #333;
  }
  .signature-box {
    width: 100%;
    height: 180px;
    background: #fff;
    border: 1px dashed #d9d9d9;
    border-radius: 8px;
    overflow: hidden;
  }
  .signature-actions {
    margin-top: 8px;
    display: flex;
    justify-content: flex-end;
  }
</style>
src/pages/managementMeetings/meetPublish/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,348 @@
// å®¡æ‰¹ç®¡ç†ä¸»é¡µé¢
<template>
  <view class="sales-account">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="会议发布"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入会议主题"
                    v-model="searchForm.title"
                    clearable />
        </view>
        <view class="search-button"
              @click="getList">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å®¡æ‰¹åˆ—表 -->
    <view class="ledger-list"
          v-if="ledgerList.length > 0">
      <view v-for="(item, index) in ledgerList"
            :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.title }}</text>
            </view>
            <view class="item-tag">
              <u-tag :type="getTagClass(item.status)">{{ formatReceiptType(item.status) }}</u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">申请人</text>
              <text class="detail-value">{{ item.applicant }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">主理人</text>
              <text class="detail-value">{{ item.host }}</text>
            </view>
            <view class="detail-row-approveReason">
              <text class="detail-label">会议时间</text>
              <text class="detail-value highlightBlue">{{ formatDateTime(item.meetingTime) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">会议地点</text>
              <text class="detail-value">{{ item.location }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">参会人数</text>
              <text class="detail-value">{{ item.participants.length }}</text>
            </view>
            <up-divider></up-divider>
            <view class="actions">
              <u-button type="primary"
                        size="small"
                        class="action-btn view"
                        @click="viewDetail(item)">
                è¯¦æƒ…
              </u-button>
              <u-button type="success"
                        size="small"
                        class="action-btn approve"
                        :disabled="item.status != 0"
                        @click="approve(item)">
                å‘布
              </u-button>
            </view>
            <!-- <view class="detail-info"
                  style="align-items: flex-end;">
              <view class="detail-row">
              </view>
            </view> -->
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无数据</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, toRefs, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    getMeetingPublish,
    getRoomEnum,
  } from "@/api/managementMeetings/meetExamine";
  import { getStaffOnJob } from "@/api/personnelManagement/onboarding";
  import { onShow } from "@dcloudio/uni-app";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  const userStore = useUserStore();
  // æ•°æ®
  const ledgerList = ref([]);
  const data = reactive({
    searchForm: {
      title: "",
    },
  });
  const { searchForm } = toRefs(data);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æˆ¿é—´æžšä¸¾
  const roomEnum = ref([]);
  // æˆ¿é—´æžšä¸¾æŸ¥è¯¢
  const getRoomEnumList = () => {
    return getRoomEnum()
      .then(res => {
        console.log(res.data, "res.data");
        roomEnum.value = res.data;
      })
      .catch(() => {
        closeToast();
      });
  };
  // å‘˜å·¥åˆ—表
  const staffList = ref([]);
  // å‘˜å·¥åˆ—表查询
  const getStaffOnJobList = () => {
    return getStaffOnJob()
      .then(res => {
        console.log(res.data, "res.data");
        staffList.value = res.data;
      })
      .catch(() => {
        closeToast();
      });
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    showLoadingToast("加载中...");
    const page = {
      current: -1,
      size: -1,
    };
    getMeetingPublish({
      ...page,
      ...searchForm.value,
    })
      .then(res => {
        console.log(res.data.records, "res.data.records");
        ledgerList.value = res.data.records.map(it => {
          console.log(it, "it1");
          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})`,
              };
            });
          console.log(it, "it2");
          return it;
        });
        closeToast();
      })
      .catch(() => {
        closeToast();
      });
  };
  // æ˜¾ç¤ºåŠ è½½æç¤º
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
  const formatDateTime = dateTime => {
    if (!dateTime) return "";
    return dateTime.replace(" ", "\n");
  };
  // å…³é—­æç¤º
  const closeToast = () => {
    uni.hideLoading();
  };
  // æ ¼å¼åŒ–回款方式
  const formatReceiptType = params => {
    if (params == 0) {
      return "待发布";
    } else if (params == 1) {
      return "已发布";
    } else if (params == 2) {
      return "已取消";
    } else {
      return "未知";
    }
  };
  // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
  const getTagClass = type => {
    if (type == 0) {
      return "info";
    } else if (type == 1) {
      return "success";
    } else if (type == 2) {
      return "danger";
    } else {
      return "info";
    }
  };
  // ç‚¹å‡»å®¡æ ¸
  const approve = item => {
    // uni.setStorageSync("approveId", item.approveId);
    uni.navigateTo({
      url:
        "/pages/managementMeetings/meetPublish/approve?item=" +
        JSON.stringify(item) +
        "&edit=true",
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    uni.navigateTo({
      url:
        "/pages/managementMeetings/meetPublish/approve?item=" +
        JSON.stringify(item) +
        "&edit=false",
    });
  };
  onShow(async () => {
    // é¡µé¢åŠ è½½å®ŒæˆåŽçš„åˆå§‹åŒ–é€»è¾‘
    try {
      // ç­‰å¾…两个异步方法执行完成
      await Promise.all([getRoomEnumList(), getStaffOnJobList()]);
      // ä¸¤ä¸ªæ–¹æ³•执行完成后再执行 getList()
      getList();
    } catch (error) {
      console.error("初始化数据失败:", error);
      // å³ä½¿å‡ºé”™ä¹Ÿæ‰§è¡Œ getList(),确保页面能正常加载
      getList();
    }
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .u-divider {
    margin: 0 !important;
  }
  // æ–‡æ¡£å›¾æ ‡æ ·å¼ - è¦†ç›–公共样式中的背景色
  .document-icon {
    background: #ed8d05;
  }
  // æµ®åŠ¨æŒ‰é’®æ ·å¼ - è¦†ç›–公共样式中的背景色
  .fab-button {
    background: #ed8d05;
  }
  // ç‰¹æœ‰æ ·å¼
  .detail-row-user {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .detail-row-approveReason {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
  }
  .detail-value.highlightBlue {
    color: #2979ff;
    font-weight: 500;
  }
  .detail-value.highlightYellow {
    color: #ed8d05;
    font-weight: 500;
  }
  .approver-value {
    display: flex;
    justify-content: flex-end;
  }
  .approver-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: #f0f6ff;
    color: #2b7cff;
    border: 1px solid #e0efff;
    border-radius: 999px;
    padding: 4px 10px;
    max-width: 100%;
  }
  .approver-name {
    font-size: 12px;
    color: #2b7cff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .actions {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: flex-end;
    margin-top: 18rpx;
  }
  .action-btn {
    border-radius: 16px;
    height: 28px;
    line-height: 28px;
    padding: 0 12px;
  }
</style>
src/pages/managementMeetings/meetSummary/approve.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,554 @@
<template>
  <view class="approve-page">
    <PageHeader title="总结"
                @back="goBack" />
    <!-- ç”³è¯·ä¿¡æ¯ -->
    <view class="application-info">
      <view class="info-header">
        <text class="info-title">会议信息</text>
      </view>
      <view class="info-content">
        <view class="info-row">
          <text class="info-label">会议主题</text>
          <text class="info-value">{{ approvalData.title }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">申请人</text>
          <text class="info-value">{{ approvalData.applicant }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">主理人</text>
          <text class="info-value">{{ approvalData.host }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">会议时间</text>
          <text class="info-value">{{ formatDateTime(approvalData.meetingTime) }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">会议地点</text>
          <text class="info-value">{{ approvalData.location }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">审批状态</text>
          <text class="info-value tag"
                :class="getTagClass(approvalData.approveNodeStatus)">
            {{ formatReceiptType(approvalData.approveNodeStatus) }}
          </text>
        </view>
        <view class="info-row">
          <text class="info-label">会议说明</text>
          <text class="info-value">{{ approvalData.description }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">参会人数</text>
          <text class="info-value">{{ approvalData.participants.length }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">参会人员</text>
          <text class="info-value">{{ approvalData.participants.map(it => it.name).join("、") }}</text>
        </view>
      </view>
    </view>
    <!-- æäº¤æ„è§è¾“å…¥ -->
    <view v-if="isEdit"
          class="approval-input">
      <view class="input-header">
        <text class="input-title">会议纪要</text>
      </view>
      <view class="input-content">
        <Editor v-model:modelValue="minutesContent"
                :height="300" />
      </view>
    </view>
    <!-- åº•部操作按钮 -->
    <view v-if="isEdit"
          class="footer-actions">
      <u-button class="approve-btn"
                @click="handleApprove">提交</u-button>
    </view>
  </view>
</template>
<script setup>
  import { ref, onMounted, nextTick } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    saveMeetingMinutes,
    getMeetingMinutesByMeetingId,
  } from "@/api/managementMeetings/meetExamine";
  import { getToken } from "@/utils/auth";
  import PageHeader from "@/components/PageHeader.vue";
  import Editor from "@/components/Editor/index.vue";
  const approvalData = ref({});
  const approvalSteps = ref([]);
  const isEdit = ref(false);
  onLoad(options => {
    console.log(options, "options");
    if (options.item) {
      approvalData.value = JSON.parse(options.item);
    }
    // ç¼–辑模式下,默认提交意见为当前审批意见
    if (options.edit) {
      isEdit.value = options.edit === "true" ? true : false;
    }
    getMeetingMinutes();
    console.log(approvalData.value, "approvalData.value");
  });
  const goBack = () => {
    uni.removeStorageSync("approveId");
    uni.navigateBack();
  };
  const formatDateTime = dateTime => {
    if (!dateTime) return "";
    return dateTime.replace(" ", "\n");
  };
  // æ ¼å¼åŒ–回款方式
  const formatReceiptType = params => {
    if (params == 0) {
      return "待审批";
    } else if (params == 1) {
      return "已通过";
    } else if (params == 2) {
      return "未通过";
    } else if (params == 3) {
      return "已取消";
    } else {
      return "未知";
    }
  };
  // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
  const getTagClass = type => {
    if (type == 0) {
      return "info";
    } else if (type == 1) {
      return "success";
    } else if (type == 2) {
      return "warning";
    } else if (type == 3) {
      return "danger";
    } else {
      return "info";
    }
  };
  const minutesContent = ref("");
  const minutesContentId = ref("");
  const getMeetingMinutes = () => {
    getMeetingMinutesByMeetingId(approvalData.value.id)
      .then(res => {
        console.log(res.data, "res.data");
        if (res.data) {
          minutesContent.value = res.data.content;
          minutesContentId.value = res.data.id;
        } else {
          minutesContent.value = `<h2>${approvalData.value.title}会议纪要</h2>
                                                                              <p><strong>会议时间:</strong>${
                                                                                approvalData
                                                                                  .value
                                                                                  .meetingTime
                                                                              }</p>
                                                                              <p><strong>会议地点:</strong>${
                                                                                approvalData
                                                                                  .value
                                                                                  .location
                                                                              }</p>
                                                                              <p><strong>主持人:</strong>${
                                                                                approvalData
                                                                                  .value
                                                                                  .host
                                                                              }</p>
                                                                              <p><strong>参会人员:</strong></p>
                                                                              <ol>
                                                                                ${approvalData.value.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>`;
        }
      })
      .catch(error => {
        console.error("获取会议纪要失败:", error);
        showToast("获取会议纪要失败,请重试");
      });
  };
  const submitForm = status => {
    console.log(minutesContent.value, "富文本");
    if (!minutesContent.value) {
      ElMessage.warning("请输入会议纪要内容");
      return;
    }
    // è°ƒç”¨åŽç«¯
    saveMeetingMinutes({
      id: minutesContentId.value,
      content: minutesContent.value,
      meetingId: approvalData.value.id,
      title: approvalData.value.title,
    })
      .then(res => {
        if (res.code === 200) {
          showToast("提交成功");
          // æç¤ºåŽè¿”回上一个页面
          setTimeout(() => {
            goBack(); // å†…部是 uni.navigateBack()
          }, 800);
        } else {
          showToast(res.message || "提交操作失败,请重试");
        }
      })
      .catch(error => {
        console.error("提交操作失败:", error);
        showToast("提交操作失败,请重试");
      });
  };
  const handleApprove = () => {
    uni.showModal({
      title: "确认操作",
      content: "确定要提交该会议总结吗?",
      success: res => {
        if (res.confirm) submitForm(1);
      },
    });
  };
  // åŽŸå§‹èŠ‚ç‚¹æ•°æ®ï¼ˆç”¨äºŽæäº¤é€»è¾‘ï¼‰
  const activities = ref([]);
</script>
<style scoped lang="scss">
  .approve-page {
    min-height: 100vh;
    background: #f8f9fa;
    padding-bottom: 80px;
  }
  .header {
    display: flex;
    align-items: center;
    background: #fff;
    padding: 16px 20px;
    border-bottom: 1px solid #f0f0f0;
    position: sticky;
    top: 0;
    z-index: 100;
  }
  .title {
    flex: 1;
    text-align: center;
    font-size: 18px;
    font-weight: 600;
    color: #333;
  }
  .application-info {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .info-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .info-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .info-content {
    padding: 16px;
  }
  .info-row {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .info-label {
    font-size: 14px;
    color: #666;
    width: 80px;
    flex-shrink: 0;
  }
  .info-value {
    font-size: 14px;
    color: #333;
    flex: 1;
  }
  .approval-process {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .process-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .process-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .process-steps {
    padding: 20px;
  }
  .process-step {
    display: flex;
    position: relative;
    margin-bottom: 24px;
    &:last-child {
      margin-bottom: 0;
      .step-line {
        display: none;
      }
    }
  }
  .step-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-right: 16px;
  }
  .step-dot {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    font-weight: 600;
    position: relative;
    z-index: 2;
  }
  .process-step.completed .step-dot {
    background: #52c41a;
    color: #fff;
  }
  .process-step.current .step-dot {
    background: #1890ff;
    color: #fff;
    animation: pulse 2s infinite;
  }
  .process-step.pending .step-dot {
    background: #d9d9d9;
    color: #999;
  }
  .step-line {
    width: 2px;
    height: 40px;
    background: #d9d9d9;
    margin-top: 8px;
  }
  .process-step.completed .step-line {
    background: #52c41a;
  }
  .process-step.rejected .step-dot {
    background: #ff4d4f;
    color: #fff;
  }
  .process-step.rejected .step-line {
    background: #ff4d4f;
  }
  .step-content {
    flex: 1;
    padding-top: 4px;
  }
  .step-info {
    margin-bottom: 8px;
  }
  .step-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
    display: block;
    margin-bottom: 4px;
  }
  .step-approver {
    font-size: 14px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .step-time {
    font-size: 12px;
    color: #999;
    display: block;
  }
  .step-opinion {
    background: #f8f9fa;
    padding: 12px;
    border-radius: 8px;
    border-left: 4px solid #52c41a;
  }
  .opinion-label {
    font-size: 12px;
    color: #666;
    display: block;
    margin-bottom: 4px;
  }
  .opinion-content {
    font-size: 14px;
    color: #333;
    line-height: 1.5;
  }
  .approval-input {
    background: #fff;
    margin: 16px;
    border-radius: 12px;
    overflow: hidden;
  }
  .input-header {
    padding: 16px;
    border-bottom: 1px solid #f0f0f0;
    background: #f8f9fa;
  }
  .input-title {
    font-size: 16px;
    font-weight: 600;
    color: #333;
  }
  .input-content {
    padding: 16px;
  }
  .footer-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
    display: flex;
    justify-content: space-around;
    align-items: center;
    padding: 16px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
    z-index: 1000;
  }
  .reject-btn {
    width: 120px;
    background: #ff4d4f;
    color: #fff;
  }
  .approve-btn {
    width: 120px;
    background: #52c41a;
    color: #fff;
  }
  /* é€‚配u-button样式 */
  :deep(.u-button) {
    border-radius: 6px;
  }
  @keyframes pulse {
    0% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
    }
    70% {
      box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
    }
    100% {
      box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
    }
  }
  .signature-section {
    background: #fff;
    padding: 12px 16px 16px;
    border-top: 1px solid #f0f0f0;
  }
  .signature-header {
    margin-bottom: 8px;
  }
  .signature-title {
    font-size: 14px;
    font-weight: 600;
    color: #333;
  }
  .signature-box {
    width: 100%;
    height: 180px;
    background: #fff;
    border: 1px dashed #d9d9d9;
    border-radius: 8px;
    overflow: hidden;
  }
  .signature-actions {
    margin-top: 8px;
    display: flex;
    justify-content: flex-end;
  }
  /* å·¥å…·æ æŒ‰é’®æ ·å¼ */
  :deep(.ql-toolbar.ql-snow .ql-button) {
    height: 28px;
    width: 28px;
    padding: 4px;
  }
  :deep(.ql-toolbar.ql-snow .ql-picker-label) {
    height: 28px;
    padding: 4px 8px;
  }
</style>
src/pages/managementMeetings/meetSummary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,352 @@
// å®¡æ‰¹ç®¡ç†ä¸»é¡µé¢
<template>
  <view class="sales-account">
    <!-- ä½¿ç”¨é€šç”¨é¡µé¢å¤´éƒ¨ç»„ä»¶ -->
    <PageHeader title="会议总结"
                @back="goBack" />
    <!-- æœç´¢å’Œç­›é€‰åŒºåŸŸ -->
    <view class="search-section">
      <view class="search-bar">
        <view class="search-input">
          <up-input class="search-text"
                    placeholder="请输入会议主题"
                    v-model="searchForm.title"
                    clearable />
        </view>
        <view class="search-button"
              @click="getList">
          <up-icon name="search"
                   size="24"
                   color="#999"></up-icon>
        </view>
      </view>
    </view>
    <!-- å®¡æ‰¹åˆ—表 -->
    <view class="ledger-list"
          v-if="ledgerList.length > 0">
      <view v-for="(item, index) in ledgerList"
            :key="index">
        <view class="ledger-item">
          <view class="item-header">
            <view class="item-left">
              <view class="document-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="item-id">{{ item.title }}</text>
            </view>
            <view class="item-tag">
              <u-tag :type="getTagClass(item.status)">{{ formatReceiptType(item.status) }}</u-tag>
            </view>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">申请人</text>
              <text class="detail-value">{{ item.applicant }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">主理人</text>
              <text class="detail-value">{{ item.host }}</text>
            </view>
            <view class="detail-row-approveReason">
              <text class="detail-label">会议时间</text>
              <text class="detail-value highlightBlue">{{ formatDateTime(item.meetingTime) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">会议地点</text>
              <text class="detail-value">{{ item.location }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">参会人数</text>
              <text class="detail-value">{{ item.participants.length }}</text>
            </view>
            <up-divider></up-divider>
            <view class="actions">
              <u-button type="primary"
                        size="small"
                        class="action-btn view"
                        @click="viewDetail(item)">
                è¯¦æƒ…
              </u-button>
              <u-button type="success"
                        size="small"
                        class="action-btn approve"
                        :disabled="item.status != 0"
                        @click="approve(item)">
                æ·»åŠ çºªè¦
              </u-button>
            </view>
            <!-- <view class="detail-info"
                  style="align-items: flex-end;">
              <view class="detail-row">
              </view>
            </view> -->
          </view>
        </view>
      </view>
    </view>
    <view v-else
          class="no-data">
      <text>暂无数据</text>
    </view>
  </view>
</template>
<script setup>
  import { ref, toRefs, reactive } from "vue";
  import PageHeader from "@/components/PageHeader.vue";
  import {
    getMeetingPublish,
    getRoomEnum,
  } from "@/api/managementMeetings/meetExamine";
  import { getStaffOnJob } from "@/api/personnelManagement/onboarding";
  import { onShow } from "@dcloudio/uni-app";
  import useUserStore from "@/store/modules/user";
  import dayjs from "dayjs";
  const userStore = useUserStore();
  // æ•°æ®
  const ledgerList = ref([]);
  const data = reactive({
    searchForm: {
      title: "",
    },
  });
  const { searchForm } = toRefs(data);
  // è¿”回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // æˆ¿é—´æžšä¸¾
  const roomEnum = ref([]);
  // æˆ¿é—´æžšä¸¾æŸ¥è¯¢
  const getRoomEnumList = () => {
    return getRoomEnum()
      .then(res => {
        console.log(res.data, "res.data");
        roomEnum.value = res.data;
      })
      .catch(() => {
        closeToast();
      });
  };
  // å‘˜å·¥åˆ—表
  const staffList = ref([]);
  // å‘˜å·¥åˆ—表查询
  const getStaffOnJobList = () => {
    return getStaffOnJob()
      .then(res => {
        console.log(res.data, "res.data");
        staffList.value = res.data;
      })
      .catch(() => {
        closeToast();
      });
  };
  // æŸ¥è¯¢åˆ—表
  const getList = () => {
    showLoadingToast("加载中...");
    const page = {
      current: -1,
      size: -1,
    };
    getMeetingPublish({
      ...page,
      ...searchForm.value,
    })
      .then(res => {
        console.log(res.data.records, "res.data.records");
        ledgerList.value = res.data.records.map(it => {
          console.log(it, "it1");
          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})`,
              };
            });
          console.log(it, "it2");
          return it;
        });
        closeToast();
      })
      .catch(() => {
        closeToast();
      });
  };
  // æ˜¾ç¤ºåŠ è½½æç¤º
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  };
  const formatDateTime = dateTime => {
    if (!dateTime) return "";
    return dateTime.replace(" ", "\n");
  };
  // å…³é—­æç¤º
  const closeToast = () => {
    uni.hideLoading();
  };
  // æ ¼å¼åŒ–回款方式
  const formatReceiptType = params => {
    if (params == 0) {
      return "待审批";
    } else if (params == 1) {
      return "已通过";
    } else if (params == 2) {
      return "未通过";
    } else if (params == 3) {
      return "已取消";
    } else {
      return "未知";
    }
  };
  // èŽ·å–æ ‡ç­¾æ ·å¼ç±»
  const getTagClass = type => {
    if (type == 0) {
      return "info";
    } else if (type == 1) {
      return "success";
    } else if (type == 2) {
      return "warning";
    } else if (type == 3) {
      return "danger";
    } else {
      return "info";
    }
  };
  // ç‚¹å‡»å®¡æ ¸
  const approve = item => {
    // uni.setStorageSync("approveId", item.approveId);
    uni.navigateTo({
      url:
        "/pages/managementMeetings/meetSummary/approve?item=" +
        JSON.stringify(item) +
        "&edit=true",
    });
  };
  // æŸ¥çœ‹è¯¦æƒ…
  const viewDetail = item => {
    uni.navigateTo({
      url:
        "/pages/managementMeetings/meetSummary/approve?item=" +
        JSON.stringify(item) +
        "&edit=false",
    });
  };
  onShow(async () => {
    // é¡µé¢åŠ è½½å®ŒæˆåŽçš„åˆå§‹åŒ–é€»è¾‘
    try {
      // ç­‰å¾…两个异步方法执行完成
      await Promise.all([getRoomEnumList(), getStaffOnJobList()]);
      // ä¸¤ä¸ªæ–¹æ³•执行完成后再执行 getList()
      getList();
    } catch (error) {
      console.error("初始化数据失败:", error);
      // å³ä½¿å‡ºé”™ä¹Ÿæ‰§è¡Œ getList(),确保页面能正常加载
      getList();
    }
  });
</script>
<style scoped lang="scss">
  @import "../../../styles/sales-common.scss";
  .u-divider {
    margin: 0 !important;
  }
  // æ–‡æ¡£å›¾æ ‡æ ·å¼ - è¦†ç›–公共样式中的背景色
  .document-icon {
    background: #ed8d05;
  }
  // æµ®åŠ¨æŒ‰é’®æ ·å¼ - è¦†ç›–公共样式中的背景色
  .fab-button {
    background: #ed8d05;
  }
  // ç‰¹æœ‰æ ·å¼
  .detail-row-user {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .detail-row-approveReason {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
  }
  .detail-value.highlightBlue {
    color: #2979ff;
    font-weight: 500;
  }
  .detail-value.highlightYellow {
    color: #ed8d05;
    font-weight: 500;
  }
  .approver-value {
    display: flex;
    justify-content: flex-end;
  }
  .approver-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: #f0f6ff;
    color: #2b7cff;
    border: 1px solid #e0efff;
    border-radius: 999px;
    padding: 4px 10px;
    max-width: 100%;
  }
  .approver-name {
    font-size: 12px;
    color: #2b7cff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .actions {
    display: flex;
    gap: 10px;
    align-items: center;
    justify-content: flex-end;
    margin-top: 18rpx;
  }
  .action-btn {
    border-radius: 16px;
    height: 28px;
    line-height: 28px;
    padding: 0 12px;
  }
</style>