Merge branch 'feature-ui' into developer
This commit is contained in:
@@ -1,24 +1,560 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="home-container">
|
||||||
<div v-if="pageLoading">
|
<!-- 顶部欢迎区 -->
|
||||||
<el-main>
|
<el-row :gutter="20" class="welcome-section">
|
||||||
<el-card shadow="never">
|
<el-col :span="24">
|
||||||
<el-skeleton :rows="1"></el-skeleton>
|
<el-card shadow="never" class="welcome-card">
|
||||||
</el-card>
|
<div class="welcome-content">
|
||||||
<el-card shadow="never" style="margin-top: 15px;">
|
<div class="welcome-left">
|
||||||
<el-skeleton></el-skeleton>
|
<h2 class="welcome-title">欢迎回来,{{ userName }}</h2>
|
||||||
</el-card>
|
<p class="welcome-tip">这是您今天的工作台</p>
|
||||||
</el-main>
|
|
||||||
</div>
|
</div>
|
||||||
<widgets/>
|
<div class="welcome-right">
|
||||||
|
<div class="current-time">
|
||||||
|
<div class="time-display">{{ currentTime }}</div>
|
||||||
|
<div class="date-display">{{ currentDate }}</div>
|
||||||
|
<div class="week-display">{{ currentWeek }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 主体内容区 -->
|
||||||
|
<el-row :gutter="20" class="main-content">
|
||||||
|
<!-- 待办工作 -->
|
||||||
|
<el-col :xs="24" :sm="24" :md="16" :lg="17" :xl="18">
|
||||||
|
<el-card shadow="never" class="todo-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<SvgIcon name="ele-Tickets" class="card-icon" />
|
||||||
|
待办工作
|
||||||
|
</span>
|
||||||
|
<span class="todo-count" v-if="todoList.length > 0">{{ todoList.length }} 项待处理</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="todo-list" v-if="todoLoading">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
<div class="todo-list" v-else-if="todoList.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="item in todoList"
|
||||||
|
:key="item.id"
|
||||||
|
class="todo-item"
|
||||||
|
@click="handleTodoClick(item)"
|
||||||
|
>
|
||||||
|
<div class="todo-item-left">
|
||||||
|
<el-tag size="small" :type="getTodoType(item.priority)">
|
||||||
|
{{ getPriorityText(item.priority) }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="todo-title">{{ item.title || item.taskName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="todo-item-right">
|
||||||
|
<span class="todo-time">{{ formatTime(item.createTime) }}</span>
|
||||||
|
<el-icon class="todo-arrow"><ele-ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无待办工作" :image-size="60" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧内容区 -->
|
||||||
|
<el-col :xs="24" :sm="24" :md="8" :lg="7" :xl="6">
|
||||||
|
<!-- 通知公告 -->
|
||||||
|
<el-card shadow="never" class="notice-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<SvgIcon name="ele-Bell" class="card-icon" />
|
||||||
|
通知公告
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="notice-list" v-if="noticeLoading">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
<div class="notice-list" v-else-if="noticeList.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="item in noticeList"
|
||||||
|
:key="item.id"
|
||||||
|
class="notice-item"
|
||||||
|
@click="handleNoticeClick(item)"
|
||||||
|
>
|
||||||
|
<div class="notice-title">{{ item.title }}</div>
|
||||||
|
<div class="notice-time">{{ formatTime(item.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无通知公告" :image-size="60" />
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 站内提醒 -->
|
||||||
|
<el-card shadow="never" class="message-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<SvgIcon name="ele-Message" class="card-icon" />
|
||||||
|
站内提醒
|
||||||
|
</span>
|
||||||
|
<el-badge :value="unreadCount" :hidden="unreadCount === 0" :max="99" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="message-list" v-if="messageLoading">
|
||||||
|
<el-skeleton :rows="3" animated />
|
||||||
|
</div>
|
||||||
|
<div class="message-list" v-else-if="messageList.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="item in messageList"
|
||||||
|
:key="item.id"
|
||||||
|
class="message-item"
|
||||||
|
:class="{ 'is-unread': !item.readFlag }"
|
||||||
|
@click="handleMessageClick(item)"
|
||||||
|
>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-title">{{ item.title }}</div>
|
||||||
|
<div class="message-summary" v-if="item.content">{{ item.content }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="message-time">{{ formatTime(item.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无站内提醒" :image-size="60" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="dashboard">
|
<script setup lang="ts" name="home">
|
||||||
const Widgets = defineAsyncComponent(() => import('./widgets/index.vue'));
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
const pageLoading = ref(true);
|
import { useRouter } from 'vue-router'
|
||||||
|
import { fetchList as fetchPendingWorkList } from '/@/api/stuwork/pendingwork'
|
||||||
|
import { fetchList as fetchNoticeList } from '/@/api/jsonflow/ws-notice'
|
||||||
|
import { fetchUserMessageList, readUserMessage } from '/@/api/admin/message'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 用户信息(模拟,实际应从store获取)
|
||||||
|
const userName = ref('用户')
|
||||||
|
|
||||||
|
// 时间相关
|
||||||
|
const currentTime = ref('')
|
||||||
|
const currentDate = ref('')
|
||||||
|
const currentWeek = ref('')
|
||||||
|
let timeTimer: ReturnType<typeof setInterval>
|
||||||
|
|
||||||
|
// 待办工作
|
||||||
|
const todoList = ref<any[]>([])
|
||||||
|
const todoLoading = ref(true)
|
||||||
|
|
||||||
|
// 通知公告
|
||||||
|
const noticeList = ref<any[]>([])
|
||||||
|
const noticeLoading = ref(true)
|
||||||
|
|
||||||
|
// 站内提醒
|
||||||
|
const messageList = ref<any[]>([])
|
||||||
|
const messageLoading = ref(true)
|
||||||
|
const unreadCount = ref(0)
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
const updateTime = () => {
|
||||||
|
const now = new Date()
|
||||||
|
currentTime.value = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
currentDate.value = now.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
|
||||||
|
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||||
|
currentWeek.value = weekDays[now.getDay()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (time: string) => {
|
||||||
|
if (!time) return ''
|
||||||
|
const date = new Date(time)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
|
||||||
|
if (days === 0) {
|
||||||
|
return '今天'
|
||||||
|
} else if (days === 1) {
|
||||||
|
return '昨天'
|
||||||
|
} else if (days < 7) {
|
||||||
|
return `${days}天前`
|
||||||
|
} else {
|
||||||
|
return time.substring(0, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取待办工作优先级文本
|
||||||
|
const getPriorityText = (priority?: number) => {
|
||||||
|
const map: Record<number, string> = {
|
||||||
|
0: '普通',
|
||||||
|
1: '重要',
|
||||||
|
2: '紧急'
|
||||||
|
}
|
||||||
|
return map[priority || 0] || '普通'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取待办工作优先级类型
|
||||||
|
const getTodoType = (priority?: number) => {
|
||||||
|
const map: Record<number, string> = {
|
||||||
|
0: 'info',
|
||||||
|
1: 'warning',
|
||||||
|
2: 'danger'
|
||||||
|
}
|
||||||
|
return map[priority || 0] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载待办工作
|
||||||
|
const loadTodoList = async () => {
|
||||||
|
try {
|
||||||
|
todoLoading.value = true
|
||||||
|
const res = await fetchPendingWorkList({ size: 10 })
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
todoList.value = res.data.rows || res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载待办工作失败:', e)
|
||||||
|
} finally {
|
||||||
|
todoLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载通知公告
|
||||||
|
const loadNoticeList = async () => {
|
||||||
|
try {
|
||||||
|
noticeLoading.value = true
|
||||||
|
const res = await fetchNoticeList({ size: 10 })
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
noticeList.value = res.data.rows || res.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载通知公告失败:', e)
|
||||||
|
} finally {
|
||||||
|
noticeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载站内提醒
|
||||||
|
const loadMessageList = async () => {
|
||||||
|
try {
|
||||||
|
messageLoading.value = true
|
||||||
|
const res = await fetchUserMessageList({ size: 10 })
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
const list = res.data.rows || res.data || []
|
||||||
|
messageList.value = list
|
||||||
|
unreadCount.value = list.filter((item: any) => !item.readFlag).length
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载站内提醒失败:', e)
|
||||||
|
} finally {
|
||||||
|
messageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击待办项
|
||||||
|
const handleTodoClick = (item: any) => {
|
||||||
|
// 根据待办类型跳转到对应页面
|
||||||
|
if (item.businessType) {
|
||||||
|
router.push({
|
||||||
|
path: '/stuwork/pendingwork',
|
||||||
|
query: { id: item.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击通知公告
|
||||||
|
const handleNoticeClick = (item: any) => {
|
||||||
|
router.push({
|
||||||
|
path: '/jsonflow/ws-notice',
|
||||||
|
query: { id: item.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击站内提醒
|
||||||
|
const handleMessageClick = async (item: any) => {
|
||||||
|
// 标记已读
|
||||||
|
if (!item.readFlag) {
|
||||||
|
try {
|
||||||
|
await readUserMessage({ id: item.id })
|
||||||
|
item.readFlag = true
|
||||||
|
unreadCount.value = Math.max(0, unreadCount.value - 1)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('标记已读失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
pageLoading.value = false;
|
updateTime()
|
||||||
});
|
timeTimer = setInterval(updateTime, 1000)
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
loadTodoList()
|
||||||
|
loadNoticeList()
|
||||||
|
loadMessageList()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timeTimer) {
|
||||||
|
clearInterval(timeTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.home-container {
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-card {
|
||||||
|
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-left {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-right {
|
||||||
|
text-align: right;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time {
|
||||||
|
.time-display {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'DIN Alternate', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-display {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-display {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
.el-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-arrow {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item:hover .todo-arrow {
|
||||||
|
transform: translateX(4px);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-list,
|
||||||
|
.message-list {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-unread {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
border-left: 3px solid var(--el-color-primary);
|
||||||
|
padding-left: 5px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-summary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-empty) {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.welcome-content {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-right {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user