471 lines
10 KiB
Vue
471 lines
10 KiB
Vue
<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>
|