yyb
2026-05-21 b014cdaf7fcf42cd2b310968f9d47d4420444a6a
src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -16,83 +16,150 @@
        <text class="loading-text">加载中...</text>
      </view>
      <template v-else-if="detail">
        <view class="section">
          <view class="section-title">基本信息</view>
          <view class="form-body">
            <view class="form-row">
              <text class="form-label required">审批标题</text>
        <up-form :model="form"
                 label-width="88"
                 input-align="right">
          <u-cell-group title="基本信息"
                        class="form-section">
            <up-form-item label="审批标题"
                          required
                          class="form-item-name">
              <up-input v-model="form.title"
                        class="name-input-inline"
                        placeholder="请输入审批标题"
                        maxlength="100"
                        clearable />
            </view>
            <view class="form-row">
              <text class="form-label">审批模板</text>
              <text class="form-readonly">{{ templateName }}</text>
            </view>
            <view class="form-row">
              <text class="form-label">申请人</text>
              <text class="form-readonly">{{ displayApplicantName }}</text>
            </view>
          </view>
        </view>
            </up-form-item>
            <up-form-item label="审批模板"
                          class="form-item-readonly">
              <up-input :model-value="templateName"
                        readonly />
            </up-form-item>
            <up-form-item label="申请人"
                          class="form-item-readonly">
              <up-input :model-value="displayApplicantName"
                        readonly />
            </up-form-item>
          </u-cell-group>
        </up-form>
        <view class="section">
          <view class="section-title">填报内容</view>
        <view class="section-card">
          <view class="section-head">
            <text class="section-title">填报内容</text>
          </view>
          <view v-if="formConfigData.prompt"
                class="form-prompt">
            {{ formConfigData.prompt }}
          </view>
          <view v-if="formConfigData.fields.length"
                class="form-body">
            <view v-for="field in formConfigData.fields"
                  :key="field.key"
                  class="form-row form-row--field">
              <text class="form-label"
                    :class="{ required: field.required }">{{ field.label }}</text>
              <up-textarea v-if="field.type === 'textarea'"
          <up-form v-if="formConfigData.fields.length"
                   :model="formValues"
                   label-width="88"
                   input-align="right"
                   class="dynamic-form">
            <up-form-item v-for="field in formConfigData.fields"
                          :key="field.key"
                          :label="field.label"
                          :required="!!field.required"
                          :class="formItemClass(field)">
              <up-textarea v-if="isTextareaField(field)"
                           v-model="formValues[field.key]"
                           :placeholder="`请输入${field.label}`"
                           maxlength="500"
                           border="surround"
                           height="80" />
              <view v-else-if="field.type === 'date'"
                    class="date-trigger"
                    @click="openDatePicker(field.key)">
                <up-input :model-value="formValues[field.key]"
              <view v-else-if="isDatetimerangeField(field)"
                    class="daterange-fill">
                <view class="range-fill-row"
                      @click="openRangePicker(field, 'start')">
                  <text class="range-fill-label">开始</text>
                  <up-input :model-value="getRangePartDisplay(field, 'start')"
                            placeholder="开始时间"
                            readonly />
                  <up-icon name="calendar"
                           size="16"
                           color="#909399" />
                </view>
                <text class="range-fill-sep">至</text>
                <view class="range-fill-row"
                      @click="openRangePicker(field, 'end')">
                  <text class="range-fill-label">结束</text>
                  <up-input :model-value="getRangePartDisplay(field, 'end')"
                            placeholder="结束时间"
                            readonly />
                  <up-icon name="calendar"
                           size="16"
                           color="#909399" />
                </view>
              </view>
              <view v-else-if="isDateLikeField(field)"
                    class="field-trigger"
                    @click="openDatePicker(field)">
                <up-input :model-value="formatFieldDisplayValue(field, formValues[field.key])"
                          :placeholder="`请选择${field.label}`"
                          readonly />
                <up-icon :name="getDatePickerMode(field) === 'time' ? 'clock' : 'calendar'"
                         size="18"
                         color="#909399" />
              </view>
              <view v-else-if="isSelectField(field)"
                    class="field-trigger"
                    @click="openSelectPicker(field)">
                <up-input :model-value="getSelectDisplayText(field)"
                          :placeholder="`请选择${field.label}`"
                          readonly />
                <up-icon name="arrow-right"
                         size="16"
                         color="#c0c4cc" />
              </view>
              <up-input v-else
                        v-model="formValues[field.key]"
                        :type="field.type === 'number' ? 'digit' : 'text'"
                        :type="isNumberField(field) ? 'digit' : 'text'"
                        :placeholder="`请输入${field.label}`"
                        clearable />
            </view>
          </view>
            </up-form-item>
          </up-form>
          <view v-else
                class="empty-hint">该模板暂无填报项</view>
        </view>
        <view class="section">
          <view class="section-title">审批流程</view>
        <view class="section-card">
          <view class="section-head">
            <text class="section-title">审批流程</text>
          </view>
          <view v-if="detail.nodes?.length"
                class="flow-list">
            <view v-for="(node, index) in detail.nodes"
                  :key="node.id || index"
                  class="flow-card">
              <view class="flow-card-head">
                <text class="flow-level">第{{ levelLabel(node.levelNo || index + 1) }}级</text>
                <text class="flow-type">{{ approveTypeText(node.approveType) }}</text>
                class="flow-wrap">
            <view v-for="(node, nodeIndex) in detail.nodes"
                  :key="node.id || nodeIndex"
                  class="flow-node-block">
              <view class="flow-node-card">
                <view class="node-header">
                  <view class="node-level-badge">{{ node.levelNo || nodeIndex + 1 }}</view>
                  <text class="node-level-text">第{{ levelLabel(node.levelNo || nodeIndex + 1) }}级</text>
                </view>
                <view class="approve-type-row approve-type-row--readonly">
                  <view class="type-btn"
                        :class="{ active: node.approveType !== 'OR' }">
                    会签
                  </view>
                  <view class="type-btn"
                        :class="{ active: node.approveType === 'OR' }">
                    或签
                  </view>
                </view>
                <view class="approver-list">
                  <view v-for="(approver, aIdx) in node.approvers || []"
                        :key="approver.id || aIdx"
                        class="approver-chip">
                    <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view>
                    <text class="approver-name">{{ approver.approverName || "-" }}</text>
                  </view>
                  <text v-if="!(node.approvers || []).length"
                        class="empty-hint inline">暂无审批人</text>
                </view>
              </view>
              <view class="approver-tags">
                <text v-for="(approver, aIdx) in node.approvers || []"
                      :key="approver.id || aIdx"
                      class="approver-tag">
                  {{ approver.approverName || "-" }}
                </text>
                <text v-if="!(node.approvers || []).length"
                      class="empty-hint inline">暂无审批人</text>
              <view v-if="nodeIndex < detail.nodes.length - 1"
                    class="flow-connector">
                <view class="flow-connector-line" />
              </view>
            </view>
          </view>
