|
@@ -54,10 +54,20 @@
|
|
|
class="value">{{ form.startDate }}</text></picker><text class="arrow">›</text>
|
|
class="value">{{ form.startDate }}</text></picker><text class="arrow">›</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
<view class="divider"></view>
|
|
|
|
|
+ <view class="field"><text class="label required">开始时间</text>
|
|
|
|
|
+ <picker mode="time" :value="form.startTime" @change="(e) => (form.startTime = e.detail.value)"><text
|
|
|
|
|
+ class="value">{{ form.startTime || "请选择" }}</text></picker><text class="arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="divider"></view>
|
|
|
<view class="field"><text class="label required">结束日期</text>
|
|
<view class="field"><text class="label required">结束日期</text>
|
|
|
<picker mode="date" :value="form.endDate" @change="(e) => (form.endDate = e.detail.value)"><text
|
|
<picker mode="date" :value="form.endDate" @change="(e) => (form.endDate = e.detail.value)"><text
|
|
|
class="value">{{ form.endDate }}</text></picker><text class="arrow">›</text>
|
|
class="value">{{ form.endDate }}</text></picker><text class="arrow">›</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <view class="divider"></view>
|
|
|
|
|
+ <view class="field"><text class="label required">结束时间</text>
|
|
|
|
|
+ <picker mode="time" :value="form.endTime" @change="(e) => (form.endTime = e.detail.value)"><text
|
|
|
|
|
+ class="value">{{ form.endTime || "请选择" }}</text></picker><text class="arrow">›</text>
|
|
|
|
|
+ </view>
|
|
|
</template>
|
|
</template>
|
|
|
<view class="divider"></view>
|
|
<view class="divider"></view>
|
|
|
<view class="field textarea-field"><text class="label"
|
|
<view class="field textarea-field"><text class="label"
|
|
@@ -67,7 +77,7 @@
|
|
|
<view class="form-card card section">
|
|
<view class="form-card card section">
|
|
|
<view class="field"><text class="label">附件</text>
|
|
<view class="field"><text class="label">附件</text>
|
|
|
<view class="upload press" @click="chooseImage"><text class="plus">+</text><text>拍照或上传</text></view>
|
|
<view class="upload press" @click="chooseImage"><text class="plus">+</text><text>拍照或上传</text></view>
|
|
|
- <view v-if="form.attachment" class="file-name">已选择 1 张图片</view>
|
|
|
|
|
|
|
+ <view v-if="attachmentSummary" class="file-name">{{ attachmentSummary }}</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="divider"></view>
|
|
<view class="divider"></view>
|
|
|
<view class="field"><text class="label">审批人</text>
|
|
<view class="field"><text class="label">审批人</text>
|
|
@@ -95,11 +105,13 @@ import {
|
|
|
saveDraft as persistDraft,
|
|
saveDraft as persistDraft,
|
|
|
} from "@/utils/storage";
|
|
} from "@/utils/storage";
|
|
|
import {
|
|
import {
|
|
|
|
|
+ ATTENDANCE_APPLY_TYPE_MAP,
|
|
|
REPAIR_TYPE_LABEL,
|
|
REPAIR_TYPE_LABEL,
|
|
|
REPAIR_TYPE_OPTIONS,
|
|
REPAIR_TYPE_OPTIONS,
|
|
|
- combineDateTime,
|
|
|
|
|
|
|
+ buildAttendancePayload,
|
|
|
submitAttendanceApplication,
|
|
submitAttendanceApplication,
|
|
|
} from "@/api/attendance";
|
|
} from "@/api/attendance";
|
|
|
|
|
+import { uploadAttachment } from "@/api/file";
|
|
|
import { getServerConfig } from "@/utils/auth";
|
|
import { getServerConfig } from "@/utils/auth";
|
|
|
const type = ref("leave");
|
|
const type = ref("leave");
|
|
|
useAuthGuard();
|
|
useAuthGuard();
|
|
@@ -108,9 +120,11 @@ const form = reactive({
|
|
|
leaveType: "",
|
|
leaveType: "",
|
|
|
certificateType: "",
|
|
certificateType: "",
|
|
|
startDate: "2026-08-06",
|
|
startDate: "2026-08-06",
|
|
|
|
|
+ startTime: "08:30",
|
|
|
endDate: "2026-08-06",
|
|
endDate: "2026-08-06",
|
|
|
|
|
+ endTime: "17:30",
|
|
|
reason: "",
|
|
reason: "",
|
|
|
- attachment: "",
|
|
|
|
|
|
|
+ attachments: [],
|
|
|
repairType: "",
|
|
repairType: "",
|
|
|
repairTime: "",
|
|
repairTime: "",
|
|
|
});
|
|
});
|
|
@@ -127,6 +141,16 @@ const reasonPlaceholder = computed(() =>
|
|
|
const repairTypeLabel = computed(() =>
|
|
const repairTypeLabel = computed(() =>
|
|
|
form.repairType ? REPAIR_TYPE_LABEL[form.repairType] : "请选择",
|
|
form.repairType ? REPAIR_TYPE_LABEL[form.repairType] : "请选择",
|
|
|
);
|
|
);
|
|
|
|
|
+const attachmentSummary = computed(() => {
|
|
|
|
|
+ const list = Array.isArray(form.attachments) ? form.attachments : [];
|
|
|
|
|
+ if (!list.length) return "";
|
|
|
|
|
+ const uploading = list.filter((a) => a && a.status === "uploading").length;
|
|
|
|
|
+ const failed = list.filter((a) => a && a.status === "failed").length;
|
|
|
|
|
+ const parts = [`已选择 ${list.length} 张图片`];
|
|
|
|
|
+ if (uploading) parts.push(`${uploading} 上传中`);
|
|
|
|
|
+ if (failed) parts.push(`${failed} 失败`);
|
|
|
|
|
+ return parts.join(" · ");
|
|
|
|
|
+});
|
|
|
function syncType(o = {}) {
|
|
function syncType(o = {}) {
|
|
|
const next = o.type || "leave";
|
|
const next = o.type || "leave";
|
|
|
if (type.value !== next) resetForm();
|
|
if (type.value !== next) resetForm();
|
|
@@ -134,6 +158,24 @@ function syncType(o = {}) {
|
|
|
uni.setNavigationBarTitle({ title: config.value.title });
|
|
uni.setNavigationBarTitle({ title: config.value.title });
|
|
|
const draft = getDraft(next);
|
|
const draft = getDraft(next);
|
|
|
if (draft) Object.assign(form, draft);
|
|
if (draft) Object.assign(form, draft);
|
|
|
|
|
+ // 草稿兼容:
|
|
|
|
|
+ // - 老草稿没有 startTime / endTime 字段 → 兜底默认值,避免 validate 误报
|
|
|
|
|
+ // - 老草稿字段叫 attachment(字符串)→ 已不在 form 上,忽略
|
|
|
|
|
+ // - 老格式(新字段 attachments)但值为字符串数组(重命名前的中间态)→ 清空让用户重选
|
|
|
|
|
+ // - 跨会话遗留的 uploading 状态 → 视为失败(upload 早已中断)
|
|
|
|
|
+ if (!form.startTime) form.startTime = "08:30";
|
|
|
|
|
+ if (!form.endTime) form.endTime = "17:30";
|
|
|
|
|
+ if (!Array.isArray(form.attachments)) {
|
|
|
|
|
+ form.attachments = [];
|
|
|
|
|
+ } else if (form.attachments.some((a) => typeof a === "string")) {
|
|
|
|
|
+ form.attachments = [];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ form.attachments = form.attachments.map((a) =>
|
|
|
|
|
+ a && a.status === "uploading"
|
|
|
|
|
+ ? { ...a, status: "failed", error: "上次会话未完成" }
|
|
|
|
|
+ : a,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
onLoad(syncType);
|
|
onLoad(syncType);
|
|
|
onShow(() => {
|
|
onShow(() => {
|
|
@@ -159,9 +201,11 @@ function resetForm() {
|
|
|
leaveType: "",
|
|
leaveType: "",
|
|
|
certificateType: "",
|
|
certificateType: "",
|
|
|
startDate: "2026-08-06",
|
|
startDate: "2026-08-06",
|
|
|
|
|
+ startTime: "08:30",
|
|
|
endDate: "2026-08-06",
|
|
endDate: "2026-08-06",
|
|
|
|
|
+ endTime: "17:30",
|
|
|
reason: "",
|
|
reason: "",
|
|
|
- attachment: "",
|
|
|
|
|
|
|
+ attachments: [],
|
|
|
repairType: "",
|
|
repairType: "",
|
|
|
repairTime: "",
|
|
repairTime: "",
|
|
|
});
|
|
});
|
|
@@ -189,8 +233,27 @@ function chooseRepairType() {
|
|
|
}
|
|
}
|
|
|
function chooseImage() {
|
|
function chooseImage() {
|
|
|
uni.chooseImage({
|
|
uni.chooseImage({
|
|
|
- count: 1,
|
|
|
|
|
- success: (r) => (form.attachment = r.tempFilePaths[0]),
|
|
|
|
|
|
|
+ count: 9,
|
|
|
|
|
+ success: (r) => {
|
|
|
|
|
+ const paths = (r.tempFilePaths || []).filter(Boolean);
|
|
|
|
|
+ if (!paths.length) return;
|
|
|
|
|
+ // 立即把每张图作为 uploading 项加入列表,触发并行上传,
|
|
|
|
|
+ // 状态变化(success/failed)由 Promise 回调直接写回对应条目。
|
|
|
|
|
+ const items = paths.map((p) => ({ localPath: p, status: "uploading" }));
|
|
|
|
|
+ form.attachments = [...form.attachments, ...items];
|
|
|
|
|
+ items.forEach((item) => {
|
|
|
|
|
+ uploadAttachment(item.localPath, "hr_attendance")
|
|
|
|
|
+ .then((file) => {
|
|
|
|
|
+ item.status = "success";
|
|
|
|
|
+ item.id = String(file?.id ?? "");
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((e) => {
|
|
|
|
|
+ item.status = "failed";
|
|
|
|
|
+ item.error = e?.message || "上传失败";
|
|
|
|
|
+ uni.showToast({ title: item.error, icon: "none" });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
function saveDraft() {
|
|
function saveDraft() {
|
|
@@ -207,8 +270,15 @@ function validate() {
|
|
|
if (!form.repairTime) return "请选择打卡时间";
|
|
if (!form.repairTime) return "请选择打卡时间";
|
|
|
if (!form.reason) return "请填写补卡原因";
|
|
if (!form.reason) return "请填写补卡原因";
|
|
|
}
|
|
}
|
|
|
- if (!["certificate", "check"].includes(type.value) && form.endDate < form.startDate)
|
|
|
|
|
- return "结束日期不能早于开始日期";
|
|
|
|
|
|
|
+ if (!["certificate", "check"].includes(type.value)) {
|
|
|
|
|
+ if (!form.startDate) return "请选择开始日期";
|
|
|
|
|
+ if (!form.endDate) return "请选择结束日期";
|
|
|
|
|
+ if (!form.startTime) return "请选择开始时间";
|
|
|
|
|
+ if (!form.endTime) return "请选择结束时间";
|
|
|
|
|
+ if (form.endDate < form.startDate) return "结束日期不能早于开始日期";
|
|
|
|
|
+ if (form.endDate === form.startDate && form.endTime <= form.startTime)
|
|
|
|
|
+ return "结束时间不能早于或等于开始时间";
|
|
|
|
|
+ }
|
|
|
if (!form.reason && type.value === "certificate") return "";
|
|
if (!form.reason && type.value === "certificate") return "";
|
|
|
if (!form.reason && !["certificate", "check"].includes(type.value))
|
|
if (!form.reason && !["certificate", "check"].includes(type.value))
|
|
|
return "请填写申请事由";
|
|
return "请填写申请事由";
|
|
@@ -218,25 +288,72 @@ async function submit() {
|
|
|
const error = validate();
|
|
const error = validate();
|
|
|
if (error) return uni.showToast({ title: error, icon: "none" });
|
|
if (error) return uni.showToast({ title: error, icon: "none" });
|
|
|
|
|
|
|
|
- // 补卡申请:服务器模式先调 POST /hr/attendance/applications
|
|
|
|
|
- if (type.value === "check" && getServerConfig().mode === "server") {
|
|
|
|
|
|
|
+ // 服务器模式 + 考勤类申请(请假 / 加班 / 出差 / 外出 / 补卡 / 调班)
|
|
|
|
|
+ // → POST /hr/attendance/applications(附件已在 chooseImage 时上传完成)
|
|
|
|
|
+ if (
|
|
|
|
|
+ ATTENDANCE_APPLY_TYPE_MAP[type.value] &&
|
|
|
|
|
+ getServerConfig().mode === "server"
|
|
|
|
|
+ ) {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 等待所有进行中的上传完成(chooseImage 触发的后台任务)
|
|
|
|
|
+ const pending = form.attachments.filter(
|
|
|
|
|
+ (a) => a && a.status === "uploading",
|
|
|
|
|
+ );
|
|
|
|
|
+ if (pending.length) {
|
|
|
|
|
+ uni.showLoading({ title: `附件上传中(${pending.length})` });
|
|
|
|
|
+ await Promise.all(
|
|
|
|
|
+ pending.map((item) =>
|
|
|
|
|
+ uploadAttachment(item.localPath, "hr_attendance")
|
|
|
|
|
+ .then((file) => {
|
|
|
|
|
+ item.status = "success";
|
|
|
|
|
+ item.id = String(file?.id ?? "");
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((e) => {
|
|
|
|
|
+ item.status = "failed";
|
|
|
|
|
+ item.error = e?.message || "上传失败";
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 有失败的附件就拦截提交
|
|
|
|
|
+ const failed = form.attachments.filter((a) => a && a.status === "failed");
|
|
|
|
|
+ if (failed.length) {
|
|
|
|
|
+ return uni.showToast({
|
|
|
|
|
+ title: `有 ${failed.length} 张附件上传失败,请重新选择`,
|
|
|
|
|
+ icon: "none",
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const attachmentFileIds = form.attachments
|
|
|
|
|
+ .filter((a) => a && a.status === "success" && a.id)
|
|
|
|
|
+ .map((a) => a.id)
|
|
|
|
|
+ .join(",");
|
|
|
|
|
+
|
|
|
uni.showLoading({ title: "提交中..." });
|
|
uni.showLoading({ title: "提交中..." });
|
|
|
- const user = currentUser || {};
|
|
|
|
|
- const userId = (user && user.raw && user.raw.userId) || (user && user.id) || "";
|
|
|
|
|
- await submitAttendanceApplication({
|
|
|
|
|
- userId,
|
|
|
|
|
- userName: user.name,
|
|
|
|
|
- applyType: "CARD_REPAIR",
|
|
|
|
|
- startTime: combineDateTime(form.startDate, form.repairTime),
|
|
|
|
|
- repairPunchTime: combineDateTime(form.startDate, form.repairTime),
|
|
|
|
|
- repairType: form.repairType,
|
|
|
|
|
- reason: form.reason,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const payload = buildAttendancePayload(
|
|
|
|
|
+ form,
|
|
|
|
|
+ type.value,
|
|
|
|
|
+ currentUser,
|
|
|
|
|
+ attachmentFileIds,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!payload) {
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ return uni.showToast({
|
|
|
|
|
+ title: "申请类型不支持",
|
|
|
|
|
+ icon: "none",
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ await submitAttendanceApplication(payload);
|
|
|
uni.hideLoading();
|
|
uni.hideLoading();
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
uni.hideLoading();
|
|
uni.hideLoading();
|
|
|
- return uni.showToast({ title: e.message || "补卡提交失败", icon: "none" });
|
|
|
|
|
|
|
+ return uni.showToast({
|
|
|
|
|
+ title: e.message || "申请提交失败",
|
|
|
|
|
+ icon: "none",
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|