604 lines
13 KiB
Vue
604 lines
13 KiB
Vue
<template>
|
|
<view class="page">
|
|
<scroll-view class="body" scroll-y :show-scrollbar="false">
|
|
<view v-if="loading && !detail" class="state-box">
|
|
<text class="state-text">加载中...</text>
|
|
</view>
|
|
|
|
<template v-else-if="detail">
|
|
<view class="head-card">
|
|
<view class="author-row">
|
|
<view class="avatar">{{ (report.user_name || '?').slice(0, 1) }}</view>
|
|
<view class="author-info">
|
|
<view class="author-line">
|
|
<text class="author-name">{{ report.user_name }}</text>
|
|
<u-tag
|
|
:text="report.status === 1 ? '已提交' : '草稿'"
|
|
:type="report.status === 1 ? 'success' : 'info'"
|
|
size="mini"
|
|
plain
|
|
/>
|
|
</view>
|
|
<text class="author-sub">
|
|
{{ report.org_name || '未分配部门' }} · {{ report.report_date }}
|
|
</text>
|
|
</view>
|
|
<view class="head-actions">
|
|
<text v-if="detail.can_edit" class="action-link" @tap="openEdit">编辑</text>
|
|
<text
|
|
v-if="detail.can_withdraw"
|
|
class="action-link is-warning"
|
|
@tap="handleWithdraw"
|
|
>
|
|
撤销提交
|
|
</text>
|
|
<text v-if="detail.can_delete" class="action-link is-danger" @tap="handleDelete">
|
|
删除
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<text v-if="report.submit_time" class="submit-time">
|
|
提交于 {{ report.submit_time }}
|
|
</text>
|
|
</view>
|
|
|
|
<view class="section-card">
|
|
<text class="section-title">今日完成</text>
|
|
<text class="section-text">{{ report.today_done || '—' }}</text>
|
|
</view>
|
|
<view class="section-card">
|
|
<text class="section-title">明日计划</text>
|
|
<text class="section-text">{{ report.tomorrow_plan || '—' }}</text>
|
|
</view>
|
|
<view class="section-card">
|
|
<text class="section-title">问题与建议</text>
|
|
<text class="section-text">{{ report.issues || '—' }}</text>
|
|
</view>
|
|
|
|
<view v-if="report.attachments && report.attachments.length" class="section-card">
|
|
<text class="section-title">附件</text>
|
|
<view class="attachment-list">
|
|
<view
|
|
v-for="file in report.attachments"
|
|
:key="file.url"
|
|
class="attachment-item"
|
|
@tap="openAttachment(file)"
|
|
>
|
|
<text class="attachment-name">{{ file.name || '附件' }}</text>
|
|
<text class="attachment-size">{{ formatSize(file.size) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="section-card comment-card">
|
|
<view class="comment-head-row">
|
|
<text class="section-title">评论</text>
|
|
<text class="comment-count">{{ comments.length }}</text>
|
|
</view>
|
|
|
|
<view v-if="!comments.length" class="empty-comments">
|
|
<text class="empty-text">还没有评论</text>
|
|
</view>
|
|
<view v-else class="comment-list">
|
|
<view
|
|
v-for="item in comments"
|
|
:key="item.id"
|
|
class="comment-item"
|
|
:class="{ 'is-reply': item.reply_to_id > 0 }"
|
|
>
|
|
<view class="comment-top">
|
|
<text class="comment-user">{{ item.user_name }}</text>
|
|
<text v-if="item.reply_to_name" class="reply-to">
|
|
回复 {{ item.reply_to_name }}
|
|
</text>
|
|
<text class="comment-time">{{ item.create_time }}</text>
|
|
</view>
|
|
<text class="comment-content">{{ item.content }}</text>
|
|
<view class="comment-actions">
|
|
<text class="action-link" @tap="startReply(item)">回复</text>
|
|
<text
|
|
v-if="item.can_delete"
|
|
class="action-link is-danger"
|
|
@tap="handleDeleteComment(item)"
|
|
>
|
|
删除
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="!detail.can_comment" class="comment-disabled">
|
|
<text class="empty-text">
|
|
{{ report.status === 1 ? '当前账号无权评论' : '日报提交后才能评论' }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="bottom-space" />
|
|
</template>
|
|
</scroll-view>
|
|
|
|
<view v-if="detail && detail.can_comment" class="comment-editor">
|
|
<view v-if="replyTo" class="reply-banner">
|
|
<text class="reply-text">回复 {{ replyTo.user_name }}</text>
|
|
<text class="reply-cancel" @tap="replyTo = null">取消</text>
|
|
</view>
|
|
<view class="editor-row">
|
|
<input
|
|
v-model="commentText"
|
|
class="comment-input"
|
|
maxlength="1000"
|
|
:placeholder="replyTo ? `回复 ${replyTo.user_name}` : '写下你的评论或建议'"
|
|
confirm-type="send"
|
|
@confirm="submitComment"
|
|
/>
|
|
<view class="send-btn" :class="{ disabled: submitting }" @tap="submitComment">
|
|
发送
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
|
import {
|
|
fetchDailyReportDetail,
|
|
withdrawDailyReport,
|
|
addDailyReportComment,
|
|
deleteDailyReportComment,
|
|
deleteDailyReport
|
|
} from '@/api/oaDailyReport.js'
|
|
|
|
const reportId = ref(0)
|
|
const detail = ref(null)
|
|
const loading = ref(false)
|
|
const commentText = ref('')
|
|
const replyTo = ref(null)
|
|
const submitting = ref(false)
|
|
|
|
const report = computed(() => detail.value?.report || {})
|
|
const comments = computed(() => detail.value?.comments || [])
|
|
|
|
onLoad((options = {}) => {
|
|
reportId.value = Number(options.id) || 0
|
|
if (!reportId.value) {
|
|
uni.showToast({ title: '缺少日报ID', icon: 'none' })
|
|
}
|
|
})
|
|
|
|
onShow(() => {
|
|
if (!reportId.value) return
|
|
loadDetail()
|
|
})
|
|
|
|
async function loadDetail() {
|
|
loading.value = true
|
|
try {
|
|
const data = await fetchDailyReportDetail(reportId.value)
|
|
detail.value = data || null
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '日报加载失败', icon: 'none' })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openEdit() {
|
|
uni.navigateTo({
|
|
url: `/pages/oa/dailyreport/edit?id=${reportId.value}&date=${report.value.report_date}`
|
|
})
|
|
}
|
|
|
|
// 撤销提交:退回草稿并留存修改痕迹,改完需重新提交
|
|
function handleWithdraw() {
|
|
uni.showModal({
|
|
title: '撤销提交',
|
|
content: '撤销后日报退回草稿,本次提交的内容会留痕,修改完成后需重新提交',
|
|
success: async (res) => {
|
|
if (!res.confirm) return
|
|
try {
|
|
await withdrawDailyReport(reportId.value)
|
|
uni.showToast({ title: '已撤销提交', icon: 'success' })
|
|
await loadDetail()
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '撤销提交失败', icon: 'none' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function startReply(item) {
|
|
replyTo.value = { id: item.id, user_name: item.user_name }
|
|
}
|
|
|
|
async function submitComment() {
|
|
const content = commentText.value.trim()
|
|
if (!content) {
|
|
uni.showToast({ title: '请输入评论内容', icon: 'none' })
|
|
return
|
|
}
|
|
if (submitting.value) return
|
|
submitting.value = true
|
|
try {
|
|
await addDailyReportComment(reportId.value, content, replyTo.value?.id || 0)
|
|
commentText.value = ''
|
|
replyTo.value = null
|
|
uni.showToast({ title: '评论成功', icon: 'success' })
|
|
await loadDetail()
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '评论失败', icon: 'none' })
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function handleDeleteComment(item) {
|
|
uni.showModal({
|
|
title: '删除评论',
|
|
content: '确定删除这条评论吗?',
|
|
success: async (res) => {
|
|
if (!res.confirm) return
|
|
try {
|
|
await deleteDailyReportComment(item.id)
|
|
uni.showToast({ title: '已删除', icon: 'success' })
|
|
await loadDetail()
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '删除失败', icon: 'none' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function handleDelete() {
|
|
uni.showModal({
|
|
title: '删除日报',
|
|
content: '删除后不可恢复,评论会一并删除。确定删除吗?',
|
|
success: async (res) => {
|
|
if (!res.confirm) return
|
|
try {
|
|
await deleteDailyReport(reportId.value)
|
|
uni.showToast({ title: '日报已删除', icon: 'success' })
|
|
setTimeout(() => uni.navigateBack(), 400)
|
|
} catch (e) {
|
|
uni.showToast({ title: e?.message || '删除失败', icon: 'none' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
function openAttachment(file) {
|
|
if (!file?.url) return
|
|
if (/\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(file.url)) {
|
|
const images = (report.value.attachments || [])
|
|
.filter((f) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(f.url))
|
|
.map((f) => f.url)
|
|
uni.previewImage({ urls: images, current: file.url })
|
|
return
|
|
}
|
|
uni.setClipboardData({
|
|
data: file.url,
|
|
success: () => uni.showToast({ title: '附件链接已复制', icon: 'none' })
|
|
})
|
|
}
|
|
|
|
function formatSize(size) {
|
|
const n = Number(size) || 0
|
|
if (!n) return ''
|
|
if (n < 1024) return `${n} B`
|
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
return `${(n / 1024 / 1024).toFixed(1)} MB`
|
|
}
|
|
</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 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.head-card {
|
|
@include card;
|
|
padding: 26rpx 24rpx;
|
|
}
|
|
|
|
.author-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18rpx;
|
|
}
|
|
|
|
.avatar {
|
|
width: 80rpx;
|
|
height: 80rpx;
|
|
border-radius: $radius-md;
|
|
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
|
color: #ffffff;
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.author-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.author-line {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.author-name {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: $color-text;
|
|
}
|
|
|
|
.author-sub {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.head-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.action-link {
|
|
font-size: 24rpx;
|
|
color: $color-primary;
|
|
|
|
&.is-danger {
|
|
color: $color-danger;
|
|
}
|
|
|
|
&.is-warning {
|
|
color: $color-warning;
|
|
}
|
|
}
|
|
|
|
.submit-time {
|
|
display: block;
|
|
margin-top: 16rpx;
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.section-card {
|
|
@include card;
|
|
padding: 26rpx 24rpx;
|
|
margin-top: 20rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 26rpx;
|
|
font-weight: 600;
|
|
color: $color-text-secondary;
|
|
}
|
|
|
|
.section-text {
|
|
display: block;
|
|
margin-top: 14rpx;
|
|
font-size: 28rpx;
|
|
line-height: 1.75;
|
|
color: $color-text;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.attachment-list {
|
|
margin-top: 14rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12rpx;
|
|
}
|
|
|
|
.attachment-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12rpx;
|
|
padding: 16rpx 20rpx;
|
|
border-radius: $radius-md;
|
|
background: $color-bg-page;
|
|
|
|
&:active {
|
|
background: $color-divider;
|
|
}
|
|
}
|
|
|
|
.attachment-name {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 25rpx;
|
|
color: $color-primary;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.attachment-size {
|
|
flex-shrink: 0;
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.comment-card {
|
|
padding-bottom: 20rpx;
|
|
}
|
|
|
|
.comment-head-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
}
|
|
|
|
.comment-count {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.empty-comments {
|
|
padding: 30rpx 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 24rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.comment-list {
|
|
margin-top: 18rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18rpx;
|
|
}
|
|
|
|
.comment-item {
|
|
padding: 18rpx 20rpx;
|
|
border-radius: $radius-md;
|
|
background: $color-bg-page;
|
|
|
|
&.is-reply {
|
|
margin-left: 32rpx;
|
|
background: $color-primary-bg;
|
|
}
|
|
}
|
|
|
|
.comment-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10rpx;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.comment-user {
|
|
font-size: 24rpx;
|
|
font-weight: 600;
|
|
color: $color-text;
|
|
}
|
|
|
|
.reply-to {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.comment-time {
|
|
margin-left: auto;
|
|
font-size: 20rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.comment-content {
|
|
display: block;
|
|
margin-top: 10rpx;
|
|
font-size: 26rpx;
|
|
line-height: 1.65;
|
|
color: $color-text;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.comment-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24rpx;
|
|
margin-top: 10rpx;
|
|
}
|
|
|
|
.comment-disabled {
|
|
padding-top: 20rpx;
|
|
}
|
|
|
|
.bottom-space {
|
|
height: 40rpx;
|
|
}
|
|
|
|
.comment-editor {
|
|
flex-shrink: 0;
|
|
background: $color-card;
|
|
padding: 16rpx $page-padding-x calc(16rpx + env(safe-area-inset-bottom));
|
|
box-shadow: 0 -2rpx 12rpx rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.reply-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding-bottom: 10rpx;
|
|
}
|
|
|
|
.reply-text {
|
|
font-size: 22rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
|
|
.reply-cancel {
|
|
font-size: 22rpx;
|
|
color: $color-primary;
|
|
}
|
|
|
|
.editor-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.comment-input {
|
|
flex: 1;
|
|
height: 72rpx;
|
|
padding: 0 24rpx;
|
|
border-radius: $radius-full;
|
|
background: $color-bg-page;
|
|
font-size: 26rpx;
|
|
color: $color-text;
|
|
}
|
|
|
|
.send-btn {
|
|
flex-shrink: 0;
|
|
padding: 16rpx 32rpx;
|
|
border-radius: $radius-full;
|
|
background: $color-primary;
|
|
color: #ffffff;
|
|
font-size: 26rpx;
|
|
|
|
&.disabled {
|
|
opacity: 0.6;
|
|
}
|
|
}
|
|
|
|
.state-box {
|
|
padding: 160rpx 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.state-text {
|
|
font-size: 26rpx;
|
|
color: $color-text-muted;
|
|
}
|
|
</style>
|