zhangwencui
3 天以前 a3e171bd892579168092e202dd71aa8eba94cf0f
请假申请增加图片上传
已修改1个文件
197 ■■■■■ 文件已修改
src/pages/hrm/leave/form.vue 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/hrm/leave/form.vue
@@ -1,11 +1,10 @@
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { createLeave, getAutoApprovalPolicy, getHrmDicts, getLeave, getLeaveProcesses, submitLeave, updateLeave, type ApproveProcess, type HrmAttachment, type Leave } from "../../../api/hrm";
import { createLeave, getAutoApprovalPolicy, getHrmDicts, getLeave, getLeaveProcesses, submitLeave, updateLeave, uploadHrmFile, type ApproveProcess, type HrmAttachment, type Leave } from "../../../api/hrm";
import type { DictData } from "../../../api/crm";
import { useAuth } from "../../../stores/auth";
import { HRM_DICT, label, options, setDateTime } from "../shared";
import HrmAttachmentField from "../HrmAttachmentField.vue";
const { hasPermission, state } = useAuth();
const id = ref<number>();
@@ -14,12 +13,82 @@
const submitting = ref(false);
const dicts = ref<DictData[]>([]);
const attachments = ref<HrmAttachment[]>([]);
const pendingImages = ref<string[]>([]);
const MAX_ATTACHMENTS = 10;
const IMAGE_PATTERN = /\.(png|jpe?g|gif|webp|bmp|heic)(\?.*)?$/i;
const uploading = computed(() => pendingImages.value.length > 0);
const form = reactive<Leave>({ userId: undefined, leaveType: undefined, startTime: "", endTime: "", duration: 0.5, reason: "", blobIds: [] });
const readonly = computed(() => mode.value === "detail");
const canSave = computed(() => !readonly.value && (id.value ? hasPermission("hrm:leave:update") : hasPermission("hrm:leave:create")));
const canSubmit = computed(() => canSave.value && hasPermission("hrm:leave:submit"));
const typeOptions = computed(() => options(dicts.value, HRM_DICT.leaveType));
const userText = computed(() => form.userName || state.user?.nickname || state.user?.username || "当前登录用户");
function isImage(file: HrmAttachment) {
  return IMAGE_PATTERN.test(file.url || "") || IMAGE_PATTERN.test(file.name || "");
}
function chooseImages() {
  if (!state.accessToken || uploading.value) return;
  const remain = MAX_ATTACHMENTS - attachments.value.length - pendingImages.value.length;
  if (remain <= 0) {
    uni.showToast({ title: `最多上传${MAX_ATTACHMENTS}个附件`, icon: "none" });
    return;
  }
  uni.chooseImage({
    count: remain,
    sourceType: ["album", "camera"],
    success: async result => {
      const paths = result.tempFilePaths || [];
      if (!paths.length) return;
      pendingImages.value.push(...paths);
      try {
        for (const path of paths) {
          try {
            const file = await uploadHrmFile(state.accessToken!, path);
            attachments.value.push(file);
            if (file.id && !form.blobIds?.includes(file.id)) {
              form.blobIds = [...(form.blobIds || []), file.id];
            }
          } catch (error) {
            uni.showToast({ title: error instanceof Error ? error.message : "图片上传失败", icon: "none" });
          }
        }
      } finally {
        pendingImages.value = pendingImages.value.filter(path => !paths.includes(path));
      }
    },
  });
}
function removeAttachment(index: number) {
  const file = attachments.value[index];
  attachments.value.splice(index, 1);
  if (file?.id !== undefined) {
    form.blobIds = (form.blobIds || []).filter(blobId => blobId !== file.id);
  }
}
function previewAttachment(index: number) {
  const file = attachments.value[index];
  if (!file?.url) {
    uni.showToast({ title: "附件地址不可用", icon: "none" });
    return;
  }
  if (isImage(file)) {
    const urls = attachments.value
      .filter(item => isImage(item) && item.url)
      .map(item => item.url as string);
    uni.previewImage({ urls, current: file.url });
    return;
  }
  // #ifdef H5
  window.open(file.url, "_blank");
  // #endif
  // #ifndef H5
  uni.showToast({ title: "非图片附件请在 Web 端查看", icon: "none" });
  // #endif
}
function validateForm() {
  if (!form.userId || form.leaveType === undefined || !form.startTime || !form.endTime || !form.duration || !form.reason?.trim()) {
@@ -58,6 +127,10 @@
async function save() {
  if (!validateForm() || !state.accessToken || saving.value || submitting.value) return;
  if (uploading.value) {
    uni.showToast({ title: "图片上传中,请稍候", icon: "none" });
    return;
  }
  saving.value = true;
  try {
    await persistDraft();
@@ -72,6 +145,10 @@
async function saveAndSubmit() {
  if (!validateForm() || !state.accessToken || saving.value || submitting.value) return;
  if (uploading.value) {
    uni.showToast({ title: "图片上传中,请稍候", icon: "none" });
    return;
  }
  submitting.value = true;
  let draftSaved = false;
  try {
@@ -156,7 +233,27 @@
      </view>
      <view class="field"><text>请假时长 <text class="required">*</text></text><input v-model.number="form.duration" :disabled="readonly" type="digit" placeholder="单位:天" /></view>
      <view class="field column"><text>请假原因 <text class="required">*</text></text><textarea v-model="form.reason" :disabled="readonly" maxlength="500" placeholder="请输入请假原因" /></view>
      <HrmAttachmentField class="leave-attachment" v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" />
      <view v-if="!readonly || attachments.length" class="field column attachment-field">
        <text>图片附件 <text class="optional">(选填,最多10张)</text></text>
        <view class="attachment-grid">
          <view v-for="(file, index) in attachments" :key="file.id" class="attachment-cell">
            <image v-if="isImage(file)" :src="file.url" mode="aspectFill" @click="previewAttachment(index)" />
            <view v-else class="attachment-doc" @click="previewAttachment(index)">
              <text>{{ file.name || `附件${index + 1}` }}</text>
            </view>
            <text v-if="!readonly" class="attachment-del" @click="removeAttachment(index)">×</text>
          </view>
          <view v-for="(path, index) in pendingImages" :key="`pending-${index}`" class="attachment-cell">
            <image :src="path" mode="aspectFill" />
            <view class="attachment-mask"><text>上传中</text></view>
          </view>
          <view v-if="!readonly && attachments.length + pendingImages.length < MAX_ATTACHMENTS"
                class="attachment-add" @click="chooseImages">
            <text class="plus">+</text>
            <text>上传图片</text>
          </view>
        </view>
      </view>
      <view class="field column"><text>备注</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请输入备注" /></view>
    </view>
    <view v-if="canSave" class="form-actions">
@@ -169,5 +266,97 @@
<style lang="scss" scoped>
@import "../form.scss";
.leave-attachment{box-sizing:border-box;padding-right:24rpx;padding-left:24rpx}
.attachment-field {
  gap: 18rpx;
}
.optional {
  color: #97a4b4;
  font-size: 22rpx;
}
.attachment-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 18rpx;
}
.attachment-cell {
  position: relative;
  width: 150rpx;
  height: 150rpx;
}
.attachment-cell image {
  width: 150rpx;
  height: 150rpx;
  background: #f2f4f7;
  border-radius: 12rpx;
}
.attachment-doc {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 150rpx;
  height: 150rpx;
  padding: 12rpx;
  background: #f2f6fc;
  border: 1rpx solid #dbe5f2;
  border-radius: 12rpx;
}
.attachment-doc text {
  color: #526780;
  font-size: 22rpx;
  text-align: center;
  word-break: break-all;
}
.attachment-del {
  position: absolute;
  top: -12rpx;
  right: -12rpx;
  width: 38rpx;
  height: 38rpx;
  color: #fff;
  font-size: 28rpx;
  line-height: 36rpx;
  text-align: center;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 50%;
}
.attachment-mask {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.4);
  border-radius: 12rpx;
}
.attachment-mask text {
  color: #fff;
  font-size: 22rpx;
}
.attachment-add {
  display: flex;
  align-items: center;
  flex-direction: column;
  justify-content: center;
  box-sizing: border-box;
  width: 150rpx;
  height: 150rpx;
  gap: 8rpx;
  color: #7c8aa0;
  font-size: 22rpx;
  background: #f7f9fc;
  border: 1rpx dashed #c3cfdf;
  border-radius: 12rpx;
}
.attachment-add .plus {
  color: #93a3ba;
  font-size: 44rpx;
  line-height: 1;
}
.attachment-add:active {
  background: #eef3fa;
}
</style>