更新若干功能
This commit is contained in:
1 parent
892ba5886d
commit
79d5ae6b66
45 files changed
+6755
-38
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import { request } from '@/api/request.js'
|
||||
import { getBaseURL } from '@/api/config.js'
|
||||
import { getToken } from '@/utils/auth.js'
|
||||
|
||||
/**
|
||||
* OA 工作日报接口封装,对应后端 /api/oa/dailyreport/*。
|
||||
* 与后台 PC 端 /backend/oa/dailyreport/* 共用同一套数据与权限:
|
||||
* 本人 / 部门主管 / 管理员三级可见,已提交日报支持评论与回复。
|
||||
*/
|
||||
|
||||
/** 当前用户身份与可见范围 */
|
||||
export function fetchDailyReportProfile() {
|
||||
return request({ url: '/api/oa/dailyreport/profile' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的日报列表
|
||||
* @param {{page?:number,pageSize?:number,startDate?:string,endDate?:string}} params
|
||||
* @returns {{list:Array,total:number,summary:Object}}
|
||||
*/
|
||||
export function fetchMyDailyReports(params = {}) {
|
||||
return request({
|
||||
url: '/api/oa/dailyreport/mine/list',
|
||||
data: {
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 20,
|
||||
start_date: params.startDate || '',
|
||||
end_date: params.endDate || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 日报详情:{ report, comments, can_edit, can_delete, can_comment } */
|
||||
export function fetchDailyReportDetail(id) {
|
||||
return request({ url: `/api/oa/dailyreport/detail/${id}` })
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存日报
|
||||
* @param {{date:string,todayDone:string,tomorrowPlan:string,issues:string,attachments:Array,submit:boolean}} payload
|
||||
*/
|
||||
export function saveDailyReport(payload) {
|
||||
return request({
|
||||
url: '/api/oa/dailyreport/save',
|
||||
method: 'POST',
|
||||
data: {
|
||||
date: payload.date,
|
||||
today_done: payload.todayDone || '',
|
||||
tomorrow_plan: payload.tomorrowPlan || '',
|
||||
issues: payload.issues || '',
|
||||
attachments: JSON.stringify(payload.attachments || []),
|
||||
submit: Boolean(payload.submit)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDailyReport(id) {
|
||||
return request({ url: `/api/oa/dailyreport/delete/${id}`, method: 'DELETE' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队日报汇总
|
||||
* @param {{date?:string,orgId?:number,keyword?:string,page?:number,pageSize?:number}} params
|
||||
* @returns {{date:string,list:Array,total:number,stats:Object}}
|
||||
*/
|
||||
export function fetchTeamDailyReports(params = {}) {
|
||||
return request({
|
||||
url: '/api/oa/dailyreport/team/status',
|
||||
data: {
|
||||
date: params.date || '',
|
||||
org_id: params.orgId || 0,
|
||||
keyword: params.keyword || '',
|
||||
page: params.page || 1,
|
||||
pageSize: params.pageSize || 20
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 团队筛选部门:{ list: [{id,parent_id,name,member_count}] } */
|
||||
export function fetchTeamDailyReportOrgs() {
|
||||
return request({ url: '/api/oa/dailyreport/team/orgs' })
|
||||
}
|
||||
|
||||
/** 评论 / 回复:replyToId=0 表示直接评论 */
|
||||
export function addDailyReportComment(reportId, content, replyToId = 0) {
|
||||
return request({
|
||||
url: '/api/oa/dailyreport/comment',
|
||||
method: 'POST',
|
||||
data: {
|
||||
report_id: Number(reportId),
|
||||
content,
|
||||
reply_to_id: Number(replyToId) || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDailyReportComment(id) {
|
||||
return request({ url: `/api/oa/dailyreport/comment/${id}`, method: 'DELETE' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传日报附件(multipart,字段名 file,单文件 ≤10MB)
|
||||
* 返回 { id, url, name },与 Web 端上传结果结构一致。
|
||||
*/
|
||||
export function uploadDailyReportFile(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${getBaseURL().replace(/\/$/, '')}/api/oa/dailyreport/upload`,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: { Authorization: `Bearer ${getToken()}` },
|
||||
success(res) {
|
||||
let body = {}
|
||||
try {
|
||||
body = JSON.parse(res.data || '{}')
|
||||
} catch (e) {
|
||||
// 非 JSON 响应按失败处理
|
||||
}
|
||||
if (res.statusCode >= 200 && res.statusCode < 300 && body.code === 200 && body.data) {
|
||||
resolve(body.data)
|
||||
} else {
|
||||
const msg = body.msg || `上传失败(${res.statusCode})`
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
reject(new Error(msg))
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
uni.showToast({ title: '网络异常,上传失败', icon: 'none' })
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 今日 "YYYY-MM-DD"(与后端本地日期口径一致) */
|
||||
export function todayString() {
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
||||
}
|
||||
@@ -257,6 +257,27 @@
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/oa/dailyreport/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作日报",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/oa/dailyreport/edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "填写日报",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/oa/dailyreport/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "日报详情",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/oa/notice/detail",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
<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_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,
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,729 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<view class="header-top">
|
||||
<view class="title-wrap">
|
||||
<text class="title">工作日报</text>
|
||||
<text class="subtitle">
|
||||
补写窗口 {{ profile.window_start || '-' }} 至 {{ profile.window_end || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="write-btn" @tap="writeToday">写今日日报</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'mine' }"
|
||||
@tap="switchTab('mine')"
|
||||
>
|
||||
我的日报
|
||||
</view>
|
||||
<view
|
||||
v-if="profile.can_view_team"
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'team' }"
|
||||
@tap="switchTab('team')"
|
||||
>
|
||||
团队日报
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 我的日报 ==================== -->
|
||||
<view v-if="activeTab === 'mine'" class="content">
|
||||
<view class="summary-row">
|
||||
<view class="summary-item">
|
||||
<text class="summary-num">{{ summary.month_total || 0 }}</text>
|
||||
<text class="summary-label">本月日报</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-num is-green">{{ summary.month_submitted || 0 }}</text>
|
||||
<text class="summary-label">本月已提交</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-num" :class="todayStatusClass">{{ todayStatusText }}</text>
|
||||
<text class="summary-label">今日状态</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="backfill-bar">
|
||||
<text class="backfill-label">补写往日</text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="backfillDate"
|
||||
:start="profile.window_start || ''"
|
||||
:end="profile.window_end || today"
|
||||
@change="onBackfillDateChange"
|
||||
>
|
||||
<view class="date-chip">{{ backfillDate }}</view>
|
||||
</picker>
|
||||
<view class="backfill-btn" @tap="openBackfill">填写该日</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading && !mineList.length" class="state-box">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<u-empty
|
||||
v-else-if="!mineList.length"
|
||||
mode="list"
|
||||
:text="'还没有日报,点击右上角开始记录'"
|
||||
:icon-size="80"
|
||||
/>
|
||||
<view v-else class="card-list">
|
||||
<view
|
||||
v-for="item in mineList"
|
||||
:key="item.id"
|
||||
class="report-card"
|
||||
@tap="openDetail(item.id)"
|
||||
>
|
||||
<view class="card-head">
|
||||
<view class="date-wrap">
|
||||
<text class="date-text">{{ item.report_date }}</text>
|
||||
<text v-if="item.report_date === today" class="today-chip">今天</text>
|
||||
</view>
|
||||
<u-tag
|
||||
:text="item.status === 1 ? '已提交' : '草稿'"
|
||||
:type="item.status === 1 ? 'success' : 'info'"
|
||||
size="mini"
|
||||
plain
|
||||
/>
|
||||
</view>
|
||||
<text class="preview">{{ previewText(item) }}</text>
|
||||
<view class="card-foot">
|
||||
<text class="meta">评论 {{ item.comment_count || 0 }}</text>
|
||||
<text class="meta">{{ item.submit_time || '未提交' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-more">
|
||||
<text v-if="hasMore" class="load-link" @tap="loadMore">加载更多</text>
|
||||
<text v-else class="load-done">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 团队日报 ==================== -->
|
||||
<view v-else class="content">
|
||||
<view class="team-filter">
|
||||
<picker mode="date" :value="teamDate" :end="today" @change="onTeamDateChange">
|
||||
<view class="date-chip">{{ teamDate }}</view>
|
||||
</picker>
|
||||
<picker
|
||||
:range="orgNames"
|
||||
:value="orgIndex"
|
||||
@change="onOrgChange"
|
||||
>
|
||||
<view class="date-chip">{{ orgNames[orgIndex] || '全部部门' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="team-stats">
|
||||
<view class="team-stat">
|
||||
<text class="summary-num">{{ teamStats.total || 0 }}</text>
|
||||
<text class="summary-label">应提交</text>
|
||||
</view>
|
||||
<view class="team-stat">
|
||||
<text class="summary-num is-green">{{ teamStats.submitted || 0 }}</text>
|
||||
<text class="summary-label">已提交</text>
|
||||
</view>
|
||||
<view class="team-stat">
|
||||
<text class="summary-num is-red">{{ teamStats.missing || 0 }}</text>
|
||||
<text class="summary-label">未提交</text>
|
||||
</view>
|
||||
<view class="team-stat">
|
||||
<text class="summary-num is-orange">{{ teamStats.rate || 0 }}%</text>
|
||||
<text class="summary-label">提交率</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="teamLoading && !teamList.length" class="state-box">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<u-empty
|
||||
v-else-if="!teamList.length"
|
||||
mode="list"
|
||||
text="暂无团队成员"
|
||||
:icon-size="80"
|
||||
/>
|
||||
<view v-else class="card-list">
|
||||
<view
|
||||
v-for="item in teamList"
|
||||
:key="item.user_id"
|
||||
class="member-card"
|
||||
:class="{ clickable: item.submitted }"
|
||||
@tap="openTeamReport(item)"
|
||||
>
|
||||
<view class="member-left">
|
||||
<view class="member-avatar">{{ (item.user_name || '?').slice(0, 1) }}</view>
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{ item.user_name }}</text>
|
||||
<text class="member-org">{{ item.org_name || '未分配部门' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-right">
|
||||
<u-tag
|
||||
v-if="item.submitted"
|
||||
text="已提交"
|
||||
type="success"
|
||||
size="mini"
|
||||
plain
|
||||
/>
|
||||
<u-tag
|
||||
v-else-if="item.status === 0"
|
||||
text="草稿"
|
||||
type="info"
|
||||
size="mini"
|
||||
plain
|
||||
/>
|
||||
<u-tag v-else text="未提交" type="error" size="mini" plain />
|
||||
<text v-if="item.submit_time" class="member-time">{{ item.submit_time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-more">
|
||||
<text v-if="teamHasMore" class="load-link" @tap="loadMoreTeam">加载更多</text>
|
||||
<text v-else class="load-done">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
||||
import {
|
||||
fetchDailyReportProfile,
|
||||
fetchMyDailyReports,
|
||||
fetchTeamDailyReports,
|
||||
fetchTeamDailyReportOrgs,
|
||||
todayString
|
||||
} from '@/api/oaDailyReport.js'
|
||||
import { isLoggedIn } from '@/utils/auth.js'
|
||||
|
||||
const today = todayString()
|
||||
const activeTab = ref('mine')
|
||||
const profile = ref({ is_admin: false, can_view_team: false, window_start: '', window_end: '' })
|
||||
const summary = ref({})
|
||||
const loading = ref(false)
|
||||
const mineList = ref([])
|
||||
const minePage = ref(1)
|
||||
const hasMore = ref(false)
|
||||
|
||||
const backfillDate = ref(today)
|
||||
|
||||
const teamDate = ref(today)
|
||||
const teamList = ref([])
|
||||
const teamStats = ref({ total: 0, submitted: 0, missing: 0, rate: 0 })
|
||||
const teamLoading = ref(false)
|
||||
const teamPage = ref(1)
|
||||
const teamHasMore = ref(false)
|
||||
const orgOptions = ref([])
|
||||
const orgIndex = ref(0)
|
||||
const orgNames = computed(() => ['全部部门', ...orgOptions.value.map((o) => o.name)])
|
||||
|
||||
const todayStatusText = computed(() => {
|
||||
const status = summary.value.today_status
|
||||
if (status === 1) return '已提交'
|
||||
if (status === 0) return '草稿'
|
||||
return '未填写'
|
||||
})
|
||||
|
||||
const todayStatusClass = computed(() => {
|
||||
const status = summary.value.today_status
|
||||
if (status === 1) return 'is-green'
|
||||
if (status === 0) return 'is-orange'
|
||||
return 'is-red'
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
reload()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (activeTab.value === 'mine') {
|
||||
if (hasMore.value && !loading.value) loadMine(false)
|
||||
} else if (teamHasMore.value && !teamLoading.value) {
|
||||
loadTeam(false)
|
||||
}
|
||||
})
|
||||
|
||||
async function reload() {
|
||||
await loadProfile()
|
||||
if (activeTab.value === 'mine') {
|
||||
await loadMine(true)
|
||||
} else {
|
||||
if (!orgOptions.value.length) await loadTeamOrgs()
|
||||
await loadTeam(true)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const data = await fetchDailyReportProfile()
|
||||
if (data) profile.value = { ...profile.value, ...data }
|
||||
if (profile.value.window_end) backfillDate.value = profile.value.window_end
|
||||
if (!profile.value.can_view_team && activeTab.value === 'team') activeTab.value = 'mine'
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '日报身份加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMine(reset = false) {
|
||||
if (reset) {
|
||||
minePage.value = 1
|
||||
loading.value = true
|
||||
}
|
||||
try {
|
||||
const data = await fetchMyDailyReports({ page: minePage.value, pageSize: 20 })
|
||||
const rows = data?.list || []
|
||||
mineList.value = reset ? rows : mineList.value.concat(rows)
|
||||
summary.value = data?.summary || {}
|
||||
hasMore.value = mineList.value.length < (data?.total || 0)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '日报加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (!hasMore.value || loading.value) return
|
||||
minePage.value += 1
|
||||
loadMine(false)
|
||||
}
|
||||
|
||||
function switchTab(name) {
|
||||
if (activeTab.value === name) return
|
||||
activeTab.value = name
|
||||
if (name === 'mine') {
|
||||
loadMine(true)
|
||||
} else {
|
||||
if (!orgOptions.value.length) loadTeamOrgs()
|
||||
loadTeam(true)
|
||||
}
|
||||
}
|
||||
|
||||
function onBackfillDateChange(e) {
|
||||
backfillDate.value = e.detail.value
|
||||
}
|
||||
|
||||
function openBackfill() {
|
||||
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?date=${backfillDate.value}` })
|
||||
}
|
||||
|
||||
function writeToday() {
|
||||
const id = summary.value.today_report_id
|
||||
if (id) {
|
||||
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?id=${id}&date=${today}` })
|
||||
} else {
|
||||
uni.navigateTo({ url: `/pages/oa/dailyreport/edit?date=${today}` })
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(id) {
|
||||
uni.navigateTo({ url: `/pages/oa/dailyreport/detail?id=${id}` })
|
||||
}
|
||||
|
||||
function previewText(item) {
|
||||
const text = item.today_done || item.tomorrow_plan || item.issues || ''
|
||||
return text || '暂无内容'
|
||||
}
|
||||
|
||||
// ---------------- 团队 ----------------
|
||||
async function loadTeamOrgs() {
|
||||
try {
|
||||
const data = await fetchTeamDailyReportOrgs()
|
||||
orgOptions.value = data?.list || []
|
||||
} catch (e) {
|
||||
// 部门列表失败不阻断团队汇总主流程
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTeam(reset = false) {
|
||||
if (reset) {
|
||||
teamPage.value = 1
|
||||
teamLoading.value = true
|
||||
}
|
||||
try {
|
||||
const orgId = orgIndex.value > 0 ? orgOptions.value[orgIndex.value - 1]?.id || 0 : 0
|
||||
const data = await fetchTeamDailyReports({
|
||||
date: teamDate.value,
|
||||
orgId,
|
||||
page: teamPage.value,
|
||||
pageSize: 20
|
||||
})
|
||||
const rows = data?.list || []
|
||||
teamList.value = reset ? rows : teamList.value.concat(rows)
|
||||
teamStats.value = data?.stats || { total: 0, submitted: 0, missing: 0, rate: 0 }
|
||||
teamHasMore.value = teamList.value.length < (data?.total || 0)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e?.message || '团队日报加载失败', icon: 'none' })
|
||||
} finally {
|
||||
teamLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function loadMoreTeam() {
|
||||
if (!teamHasMore.value || teamLoading.value) return
|
||||
teamPage.value += 1
|
||||
loadTeam(false)
|
||||
}
|
||||
|
||||
function onTeamDateChange(e) {
|
||||
teamDate.value = e.detail.value
|
||||
loadTeam(true)
|
||||
}
|
||||
|
||||
function onOrgChange(e) {
|
||||
orgIndex.value = Number(e.detail.value) || 0
|
||||
loadTeam(true)
|
||||
}
|
||||
|
||||
function openTeamReport(item) {
|
||||
if (!item.submitted) {
|
||||
uni.showToast({ title: '该成员当日未提交日报', icon: 'none' })
|
||||
return
|
||||
}
|
||||
openDetail(item.report_id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: $color-card;
|
||||
padding: 28rpx $page-padding-x 0;
|
||||
box-shadow: $shadow-header;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.write-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: $radius-full;
|
||||
background: $color-primary;
|
||||
color: #ffffff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 48rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
padding: 16rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
|
||||
&.active {
|
||||
color: $color-primary;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
width: 40rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 3rpx;
|
||||
background: $color-primary;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx $page-padding-x 0;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
@include card;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.summary-num {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.is-green {
|
||||
color: $color-success;
|
||||
}
|
||||
|
||||
.is-red {
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
.is-orange {
|
||||
color: $color-warning;
|
||||
}
|
||||
|
||||
.backfill-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-top: 20rpx;
|
||||
padding: 18rpx 24rpx;
|
||||
@include card;
|
||||
}
|
||||
|
||||
.backfill-label {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.date-chip {
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: $radius-sm;
|
||||
background: $color-primary-bg;
|
||||
color: $color-primary;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.backfill-btn {
|
||||
margin-left: auto;
|
||||
font-size: 24rpx;
|
||||
color: $color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
@include card;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.date-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.today-chip {
|
||||
@include chip;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: $color-text-secondary;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.team-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.team-stats {
|
||||
display: flex;
|
||||
margin-top: 20rpx;
|
||||
padding: 24rpx 0;
|
||||
@include card;
|
||||
}
|
||||
|
||||
.team-stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.member-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
@include card;
|
||||
padding: 22rpx 24rpx;
|
||||
margin-bottom: 18rpx;
|
||||
|
||||
&.clickable:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
|
||||
.member-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: $radius-md;
|
||||
background: linear-gradient(135deg, #60a5fa, #2563eb);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.member-org {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.member-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-time {
|
||||
font-size: 20rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.state-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 140rpx 0;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 0 8rpx;
|
||||
}
|
||||
|
||||
.load-link {
|
||||
font-size: 24rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.load-done {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -45,6 +45,7 @@ const NATIVE_RULES = [
|
||||
// 报销管理(注意放在日程规则之后即可,与日程关键词无冲突)
|
||||
{ pattern: /报销|reimburse|expense/i, route: '/pages/tools/expense/index' },
|
||||
// OA 协同办公
|
||||
{ pattern: /工作日报|日报|dailyreport/i, route: '/pages/oa/dailyreport/index' },
|
||||
{ pattern: /通知公告|公告|notice/i, route: '/pages/oa/notice/index' },
|
||||
{ pattern: /文档库|文档管理|文档图谱|document/i, route: '/pages/oa/document/index' },
|
||||
{ pattern: /薪酬|工资|薪资|compensation|payroll/i, route: '/pages/oa/compensation/index' },
|
||||
|
||||
Reference in new issue
Block a user