前端首页的待办卡片中新增基于localStorage的待办添加与删除功能,并优化页面顶部的消息卡片

This commit is contained in:
zhoumingfa 2024-08-03 23:38:05 +08:00
parent 7d8879abcb
commit 694aa18452
13 changed files with 337 additions and 181 deletions

View File

@ -30,4 +30,6 @@ export default {
HOME_QUICK_ENTRY: `${KEY_PREFIX}home_quick_entry`,
// 通知信息已读
NOTICE_READ: `${KEY_PREFIX}notice_read`,
// 待办
TO_BE_DONE: `${KEY_PREFIX}to_be_done`,
};

View File

@ -37,7 +37,6 @@
import { computed, ref, onMounted } from 'vue';
import { loginApi } from '/src/api/system/login-api';
import { useUserStore } from '/@/store/modules/system/user';
import { localClear } from '/@/utils/local-util';
import { smartSentry } from '/@/lib/smart-sentry';
import HeaderResetPassword from './header-reset-password-modal/index.vue';
import { useRouter } from 'vue-router';
@ -53,7 +52,6 @@
} catch (e) {
smartSentry.captureError(e);
} finally {
localClear();
useUserStore().logout();
location.reload();
}

View File

@ -10,19 +10,21 @@
<template>
<a-dropdown trigger="click" v-model:open="show">
<a-button type="text" @click="queryMessage" style="padding: 4px 5px">
<a-badge :count="unreadMessageCount">
<div style="width: 26px; height: 26px">
<BellOutlined :style="{ fontSize: '16px' }" />
</div>
</a-badge>
</a-button>
<a-badge :count="unreadMessageCount + toBeDoneCount">
<div style="width: 26px; height: 26px">
<BellOutlined :style="{ fontSize: '16px' }" />
</div>
</a-badge>
<template #overlay>
<a-card class="message-container" :bodyStyle="{ padding: 0 }">
<a-spin :spinning="loading">
<a-tabs class="dropdown-tabs" centered :tabBarStyle="{ textAlign: 'center' }" style="width: 300px">
<a-tab-pane tab="未读消息" key="message">
<a-tab-pane key="message">
<template #tab>
未读消息
<a-badge :count="unreadMessageCount" showZero :offset="[0, -20]" />
</template>
<a-list class="tab-pane" size="small">
<a-list-item v-for="item in messageList" :key="item.messageId">
<a-list-item-meta>
@ -36,16 +38,34 @@
</template>
</a-list-item-meta>
</a-list-item>
<a-list-item v-if="unreadMessageCount !== 0">
<a-button type="text" @click="gotoMessage" style="padding: 4px 5px"> ... 查看更多 </a-button>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane tab="待办工作" key="todo">
<a-list class="tab-pane" />
<a-tab-pane key="TO_BE_DONE">
<template #tab>
待办工作
<a-badge :count="toBeDoneCount" showZero :offset="[0, -20]" />
</template>
<a-list class="tab-pane" size="small" :locale="{ emptyText: '暂无待办' }">
<a-list-item v-for="(item, index) in toBeDoneList" :key="index">
<a-list-item-meta>
<template #title>
<a-badge status="error" />
<a-tag v-if="item.starFlag" color="red">重要</a-tag>
<span>{{ item.title }}</span>
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-spin>
</a-card>
</template>
</a-dropdown>
<MessageDetail ref="messageDetailRef" @refresh="queryMessage" />
</template>
<script setup>
@ -57,6 +77,9 @@
import dayjs from 'dayjs';
import { theme } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import MessageDetail from '/@/views/system/account/components/message/components/message-detail.vue';
import localKey from '/@/constants/local-storage-key-const';
import { localRead } from '/@/utils/local-util';
const { useToken } = theme;
const { token } = useToken();
@ -64,6 +87,8 @@
defineExpose({ showMessage });
function showMessage() {
queryMessage();
loadToBeDoneList();
show.value = true;
}
@ -90,6 +115,8 @@
readFlag: false,
});
messageList.value = responseModel.data.list;
//
useUserStore().queryUnreadMessageCount();
} catch (e) {
smartSentry.captureError(e);
} finally {
@ -103,6 +130,30 @@
router.push({ path: '/account', query: { menuId: 'message' } });
}
// ------------------------- -------------------------
//
const toBeDoneCount = computed(() => {
return useUserStore().toBeDoneCount;
});
//
const toBeDoneList = ref([]);
const loadToBeDoneList = async () => {
try {
loading.value = true;
let localToBeDoneList = localRead(localKey.TO_BE_DONE);
if (localToBeDoneList) {
toBeDoneList.value = JSON.parse(localToBeDoneList).filter((e) => !e.doneFlag);
}
} catch (err) {
smartSentry.captureError(err);
} finally {
loading.value = false;
}
};
// ------------------------- -------------------------
function timeago(dateStr) {
let dateTimeStamp = dayjs(dateStr).toDate().getTime();
@ -189,4 +240,9 @@
background-color: @base-bg-color;
border-radius: 4px;
}
.tab-pane {
height: 250px;
overflow-y: auto;
}
</style>

