feat: work-report module V1 — four report types with review flow
客户 2026-07-18 语音需求落地:「报」分四种——优秀案例/操作视频/
完整SOP/有用知识,员工提交、主管或运营审核。
后端(新包 org.dromara.aihr.report):
- aihr_work_report 表(自建,utf8mb4_general_ci 显式声明,含提交人/
审核人/状态/附件 OSS 编号,提交人与状态双索引)
- POST /api/aihr/work-report/reports 员工提交(类型/标题/说明校验)
- POST /attachment 附件上传(≤100MB,OSS 落盘,ossId 字符串安全)
- GET /reports/mine 我的上报;GET /reports 审核队列(canModerate 门禁,
复用社区 Policy);POST /{id}/review APPROVE/REJECT
(仅 PENDING 可审,重复审核 400)
- 审核通过后的入库动作留给运营在案例库/知识库人工完成(不自动入库,
遵守'入知识库必须人工确认'原则)
员工端:
- 新页 pages/user/report(替换'建设中'占位):四类型卡片选择、
标题+说明(说明支持录音转写)、附件选择上传(视频类必传校验)、
我的上报列表(待审核/已通过/已驳回+审核意见)
- 「我」页常用功能入口文案更新
生产验证(端到端):
- API:员工提交→我的上报可见→员工查审核队列403→主管队列可见
→审核通过→重复审核400 全通过
- UI:我页入口→上报页→选类型→填表→提交→列表显示'待审核',
历史记录显示'已通过'+审核意见
- 后端发布(备份 ruoyi-admin.jar.bak-20260718123032,25s恢复),
H5 已发布(备份 h5-before-20260718123032)
二期待办:主管端待审列表页(现主管可用接口审核)、通过后一键转
案例库/知识库、上报采纳积分奖励(等客户定规则)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,12 @@
|
||||
"navigationBarTitleText": "我的积分"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/report/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作上报"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/exam/index",
|
||||
"style": {
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
<view class="feature-entry-icon report">报</view>
|
||||
<view class="feature-entry-copy">
|
||||
<text class="feature-entry-title">工作上报</text>
|
||||
<text class="feature-entry-desc">日报 / 任务 / 问题上报 · 建设中</text>
|
||||
<text class="feature-entry-desc">上报案例 / 视频 / SOP / 知识</text>
|
||||
</view>
|
||||
<text class="points-entry-arrow">›</text>
|
||||
</button>
|
||||
@@ -298,7 +298,7 @@ const openPracticeSummary = () => {
|
||||
};
|
||||
|
||||
const openReport = () => {
|
||||
uni.navigateTo({ url: `/pages/common/building/index?title=${encodeURIComponent('工作上报')}` });
|
||||
uni.navigateTo({ url: '/pages/user/report/index' });
|
||||
};
|
||||
|
||||
const clearProfileData = () => {
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
<template>
|
||||
<view class="page report-page employee-page-shell">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" aria-label="返回" @click="goBack">←</button>
|
||||
<view class="subpage-heading">
|
||||
<text class="employee-kicker">经验沉淀 · 审核后入库</text>
|
||||
<text class="page-title">工作上报</text>
|
||||
<text class="page-subtitle">把现场的好经验报上来,审核通过后沉淀进企业知识。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!loggedIn" class="panel employee-card login-panel">
|
||||
<view>
|
||||
<text class="panel-title">需要登录</text>
|
||||
<text class="muted block">登录后才能提交上报并查看审核进度。</text>
|
||||
</view>
|
||||
<button class="primary-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="panel employee-card type-panel">
|
||||
<text class="panel-title">选择上报类型</text>
|
||||
<view class="type-grid">
|
||||
<button
|
||||
v-for="item in workReportTypes"
|
||||
:key="item.value"
|
||||
class="type-tile"
|
||||
:class="{ active: form.type === item.value }"
|
||||
@click="form.type = item.value"
|
||||
>
|
||||
<text class="type-icon">{{ item.icon }}</text>
|
||||
<text class="type-label">{{ item.label }}</text>
|
||||
<text class="type-desc">{{ item.desc }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel employee-card form-panel">
|
||||
<text class="field-label">标题</text>
|
||||
<input v-model="form.title" class="input" maxlength="100" placeholder="一句话说清上报内容" />
|
||||
|
||||
<text class="field-label">说明</text>
|
||||
<textarea
|
||||
v-model="form.content"
|
||||
class="textarea report-textarea"
|
||||
maxlength="2000"
|
||||
:placeholder="contentPlaceholder"
|
||||
/>
|
||||
<view class="voice-row">
|
||||
<button
|
||||
class="secondary-button compact-button"
|
||||
:class="{ 'recording-active': recording }"
|
||||
:disabled="transcribing || submitting"
|
||||
@click="toggleRecording"
|
||||
>
|
||||
{{ recording ? '停止录音' : transcribing ? '转写中' : '语音转写' }}
|
||||
</button>
|
||||
<text class="voice-hint">说一段话自动转成文字,可修改后提交</text>
|
||||
</view>
|
||||
|
||||
<text class="field-label">附件{{ form.type === 'VIDEO' ? '(视频,必传)' : '(可选)' }}</text>
|
||||
<view v-if="attachment" class="media-file">
|
||||
<text class="media-name">{{ attachment.fileName }}</text>
|
||||
<button class="media-remove" :disabled="submitting" aria-label="移除附件" @click="attachment = null">×</button>
|
||||
</view>
|
||||
<button v-else class="ghost-button" :disabled="uploading || submitting" @click="pickAttachment">
|
||||
{{ uploading ? '上传中' : '+ 选择文件' }}
|
||||
</button>
|
||||
<text class="muted attach-hint">图片≤10MB,视频≤100MB;音频/文档也可作为附件</text>
|
||||
|
||||
<button class="primary-button submit-button" :disabled="submitting || uploading" @click="submit">
|
||||
{{ submitting ? '提交中' : '提交审核' }}
|
||||
</button>
|
||||
<text v-if="message" class="status-message">{{ message }}</text>
|
||||
</view>
|
||||
|
||||
<view class="panel employee-card mine-panel">
|
||||
<view class="panel-head-row">
|
||||
<text class="panel-title">我的上报</text>
|
||||
<button class="ghost-button compact-button" :disabled="loadingMine" @click="loadMine">刷新</button>
|
||||
</view>
|
||||
<view v-if="myReports.length" class="report-list">
|
||||
<view v-for="item in myReports" :key="item.id" class="report-row">
|
||||
<view class="report-copy">
|
||||
<view class="report-meta">
|
||||
<text class="report-type">{{ item.typeLabel }}</text>
|
||||
<text class="report-status" :class="item.status.toLowerCase()">{{ item.statusLabel }}</text>
|
||||
</view>
|
||||
<text class="report-title">{{ item.title }}</text>
|
||||
<text class="report-desc">{{ item.createdAt }}{{ item.attachmentName ? ` · 📎 ${item.attachmentName}` : '' }}</text>
|
||||
<text v-if="item.reviewNote" class="report-note">审核意见:{{ item.reviewNote }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!loadingMine" class="empty">还没有上报记录,报一条试试</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onHide, onShow, onUnload } from '@dcloudio/uni-app';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { chooseKnowledgeMedia } from '@/services/knowledge';
|
||||
import { transcribeSpeechBlob } from '@/services/speech';
|
||||
import {
|
||||
listMyWorkReports,
|
||||
submitWorkReport,
|
||||
uploadWorkReportAttachment,
|
||||
workReportTypes,
|
||||
type WorkReportAttachment,
|
||||
type WorkReportRecord,
|
||||
type WorkReportType
|
||||
} from '@/services/work-report';
|
||||
|
||||
const path = '/pages/user/report/index';
|
||||
const loggedIn = ref(false);
|
||||
const form = ref<{ type: WorkReportType; title: string; content: string }>({ type: 'CASE', title: '', content: '' });
|
||||
const attachment = ref<WorkReportAttachment | null>(null);
|
||||
const myReports = ref<WorkReportRecord[]>([]);
|
||||
const message = ref('');
|
||||
const submitting = ref(false);
|
||||
const uploading = ref(false);
|
||||
const loadingMine = ref(false);
|
||||
const recording = ref(false);
|
||||
const transcribing = ref(false);
|
||||
let mediaRecorder: MediaRecorder | null = null;
|
||||
let mediaStream: MediaStream | null = null;
|
||||
let transcribeGeneration = 0;
|
||||
|
||||
const contentPlaceholder = computed(() => {
|
||||
if (form.value.type === 'CASE') return '写清楚:什么场景、你怎么处理的、结果如何。';
|
||||
if (form.value.type === 'VIDEO') return '说明视频里演示的是什么操作、适用什么场景。';
|
||||
if (form.value.type === 'SOP') return '贴出完整流程步骤,或说明附件里 SOP 的适用范围。';
|
||||
return '写下你觉得对同事有帮助的经验或知识点。';
|
||||
});
|
||||
|
||||
const goBack = () => uni.navigateBack({ fail: () => uni.switchTab({ url: '/pages/user/profile/index' }) });
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect(path);
|
||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
const loadMine = async () => {
|
||||
if (!loggedIn.value || loadingMine.value) return;
|
||||
loadingMine.value = true;
|
||||
try {
|
||||
myReports.value = await listMyWorkReports();
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '上报记录加载失败';
|
||||
} finally {
|
||||
loadingMine.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const pickAttachment = async () => {
|
||||
if (uploading.value) return;
|
||||
try {
|
||||
const media = await chooseKnowledgeMedia();
|
||||
uploading.value = true;
|
||||
attachment.value = await uploadWorkReportAttachment(media.path);
|
||||
message.value = '';
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : '附件选择失败';
|
||||
if (text !== '未选择附件') message.value = text;
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const stopTracks = () => {
|
||||
mediaStream?.getTracks().forEach((track) => track.stop());
|
||||
mediaStream = null;
|
||||
};
|
||||
|
||||
const toggleRecording = async () => {
|
||||
if (transcribing.value) return;
|
||||
if (recording.value) {
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
|
||||
return;
|
||||
}
|
||||
if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
|
||||
message.value = '当前环境不支持录音,请直接输入文字';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaStream = stream;
|
||||
const recorder = new MediaRecorder(stream);
|
||||
const chunks: Blob[] = [];
|
||||
mediaRecorder = recorder;
|
||||
recorder.ondataavailable = (event) => {
|
||||
if (event.data?.size) chunks.push(event.data);
|
||||
};
|
||||
recorder.onstop = () => {
|
||||
recording.value = false;
|
||||
mediaRecorder = null;
|
||||
stopTracks();
|
||||
const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' });
|
||||
if (blob.size) void transcribe(blob);
|
||||
};
|
||||
recorder.start();
|
||||
recording.value = true;
|
||||
message.value = '正在录音,说完再点一次停止';
|
||||
} catch {
|
||||
stopTracks();
|
||||
message.value = '麦克风不可用,请直接输入文字';
|
||||
}
|
||||
};
|
||||
|
||||
const transcribe = async (blob: Blob) => {
|
||||
const generation = ++transcribeGeneration;
|
||||
transcribing.value = true;
|
||||
message.value = '语音转写中';
|
||||
try {
|
||||
const result = await transcribeSpeechBlob(blob, 'work-report.webm');
|
||||
if (generation !== transcribeGeneration) return;
|
||||
const text = (result.text || '').trim();
|
||||
if (!text) throw new Error('语音没有识别到文字');
|
||||
const current = form.value.content.trim();
|
||||
form.value.content = current ? `${current}\n${text}` : text;
|
||||
message.value = '语音已转成文字,请确认后提交';
|
||||
} catch (error) {
|
||||
if (generation !== transcribeGeneration) return;
|
||||
message.value = error instanceof Error ? error.message : '语音转写失败,请直接输入文字';
|
||||
} finally {
|
||||
if (generation === transcribeGeneration) transcribing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelRecording = () => {
|
||||
transcribeGeneration += 1;
|
||||
transcribing.value = false;
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.ondataavailable = null;
|
||||
mediaRecorder.onstop = null;
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
mediaRecorder = null;
|
||||
stopTracks();
|
||||
recording.value = false;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (submitting.value) return;
|
||||
const title = form.value.title.trim();
|
||||
const content = form.value.content.trim();
|
||||
if (!title) {
|
||||
message.value = '请填写标题';
|
||||
return;
|
||||
}
|
||||
if (!content) {
|
||||
message.value = '请填写说明';
|
||||
return;
|
||||
}
|
||||
if (form.value.type === 'VIDEO' && !attachment.value) {
|
||||
message.value = '操作视频上报需要上传视频附件';
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
await submitWorkReport({
|
||||
type: form.value.type,
|
||||
title,
|
||||
content,
|
||||
attachmentOssId: attachment.value?.ossId,
|
||||
attachmentName: attachment.value?.fileName
|
||||
});
|
||||
form.value.title = '';
|
||||
form.value.content = '';
|
||||
attachment.value = null;
|
||||
uni.showToast({ title: '已提交,等待审核', icon: 'none' });
|
||||
await loadMine();
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '提交失败,请稍后重试';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
loggedIn.value = isLoggedIn();
|
||||
if (loggedIn.value) void loadMine();
|
||||
});
|
||||
|
||||
onHide(cancelRecording);
|
||||
onUnload(cancelRecording);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.report-page {
|
||||
max-width: var(--mobile-shell-max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.type-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.type-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
text-align: left;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.type-tile.active {
|
||||
border-color: var(--brand);
|
||||
background: var(--employee-primary-soft);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.type-label {
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.type-desc {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.report-textarea {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.voice-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.voice-hint {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.media-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.media-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.media-remove {
|
||||
flex-shrink: 0;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
background: #eef0f3;
|
||||
color: #6b7480;
|
||||
font-size: 15px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.attach-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff0f1;
|
||||
color: #a9000d;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mine-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-head-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.report-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.report-row {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.report-type {
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--brand);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.report-status {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.report-status.pending {
|
||||
color: #d98200;
|
||||
}
|
||||
|
||||
.report-status.approved {
|
||||
color: #168243;
|
||||
}
|
||||
|
||||
.report-status.rejected {
|
||||
color: #bd0010;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
display: block;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.report-desc {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-note {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #78500a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 12px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ApiPayload } from '@/types/api';
|
||||
import { apiRequest, apiUrl, authHeaders, readTextPayload } from './api';
|
||||
|
||||
export type WorkReportType = 'CASE' | 'VIDEO' | 'SOP' | 'KNOWLEDGE';
|
||||
|
||||
export interface WorkReportRecord {
|
||||
id: number;
|
||||
type: WorkReportType;
|
||||
typeLabel: string;
|
||||
title: string;
|
||||
content: string;
|
||||
attachmentName?: string;
|
||||
attachmentOssId?: number | string;
|
||||
status: 'PENDING' | 'APPROVED' | 'REJECTED';
|
||||
statusLabel: string;
|
||||
submitterName: string;
|
||||
reviewNote?: string;
|
||||
createdAt: string;
|
||||
reviewedAt?: string;
|
||||
}
|
||||
|
||||
export interface WorkReportAttachment {
|
||||
ossId: number | string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export const workReportTypes: Array<{ value: WorkReportType; label: string; desc: string; icon: string }> = [
|
||||
{ value: 'CASE', label: '优秀案例', desc: '现场处理得好的真实案例', icon: '⭐' },
|
||||
{ value: 'VIDEO', label: '操作视频', desc: '值得推广的操作演示视频', icon: '🎬' },
|
||||
{ value: 'SOP', label: '完整SOP', desc: '整理好的完整工作流程', icon: '📋' },
|
||||
{ value: 'KNOWLEDGE', label: '有用知识', desc: '对工作有帮助的经验知识', icon: '💡' }
|
||||
];
|
||||
|
||||
export const submitWorkReport = (params: {
|
||||
type: WorkReportType;
|
||||
title: string;
|
||||
content: string;
|
||||
attachmentOssId?: number | string;
|
||||
attachmentName?: string;
|
||||
}) =>
|
||||
apiRequest<WorkReportRecord>({
|
||||
url: '/api/aihr/work-report/reports',
|
||||
method: 'POST',
|
||||
data: params,
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
export const listMyWorkReports = () =>
|
||||
apiRequest<WorkReportRecord[]>({
|
||||
url: '/api/aihr/work-report/reports/mine',
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const listReviewWorkReports = (status = '') =>
|
||||
apiRequest<WorkReportRecord[]>({
|
||||
url: `/api/aihr/work-report/reports${status ? `?status=${encodeURIComponent(status)}` : ''}`,
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const reviewWorkReport = (id: number, decision: 'APPROVE' | 'REJECT', note = '') =>
|
||||
apiRequest<WorkReportRecord>({
|
||||
url: `/api/aihr/work-report/reports/${id}/review`,
|
||||
method: 'POST',
|
||||
data: { decision, note },
|
||||
timeout: 20000
|
||||
});
|
||||
|
||||
export const uploadWorkReportAttachment = (filePath: string) =>
|
||||
new Promise<WorkReportAttachment>((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: apiUrl('/api/aihr/work-report/attachment'),
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: authHeaders(false),
|
||||
success: (response) => {
|
||||
try {
|
||||
resolve(readTextPayload<WorkReportAttachment>(response.statusCode, response.data));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: () => reject(new Error('附件上传失败'))
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user