mirror of
https://github.com/blossom-editor/blossom
synced 2024-11-17 14:39:21 +08:00
博客支持查看待办事项与日历计划
This commit is contained in:
parent
2e6df9491b
commit
8ceb7ace71
8
blossom-web/components.d.ts
vendored
8
blossom-web/components.d.ts
vendored
@ -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']
|
||||
|
1
blossom-web/declaration.d.ts
vendored
1
blossom-web/declaration.d.ts
vendored
@ -0,0 +1 @@
|
||||
declare module 'element-plus/dist/locale/zh-cn.mjs'
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
</script>
|
@ -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>
|
||||
|
@ -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 } }
|
||||
]
|
||||
})
|
||||
|
@ -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: {
|
||||
// /**
|
||||
// * 根据用户名密码登录
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
47
blossom-web/src/views/plan/PlanColor.scss
Normal file
47
blossom-web/src/views/plan/PlanColor.scss
Normal 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;
|
||||
}
|
362
blossom-web/src/views/plan/PlanIndex.vue
Normal file
362
blossom-web/src/views/plan/PlanIndex.vue
Normal 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>
|
610
blossom-web/src/views/todo/TodoIndex.vue
Normal file
610
blossom-web/src/views/todo/TodoIndex.vue
Normal 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>
|
84
blossom-web/src/views/todo/scripts/types.ts
Normal file
84
blossom-web/src/views/todo/scripts/types.ts
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user