View File

@ -18,7 +18,9 @@
size="small"
/>
<!---消息通知--->
<HeaderMessage ref="headerMessage" />
<a-button type="text" @click="showMessage" style="padding: 4px 5px">
<HeaderMessage ref="headerMessage" />
</a-button>
<!---国际化--->
<!-- <a-button type="text" @click="showSetting" class="operate-icon">
<template #icon><switcher-outlined /></template>

View File

@ -9,7 +9,8 @@
*/
import { message, Modal } from 'ant-design-vue';
import axios from 'axios';
import { localClear, localRead } from '/@/utils/local-util';
import { localRead } from '/@/utils/local-util';
import { useUserStore } from '/@/store/modules/system/user';
import { decryptData, encryptData } from './encrypt';
import { DATA_TYPE_ENUM } from '../constants/common-const';
import _ from 'lodash';
@ -25,7 +26,7 @@ const smartAxios = axios.create({
// 退出系统
function logout() {
localClear();
useUserStore.logout();
location.href = '/';
}

View File

@ -16,7 +16,7 @@ import { PAGE_PATH_404, PAGE_PATH_LOGIN } from '/@/constants/common-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import SmartLayout from '../layout/index.vue';
import { useUserStore } from '/@/store/modules/system/user';
import { localClear, localRead } from '/@/utils/local-util';
import { localRead } from '/@/utils/local-util';
import _ from 'lodash';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
@ -41,7 +41,7 @@ router.beforeEach(async (to, from, next) => {
// 验证登录
const token = localRead(LocalStorageKeyConst.USER_TOKEN);
if (!token) {
localClear();
useUserStore().logout();
next({ path: PAGE_PATH_LOGIN });
return;
}

View File

@ -12,10 +12,10 @@ import { defineStore } from 'pinia';
import localKey from '/@/constants/local-storage-key-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import { localClear, localRead, localSave } from '/@/utils/local-util';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const';
import { messageApi } from '/@/api/support/message-api.js';
import { smartSentry } from '/@/lib/smart-sentry.js';
import { localRead, localSave, localRemove } from '/@/utils/local-util';
export const useUserStore = defineStore({
id: 'userStore',
@ -61,13 +61,15 @@ export const useUserStore = defineStore({
keepAliveIncludes: [],
// 未读消息数量
unreadMessageCount: 0,
// 待办工作数
toBeDoneCount: 0,
}),
getters: {
getToken(state) {
if (state.token) {
return state.token;
}
return localRead(LocalStorageKeyConst.USER_TOKEN);
return localRead(localKey.USER_TOKEN);
},
//是否初始化了 路由
getMenuRouterInitFlag(state) {
@ -115,7 +117,9 @@ export const useUserStore = defineStore({
this.tagNav = [];
this.userInfo = {};
this.unreadMessageCount = 0;
localClear();
localRemove(localKey.USER_TOKEN);
localRemove(localKey.USER_POINTS);
localRemove(localKey.USER_TAG_NAV);
},
// 查询未读消息数量
async queryUnreadMessageCount() {
@ -126,6 +130,16 @@ export const useUserStore = defineStore({
smartSentry.captureError(e);
}
},
async queryToBeDoneList() {
try {
let localToBeDoneList = localRead(localKey.TO_BE_DONE);
if (localToBeDoneList) {
this.toBeDoneCount = JSON.parse(localToBeDoneList).filter((e) => !e.doneFlag).length;
}
} catch (err) {
smartSentry.captureError(err);
}
},
//设置登录信息
setUserLoginInfo(data) {
// 用户基本信息
@ -157,6 +171,8 @@ export const useUserStore = defineStore({
// 获取用户未读消息
this.queryUnreadMessageCount();
// 获取待办工作数
this.queryToBeDoneList();
},
setToken(token) {
this.token = token;

View File

@ -19,3 +19,7 @@ export const localRead = (key) => {
export const localClear = () => {
localStorage.clear();
};
export const localRemove = (key) => {
localStorage.removeItem(key);
};

View File

@ -43,7 +43,7 @@
</a-form>
<a-table size="small" :dataSource="tableData" :columns="columns" rowKey="messageId" :pagination="false" bordered>
<template #bodyCell="{ text, record, index, column }">
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'messageType'">
<span>{{ $smartEnumPlugin.getDescByValue('MESSAGE_TYPE_ENUM', text) }}</span>
</template>

View File

@ -1,158 +0,0 @@
<!--
* 已办/代办
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<default-home-card icon="Star" title="已办待办">
<div style="height: 280px">
<div class="center column">
<a-space direction="vertical" style="width: 100%">
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
</div>
<div v-for="(item, index) in doneList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
</div>
</a-space>
</div>
</div>
</default-home-card>
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import { computed, ref } from 'vue';
import dayjs from 'dayjs';
import { message } from 'ant-design-vue';
let taskList = ref([
{
title: '周五下班前需要提交周报',
doneFlag: true,
starFlag: true,
starTime: 0,
},
{
title: '为SmartAdmin前端小组分配任务',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '跟进团建内容事宜',
doneFlag: false,
starFlag: true,
starTime: 0,
},
{
title: '跟进客户定制一个软件平台',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '下个版本的需求确认',
doneFlag: false,
starFlag: false,
starTime: 0,
},
{
title: '线上版本发布',
doneFlag: true,
starFlag: true,
starTime: dayjs().unix(),
},
{
title: '周一财务报销',
doneFlag: true,
starFlag: false,
starTime: 0,
},
]);
let toDoList = computed(() => {
return taskList.value.filter((e) => !e.doneFlag).sort((a, b) => b.starTime - a.starTime);
});
let doneList = computed(() => {
return taskList.value.filter((e) => e.doneFlag);
});
function itemStar(item) {
item.starFlag = !item.starFlag;
if (item.starFlag) {
item.starTime = dayjs().unix();
}
}
//------------------------------------------------
let taskTitle = ref('');
function addTask() {
if (!taskTitle.value) {
message.warn('请输入任务标题');
return;
}
let data = {
title: taskTitle.value,
doneFlag: false,
starFlag: false,
starTime: 0,
};
taskList.value.unshift(data);
taskTitle.value = '';
}
</script>
<style lang="less" scoped>
.center {
display: flex;
justify-content: center;
height: 100%;
&.column {
flex-direction: column;
width: 100%;
padding: 0 10px;
justify-content: flex-start;
}
}
.to-do {
width: 100%;
border: 1px solid #d3d3d3;
border-radius: 4px;
padding: 4px;
display: flex;
align-items: center;
.star-icon {
margin-left: auto;
cursor: pointer;
}
&.done {
text-decoration: line-through;
color: #8c8c8c;
.task {
color: #8c8c8c;
}
}
}
</style>

View File

@ -0,0 +1,172 @@
<!--
* 已办/代办
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-09-12 22:34:00
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<default-home-card extra="添加" icon="StarTwoTone" title="待办工作" @extraClick="showAddToBeDone">
<div style="height: 280px">
<div class="center column">
<a-space direction="vertical" style="width: 100%">
<a-empty v-if="$lodash.isEmpty(toBeDoneList)" description="暂无待办工作" />
<div v-for="(item, index) in toDoList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag" @change="handleCheckbox">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div v-if="!item.doneFlag" class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
<close-circle-outlined class="delete-icon" @click="toDelete(item)" />
</div>
<div v-for="(item, index) in doneList" :key="index" :class="['to-do', { done: item.doneFlag }]">
<a-checkbox v-model:checked="item.doneFlag" @change="handleCheckbox">
<span class="task">{{ item.title }}</span>
</a-checkbox>
<div v-if="!item.doneFlag" class="star-icon" @click="itemStar(item)">
<StarFilled v-if="item.starFlag" style="color: #ff8c00" />
<StarOutlined v-else style="color: #c0c0c0" />
</div>
<close-circle-outlined class="delete-icon" @click="toDelete(item)" />
</div>
</a-space>
</div>
</div>
</default-home-card>
<ToBeDoneModal ref="toBeDoneModalRef" @addToBeDone="addToBeDone" />
</template>
<script setup>
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import ToBeDoneModal from './to-be-done-modal.vue';
import localKey from '/@/constants/local-storage-key-const';
import { localRead, localSave } from '/@/utils/local-util';
import { useUserStore } from '/@/store/modules/system/user.js';
import { computed, ref, onMounted } from 'vue';
import { Modal } from 'ant-design-vue';
let toBeDoneList = ref([]);
onMounted(() => {
initTaskList();
});
function initTaskList() {
let localTaskList = localRead(localKey.TO_BE_DONE);
if (localTaskList) {
toBeDoneList.value = JSON.parse(localTaskList);
}
}
let toDoList = computed(() => {
return toBeDoneList.value.filter((e) => !e.doneFlag);
});
let doneList = computed(() => {
return toBeDoneList.value.filter((e) => e.doneFlag);
});
function handleCheckbox(e) {
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
}
function itemStar(data) {
data.starFlag = !data.starFlag;
const index = toBeDoneList.value.findIndex((item) => item.title === data.title);
toBeDoneList.value.splice(index, 1);
if (data.starFlag) {
toBeDoneList.value.unshift(data);
} else {
toBeDoneList.value.push(data);
}
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
}
//------------------------------------------------
let toBeDoneModalRef = ref();
function showAddToBeDone() {
toBeDoneModalRef.value.showModal();
}
//
function addToBeDone(data) {
toBeDoneList.value.unshift(data);
useUserStore().toBeDoneCount = toBeDoneList.value.length;
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
}
function toDelete(data) {
if (!data.doneFlag) {
Modal.confirm({
title: '提示',
content: '确定要删除吗?',
okText: '删除',
okType: 'danger',
onOk() {
deleteToBeDone(data);
},
cancelText: '取消',
onCancel() {},
});
} else {
deleteToBeDone(data);
}
}
//
function deleteToBeDone(data) {
const index = toBeDoneList.value.findIndex((item) => item.title === data.title);
toBeDoneList.value.splice(index, 1);
useUserStore().toBeDoneCount = toBeDoneList.value.length;
localSave(localKey.TO_BE_DONE, JSON.stringify(toBeDoneList.value));
}
</script>
<style lang="less" scoped>
.center {
display: flex;
justify-content: center;
height: 100%;
overflow-y: auto;
&.column {
flex-direction: column;
width: 100%;
padding: 0 10px;
justify-content: flex-start;
}
}
.to-do {
width: 100%;
border: 1px solid #d3d3d3;
border-radius: 4px;
padding: 4px;
display: flex;
align-items: center;
.star-icon {
margin-left: auto;
cursor: pointer;
}
&.done {
text-decoration: line-through;
color: #8c8c8c;
.task {
color: #8c8c8c;
}
}
}
.delete-icon {
color: #f08080;
padding-left: 10px;
top: -5px;
right: -5px;
float: right;
}
</style>

View File

@ -0,0 +1,63 @@
<template>
<a-modal v-model:open="visible" title="新建待办" @close="onClose">
<a-form ref="formRef" :model="form" :rules="rules">
<a-form-item label="标题" name="title">
<a-input v-model:value="form.title" placeholder="请输入标题" />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">确定</a-button>
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import _ from 'lodash';
defineExpose({
showModal,
});
const emit = defineEmits(['addToBeDone']);
// ref
const formRef = ref();
const formDefault = {
title: undefined,
doneFlag: false,
starFlag: false,
starTime: 0,
};
let form = reactive({ ...formDefault });
const rules = {
title: [{ required: true, message: '标题不能为空' }],
};
const visible = ref(false);
function showModal() {
visible.value = true;
}
function onClose() {
Object.assign(form, formDefault);
visible.value = false;
}
function onSubmit() {
formRef.value
.validate()
.then(() => {
emit('addToBeDone', _.cloneDeep(form));
onClose();
})
.catch((error) => {
console.log('error', error);
message.error('参数验证错误,请仔细填写表单数据!');
});
}
</script>
<style lang="less" scoped></style>

View File

@ -70,7 +70,7 @@
import HomeNotice from './home-notice.vue';
import HomeQuickEntry from './components/quick-entry/home-quick-entry.vue';
import OfficialAccountCard from './components/official-account-card.vue';
import ToBeDoneCard from './components/to-be-done-card.vue';
import ToBeDoneCard from './components/to-be-done-card/home-to-be-done.vue';
import ChangelogCard from './components/changelog-card.vue';
import Gauge from './components/echarts/gauge.vue';
import Category from './components/echarts/category.vue';