@@ -119,10 +186,16 @@
              @close="showDatePicker = false">
      <up-datetime-picker :show="true"
                          v-model="datePickerTs"
                          mode="date"
                          :mode="datePickerMode"
                          @confirm="onDateConfirm"
                          @cancel="showDatePicker = false" />
                          @cancel="onDatePickerCancel" />
    </up-popup>
    <up-action-sheet :show="showSelectSheet"
                     :title="selectSheetTitle"
                     :actions="selectSheetActions"
                     @select="onSelectOption"
                     @close="showSelectSheet = false" />
  </view>
</template>
@@ -137,7 +210,28 @@
    updateApprovalInstance,
  } from "@/api/oa/approvalInstance.js";
  import useUserStore from "@/store/modules/user";
  import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
  import { parseTime } from "@/utils/ruoyi";
  import { getDept } from "@/api/collaborativeApproval/approvalProcess.js";
  import { userListNoPageByTenantId } from "@/api/system/user";
  import {
    formatDatetimerangeDisplay,
    formatFieldDateValue,
    formatFieldDisplayValue,
    getDatePickerMode,
    getFieldInitialValue,
    getFieldOptionLabel,
    isDatetimerangeField,
    isDateLikeField,
    isNumberField,
    isSelectField,
    isTextareaField,
    joinDatetimerangeValue,
    mergeFormConfigForEdit,
    parseDatetimerangeValue,
    resolveFieldOptions,
    parseApprovalFormConfig,
    parseFieldDateToTs,
  } from "../../_utils/approvalFormField.js";
  const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
  const LEVEL_TEXT = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
