更新若干功能

This commit is contained in:
hero920103 committed 2026-10-21 17:45:50 +08:00
1 parent 892ba5886d
commit 79d5ae6b66
45 files changed
+6755 -38

No files matched your search

+458
View File
@@ -0,0 +1,458 @@
<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
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>