Files
yunzerwebsiteallinone/uniapp/pages/oa/dailyreport/edit.vue
T
2026-10-22 00:27:26 +08:00

471 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<scroll-view class="body" scroll-y :show-scrollbar="false">
<view class="form-card">
<view class="date-row">
<text class="label">日报日期</text>
<picker
mode="date"
:value="reportDate"
:start="profile.window_start || ''"
:end="profile.window_end || today"
@change="onDateChange"
>
<view class="date-chip">{{ reportDate }}</view>
</picker>
<text class="date-tip">可补写最近 7 天</text>
</view>
<view class="field">
<text class="label">今日完成</text>
<textarea
v-model="form.todayDone"
class="textarea"
maxlength="5000"
placeholder="今天完成了哪些工作?可按条列出"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">明日计划</text>
<textarea
v-model="form.tomorrowPlan"
class="textarea"
maxlength="5000"
placeholder="明天计划推进哪些事项?"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">问题与建议</text>
<textarea
v-model="form.issues"
class="textarea is-short"
maxlength="5000"
placeholder="遇到的问题、需要的支持或改进建议(选填)"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">附件</text>
<view class="attachment-list">
<view
v-for="(file, index) in form.attachments"
:key="file.url || index"
class="attachment-item"
>
<text class="attachment-name">{{ file.name || '附件' }}</text>
<text class="remove" @tap="removeAttachment(index)">移除</text>
</view>
<view
v-if="form.attachments.length < 9"
class="upload-btn"
@tap="chooseImages"
>
{{ uploading ? '上传中...' : '+ 添加附件' }}
</view>
<text class="upload-tip">支持图片,单个不超过 10MB</text>
</view>
</view>
</view>
<view v-if="loading" class="state-box">
<text class="state-text">加载中...</text>
</view>
</scroll-view>
<view class="footer">
<button class="btn ghost" :disabled="saving" @tap="save(false)">保存草稿</button>
<button class="btn primary" :disabled="saving" @tap="save(true)">提交日报</button>
</view>
</view>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import {
fetchDailyReportProfile,
fetchDailyReportDetail,
fetchMyDailyReports,
saveDailyReport,
uploadDailyReportFile,
todayString
} from '@/api/oaDailyReport.js'
const today = todayString()
const loading = ref(false)
const saving = ref(false)
const uploading = ref(false)
const reportId = ref(0)
const reportDate = ref(today)
const profile = ref({ window_start: '', window_end: '' })
const form = reactive({
todayDone: '',
tomorrowPlan: '',
issues: '',
attachments: []
})
onLoad(async (options = {}) => {
reportDate.value = options.date || today
if (options.id) reportId.value = Number(options.id)
await loadProfile()
if (reportId.value) {
await loadDetail(reportId.value)
} else {
await loadByDate(reportDate.value, false)
}
})
async function loadProfile() {
try {
const data = await fetchDailyReportProfile()
if (data) profile.value = { ...profile.value, ...data }
} catch (e) {
// 窗口信息失败时仍允许按今日编辑
}
}
async function loadDetail(id) {
loading.value = true
try {
const data = await fetchDailyReportDetail(id)
applyDetail(data)
} catch (e) {
uni.showToast({ title: e?.message || '日报加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function applyDetail(data) {
const report = data?.report
if (!report) return
// 已提交的日报已锁定:不进入编辑,引导到详情中撤销提交
if (report.status === 1) {
uni.showModal({
title: '日报已提交',
content: '已提交的日报已锁定,如需修改请先在详情中撤销提交',
showCancel: false,
success: () => {
uni.redirectTo({ url: `/pages/oa/dailyreport/detail?id=${report.id}` })
}
})
return
}
reportId.value = report.id
reportDate.value = report.report_date
form.todayDone = report.today_done || ''
form.tomorrowPlan = report.tomorrow_plan || ''
form.issues = report.issues || ''
form.attachments = Array.isArray(report.attachments)
? report.attachments.map((item) => ({ ...item }))
: []
}
/** 按日期查找已有日报;载入后可直接编辑 */
async function loadByDate(date, notify = true) {
const data = await fetchMyDailyReports({ page: 1, pageSize: 1, startDate: date, endDate: date })
const item = data?.list?.[0]
if (!item) {
reportId.value = 0
return false
}
if (notify) {
const confirmed = await confirmModal('该日期已有日报', '是否载入已有内容?')
if (!confirmed) return false
}
await loadDetail(item.id)
return true
}
async function onDateChange(e) {
const next = e.detail.value
if (!next || next === reportDate.value) return
const existing = await fetchMyDailyReports({
page: 1,
pageSize: 1,
startDate: next,
endDate: next
})
const item = existing?.list?.[0]
if (item) {
const confirmed = await confirmModal(
'该日期已有日报',
'是否载入该日期已有内容?当前未保存的内容将被覆盖。'
)
if (!confirmed) return
reportDate.value = next
await loadDetail(item.id)
return
}
if (hasContent()) {
const confirmed = await confirmModal(
'该日期暂无日报',
'切换日期会清空当前内容,是否继续?'
)
if (!confirmed) return
}
reportDate.value = next
reportId.value = 0
form.todayDone = ''
form.tomorrowPlan = ''
form.issues = ''
form.attachments = []
}
function hasContent() {
return Boolean(
form.todayDone.trim() ||
form.tomorrowPlan.trim() ||
form.issues.trim() ||
form.attachments.length
)
}
function removeAttachment(index) {
form.attachments.splice(index, 1)
}
async function chooseImages() {
const remain = Math.max(1, 9 - form.attachments.length)
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
uploading.value = true
const paths = res.tempFilePaths || []
const files = res.tempFiles || []
for (let i = 0; i < paths.length; i += 1) {
try {
const data = await uploadDailyReportFile(paths[i])
form.attachments.push({
id: data.id || 0,
url: data.url,
name: data.name || '附件',
size: files[i]?.size || 0
})
} catch (e) {
// 上传失败已提示,继续处理其余文件
}
}
uploading.value = false
}
})
}
async function save(submit) {
if (!hasContent()) {
uni.showToast({ title: '请至少填写一项内容', icon: 'none' })
return
}
saving.value = true
try {
await saveDailyReport({
date: reportDate.value,
todayDone: form.todayDone,
tomorrowPlan: form.tomorrowPlan,
issues: form.issues,
attachments: form.attachments,
submit
})
uni.showToast({ title: submit ? '日报已提交' : '草稿已保存', icon: 'success' })
setTimeout(() => uni.navigateBack(), 500)
} catch (e) {
uni.showToast({ title: e?.message || '保存失败', icon: 'none' })
} finally {
saving.value = false
}
}
function confirmModal(title, content) {
return new Promise((resolve) => {
uni.showModal({
title,
content,
confirmText: '确定',
cancelText: '取消',
success: (res) => resolve(Boolean(res.confirm)),
fail: () => resolve(false)
})
})
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
height: 100vh;
background: $color-bg-page;
display: flex;
flex-direction: column;
}
.body {
flex: 1;
min-height: 0;
padding: 24rpx $page-padding-x 24rpx;
box-sizing: border-box;
}
.form-card {
@include card;
padding: 28rpx 24rpx;
}
.date-row {
display: flex;
align-items: center;
gap: 16rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid $color-divider;
}
.label {
font-size: 28rpx;
font-weight: 500;
color: $color-text;
}
.date-chip {
padding: 10rpx 20rpx;
border-radius: $radius-sm;
background: $color-primary-bg;
color: $color-primary;
font-size: 26rpx;
}
.date-tip {
margin-left: auto;
font-size: 22rpx;
color: $color-text-muted;
}
.field {
margin-top: 24rpx;
}
.textarea {
width: 100%;
box-sizing: border-box;
margin-top: 14rpx;
padding: 20rpx;
min-height: 220rpx;
border-radius: $radius-md;
background: $color-bg-page;
font-size: 27rpx;
line-height: 1.6;
color: $color-text;
&.is-short {
min-height: 150rpx;
}
}
.placeholder {
color: $color-text-muted;
font-size: 26rpx;
}
.attachment-list {
margin-top: 14rpx;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
border-radius: $radius-md;
background: $color-bg-page;
}
.attachment-name {
flex: 1;
min-width: 0;
font-size: 25rpx;
color: $color-text-secondary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.remove {
margin-left: 16rpx;
font-size: 24rpx;
color: $color-danger;
}
.upload-btn {
align-self: flex-start;
padding: 14rpx 26rpx;
border-radius: $radius-full;
border: 1rpx dashed $color-primary-light;
color: $color-primary;
font-size: 25rpx;
background: $color-primary-bg;
}
.upload-tip {
font-size: 22rpx;
color: $color-text-muted;
}
.state-box {
padding: 60rpx 0;
display: flex;
justify-content: center;
}
.state-text {
font-size: 26rpx;
color: $color-text-muted;
}
.footer {
flex-shrink: 0;
display: flex;
gap: 20rpx;
padding: 20rpx $page-padding-x calc(20rpx + env(safe-area-inset-bottom));
background: $color-card;
box-shadow: 0 -2rpx 12rpx rgba(15, 23, 42, 0.04);
}
.btn {
flex: 1;
height: 84rpx;
line-height: 84rpx;
border-radius: $radius-full;
font-size: 28rpx;
font-weight: 500;
border: none;
&.ghost {
background: $color-bg-page;
color: $color-text-secondary;
}
&.primary {
background: $color-primary;
color: #ffffff;
}
&[disabled] {
opacity: 0.6;
}
}
</style>