@@ -154,12 +248,25 @@
  const showDatePicker = ref(false);
  const datePickerTs = ref(Date.now());
  const activeDateFieldKey = ref("");
  const activeDateField = ref(null);
  const activeRangePart = ref("start");
  const datePickerMode = computed(() => {
    const field = activeDateField.value;
    if (!field) return "date";
    if (isDatetimerangeField(field)) return "datetime";
    return getDatePickerMode(field);
  });
  const showSelectSheet = ref(false);
  const activeSelectField = ref(null);
  const pickerUserList = ref([]);
  const pickerDeptList = ref([]);
  const isEditMode = computed(() => !!instanceId.value);
  const pageTitle = computed(() => (isEditMode.value ? "编辑审批" : "发起审批"));
  const confirmText = computed(() => (isEditMode.value ? "保存" : "提交审批"));
  const pageTitle = computed(() => (isEditMode.value ? "修改审批" : "发起审批"));
  const confirmText = computed(() => (isEditMode.value ? "保存修改" : "提交审批"));
  const applicantName = computed(
    () => userStore.nickName || userStore.name || "-"
@@ -173,28 +280,72 @@
    () => detail.value?.templateName || instanceRow.value?.templateName || "-"
  );
  const parseFormConfig = raw => {
    if (!raw) return { prompt: "", fields: [] };
    try {
      const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
      return {
        prompt: obj?.prompt || "",
        fields: Array.isArray(obj?.fields) ? obj.fields : [],
      };
    } catch {
      return { prompt: "", fields: [] };
    }
  };
  const formConfigData = computed(() => {
    const raw = isEditMode.value
      ? instanceRow.value?.formConfig
      : detail.value?.formConfig;
    return parseFormConfig(raw);
    if (isEditMode.value) {
      return mergeFormConfigForEdit(
        detail.value?.formConfig,
        instanceRow.value?.formConfig
      );
    }
    return parseApprovalFormConfig(detail.value?.formConfig);
  });
  const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
  const approveTypeText = type => (type === "OR" ? "或签" : "会签");
  const selectSheetTitle = computed(
    () => (activeSelectField.value?.label ? `选择${activeSelectField.value.label}` : "请选择")
  );
  const selectSheetActions = computed(() => {
    const field = activeSelectField.value;
    if (!field) return [];
    return resolveFieldOptions(field, {
      users: pickerUserList.value,
      depts: pickerDeptList.value,
    }).map(opt => ({
      name: opt.label,
      value: opt.value,
    }));
  });
  const formItemClass = field => {
    if (isTextareaField(field)) return "form-item-textarea";
    if (isDatetimerangeField(field)) return "form-item-daterange";
    if (isSelectField(field) || isDateLikeField(field)) return "form-item-select";
    return "form-item-inline";
  };
  const getRangePartDisplay = (field, part) => {
    const parts = parseDatetimerangeValue(formValues[field.key]);
    const val = part === "start" ? parts.start : parts.end;
    return val ? formatFieldDisplayValue({ type: "datetime" }, val) : "";
  };
  const openRangePicker = (field, part) => {
    activeDateField.value = field;
    activeRangePart.value = part;
    const parts = parseDatetimerangeValue(formValues[field.key]);
    const val = part === "start" ? parts.start : parts.end;
    datePickerTs.value = parseFieldDateToTs(val) ?? Date.now();
    showDatePicker.value = true;
  };
  const getSelectDisplayText = field => {
    const stored = formValues[field.key];
    const options = resolveFieldOptions(field, {
      users: pickerUserList.value,
      depts: pickerDeptList.value,
    });
    const matched = options.find(
      opt =>
        String(opt.value) === String(stored) || String(opt.label) === String(stored)
    );
    return (
      matched?.label ||
      getFieldOptionLabel(field, stored) ||
      (stored !== undefined && stored !== null ? String(stored) : "")
    );
  };
  const initFormValues = fields => {
    Object.keys(formValues).forEach(key => {
@@ -202,23 +353,60 @@
    });
    fields.forEach(field => {
      if (!field?.key) return;
      formValues[field.key] = field.value ?? field.defaultValue ?? "";
      formValues[field.key] = getFieldInitialValue(field);
    });
  };
  const openDatePicker = fieldKey => {
    activeDateFieldKey.value = fieldKey;
    const current = formValues[fieldKey];
    datePickerTs.value = current ? new Date(current).getTime() : Date.now();
  const openSelectPicker = field => {
    const options = resolveFieldOptions(field, {
      users: pickerUserList.value,
      depts: pickerDeptList.value,
    });
    if (!options.length) {
      uni.showToast({ title: "该字段未配置下拉选项", icon: "none" });
      return;
    }
    activeSelectField.value = field;
    showSelectSheet.value = true;
  };
  const onSelectOption = action => {
    const key = activeSelectField.value?.key;
    if (key) {
      formValues[key] = action.value;
    }
    showSelectSheet.value = false;
    activeSelectField.value = null;
  };
  const openDatePicker = field => {
    activeDateField.value = field;
    const current = formValues[field.key];
    datePickerTs.value = parseFieldDateToTs(current) ?? Date.now();
    showDatePicker.value = true;
  };
  const onDatePickerCancel = () => {
    showDatePicker.value = false;
    activeDateField.value = null;
  };
  const onDateConfirm = e => {
    const ts = e?.value ?? datePickerTs.value;
    if (activeDateFieldKey.value) {
      formValues[activeDateFieldKey.value] = formatDateToYMD(ts);
    const field = activeDateField.value;
    if (field?.key) {
      if (isDatetimerangeField(field)) {
        const parts = parseDatetimerangeValue(formValues[field.key]);
        const formatted = formatFieldDateValue({ type: "datetime" }, ts);
        formValues[field.key] = joinDatetimerangeValue(
          activeRangePart.value === "start" ? formatted : parts.start,
          activeRangePart.value === "end" ? formatted : parts.end
        );
      } else {
        formValues[field.key] = formatFieldDateValue(field, ts);
      }
    }
    showDatePicker.value = false;
    onDatePickerCancel();
  };
  const validateForm = () => {
@@ -230,8 +418,35 @@
      if (!field.required) continue;
      const val = formValues[field.key];
      if (val === undefined || val === null || String(val).trim() === "") {
        uni.showToast({ title: `请填写${field.label}`, icon: "none" });
        const action =
          isSelectField(field) || isDateLikeField(field) || isDatetimerangeField(field)
            ? "请选择"
            : "请填写";
        uni.showToast({ title: `${action}${field.label}`, icon: "none" });
        return false;
      }
      if (isDatetimerangeField(field)) {
        const { start, end } = parseDatetimerangeValue(val);
        if (!start || !end) {
          uni.showToast({ title: `请完整选择${field.label}`, icon: "none" });
          return false;
        }
      }
      if (isSelectField(field)) {
        const options = resolveFieldOptions(field, {
          users: pickerUserList.value,
          depts: pickerDeptList.value,
        });
        if (
          options.length &&
          !options.some(
            opt =>
              String(opt.value) === String(val) || String(opt.label) === String(val)
          )
        ) {
          uni.showToast({ title: `${field.label}选项无效`, icon: "none" });
          return false;
        }
      }
    }
    if (!detail.value?.nodes?.length) {
@@ -272,15 +487,23 @@
      templateId: row.templateId ?? detail.value?.id,
      templateName: row.templateName ?? detail.value?.templateName,
      businessId: row.businessId,
      businessType: row.businessType,
      businessType: row.businessType ?? detail.value?.businessType,
      title: form.title.trim(),
      status: row.status || "PENDING",
      currentLevel: row.currentLevel,
      applicantId: row.applicantId,
      applicantName: row.applicantName,
      applyTime: row.applyTime,
      finishTime: row.finishTime,
      createUser: row.createUser,
      createTime: row.createTime,
      updateUser: row.updateUser,
      updateTime: row.updateTime,
      deptId: row.deptId,
      deleted: row.deleted,
      formConfig: buildFormConfigPayload(),
      approveAction: row.approveAction,
      approveComment: row.approveComment,
    };
  };
@@ -296,7 +519,7 @@
    submitApi(payload)
      .then(() => {
        uni.showToast({
          title: isEditMode.value ? "保存成功" : "提交成功",
          title: isEditMode.value ? "修改成功" : "提交成功",
          icon: "success",
        });
        if (isEditMode.value) {
@@ -308,7 +531,7 @@
      })
      .catch(() => {
        uni.showToast({
          title: isEditMode.value ? "保存失败" : "提交失败",
          title: isEditMode.value ? "修改失败" : "提交失败",
          icon: "none",
        });
      })
@@ -352,9 +575,9 @@
    const row = uni.getStorageSync(EDIT_STORAGE_KEY);
    if (!row || String(row.id) !== String(instanceId.value)) {
      uni.showToast({ title: "未获取到审批数据", icon: "none" });
      setTimeout(() => uni.navigateBack(), 500);
      return;
    }
    uni.removeStorageSync(EDIT_STORAGE_KEY);
    instanceRow.value = row;
    templateId.value = row.templateId;
    form.title = row.title || "";
@@ -363,6 +586,7 @@
    detail.value = null;
    try {
      await loadTemplateDetail();
      if (!detail.value) return;
      initFormValues(formConfigData.value.fields);
    } finally {
      loading.value = false;
@@ -373,7 +597,25 @@
    uni.navigateBack();
  };
  const loadPickerSourceData = () => {
    userListNoPageByTenantId()
      .then(res => {
        pickerUserList.value = res?.data || [];
      })
      .catch(() => {
        pickerUserList.value = [];
      });
    getDept()
      .then(res => {
        pickerDeptList.value = res?.data || [];
      })
      .catch(() => {
        pickerDeptList.value = [];
      });
  };
  onLoad(options => {
    loadPickerSourceData();
    if (options?.id) {
      instanceId.value = options.id;
      loadForEdit();
@@ -389,11 +631,22 @@
</script>
<style scoped lang="scss">
  @import "@/static/scss/form-common.scss";
  $primary: #2979ff;
  $text: #1f2d3d;
  $text-secondary: #606266;
  $text-muted: #909399;
  $bg-page: #f0f3f8;
  $radius-lg: 12px;
  $radius-md: 10px;
  $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05);
  .approve-apply-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background: #f0f3f8;
    background: $bg-page;
  }
  .form-scroll {
@@ -412,130 +665,347 @@
  .loading-text {
    font-size: 14px;
    color: $text-muted;
  }
  .form-section {
    margin-bottom: 10px;
    border-radius: $radius-lg;
    overflow: hidden;
    box-shadow: $shadow-card;
  }
  :deep(.form-section .u-cell-group__title) {
    padding: 12px 16px 8px !important;
    font-size: 15px !important;
    font-weight: 600 !important;
    color: $text !important;
    background: #fff !important;
  }
  :deep(.form-section .u-form-item) {
    padding: 0 16px !important;
  }
  :deep(.form-section .u-form-item__body) {
    padding: 10px 0 !important;
    min-height: auto !important;
  }
  :deep(.form-item-name .u-form-item__body) {
    flex-direction: row !important;
    align-items: center !important;
  }
  :deep(.form-item-name .u-form-item__content) {
    flex: 1 !important;
    min-width: 0 !important;
    justify-content: flex-end !important;
  }
  :deep(.name-input-inline),
  :deep(.name-input-inline .u-input__content) {
    width: 100% !important;
    flex: 1 !important;
  }
  :deep(.name-input-inline input),
  :deep(.name-input-inline .u-input__content__field-wrapper__field) {
    width: 100% !important;
    text-align: right !important;
    font-size: 15px !important;
  }
  :deep(.form-item-readonly .u-form-item__body) {
    align-items: center !important;
  }
  :deep(.form-item-readonly .u-form-item__content) {
    flex: 1 !important;
    min-width: 0 !important;
    justify-content: flex-end !important;
  }
  :deep(.form-item-readonly .u-input__content__field-wrapper__field) {
    text-align: right !important;
    color: #303133 !important;
  }
  .dynamic-form {
    padding: 0 0 4px;
  }
  :deep(.dynamic-form .u-form-item) {
    padding: 0 16px !important;
  }
  :deep(.dynamic-form .u-form-item__body) {
    padding: 10px 0 !important;
    min-height: auto !important;
  }
  :deep(.form-item-inline .u-form-item__body) {
    flex-direction: row !important;
    align-items: center !important;
  }
  :deep(.form-item-inline .u-form-item__content) {
    flex: 1 !important;
    min-width: 0 !important;
    justify-content: flex-end !important;
  }
  :deep(.form-item-inline input),
  :deep(.form-item-inline .u-input__content__field-wrapper__field) {
    text-align: right !important;
    font-size: 15px !important;
  }
  :deep(.form-item-select .u-form-item__body) {
    align-items: center !important;
  }
  :deep(.form-item-select .u-form-item__content) {
    flex: 1 !important;
    min-width: 0 !important;
    justify-content: flex-end !important;
  }
  :deep(.form-item-textarea .u-form-item__body) {
    flex-direction: column !important;
    align-items: stretch !important;
    padding: 10px 0 12px !important;
  }
  :deep(.form-item-textarea .u-form-item__content) {
    width: 100% !important;
    justify-content: stretch !important;
  }
  .field-trigger {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 6px;
    width: 100%;
    min-width: 0;
  }
  :deep(.field-trigger .u-input) {
    flex: 1 !important;
    min-width: 0 !important;
  }
  :deep(.field-trigger .u-input__content__field-wrapper__field) {
    text-align: right !important;
    font-size: 15px !important;
  }
  :deep(.form-item-daterange .u-form-item__body) {
    flex-direction: column !important;
    align-items: stretch !important;
  }
  :deep(.form-item-daterange .u-form-item__content) {
    width: 100% !important;
    justify-content: stretch !important;
  }
  .daterange-fill {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .range-fill-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    background: #f7f9fc;
    border: 1px solid #eef1f6;
    border-radius: 8px;
  }
  .range-fill-label {
    flex-shrink: 0;
    width: 36px;
    font-size: 13px;
    color: #909399;
  }
  .section {
    background: #fff;
    border-radius: 12px;
  .range-fill-sep {
    font-size: 12px;
    color: #c0c4cc;
    text-align: center;
  }
  :deep(.range-fill-row .u-input) {
    flex: 1 !important;
    min-width: 0 !important;
  }
  .section-card {
    margin-bottom: 10px;
    background: #fff;
    border-radius: $radius-lg;
    overflow: hidden;
    box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
    box-shadow: $shadow-card;
  }
  .section-head {
    padding: 12px 16px;
    border-bottom: 1px solid #f2f4f7;
  }
  .section-title {
    padding: 12px 16px;
    font-size: 15px;
    font-weight: 600;
    color: #1f2d3d;
    border-bottom: 1px solid #f2f4f7;
    border-left: 3px solid #2979ff;
    padding-left: 13px;
  }
  .form-body {
    padding: 8px 16px 16px;
  }
  .form-row {
    padding: 10px 0;
    border-bottom: 1px solid #f5f7fa;
    &:last-child {
      border-bottom: none;
    }
    &--field {
      flex-direction: column;
      align-items: stretch;
    }
  }
  .form-label {
    display: block;
    margin-bottom: 8px;
    font-size: 14px;
    color: #606266;
    &.required::before {
      content: "*";
      color: #f56c6c;
      margin-right: 4px;
    }
  }
  .form-readonly {
    font-size: 14px;
    color: #303133;
    color: $text;
    padding-left: 10px;
    border-left: 3px solid $primary;
    line-height: 1.2;
  }
  .form-prompt {
    margin: 12px 16px 0;
    padding: 10px 12px;
    font-size: 13px;
    color: #606266;
    color: $text-secondary;
    background: #f8fafc;
    border-radius: 8px;
    line-height: 1.5;
  }
  .date-trigger {
    width: 100%;
  .flow-wrap {
    padding: 10px 16px 14px;
  }
  .flow-list {
  .flow-node-block {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }
  .flow-node-card {
    background: #fafbfd;
    border: 1px solid #e8eef5;
    border-radius: $radius-md;
    padding: 12px;
  }
  .flow-card {
    padding: 12px;
    margin-bottom: 8px;
    background: #f8fafc;
  .node-header {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
  }
  .node-level-badge {
    width: 26px;
    height: 26px;
    border-radius: 8px;
    border: 1px solid #eef2f6;
    background: $primary;
    color: #fff;
    font-size: 14px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
    &:last-child {
      margin-bottom: 0;
  .node-level-text {
    flex: 1;
    font-size: 15px;
    font-weight: 600;
    color: $text;
  }
  .approve-type-row {
    display: flex;
    background: #f0f3f8;
    border-radius: 8px;
    padding: 3px;
    margin-bottom: 10px;
    &--readonly {
      pointer-events: none;
    }
  }
  .flow-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
  }
  .flow-level {
  .type-btn {
    flex: 1;
    text-align: center;
    padding: 8px 0;
    font-size: 14px;
    font-weight: 600;
    color: #303133;
    color: $text-secondary;
    border-radius: 6px;
    &.active {
      background: #fff;
      color: $primary;
      font-weight: 500;
    }
  }
  .flow-type {
    font-size: 13px;
    color: #2979ff;
  }
  .approver-tags {
  .approver-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
  }
  .approver-tag {
    padding: 4px 10px;
    font-size: 13px;
    color: #303133;
  .approver-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px 6px 6px;
    background: #fff;
    border: 1px solid #dce8f8;
    border-radius: 16px;
    border-radius: 24px;
    box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06);
  }
  .approver-avatar {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .approver-name {
    font-size: 13px;
    color: $text;
    max-width: 120px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .flow-connector {
    display: flex;
    justify-content: center;
    padding: 4px 0;
  }
  .flow-connector-line {
    width: 2px;
    height: 14px;
    background: #d0dff0;
  }
  .empty-hint {
    padding: 12px 16px 16px;
    font-size: 13px;
    color: #909399;
    color: $text-muted;
    &.inline {
      padding: 0;