<template>
|
<view class="container">
|
<view class="header">
|
<uni-datetime-picker
|
type="date"
|
v-model="selectedDate"
|
@change="onDateChange"
|
/>
|
<button class="add-btn" type="primary" size="mini" @click="handleAddReport()">提交上报</button>
|
</view>
|
|
<!-- 上报列表 -->
|
<scroll-view scroll-y class="report-list">
|
<view v-if="reports.length === 0" class="empty-state">
|
<text>暂无上报记录</text>
|
</view>
|
<view v-for="item in reports" :key="item.id" class="report-card">
|
<view class="card-header">
|
<text class="project-name">{{ getProjectName(item.projectId) }}</text>
|
<text class="report-time">{{ item.time }}</text>
|
</view>
|
<view class="card-body">
|
<text class="report-content">{{ item.title }}</text>
|
</view>
|
<view class="card-footer">
|
<view class="status-tag" :class="item.status">{{ getStatusText(item.status) }}</view>
|
<view class="actions">
|
<button class="action-btn" size="mini" @click="handleEditReport(item)">查看</button>
|
<button
|
class="action-btn delete"
|
size="mini"
|
@click="handleReturn(item)"
|
:disabled="item.status !== 'PendingReview'"
|
>撤回</button>
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
|
<!-- 提交上报弹窗 (自定义实现) -->
|
<view v-if="showPopup" class="custom-popup-mask" @click="closePopup">
|
<view class="custom-popup-content" @click.stop>
|
<view class="popup-header">
|
<text>{{ reportTitle }}</text>
|
<uni-icons type="closeempty" size="24" @click="closePopup"></uni-icons>
|
</view>
|
|
<view class="form-container">
|
<view class="form-item">
|
<text class="label">选择项目 <text class="required" v-if="!isViewOnly">*</text></text>
|
<picker
|
@change="onProjectChange"
|
:value="projectIndex"
|
:range="allProjects"
|
range-key="name"
|
:disabled="isViewOnly"
|
>
|
<view class="picker-view" :class="{ disabled: isViewOnly }">
|
{{ projectIndex > -1 ? allProjects[projectIndex].name : '请选择项目' }}
|
</view>
|
</picker>
|
</view>
|
|
<view class="form-item">
|
<text class="label">上报内容 <text class="required" v-if="!isViewOnly">*</text></text>
|
<textarea
|
v-model="reportForm.content"
|
placeholder="请输入上报内容"
|
class="custom-textarea"
|
:class="{ disabled: isViewOnly }"
|
auto-height
|
:disabled="isViewOnly"
|
/>
|
</view>
|
|
<view class="form-item">
|
<text class="label">附件</text>
|
<view class="file-list" v-if="reportForm.fileUrl && reportForm.fileUrl.length > 0">
|
<view v-for="(file, index) in reportForm.fileUrl" :key="index" class="file-item">
|
<template v-if="isImage(file)">
|
<image :src="file" mode="aspectFill" class="thumbnail" @click="previewImage(file)"></image>
|
<view class="delete-icon" @click="removeFile(index)" v-if="!isViewOnly">
|
<uni-icons type="closeempty" size="14" color="#fff"></uni-icons>
|
</view>
|
</template>
|
<template v-else>
|
<view class="file-info" @click="downloadFile(file)">
|
<uni-icons type="images" size="40" color="#999"></uni-icons>
|
</view>
|
<view class="delete-icon" @click="removeFile(index)" v-if="!isViewOnly">
|
<uni-icons type="closeempty" size="14" color="#fff"></uni-icons>
|
</view>
|
</template>
|
</view>
|
<view class="upload-btn-mini" @click="handleUpload" v-if="!isViewOnly">
|
<uni-icons type="plusempty" size="24" color="#999"></uni-icons>
|
</view>
|
</view>
|
<view v-else-if="!isViewOnly" class="upload-btn" @click="handleUpload">
|
<uni-icons type="plusempty" size="20" color="#999"></uni-icons>
|
<text>上传附件</text>
|
</view>
|
<view v-else class="empty-files">
|
<text>暂无附件</text>
|
</view>
|
</view>
|
</view>
|
|
<view class="popup-footer" v-if="!isViewOnly">
|
<button class="submit-btn" type="primary" @click="submitReport">确 定</button>
|
</view>
|
</view>
|
</view>
|
|
<BottomTabBar active="report" />
|
</view>
|
</template>
|
|
<script>
|
import { addReport, reportByDate, delReport, uploadFile } from "@/api/report/report";
|
import { getProjectSelectList } from "@/api/index";
|
import { getDayStartAndEnd, formatCalendarDate } from "@/utils/date.js";
|
import BottomTabBar from '@/components/BottomTabBar.vue'
|
|
export default {
|
components: { BottomTabBar },
|
data() {
|
return {
|
showPopup: false,
|
projectIndex: -1,
|
selectedDate: formatCalendarDate(new Date()),
|
reportTitle: "提交上报",
|
reports: [],
|
allProjects: [],
|
reportForm: {
|
id: '',
|
projectId: '',
|
content: '',
|
fileUrl: []
|
},
|
reportRules: {
|
projectId: {
|
rules: [{ required: true, errorMessage: '请选择项目' }]
|
},
|
content: {
|
rules: [{ required: true, errorMessage: '请输入上报内容' }]
|
}
|
}
|
};
|
},
|
computed: {
|
projectOptions() {
|
return this.allProjects.map(p => ({ value: p.id, text: p.name }));
|
},
|
isViewOnly() {
|
return this.reportTitle === '查看上报';
|
}
|
},
|
onLoad() {
|
this.initData();
|
},
|
methods: {
|
async initData() {
|
try {
|
const res = await getProjectSelectList();
|
if(res.statusCode === 200){
|
// 转换一下
|
this.allProjects = Object.entries(res.data.data).map(([id, name]) => ({
|
id: id, // 项目ID
|
name: name // 项目名称
|
}));
|
}
|
this.getReportList();
|
} catch (e) {
|
console.error(e);
|
this.allProjects = [];
|
}
|
},
|
async getReportList() {
|
try {
|
const { startTime, endTime } = getDayStartAndEnd(this.selectedDate);
|
const form = {
|
startTime,
|
endTime,
|
projectId: "all"
|
}
|
const res = await reportByDate(form);
|
if(res.statusCode === 200){
|
this.reports = res.data.data || [];
|
}
|
} catch (e) {
|
console.error(e);
|
this.reports = [];
|
}
|
},
|
onDateChange(val) {
|
this.selectedDate = val;
|
this.getReportList();
|
},
|
getProjectName(id) {
|
const project = this.allProjects.find(p => p.id == id);
|
return project ? project.name : '未知项目';
|
},
|
getStatusText(status) {
|
const map = {
|
'PendingReview': '待审核',
|
'Approved': '已通过',
|
'Rejected': '已拒绝'
|
};
|
return map[status] || status || '待审核';
|
},
|
onProjectChange(e) {
|
this.projectIndex = e.detail.value;
|
this.reportForm.projectId = this.allProjects[this.projectIndex].id;
|
},
|
handleUpload() {
|
// 使用 chooseMessageFile 支持从聊天记录选择文件(适用于微信小程序)
|
uni.chooseMessageFile({
|
count: 1,
|
type: 'all',
|
success: async (res) => {
|
const tempFilePath = res.tempFiles[0].path;
|
console.log('选择文件成功:', tempFilePath)
|
uni.showLoading({ title: '上传中...' });
|
try {
|
const uploadRes = await uploadFile(tempFilePath);
|
const data = uploadRes.data;
|
if (data.code === 200) {
|
this.reportForm.fileUrl.push(data.url);
|
uni.showToast({ title: '上传成功' });
|
} else {
|
uni.showToast({ title: data.msg || '上传失败', icon: 'none' });
|
}
|
} catch (e) {
|
console.error('上传失败详情:', e);
|
uni.showToast({ title: '上传失败', icon: 'none' });
|
} finally {
|
uni.hideLoading();
|
}
|
},
|
fail: (err) => {
|
console.error('选择文件失败:', err);
|
}
|
});
|
},
|
removeFile(index) {
|
this.reportForm.fileUrl.splice(index, 1);
|
},
|
isImage(url) {
|
if (!url) return false;
|
const ext = url.split('.').pop().toLowerCase();
|
return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext);
|
},
|
previewImage(url) {
|
const images = this.reportForm.fileUrl.filter(item => this.isImage(item));
|
uni.previewImage({
|
current: url,
|
urls: images
|
});
|
},
|
downloadFile(url) {
|
uni.showLoading({ title: '文件打开中...' });
|
|
// 获取文件后缀
|
const fileExt = url.split('.').pop().toLowerCase();
|
|
uni.downloadFile({
|
url: url,
|
success: (res) => {
|
if (res.statusCode === 200) {
|
const filePath = res.tempFilePath;
|
uni.openDocument({
|
filePath: filePath,
|
fileType: fileExt, // 明确指定文件类型
|
showMenu: true,
|
success: () => {
|
console.log('打开文档成功');
|
},
|
fail: (err) => {
|
uni.showToast({ title: '打开失败,请检查手机是否安装相关应用', icon: 'none' });
|
console.error('打开文档失败:', err);
|
}
|
});
|
}
|
},
|
fail: (err) => {
|
uni.showToast({ title: '下载失败', icon: 'none' });
|
console.error('下载文件失败:', err);
|
},
|
complete: () => {
|
uni.hideLoading();
|
}
|
});
|
},
|
handleAddReport() {
|
|
this.reportTitle = "提交上报";
|
this.resetReportForm();
|
this.showPopup = true;
|
},
|
handleEditReport(row) {
|
this.reportTitle = "查看上报";
|
this.reportForm = {
|
id: row.id,
|
projectId: row.projectId,
|
content: row.title,
|
fileUrl: row.fileUrl || []
|
};
|
this.projectIndex = this.allProjects.findIndex(p => p.id == row.projectId);
|
|
// 处理文件显示
|
if (typeof row.fileUrl === 'string' && row.fileUrl) {
|
this.reportForm.fileUrl = row.fileUrl.split(',');
|
} else {
|
this.reportForm.fileUrl = Array.isArray(row.fileUrl) ? row.fileUrl : [];
|
}
|
|
this.showPopup = true;
|
},
|
async handleReturn(row) {
|
uni.showModal({
|
title: '提示',
|
content: '确定要撤回该上报吗?',
|
success: async (res) => {
|
if (res.confirm) {
|
try {
|
const result = await delReport(row.id);
|
if (result.statusCode === 200) {
|
uni.showToast({ title: '撤回成功' });
|
this.getReportList();
|
}
|
} catch (e) {
|
console.error(e);
|
}
|
}
|
}
|
});
|
},
|
closePopup() {
|
this.showPopup = false;
|
},
|
resetReportForm() {
|
this.reportForm = {
|
id: '',
|
projectId: '',
|
content: '',
|
fileUrl: []
|
};
|
this.projectIndex = -1;
|
},
|
async submitReport() {
|
if (!this.reportForm.projectId) {
|
uni.showToast({ title: '请选择项目', icon: 'none' });
|
return;
|
}
|
if (!this.reportForm.content) {
|
uni.showToast({ title: '请输入上报内容', icon: 'none' });
|
return;
|
}
|
try {
|
const matchItem = this.allProjects.find(item => item.id == this.reportForm.projectId);
|
|
const data = {
|
projectId: Number(this.reportForm.projectId),
|
content: this.reportForm.content,
|
projectName:matchItem.name,
|
fileUrl: this.reportForm.fileUrl,
|
};
|
console.log(data)
|
const result = await addReport(data);
|
if (result.statusCode === 200) {
|
uni.showToast({ title: '上报成功' });
|
this.closePopup();
|
this.getReportList();
|
}
|
} catch (e) {
|
console.error(e);
|
}
|
}
|
}
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.container {
|
background-color: #f5f7fa;
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
box-sizing: border-box;
|
padding: 20rpx;
|
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
}
|
|
.header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20rpx;
|
padding: 20rpx;
|
background-color: #fff;
|
border-radius: 12rpx;
|
flex-shrink: 0;
|
|
.title {
|
font-size: 32rpx;
|
font-weight: bold;
|
color: #333;
|
}
|
|
.add-btn {
|
margin: 0;
|
background-color: #2979ff;
|
flex-shrink: 0;
|
margin-left: 20rpx;
|
}
|
|
::v-deep .uni-date-editor {
|
flex: 1;
|
.uni-date-x {
|
height: 70rpx;
|
border-color: #dcdfe6;
|
border-radius: 8rpx;
|
background-color: #fff;
|
}
|
.uni-date-x--border {
|
border-color: #dcdfe6;
|
}
|
.uni-date-input {
|
font-size: 26rpx;
|
color: #333;
|
}
|
}
|
}
|
|
.report-list {
|
flex: 1;
|
overflow: hidden;
|
}
|
|
.report-card {
|
background-color: #fff;
|
border-radius: 12rpx;
|
padding: 24rpx;
|
margin-bottom: 20rpx;
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 16rpx;
|
|
.project-name {
|
font-size: 28rpx;
|
font-weight: bold;
|
color: #333;
|
}
|
|
.report-time {
|
font-size: 24rpx;
|
color: #999;
|
}
|
}
|
|
.card-body {
|
margin-bottom: 20rpx;
|
|
.report-content {
|
font-size: 26rpx;
|
color: #666;
|
line-height: 1.5;
|
}
|
}
|
|
.card-footer {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
border-top: 1rpx solid #eee;
|
padding-top: 16rpx;
|
|
.status-tag {
|
font-size: 22rpx;
|
padding: 4rpx 12rpx;
|
border-radius: 4rpx;
|
background-color: #f0f0f0;
|
color: #999;
|
|
&.PendingReview {
|
background-color: #e3f2fd;
|
color: #2196f3;
|
}
|
&.Approved {
|
background-color: #e8f5e9;
|
color: #4caf50;
|
}
|
&.Rejected {
|
background-color: #ffebee;
|
color: #f44336;
|
}
|
}
|
|
.actions {
|
display: flex;
|
gap: 12rpx;
|
|
.action-btn {
|
margin: 0;
|
font-size: 22rpx;
|
background-color: #f5f7fa;
|
color: #666;
|
border: 1rpx solid #dcdfe6;
|
|
&.delete {
|
color: #f56c6c;
|
border-color: #fab6b6;
|
}
|
}
|
}
|
}
|
}
|
|
.empty-state {
|
text-align: center;
|
padding: 100rpx 0;
|
color: #999;
|
font-size: 28rpx;
|
}
|
|
.custom-popup-mask {
|
position: fixed;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background-color: rgba(0, 0, 0, 0.5);
|
z-index: 1000;
|
display: flex;
|
flex-direction: column;
|
justify-content: flex-end;
|
}
|
|
.custom-popup-content {
|
background-color: #fff;
|
border-radius: 24rpx 24rpx 0 0;
|
padding: 30rpx;
|
animation: slideUp 0.3s ease-out;
|
|
.popup-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 30rpx;
|
font-size: 32rpx;
|
font-weight: bold;
|
color: #333;
|
}
|
}
|
|
@keyframes slideUp {
|
from { transform: translateY(100%); }
|
to { transform: translateY(0); }
|
}
|
|
.form-container {
|
.form-item {
|
margin-bottom: 30rpx;
|
|
.label {
|
display: block;
|
font-size: 28rpx;
|
color: #666;
|
margin-bottom: 12rpx;
|
|
.required {
|
color: #f56c6c;
|
margin-left: 4rpx;
|
}
|
}
|
|
.picker-view {
|
height: 80rpx;
|
line-height: 80rpx;
|
border: 1rpx solid #dcdfe6;
|
border-radius: 8rpx;
|
padding: 0 20rpx;
|
font-size: 28rpx;
|
color: #333;
|
background-color: #fff;
|
|
&.disabled {
|
background-color: #f5f7fa;
|
color: #999;
|
}
|
}
|
|
.custom-textarea {
|
width: 100%;
|
min-height: 160rpx;
|
border: 1rpx solid #dcdfe6;
|
border-radius: 8rpx;
|
padding: 20rpx;
|
font-size: 28rpx;
|
color: #333;
|
box-sizing: border-box;
|
|
&.disabled {
|
background-color: #f5f7fa;
|
color: #999;
|
}
|
}
|
}
|
}
|
|
.empty-files {
|
padding: 40rpx 0;
|
text-align: center;
|
color: #999;
|
font-size: 24rpx;
|
background-color: #f8f8f8;
|
border-radius: 8rpx;
|
}
|
|
.popup-footer {
|
margin-top: 40rpx;
|
padding-bottom: env(safe-area-inset-bottom);
|
.submit-btn {
|
width: 100%;
|
background-color: #2979ff;
|
height: 88rpx;
|
line-height: 88rpx;
|
border-radius: 44rpx;
|
font-size: 30rpx;
|
}
|
}
|
|
.file-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 20rpx;
|
margin-bottom: 20rpx;
|
|
.file-item {
|
position: relative;
|
width: 160rpx;
|
height: 160rpx;
|
background-color: #f8f8f8;
|
border-radius: 8rpx;
|
overflow: hidden;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.thumbnail {
|
width: 100%;
|
height: 100%;
|
}
|
|
.delete-icon {
|
position: absolute;
|
top: 0;
|
right: 0;
|
background-color: rgba(0, 0, 0, 0.5);
|
width: 36rpx;
|
height: 36rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-bottom-left-radius: 8rpx;
|
}
|
|
.file-info {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
width: 100%;
|
height: 100%;
|
}
|
|
& > uni-icons[type="closeempty"] {
|
position: absolute;
|
top: 4rpx;
|
right: 4rpx;
|
background: rgba(255, 255, 255, 0.8);
|
border-radius: 50%;
|
}
|
}
|
}
|
|
.upload-btn-mini {
|
width: 160rpx;
|
height: 160rpx;
|
background-color: #fbfbfb;
|
border: 1rpx dashed #dcdfe6;
|
border-radius: 8rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
&:active {
|
background-color: #f0f0f0;
|
}
|
}
|
|
.upload-btn {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
background-color: #fbfbfb;
|
border: 1rpx dashed #dcdfe6;
|
border-radius: 12rpx;
|
padding: 40rpx 0;
|
color: #999;
|
font-size: 26rpx;
|
gap: 12rpx;
|
transition: all 0.3s;
|
|
&:active {
|
background-color: #f0f0f0;
|
}
|
}
|
|
.file-placeholder {
|
background-color: #f8f8f8;
|
padding: 40rpx;
|
border: 1rpx dashed #dcdfe6;
|
text-align: center;
|
color: #999;
|
font-size: 24rpx;
|
border-radius: 8rpx;
|
}
|
</style>
|