更新若干功能
This commit is contained in:
1 parent
892ba5886d
commit
79d5ae6b66
45 files changed
+6755
-38
No files matched your search
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="日报详情"
|
||||
size="600px"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<div v-loading="loading" class="report-detail">
|
||||
<template v-if="detail">
|
||||
<div class="detail-head">
|
||||
<el-avatar :size="40" class="author-avatar">
|
||||
{{ (detail.report.user_name || '?').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div class="head-main">
|
||||
<div class="author-line">
|
||||
<span class="author-name">{{ detail.report.user_name }}</span>
|
||||
<el-tag
|
||||
:type="detail.report.status === 1 ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ detail.report.status === 1 ? '已提交' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="author-sub">
|
||||
{{ detail.report.org_name || '未分配部门' }} ·
|
||||
{{ detail.report.report_date }}
|
||||
<template v-if="detail.report.submit_time">
|
||||
· 提交于 {{ detail.report.submit_time }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button
|
||||
v-if="detail.can_edit"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('edit', detail.report)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteReport"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="detail-section">
|
||||
<h4>今日完成</h4>
|
||||
<div class="section-text">{{ detail.report.today_done || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>明日计划</h4>
|
||||
<div class="section-text">{{ detail.report.tomorrow_plan || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>问题与建议</h4>
|
||||
<div class="section-text">{{ detail.report.issues || '—' }}</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.report.attachments?.length" class="detail-section">
|
||||
<h4>附件</h4>
|
||||
<div class="attachment-list">
|
||||
<a
|
||||
v-for="file in detail.report.attachments"
|
||||
:key="file.url"
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="attachment-item"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
<span class="file-name">{{ file.name || '附件' }}</span>
|
||||
<span v-if="file.size" class="file-size">{{ formatSize(file.size) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section comment-section">
|
||||
<h4>评论 <span class="comment-count">{{ detail.comments.length }}</span></h4>
|
||||
|
||||
<el-empty
|
||||
v-if="!detail.comments.length"
|
||||
description="还没有评论"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div v-else class="comment-list">
|
||||
<div
|
||||
v-for="item in detail.comments"
|
||||
:key="item.id"
|
||||
class="comment-item"
|
||||
:class="{ 'is-reply': item.reply_to_id > 0 }"
|
||||
>
|
||||
<div class="comment-head">
|
||||
<span class="comment-user">{{ item.user_name }}</span>
|
||||
<span v-if="item.reply_to_name" class="reply-to">
|
||||
回复 {{ item.reply_to_name }}
|
||||
</span>
|
||||
<span class="comment-time">{{ item.create_time }}</span>
|
||||
</div>
|
||||
<div class="comment-content">{{ item.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<el-button link type="primary" @click="startReply(item)">
|
||||
回复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteComment(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.can_comment" class="comment-editor">
|
||||
<div v-if="replyTo" class="reply-banner">
|
||||
正在回复 {{ replyTo.user_name }}
|
||||
<el-button link type="primary" @click="replyTo = null">取消</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="commentText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
:placeholder="replyTo ? `回复 ${replyTo.user_name}` : '写下你的评论或建议'"
|
||||
/>
|
||||
<div class="editor-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submitComment"
|
||||
>
|
||||
发表评论
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="detail.report.status !== 1" class="comment-disabled">
|
||||
日报提交后才能评论
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Paperclip } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportDetail,
|
||||
addDailyReportComment,
|
||||
deleteDailyReportComment,
|
||||
deleteDailyReport
|
||||
} from '@/api/oaDailyReport'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
reportId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'edit', 'deleted'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const commentText = ref('')
|
||||
const replyTo = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.reportId],
|
||||
([open]) => {
|
||||
if (open && props.reportId) loadDetail()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDailyReportDetail(props.reportId)
|
||||
detail.value = res?.data || null
|
||||
if (!detail.value) ElMessage.error(res?.msg || '日报加载失败')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '日报加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startReply(comment) {
|
||||
replyTo.value = { id: comment.id, user_name: comment.user_name }
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const content = commentText.value.trim()
|
||||
if (!content) {
|
||||
ElMessage.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await addDailyReportComment({
|
||||
report_id: Number(props.reportId),
|
||||
content,
|
||||
reply_to_id: replyTo.value?.id || 0
|
||||
})
|
||||
ElMessage.success('评论成功')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
await loadDetail()
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '评论失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteComment(comment) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除这条评论吗?', '删除评论', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReportComment(comment.id)
|
||||
ElMessage.success('评论已删除')
|
||||
await loadDetail()
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'删除后不可恢复,且该日期的评论会一并删除。确定删除这条日报吗?',
|
||||
'删除日报',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReport(props.reportId)
|
||||
ElMessage.success('日报已删除')
|
||||
visible.value = false
|
||||
emit('deleted', Number(props.reportId))
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
const n = Number(size) || 0
|
||||
if (n < 1024) return `${n} B`
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
detail.value = null
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.report-detail {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.author-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.author-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.section-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f7f8fa;
|
||||
|
||||
&.is-reply {
|
||||
margin-left: 24px;
|
||||
background: #f2f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comment-user {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
margin-left: auto;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.reply-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.comment-disabled {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.detail-head,
|
||||
.detail-section {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.author-name,
|
||||
.section-text,
|
||||
.comment-user,
|
||||
.comment-content {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.author-sub,
|
||||
.detail-section h4,
|
||||
.comment-time,
|
||||
.file-size,
|
||||
.comment-disabled,
|
||||
.reply-banner {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attachment-item,
|
||||
.comment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑日报' : '写日报'"
|
||||
width="720px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form v-loading="loading" :model="form" label-width="88px">
|
||||
<el-form-item label="日报日期" required>
|
||||
<el-date-picker
|
||||
v-model="form.date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
disabled
|
||||
style="width: 180px"
|
||||
/>
|
||||
<span class="date-tip">
|
||||
可补写 {{ windowStart }} 至 {{ windowEnd }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="今日完成">
|
||||
<el-input
|
||||
v-model="form.today_done"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="今天完成了哪些工作?可按条列出"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="明日计划">
|
||||
<el-input
|
||||
v-model="form.tomorrow_plan"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="明天计划推进哪些事项?"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="问题与建议">
|
||||
<el-input
|
||||
v-model="form.issues"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="遇到的问题、需要的支持或改进建议(选填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="附件">
|
||||
<div class="attachment-block">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,.pdf"
|
||||
>
|
||||
<el-button :icon="Upload" :loading="uploading">上传附件</el-button>
|
||||
</el-upload>
|
||||
<span class="upload-tip">图片或 PDF,单个不超过 10MB</span>
|
||||
<div v-if="form.attachments.length" class="attachment-list">
|
||||
<div
|
||||
v-for="(file, index) in form.attachments"
|
||||
:key="file.url || index"
|
||||
class="attachment-item"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
<a :href="file.url" target="_blank" rel="noopener" class="file-name">
|
||||
{{ file.name || '附件' }}
|
||||
</a>
|
||||
<el-button link type="danger" @click="removeAttachment(index)">
|
||||
移除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button :loading="savingDraft" @click="handleSave(false)">保存草稿</el-button>
|
||||
<el-button type="primary" :loading="savingSubmit" @click="handleSave(true)">
|
||||
提交日报
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload, Paperclip } from '@element-plus/icons-vue'
|
||||
import { uploadFile } from '@/api/file'
|
||||
import { saveDailyReport } from '@/api/oaDailyReport'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
// 传入已有日报时为编辑,否则为新建
|
||||
report: { type: Object, default: null },
|
||||
defaultDate: { type: String, default: '' },
|
||||
windowStart: { type: String, default: '' },
|
||||
windowEnd: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => Boolean(props.report?.id))
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const savingDraft = ref(false)
|
||||
const savingSubmit = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
date: '',
|
||||
today_done: '',
|
||||
tomorrow_plan: '',
|
||||
issues: '',
|
||||
attachments: []
|
||||
})
|
||||
|
||||
function fillForm(report) {
|
||||
form.date = report?.report_date || props.defaultDate || todayStr()
|
||||
form.today_done = report?.today_done || ''
|
||||
form.tomorrow_plan = report?.tomorrow_plan || ''
|
||||
form.issues = report?.issues || ''
|
||||
form.attachments = Array.isArray(report?.attachments)
|
||||
? report.attachments.map((item) => ({ ...item }))
|
||||
: []
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) fillForm(props.report)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function todayStr() {
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
||||
}
|
||||
|
||||
async function handleUpload(options) {
|
||||
const file = options.file
|
||||
if (!file) return
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
uploading.value = true
|
||||
try {
|
||||
const res = await uploadFile(fd, { scope: 'user' })
|
||||
const data = res?.data || {}
|
||||
if (!data.url) throw new Error(res?.msg || '上传失败')
|
||||
if (form.attachments.length >= 20) {
|
||||
ElMessage.warning('附件最多 20 个')
|
||||
return
|
||||
}
|
||||
form.attachments.push({
|
||||
id: data.id || 0,
|
||||
url: data.url,
|
||||
name: data.name || file.name,
|
||||
size: file.size || 0
|
||||
})
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '上传失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeAttachment(index) {
|
||||
form.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
async function handleSave(submit) {
|
||||
if (!form.date) {
|
||||
ElMessage.warning('请选择日报日期')
|
||||
return
|
||||
}
|
||||
const hasContent =
|
||||
form.today_done.trim() || form.tomorrow_plan.trim() || form.issues.trim()
|
||||
if (!hasContent) {
|
||||
ElMessage.warning('请至少填写今日完成、明日计划、问题与建议中的一项')
|
||||
return
|
||||
}
|
||||
|
||||
const setLoading = submit ? savingSubmit : savingDraft
|
||||
setLoading.value = true
|
||||
try {
|
||||
const res = await saveDailyReport({
|
||||
date: form.date,
|
||||
today_done: form.today_done,
|
||||
tomorrow_plan: form.tomorrow_plan,
|
||||
issues: form.issues,
|
||||
attachments: JSON.stringify(form.attachments),
|
||||
submit
|
||||
})
|
||||
ElMessage.success(submit ? '日报已提交' : '草稿已保存')
|
||||
emit('saved', res?.data || null)
|
||||
visible.value = false
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '保存失败')
|
||||
} finally {
|
||||
setLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
loading.value = false
|
||||
uploading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.date-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.attachment-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.date-tip,
|
||||
.upload-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="report-view">
|
||||
<template v-if="detail">
|
||||
<div class="detail-head">
|
||||
<el-avatar :size="40" class="author-avatar">
|
||||
{{ (detail.report.user_name || '?').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div class="head-main">
|
||||
<div class="author-line">
|
||||
<span class="author-name">{{ detail.report.user_name }}</span>
|
||||
<el-tag
|
||||
:type="detail.report.status === 1 ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ detail.report.status === 1 ? '已提交' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="author-sub">
|
||||
{{ detail.report.org_name || '未分配部门' }} ·
|
||||
{{ detail.report.report_date }}
|
||||
<template v-if="detail.report.submit_time">
|
||||
· 提交于 {{ detail.report.submit_time }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button
|
||||
v-if="detail.can_edit"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('edit', detail.report)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteReport"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="detail-section">
|
||||
<h4>今日完成</h4>
|
||||
<div class="section-text">{{ detail.report.today_done || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>明日计划</h4>
|
||||
<div class="section-text">{{ detail.report.tomorrow_plan || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>问题与建议</h4>
|
||||
<div class="section-text">{{ detail.report.issues || '—' }}</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.report.attachments?.length" class="detail-section">
|
||||
<h4>附件</h4>
|
||||
<div class="attachment-list">
|
||||
<a
|
||||
v-for="file in detail.report.attachments"
|
||||
:key="file.url"
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="attachment-item"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
<span class="file-name">{{ file.name || '附件' }}</span>
|
||||
<span v-if="file.size" class="file-size">{{ formatSize(file.size) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section comment-section">
|
||||
<h4>评论 <span class="comment-count">{{ detail.comments.length }}</span></h4>
|
||||
|
||||
<el-empty
|
||||
v-if="!detail.comments.length"
|
||||
description="还没有评论"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div v-else class="comment-list">
|
||||
<div
|
||||
v-for="item in detail.comments"
|
||||
:key="item.id"
|
||||
class="comment-item"
|
||||
:class="{ 'is-reply': item.reply_to_id > 0 }"
|
||||
>
|
||||
<div class="comment-head">
|
||||
<span class="comment-user">{{ item.user_name }}</span>
|
||||
<span v-if="item.reply_to_name" class="reply-to">
|
||||
回复 {{ item.reply_to_name }}
|
||||
</span>
|
||||
<span class="comment-time">{{ item.create_time }}</span>
|
||||
</div>
|
||||
<div class="comment-content">{{ item.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<el-button link type="primary" @click="startReply(item)">
|
||||
回复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteComment(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.can_comment" class="comment-editor">
|
||||
<div v-if="replyTo" class="reply-banner">
|
||||
正在回复 {{ replyTo.user_name }}
|
||||
<el-button link type="primary" @click="replyTo = null">取消</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="commentText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
:placeholder="replyTo ? `回复 ${replyTo.user_name}` : '写下你的评论或建议'"
|
||||
/>
|
||||
<div class="editor-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submitComment"
|
||||
>
|
||||
发表评论
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="detail.report.status !== 1" class="comment-disabled">
|
||||
日报提交后才能评论
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<el-empty
|
||||
v-else-if="!loading"
|
||||
description="选择左侧日报查看详情"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Paperclip } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportDetail,
|
||||
addDailyReportComment,
|
||||
deleteDailyReportComment,
|
||||
deleteDailyReport
|
||||
} from '@/api/oaDailyReport'
|
||||
|
||||
const props = defineProps({
|
||||
reportId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['edit', 'deleted', 'changed'])
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const commentText = ref('')
|
||||
const replyTo = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.reportId,
|
||||
(id) => {
|
||||
if (id) loadDetail()
|
||||
else detail.value = null
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDailyReportDetail(props.reportId)
|
||||
detail.value = res?.data || null
|
||||
if (!detail.value) ElMessage.error(res?.msg || '日报加载失败')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
} catch (err) {
|
||||
detail.value = null
|
||||
ElMessage.error(err?.message || '日报加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startReply(comment) {
|
||||
replyTo.value = { id: comment.id, user_name: comment.user_name }
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const content = commentText.value.trim()
|
||||
if (!content) {
|
||||
ElMessage.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await addDailyReportComment({
|
||||
report_id: Number(props.reportId),
|
||||
content,
|
||||
reply_to_id: replyTo.value?.id || 0
|
||||
})
|
||||
ElMessage.success('评论成功')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
await loadDetail()
|
||||
emit('changed')
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '评论失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteComment(comment) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除这条评论吗?', '删除评论', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReportComment(comment.id)
|
||||
ElMessage.success('评论已删除')
|
||||
await loadDetail()
|
||||
emit('changed')
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'删除后不可恢复,且该日期的评论会一并删除。确定删除这条日报吗?',
|
||||
'删除日报',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReport(props.reportId)
|
||||
ElMessage.success('日报已删除')
|
||||
emit('deleted', Number(props.reportId))
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
const n = Number(size) || 0
|
||||
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="less" scoped>
|
||||
.report-view {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.author-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.author-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.section-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f7f8fa;
|
||||
|
||||
&.is-reply {
|
||||
margin-left: 24px;
|
||||
background: #f2f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comment-user {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
margin-left: auto;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.reply-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.comment-disabled {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.detail-head,
|
||||
.detail-section {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.author-name,
|
||||
.section-text,
|
||||
.comment-user,
|
||||
.comment-content {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.author-sub,
|
||||
.detail-section h4,
|
||||
.comment-time,
|
||||
.file-size,
|
||||
.comment-disabled,
|
||||
.reply-banner {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attachment-item,
|
||||
.comment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user