博客支持查看待办事项与日历计划

This commit is contained in:
xiaozzzi 2023-11-17 15:58:57 +08:00
parent 2e6df9491b
commit 8ceb7ace71
15 changed files with 1259 additions and 120 deletions

View File

@ -11,13 +11,17 @@ declare module 'vue' {
BLRow: typeof import('./src/components/BLRow.vue')['default']
BLTag: typeof import('./src/components/BLTag.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputPassword: typeof import('element-plus/es')['ElInputPassword']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
NotFound: typeof import('./src/components/NotFound.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@ -0,0 +1 @@
declare module 'element-plus/dist/locale/zh-cn.mjs'

View File

@ -9,7 +9,6 @@
<style>
* {
font-family: 'ubuntu mono', 'Consolas', 'Menlo', 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
/* font-family: 'Consolas', 'Menlo', 'PingFang SC', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; */
box-sizing: border-box;
}

View File

@ -1,9 +1,10 @@
<template>
<el-config-provider :size="'small'">
<el-config-provider :locale="zhCn">
<RouterView />
</el-config-provider>
</template>
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
</script>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

View File

@ -1,5 +1,5 @@
<template>
<span
<div
class="tag-root"
:style="{
color: props.color,
@ -7,12 +7,9 @@
fontSize: props.size + 'px',
fontWeight: props.weight
}">
<!-- {{ !!slots.default }}| -->
<span v-if="props.icon" :class="['tag-iconfont iconbl', props.icon, !!slots.default ? 'tag-icon-margin' : '']" />
<span class="tag-content">
<slot />
</span>
</span>
<span class="tag-content"><slot /></span>
</div>
</template>
<script setup lang="ts">
@ -56,15 +53,16 @@ const props = defineProps({
@include flex(row, center, center);
box-shadow: 2px 2px 3px 0 #999999;
border-radius: 4px;
padding: 1px 4px;
padding: 0px 4px;
margin: 3px;
height: 15px;
min-height: 15px;
max-height: 15px;
text-align: center;
width: auto;
.tag-iconfont {
font-size: 12px;
font-size: 14px;
}
.tag-icon-margin {
@ -72,8 +70,10 @@ const props = defineProps({
}
.tag-content {
font-size: inherit;
font-weight: inherit;
line-height: 12px;
transform: scale(0.9);
white-space: nowrap;
}
}
</style>

View File

@ -11,6 +11,8 @@ const Index = () => import('../views/Index.vue')
const Home = () => import('../views/index/Home.vue')
const Login = () => import('@/views/index/Login.vue')
const Articles = () => import('@/views/article/Articles.vue')
const TodoIndex = () => import('@/views/todo/TodoIndex.vue')
const PlanIndex = () => import('@/views/plan/PlanIndex.vue')
router.addRoute({ path: '/404', component: NotFound })
router.addRoute({ path: '/:pathMatch(.*)', redirect: '/404' })
@ -23,6 +25,8 @@ router.addRoute({
children: [
{ path: '/home', name: 'Home', component: Home, meta: { keepAlive: true } },
{ path: '/login', name: 'Login', component: Login, meta: { keepAlive: true } },
{ path: '/articles', name: 'Articles', component: Articles, meta: { keepAlive: false } }
{ path: '/articles', name: 'Articles', component: Articles, meta: { keepAlive: false } },
{ path: '/todo', name: 'TodoIndex', component: TodoIndex, meta: { keepAlive: false } },
{ path: '/plan', name: 'PlanIndex', component: PlanIndex, meta: { keepAlive: false } }
]
})

View File

@ -47,6 +47,14 @@ export const useUserStore = defineStore('userStore', {
auth: Local.get(storeKey) || initAuth(),
userinfo: Local.get(userinfoKey) || initUserinfo()
}),
getters: {
isLogin: (state): boolean => {
if (!state.auth) {
return false
}
return state.auth.status === AuthStatus.Succ
}
},
actions: {
// /**
// * 根据用户名密码登录

View File

@ -28,46 +28,60 @@
<div v-for="L1 in docTreeData" :key="L1.i" class="menu-level-one">
<el-menu-item v-if="isEmpty(L1.children)" :index="L1.i">
<template #title>
<DocTitle :trees="L1" @click-doc="clickCurDoc" />
<div class="menu-item-wrapper" @click="clickCurDoc(L1)">
<DocTitle :trees="L1" :level="1" />
</div>
</template>
</el-menu-item>
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L1.i">
<template #title>
<DocTitle :trees="L1" @click-doc="clickCurDoc" style="font-size: 15px" />
<div class="menu-item-wrapper">
<DocTitle :trees="L1" :level="1" style="font-size: 15px" />
</div>
</template>
<!-- ================================================ L2 ================================================ -->
<div v-for="L2 in L1.children" :key="L2.i">
<el-menu-item v-if="isEmpty(L2.children)" :index="L2.i">
<template #title>
<DocTitle :trees="L2" @click-doc="clickCurDoc" />
<div class="menu-item-wrapper" @click="clickCurDoc(L2)">
<DocTitle :trees="L2" :level="2" />
</div>
</template>
</el-menu-item>
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L2.i">
<template #title>
<DocTitle :trees="L2" @click-doc="clickCurDoc" />
<div class="menu-item-wrapper">
<DocTitle :trees="L2" :level="2" />
</div>
</template>
<!-- ================================================ L3 ================================================ -->
<div v-for="L3 in L2.children" :key="L3.i">
<el-menu-item v-if="isEmpty(L3.children)" :index="L3.i">
<template #title>
<DocTitle :trees="L3" @click-doc="clickCurDoc" />
<div class="menu-item-wrapper" @click="clickCurDoc(L3)">
<DocTitle :trees="L3" :level="3" />
</div>
</template>
</el-menu-item>
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L3.i">
<template #title>
<DocTitle :trees="L3" @click-doc="clickCurDoc" />
<div class="menu-item-wrapper">
<DocTitle :trees="L3" :level="3" />
</div>
</template>
<!-- ================================================ L4 ================================================ -->
<div v-for="L4 in L3.children" :key="L4.i">
<el-menu-item v-if="isEmpty(L4.children)" :index="L4.i">
<template #title>
<DocTitle :trees="L4" @click-doc="clickCurDoc" />
<div class="menu-item-wrapper" @click="clickCurDoc(L4)">
<DocTitle :trees="L4" :level="4" />
</div>
</template>
</el-menu-item>
</div>
@ -117,12 +131,14 @@
import { useRoute } from 'vue-router'
import { ref, onActivated, onUnmounted, onMounted } from 'vue'
import { ArrowDownBold, ArrowRightBold } from '@element-plus/icons-vue'
import { articleInfoOpenApi, docTreeApi } from '@/api/blossom'
import { articleInfoOpenApi, articleInfoApi, docTreeOpenApi, docTreeApi } from '@/api/blossom'
import { useUserStore } from '@/stores/user'
import { isNull, isEmpty, isNotNull } from '@/assets/utils/obj'
import DocTitle from './DocTitle.vue'
import IndexHeader from '../index/IndexHeader.vue'
import 'katex/dist/katex.min.css'
import { toRoute } from '@/router'
const userStore = useUserStore()
onMounted(() => {
window.onHtmlEventDispatch = onHtmlEventDispatch
@ -186,16 +202,23 @@ const getDocTree = () => {
docTreeLoading.value = true
docTreeData.value = []
defaultOpeneds.value = []
docTreeApi({ onlyOpen: true })
.then((resp) => {
docTreeData.value = resp.data
docTreeData.value.forEach((l1: DocTree) => {
defaultOpeneds.value.push(l1.i.toString())
})
})
.finally(() => {
docTreeLoading.value = false
const then = (resp: any) => {
docTreeData.value = resp.data
docTreeData.value.forEach((l1: DocTree) => {
defaultOpeneds.value.push(l1.i.toString())
})
}
if (userStore.isLogin) {
docTreeApi()
.then((resp) => then(resp))
.finally(() => (docTreeLoading.value = false))
} else {
docTreeOpenApi()
.then((resp) => then(resp))
.finally(() => (docTreeLoading.value = false))
}
}
const clickCurDoc = async (tree: DocTree) => {
@ -210,13 +233,16 @@ const clickCurDoc = async (tree: DocTree) => {
* 如果点击的是文章, 则查询文章信息和正文, 并在编辑器中显示.
*/
const getCurEditArticle = async (id: number) => {
await articleInfoOpenApi({ id: id, showToc: true, showMarkdown: false, showHtml: true })
.then((resp) => {
if (isNull(resp.data)) return
article.value = resp.data
tocList.value = JSON.parse(resp.data.toc)
})
.finally(() => {})
const then = (resp: any) => {
if (isNull(resp.data)) return
article.value = resp.data
tocList.value = JSON.parse(resp.data.toc)
}
if (userStore.isLogin) {
await articleInfoApi({ id: id, showToc: true, showMarkdown: false, showHtml: true }).then((resp) => then(resp))
} else {
await articleInfoOpenApi({ id: id, showToc: true, showMarkdown: false, showHtml: true }).then((resp) => then(resp))
}
}
const toScroll = (level: number, content: string) => {
@ -384,23 +410,32 @@ const onresize = () => {
border-right: 1px solid var(--el-border-color);
font-weight: 200;
transition: 0.1s;
&:hover {
:deep(.folder-level-line) {
opacity: 1;
}
}
.menu-level-one {
margin-top: 10px;
margin-top: 8px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 10px;
padding-bottom: 8px;
&:first-child {
margin-top: 0px;
}
}
.menu-item-wrapper {
width: 100%;
}
.doc-trees {
@include box(100%, 100%);
font-weight: 200;
padding-right: 0;
border: 0;
overflow-y: overlay;
overflow-y: scroll;
// padding-right: 6px;
// padding
--el-menu-base-level-padding: 25px;
@ -433,8 +468,8 @@ const onresize = () => {
border-radius: 5px;
.el-sub-menu__icon-arrow {
right: calc(220px - var(--el-menu-level) * 10px);
font-size: 12px;
right: calc(220px - var(--el-menu-level) * 14px);
color: #b3b3b3;
width: 0.8em;
height: 0.8em;
}
@ -566,7 +601,7 @@ const onresize = () => {
width: 1260px;
max-width: 1260px;
overflow-y: overlay;
padding: 0 20px;
padding: 0 30px;
.bl-preview {
$borderRadius: 4px;
@ -1010,7 +1045,7 @@ const onresize = () => {
}
.article {
padding: 0 20px;
padding: 0 10px;
overflow-x: hidden;
.bl-preview {

View File

@ -1,5 +1,5 @@
<template>
<div :class="['doc-title', props.trees.t?.includes('subject') ? 'subject-title' : '']" @click="handlClick">
<div :class="['doc-title']">
<div class="doc-name">
<img
class="menu-icon-img"
@ -8,13 +8,18 @@
<svg v-else-if="isNotBlank(props.trees.icon)" class="icon menu-icon" aria-hidden="true">
<use :xlink:href="'#' + props.trees.icon"></use>
</svg>
<el-tooltip :content="props.trees.n" placement="top" :show-after="1000" :hide-after="0" :transition="'none'" :offset="2" :persistent="false">
<div class="name-wrapper" :style="nameWrapperStyle">
{{ props.trees.n }}
</div>
</el-tooltip>
<div class="name-wrapper" :style="nameWrapperStyle">
{{ props.trees.n }}
</div>
<bl-tag v-for="tag in tags" style="margin-top: 5px" :bg-color="tag.bgColor" :icon="tag.icon">{{ tag.content }}</bl-tag>
</div>
<div v-if="level === 2" class="folder-level-line" style="left: -26px"></div>
<div v-if="level === 3" class="folder-level-line" style="left: -36px"></div>
<div v-if="level === 3" class="folder-level-line" style="left: -22px"></div>
<!-- -->
<div v-if="level === 4" class="folder-level-line" style="left: -46px"></div>
<div v-if="level === 4" class="folder-level-line" style="left: -32px"></div>
<div v-if="level === 4" class="folder-level-line" style="left: -18px"></div>
</div>
</template>
@ -22,12 +27,12 @@
import { computed } from 'vue'
import type { PropType } from 'vue'
import { isNotBlank } from '@/assets/utils/obj'
import BLTag from '@/components/BLTag.vue'
//#region ----------------------------------------< >--------------------------------------
const props = defineProps({
trees: { type: Object as PropType<DocTree>, default: {} }
trees: { type: Object as PropType<DocTree>, default: {} },
level: { type: Number, required: true }
})
const nameWrapperStyle = computed(() => {
@ -53,16 +58,7 @@ const tags = computed(() => {
return icons
})
/**
* 点击文档菜单标题后的回调
*/
const handlClick = () => {
emits('clickDoc', props.trees)
}
//#endregion
const emits = defineEmits(['clickDoc'])
</script>
<style scoped lang="scss">
@ -96,33 +92,33 @@ $icon-size: 17px;
@include ellipsis();
}
}
.sort {
position: absolute;
padding: 0 2px;
right: 0px;
top: 2px;
z-index: 10;
}
.folder-level-line {
height: 100%;
}
}
// ,
.subject-title {
position: relative;
padding: 2px 5px;
margin: 5px 0 10px 0;
border-radius: 4px;
box-shadow: 1px 1px 5px #a2a2a2;
background: linear-gradient(135deg, #ffffff, #f0f0f0, #cacaca);
.doc-name {
min-width: 145px;
max-width: 145px;
color: #4a545e;
text-shadow: 2px 2px 3px #9393939d;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.folder-level-line {
width: 1.5px;
background-color: var(--el-border-color);
box-sizing: border-box;
position: absolute;
opacity: 0;
transition: opacity 0.5s;
}
// ,
.subject-title {
@include flex(row, flex-start, flex-start);
box-shadow: 1px 1px 5px #a2a2a2;
background: linear-gradient(135deg, #ffffff, #f0f0f0, #cacaca);
box-shadow: 1px 1px 5px #a2a2a2;
max-width: calc(100% - 15px);
min-width: calc(100% - 15px);
padding: 2px 5px;
@ -132,6 +128,7 @@ $icon-size: 17px;
.doc-name {
@include flex(row, flex-start, flex-start);
color: var(--el-color-primary);
align-content: flex-start;
flex-wrap: wrap;
width: 100%;
@ -153,5 +150,15 @@ $icon-size: 17px;
min-width: calc(100% - 25px);
}
}
.sort {
position: absolute;
right: -15px;
}
.folder-level-line {
height: calc(100% + 25px);
top: -5px;
}
}
</style>

View File

@ -39,9 +39,11 @@
</div>
</template>
<div class="popper-content">
<div class="item"><span class="iconbl bl-a-texteditorhighlightcolor-line"></span>文章列表</div>
<div class="item"><span class="iconbl bl-calendar-line"></span>日历计划</div>
<div class="item"><span class="iconbl bl-a-labellist-line"></span>待办事项</div>
<div class="item" @click="toRoute('/home')"><span class="iconbl bl-a-home1-line"></span>首页</div>
<div class="item-divider"></div>
<div class="item" @click="toRoute('/articles')"><span class="iconbl bl-a-texteditorhighlightcolor-line"></span>文章列表</div>
<div class="item" @click="toRoute('/todo')"><span class="iconbl bl-a-labellist-line"></span>待办事项</div>
<div class="item" @click="toRoute('/plan')"><span class="iconbl bl-calendar-line"></span>日历计划</div>
<div class="item"><span class="iconbl bl-note-line"></span>便签</div>
<div class="item-divider"></div>
<div class="item" @click="handlLogout"><span class="iconbl bl-logout-circle-line"></span>退出登录</div>
@ -144,6 +146,12 @@ const handlLogout = () => {
color: #909090;
padding: 0 10px;
text-shadow: 3px 3px 5px #000;
user-select: none;
transition: color 0.3s;
&:hover {
color: #fff3fc;
}
.iconbl {
font-size: 18px;
@ -158,6 +166,7 @@ const handlLogout = () => {
border-radius: 5px;
transition: 0.3s;
white-space: pre-line;
text-shadow: 3px 3px 5px #1d1d1d;
cursor: pointer;
img {
@ -171,8 +180,8 @@ const handlLogout = () => {
&:hover {
font-weight: bold;
color: #e8e8e8;
text-shadow: 3px 3px 10px #cccccc;
color: #fff3fc;
// text-shadow: 3px 3px 10px #cccccc;
}
}

View File

@ -29,46 +29,14 @@ import { ref } from 'vue'
import { toRoute } from '@/router'
import { login } from '@/scripts/auth'
// const userStore = useUserStore()
// const { auth, userinfo } = storeToRefs(userStore)
const formLogin = ref({
username: '',
password: ''
})
const logingIn = ref(false)
const handleLogin = () => {
login(formLogin.value.username, formLogin.value.password)
}
// const login = async () => {
// if (logingIn.value) {
// return
// }
// logingIn.value = true
// auth.value.status = AuthStatus.Loging
// await loginApi({ username: formLogin.value.username, password: formLogin.value.password, clientId: 'blossom', grantType: 'password' })
// .then((resp: any) => {
// auth.value = { token: resp.data.token, status: AuthStatus.Succ }
// Local.set(storeKey, auth)
// getUserinfo()
// toRoute('/home')
// })
// .catch((_e) => {
// userStore.reset()
// //
// auth.value = { token: '', status: AuthStatus.Fail }
// })
// .finally(() => (logingIn.value = false))
// }
// const getUserinfo = () => {
// userinfoApi().then((resp) => {
// userinfo.value = resp.data
// Local.set(userinfoKey, resp.data)
// })
// }
</script>
<style scoped lang="scss">

View File

@ -0,0 +1,47 @@
.gray {
background-color: #858585;
}
.gray.hl {
background-color: #646464;
}
.red {
background-color: #fb0036;
}
.red.hl {
background-color: #c9002c;
}
.yellow {
background-color: #d8a600;
}
.yellow.hl {
background-color: #ffc400;
}
.blue {
background-color: #00a3cb;
}
.blue.hl {
background-color: #00657e;
}
.green {
background-color: #339a00;
}
.green.hl {
background-color: #2d8700;
}
.purple {
background-color: #ad8cf2;
}
.purple.hl {
background-color: #9665ff;
}

View File

@ -0,0 +1,362 @@
<template>
<div class="plan-index-root">
<div class="header">
<IndexHeader :bg="true"></IndexHeader>
</div>
<bl-row just="space-between" class="workbench" height="45px">
<div class="month">{{ calendarDate.getMonth() + 1 }}</div>
<el-button-group size="small">
<el-button @click="selectDate('prev-month')">上月</el-button>
<el-button @click="selectDate('today')">今日</el-button>
<el-button @click="selectDate('next-month')">下月</el-button>
</el-button-group>
</bl-row>
<el-calendar class="bl-calendar" v-model="calendarDate" ref="CalendarRef">
<template #header="{ date }"><div></div></template>
<template #date-cell="{ data }">
<div class="date-title">
<span>{{ data.day.split('-').slice(2).join('-') }}</span>
<span class="iconbl bl-a-addline-line" @click="handleShowPlanAddDialog(data.day)"></span>
</div>
<div class="plan-group">
<div v-for="(plan, index) in planDays[data.day + ' 00:00:00']" :key="plan.id">
<el-popover
placement="right"
popper-class="plan-popover"
:width="200"
trigger="click"
:hide-after="0"
:disabled="plan.id < 0"
:persistent="false">
<!-- 触发元素 -->
<template #reference>
<div :class="'plan-line ' + plan.color + ' ' + plan.position + ' ' + plan.hl" :style="{ top: index * 21 + 'px' }">
<div v-if="plan.position == 'head' || plan.position == 'all'" class="plan-title">
{{ plan.title }}
</div>
</div>
</template>
<!-- 弹出框内容 -->
<bl-col class="plan-popover-inner">
<div :class="['plan-popover-title', plan.color]">
{{ plan.title }}
</div>
<div class="plan-popover-time">
<div><span class="iconbl bl-date-line"></span> {{ data.day }}</div>
<span class="iconbl bl-a-clock3-line"></span> {{ plan.planStartTime }} - {{ plan.planEndTime }}
</div>
<div class="plan-popover-content">
{{ plan.content }}
</div>
</bl-col>
</el-popover>
</div>
</div>
</template>
</el-calendar>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from 'vue'
import type { CalendarDateType, CalendarInstance } from 'element-plus'
import { planListDayApi, planDelApi } from '@/api/plan'
import { getDateTimeFormat, getNextDay, timestampToDatetime } from '@/assets/utils/util'
import IndexHeader from '@/views/index/IndexHeader.vue'
onMounted(() => {
getPlanAll(getDateTimeFormat().substring(0, 7))
})
const PlanDayInfoRef = ref()
//
const planDays = ref<any>({})
// ,
let lastMonth: string = ''
const calendarDate = ref<Date>(new Date())
const CalendarRef = ref<CalendarInstance>()
const selectDate = (val: CalendarDateType) => {
if (!CalendarRef.value) return
CalendarRef.value.selectDate(val)
}
watch(
() => calendarDate.value,
(data) => {
getPlanAll(timestampToDatetime(data).substring(0, 7))
}
)
const getPlanAll = (month: string, force: boolean = false) => {
if (!force && month == lastMonth) {
return
}
lastMonth = month
planListDayApi({ month: month }).then((resp) => {
planDays.value = resp.data
})
}
//#region ----------------------------------------< >-------------------------------------
const isShowPlanAddDialog = ref(false)
const handleShowPlanAddDialog = (ymd: string) => {
isShowPlanAddDialog.value = true
nextTick(() => {
PlanDayInfoRef.value.setPlanDate(ymd)
})
}
const savedCallback = () => {
getPlanAll(lastMonth, true)
isShowPlanAddDialog.value = false
}
const delDay = (groupId: number) => {
planDelApi({ groupId: groupId }).then((_resp) => {
getPlanAll(lastMonth, true)
})
}
//#endregion
</script>
<style scoped lang="scss">
.plan-index-root {
@include box(100%, 100%);
@include flex(column, flex-start, center);
overflow: hidden;
.header {
@include box(100%, 60px);
}
.workbench,
.bl-calendar {
max-width: 900px;
}
.workbench {
padding: 10px 10px;
:deep(.el-button, .el-radio-button__inner) {
padding: 8px 10px;
}
}
.bl-calendar {
--el-calendar-border: 1px solid var(--el-border-color);
overflow: hidden;
flex: 1;
::-webkit-scrollbar {
width: 0;
height: 0;
}
:deep(.el-calendar__header) {
padding: 0;
}
:deep(.el-calendar__body) {
height: 100%;
padding: 0;
th {
font-size: 13px;
padding: 3px 0;
}
.el-calendar-table {
height: 100%;
border: 0;
thead {
th {
border-bottom: var(--el-calendar-border);
}
}
tbody {
@include box(100%, calc(100% - 45px));
tr {
td.prev,
td.next {
.date-title {
filter: blur(2px);
}
&:hover {
.date-title {
.iconbl {
opacity: 0 !important;
}
}
}
}
td.current {
.date-title {
color: var(--el-color-primary-light-5);
}
}
td.is-today {
background-color: var(--el-color-primary-light-8) !important;
}
td.is-selected {
background: transparent;
}
td {
border-right: 0;
border-left: 0;
border-top: 0;
overflow: scroll;
padding: 0;
&:last-child {
.el-calendar-day {
border-right: 0;
}
}
.el-calendar-day {
min-height: 100%;
padding: 0;
border-right: var(--el-calendar-border);
&:hover {
background-color: transparent;
.date-title {
.iconbl {
opacity: 1;
}
}
}
.date-title {
@include box(100%, 25px);
@include flex(row, space-between, center);
padding: 3px 10px;
.iconbl {
opacity: 0;
transition: 0.3s;
}
}
.plan-group {
@include box(100%, calc(100% - 25px));
@include flex(column, flex-start, center);
position: relative;
.plan-line {
@include flex(row, flex-start, center);
@include box(calc(100% + 2px), 15px);
box-shadow: 0 2px 3px 1px rgba(104, 104, 104, 0.5);
position: absolute;
left: 0;
padding-left: 4px;
color: #fff;
transition: 0.3s;
z-index: 10;
.plan-title {
width: calc(100% - 10px);
font-size: 0.6rem;
text-align: left;
overflow: visible;
white-space: nowrap;
}
}
.hold {
box-shadow: none !important;
}
$bolder-radius: 20px;
.head {
width: calc(100% - 4px);
border-top-left-radius: $bolder-radius;
border-bottom-left-radius: $bolder-radius;
margin-left: 5px;
z-index: 100;
padding-right: 0;
}
.tail {
width: calc(100% - 5px);
border-top-right-radius: $bolder-radius;
border-bottom-right-radius: $bolder-radius;
margin-right: 5px;
}
.all {
width: calc(100% - 10px);
border-radius: $bolder-radius;
margin-left: 5px;
margin-right: 5px;
}
.tail,
.all {
&:hover {
.bl-delete-line {
opacity: 1;
}
}
}
}
}
}
}
}
}
}
}
}
</style>
<style lang="scss">
@import url('./PlanColor.scss');
.plan-popover {
--el-popover-padding: 0 !important;
border: 0 !important;
width: 170px !important;
.plan-popover-inner {
.iconbl {
font-size: 13px;
}
.plan-popover-title {
@include font(15px, 500);
width: 100%;
padding: 10px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
color: #fff;
}
.plan-popover-time {
width: 100%;
padding: 5px 10px;
text-align: left;
}
.plan-popover-content {
width: 100%;
text-align: left;
padding: 0 10px 10px;
white-space: pre-wrap;
}
}
}
</style>

View File

@ -0,0 +1,610 @@
<template>
<div class="todo-root">
<div class="header">
<IndexHeader :bg="true"></IndexHeader>
</div>
<bl-row just="space-between" class="workbench" height="45px">
<el-radio-group v-model="todoType" size="small">
<el-radio-button :label="10">每日</el-radio-button>
<el-radio-button :label="20">阶段性</el-radio-button>
</el-radio-group>
<div class="month" v-show="todoType === 10">{{ calendarDate.getMonth() + 1 }}</div>
<el-button-group v-show="todoType === 10" size="small">
<el-button @click="selectDate('prev-month')">上月</el-button>
<el-button @click="selectDate('today')">今日</el-button>
<el-button @click="selectDate('next-month')">下月</el-button>
</el-button-group>
</bl-row>
<!-- 日历 -->
<el-calendar v-show="todoType === 10" v-model="calendarDate" class="task-day-calendar" ref="CalendarRef">
<template #header="{ date }"><div></div></template>
<template #date-cell="{ data }">
<bl-col just="space-around" class="cell-wrapper" @click="toTask(data.day)">
<div class="day">{{ data.day.split('-')[2] }}</div>
<div v-if="getCount(data.day) > 0">
<bl-tag :size="14">{{ getCount(data.day) }}</bl-tag>
</div>
</bl-col>
</template>
</el-calendar>
<div v-show="todoType === 20" class="phaseds">
<bl-row v-for="phased in todoPhased" class="phased" just="space-between" width="90%" @click="toTask(phased.todoId)">
<span style="padding-left: 5px">{{ phased.todoName }}</span>
<bl-tag v-if="phased.taskCount > 0">{{ phased.taskCount }}</bl-tag>
</bl-row>
</div>
<div class="segmented-control" @change="updatePillPosition">
<span class="selection"></span>
<div class="option">
<input type="radio" id="WAITING" name="sample" value="WAITING" v-model="taskShowStatus" checked />
<label for="WAITING"><span> </span></label>
<span v-show="countWait > 0" class="count">{{ countWait }}</span>
</div>
<div class="option">
<input type="radio" id="PROCESSING" name="sample" value="PROCESSING" v-model="taskShowStatus" />
<label for="PROCESSING"><span>进行中</span></label>
<span v-show="countProc > 0" class="count">{{ countProc }}</span>
</div>
<div class="option">
<input type="radio" id="COMPLETED" name="sample" value="COMPLETED" v-model="taskShowStatus" />
<label for="COMPLETED"><span> </span></label>
<span v-show="countComp > 0" class="count">{{ countComp }}</span>
</div>
</div>
<!-- -->
<bl-col class="tasks" just="flex-start" align="center">
<div v-for="task in taskShow" class="task" @click="showDetail(task)">
<bl-row just="space-between">
<div class="title">{{ task.taskName }}</div>
<div class="title">{{ task.deadLine }}</div>
</bl-row>
<div class="content">{{ task.taskContent }}</div>
<div class="cornor" :style="{ borderTop: `20px solid ${isBlank(task.color) ? '#E5E5E5' : task.color}` }"></div>
</div>
</bl-col>
</div>
<!-- -->
<el-drawer v-model="isShowDetail" direction="btt" :with-header="true" size="345px">
<template #header="{ close, titleId, titleClass }">
<div class="drawer-header">
<el-input style="--el-input-border-color: #ffffff00" v-model="curTask.taskName"></el-input>
</div>
</template>
<div class="detail">
<el-input type="textarea" placeholder="无内容" v-model="curTask.taskContent" resize="none" :rows="4"></el-input>
<bl-row class="tags">
<bl-tag v-for="tag in curTask.taskTags" :bg-color="isBlank(curTask.color) ? '#000' : curTask.color" :size="13" style="min-height: 20px">
{{ tag }}
</bl-tag>
</bl-row>
<bl-col class="times">
<bl-row style="color: red">截止至{{ curTask.deadLine }}</bl-row>
<!-- <bl-row>创建于{{ curTask.creTime }} </bl-row>
<bl-row>开始于{{ curTask.startTime }} </bl-row>
<bl-row>完成于{{ curTask.endTime }} </bl-row> -->
</bl-col>
<bl-row>
<el-slider v-model="curTask.process" :show-tooltip="false" size="small" />
<div class="process">{{ curTask.process }}%</div>
</bl-row>
<bl-row class="btns" just="space-between">
<el-button-group>
<el-button @click="updTask"><i class="iconbl bl-a-texteditorsave-line"></i></el-button>
<el-button @click="updTask">123</el-button>
</el-button-group>
<el-button-group>
<el-button color="#000" v-if="curTask.taskStatus === 'WAITING'" @click="toProcessing">
<span class="iconbl bl-a-boxaddition-line"></span>
</el-button>
<el-button color="#000" v-if="curTask.taskStatus === 'WAITING' || curTask.taskStatus === 'PROCESSING'" @click="toCompleted">
<span class="iconbl bl-a-boxchoice-line"></span>
</el-button>
</el-button-group>
</bl-row>
<div class="status">
{{ curTask.taskStatus }}
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { CalendarDateType, CalendarInstance } from 'element-plus'
import { tasksApi, todosApi, toProcessingApi, toCompletedApi, updTaskApi } from '@/api/todo'
import type { TodoList, TaskInfo, TodoType, TaskStatus } from './scripts/types'
import { isBlank } from '@/assets/utils/obj'
import IndexHeader from '@/views/index/IndexHeader.vue'
onMounted(() => {
getTodos()
})
//#region ----------------------------------------< >--------------------------------------
const calendarDate = ref<Date>(new Date())
const CalendarRef = ref<CalendarInstance>()
const selectDate = (val: CalendarDateType) => {
if (!CalendarRef.value) return
CalendarRef.value.selectDate(val)
}
const todoType = ref<TodoType>(10)
const todoDayMaps = ref<Map<string, TodoList>>(new Map())
const todoPhased = ref<TodoList[]>([])
const getTodos = () => {
todosApi().then((resp) => {
todoDayMaps.value.clear()
for (let key in resp.data.todoDays) {
let todo = resp.data.todoDays[key] as TodoList
todoDayMaps.value.set(key, {
todoId: todo.todoId,
todoName: todo.todoName,
todoStatus: 1,
todoType: 10,
today: false,
taskCount: todo.taskCount > 0 ? todo.taskCount : 0,
updTodoName: false
})
}
todoPhased.value = resp.data.taskPhased
})
}
const getCount = (day: string): number => {
if (!todoDayMaps.value) return 0
if (!todoDayMaps.value.get(day)) return 0
return todoDayMaps.value.get(day)!.taskCount
}
//#endregion
//#region ----------------------------------------< >--------------------------------------
//
const curTask = ref<TaskInfo>({
id: '',
todoId: '',
todoName: '',
todoType: 10,
taskName: '',
taskContent: 'content 张三李四',
taskTags: ['张三', '李四', 'Article'],
deadLine: '今天下午两点30分',
creTime: '2023-01-01 12:21:32',
startTime: '2023-01-01 12:21:32',
endTime: '2023-01-01 12:21:32',
process: 0,
color: '#000',
taskStatus: 'WAITING',
updTaskName: false,
updTaskContent: false
})
const taskWait = ref<TaskInfo[]>([])
const taskProc = ref<TaskInfo[]>([])
const taskComp = ref<TaskInfo[]>([])
const countWait = computed<number>(() => taskWait.value.length)
const countProc = computed<number>(() => taskProc.value.filter((t: { todoType: number }) => t.todoType != 99).length)
const countComp = computed<number>(() => taskComp.value.filter((t: { todoType: number }) => t.todoType != 99).length)
const taskShowStatus = ref<TaskStatus>('WAITING')
const taskShow = computed(() => {
if (taskShowStatus.value === 'WAITING') {
return taskWait.value
}
if (taskShowStatus.value === 'PROCESSING') {
return taskProc.value.filter((t: { todoType: number }) => t.todoType != 99)
}
if (taskShowStatus.value === 'COMPLETED') {
return taskComp.value.filter((t: { todoType: number }) => t.todoType != 99)
}
})
const updatePillPosition = () => {
Array.from(document.querySelectorAll('.segmented-control .option input')).forEach((elem: Element, index) => {
let input = elem as HTMLInputElement
if (input.checked) {
let selection = document.querySelector('.segmented-control .selection')
if (selection) {
;(selection as HTMLElement).style.transform = 'translateX(' + input.offsetWidth * index + 'px)'
}
}
})
}
/**
* 查看指定待办事项
* @param todoId 待办事项ID
* @param todoName 待办事项名称
* @param todoType 待办事项类型
*/
const toTask = (todoId: string) => {
tasksApi({ todoId: todoId }).then((resp) => {
taskWait.value = resp.data.waiting
taskProc.value = resp.data.processing
taskComp.value = resp.data.completed
})
}
const savedCallback = (data: any) => {
taskWait.value = data.waiting
taskProc.value = data.processing
taskComp.value = data.completed
}
const toProcessing = () => {
toProcessingApi({ id: curTask.value.id!, todoId: curTask.value.todoId }).then((resp) => {
ElMessage('任务已开始')
savedCallback(resp.data)
isShowDetail.value = false
})
}
const toCompleted = () => {
toCompletedApi({ id: curTask.value.id!, todoId: curTask.value.todoId }).then((resp) => {
ElMessage('任务已完成')
savedCallback(resp.data)
isShowDetail.value = false
})
}
const updTask = () => {
updTaskApi(curTask.value).then((resp) => {
ElMessage('修改成功')
isShowDetail.value = false
})
}
const isShowDetail = ref(false)
const showDetail = (task: TaskInfo) => {
isShowDetail.value = true
curTask.value = task
}
//#region
</script>
<style scoped lang="scss">
.todo-root {
@include box(100%, 100%);
@include flex(column, flex-start, center);
overflow: hidden;
--primary-light: #8abdff;
--primary: #6d5dfc;
--primary-dark: #5b0eeb;
--white: #ffffff;
--greyLight-1: #e4ebf5;
--greyLight-2: #c8d0e7;
--greyLight-3: #bec8e4;
--greyDark: #9baacf;
.header {
@include box(100%, 60px);
}
.workbench,
.task-day-calendar,
.types,
.tasks,
.phaseds {
max-width: 900px;
}
.workbench {
padding: 10px 10px;
:deep(.el-button, .el-radio-button__inner) {
padding: 8px 10px;
}
}
.task-day-calendar {
min-height: 230px;
background-color: transparent;
--el-calendar-cell-width: 40px;
:deep(.el-calendar__header) {
padding: 0;
}
:deep(.el-calendar__body) {
padding: 0;
th {
font-size: 13px;
padding: 3px 0;
}
.el-calendar-table {
tr:first-child td {
border-left: none;
}
td:first-child {
border-left: none;
}
}
.el-calendar-table__row {
.prev,
.current,
.next {
.el-calendar-day {
padding: 0;
.cell-wrapper {
@include box(100%, 100%);
.day {
@include font(14px, 300);
text-align: center;
}
.tag-root {
font-size: 14px;
}
}
}
&:last-child {
border-right: none;
}
}
}
}
}
.phaseds {
@include box(100%, 230px);
@include flex(row, center, flex-start);
align-content: flex-start;
flex-wrap: wrap;
padding-left: 10px;
overflow: scroll;
.phased {
line-height: 33px;
font-size: 14px;
margin: 0 10px 10px 0;
border-radius: 4px;
padding: 3px 5px;
background-color: #eeeeee;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: background-color 0.3s;
&:active {
background-color: #d8d8d8;
}
}
}
.segmented-control {
--background: rgba(239, 239, 240, 1);
background: var(--background);
border-radius: 5px;
margin-top: 10px;
padding: 2px;
border: none;
outline: none;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.selection {
background: rgba(255, 255, 255, 1);
border: 0.5px solid rgba(0, 0, 0, 0.04);
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.12), 0 3px 1px 0 rgba(0, 0, 0, 0.04);
border-radius: 5px;
grid-column: 1;
grid-row: 1;
z-index: 2;
will-change: transform;
-webkit-transition: transform 0.2s ease;
transition: transform 0.2s ease;
}
.option {
position: relative;
cursor: pointer;
&:first-of-type {
grid-column: 1;
grid-row: 1;
box-shadow: none;
label::before {
opacity: 0;
}
}
input {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
outline: none;
border: none;
opacity: 0;
&:checked + label::before,
&:checked + label::after {
z-index: 1;
}
&::checked + label {
cursor: default;
}
}
label {
position: relative;
display: block;
text-align: center;
padding: 3px 20px;
background: rgba(255, 255, 255, 0);
font-weight: 500;
color: rgb(105, 105, 105);
font-size: 14px;
transition: transform 0.3s;
cursor: pointer;
&:active {
color: #000;
}
&::after {
right: 0;
transform: translateX(0.5px);
}
span {
display: block;
position: relative;
z-index: 2;
-webkit-transition: all 0.2s ease;
transition: all 0.2s ease;
will-change: transform;
padding: 5px;
line-height: 18px;
pointer-events: none;
}
}
.count {
font-size: 12px;
font-style: italic;
color: #bebebe;
position: absolute;
top: 1px;
right: 5px;
z-index: 3;
text-align: right;
}
}
}
.tasks {
width: 95%;
max-width: 650px;
padding: 0 10px 10px 10px;
margin-top: 10px;
border-radius: 10px;
overflow-y: auto;
flex: 1;
.task {
width: 100%;
font-size: 14px;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
background-color: #f3f3f3;
position: relative;
transition: background-color 0.3s;
&:active {
background-color: #d8d8d8;
}
.title {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:last-child {
width: 30%;
text-align: right;
}
}
.content {
font-size: 0.8rem;
margin-top: 3px;
color: #a3a3a3;
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.cornor {
// @include box(20px, 20px);
// transform: rotate(45deg);
// border-top-left-radius: 10px;
position: absolute;
top: 0;
right: 0;
border-left: 20px solid transparent;
border-top-right-radius: 4px;
}
}
}
}
.drawer-header {
--el-font-size-base: 15px;
}
.detail {
@include flex(column, space-between, flex-start);
--el-font-size-base: 15px;
.el-textarea {
--el-input-border-color: #ffffff00 !important;
}
.tags {
height: 44px;
padding: 10px 0 3px 0;
overflow: scroll;
}
.times {
margin-top: 10px;
font-size: 13px;
.bl-row-root {
margin-bottom: 5px;
white-space: nowrap;
}
}
.process {
width: 50px;
font-style: italic;
color: #bebebe;
text-align: right;
}
.btns {
margin-top: 10px;
.iconbl {
margin-right: 5px;
}
}
.status {
font-size: 12px;
font-style: italic;
color: #bebebe;
position: absolute;
right: 30px;
bottom: 10px;
}
}
</style>

View File

@ -0,0 +1,84 @@
/**
*
* 10: 每日待办事项
* 20: 阶段性事项
* 99: 中午12点分割线
*/
export type TodoType = 10 | 20 | 99
/**
* , , TodoType = 1 , 1
* 1: 未完成
* 2: 完成
*/
export type TodoStatus = 1 | 2
/**
*
*/
export type TaskStatus = 'WAITING' | 'PROCESSING' | 'COMPLETED'
/**
*
* todo
*/
export interface TodoList {
id?: string
/**
* type = 1 , todoId
* type = 2 , todoId ID
*/
todoId: string
/**
* type = 1 , todoName
* type = 2 , todoName
*/
todoName: string
/**
*
*/
updTodoName: boolean
/**
*
*/
todoStatus: TodoStatus
/**
*
*/
todoType: TodoType
/**
* , type = 1
*/
today: boolean
/**
*
*/
taskCount: number
}
/**
* task
*/
export interface TaskInfo {
id?: string
todoId: string
todoName: string
todoType: TodoType
taskName: string
taskContent: string
taskTags: string[]
deadLine: string
creTime: string
startTime?: string
endTime?: string
/**
* , 0 ~ 100
*/
process: number
color: string
taskStatus: TaskStatus
//
updTaskName: boolean
updTaskContent: boolean
}