feat:增加日程计划模块

This commit is contained in:
2026-06-04 18:30:13 +08:00
parent f7e98f2a28
commit 98f2842869
15 changed files with 328 additions and 331 deletions

View File

@@ -78,7 +78,7 @@ const eventClickEvent = (eventClick: EventClickArg) => {
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@/styles/home.variable.module"; @use "@/styles/home.variable.module" as variable;
$cell-height: 3.8vh; $cell-height: 3.8vh;
$time-slot-width: 70px; $time-slot-width: 70px;
@@ -104,7 +104,7 @@ $month-cell-height: 120px;
} }
.fc-button-active { .fc-button-active {
background-color: $theme-color; background-color: variable.$theme-color;
color: #FFFFFF; color: #FFFFFF;
} }
} }
@@ -137,7 +137,7 @@ $month-cell-height: 120px;
// 表头区域 // 表头区域
.fc-col-header { .fc-col-header {
color: $theme-color; color: variable.$theme-color;
height: 50px; height: 50px;
font-size: 1.4rem; font-size: 1.4rem;
background-color: var(--color-record-card-bg); background-color: var(--color-record-card-bg);

View File

@@ -84,7 +84,7 @@ const props = defineProps({
const editPlanClick = async () => { const editPlanClick = async () => {
planStore.planApiType = 'UPDATE' planStore.planApiType = 'UPDATE'
planStore.currentPlan = props.plan as IPlanInfo planStore.currentPlan = props.plan as IPlanInfo
await router.push({ name: 'MobileHomePlanDetailId', params: { id: planStore.currentPlan.id } }) await router.push({ name: 'PlanDetailId', params: { id: planStore.currentPlan.id } })
} }
const changeCompleteEvent = (value: boolean) => { const changeCompleteEvent = (value: boolean) => {

View File

@@ -1,211 +0,0 @@
<template>
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
center @open="openDialogEvent"
class="plan-dialog">
<el-form ref="formRef" :model="dialogData.data"
:rules="rules" label-width="80px" class="plan-form">
<el-form-item label="计划标题" prop="title">
<el-input v-model="dialogData.data.title" autocomplete="off"
placeholder="请输入标题" clearable
show-word-limit maxlength="10">
</el-input>
</el-form-item>
<el-form-item label="计划内容">
<el-input v-model="dialogData.data.content" autocomplete="off"
placeholder="请输入标题" :rows="2" type="textarea" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<div style="display: flex;justify-content: space-between;">
<el-form-item label="开始日期">
<el-date-picker v-model="dialogData.data.date"
type="date" placeholder="请选择日期" :editable="false"
value-format="YYYY-MM-DD" style="width: 150px;"
:disabled-date="disabledDate" @change="changeDateEvent"/>
</el-form-item>
<el-form-item label="开始时间">
<el-time-picker v-model="dialogData.data.startTime"
placeholder="请选择开始时间" :editable="false"
format="HH:mm" value-format="HH:mm" style="width: 150px;"
:disabled="dialogData.data.date === null"
@change="changeStartTimeEvent"/>
</el-form-item>
<el-form-item label="结束时间">
<el-time-picker v-model="dialogData.data.endTime"
placeholder="请选择结束时间" :editable="false"
:disabled="dialogData.data.startTime === null"
format="HH:mm" value-format="HH:mm" style="width: 150px;"/>
</el-form-item>
</div>
<div style="display: flex;gap: 40px;">
<el-form-item label="提醒时间">
<el-date-picker v-model="dialogData.data.alterTime"
type="datetime" placeholder="请选择提醒时间" :editable="false"
format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss"
@change="changeAlterTimeEvent"
:disabled-date="disabledDate" style="width: 200px;"/>
</el-form-item>
<el-form-item label="邮箱地址" prop="email">
<el-input v-model="dialogData.data.email"
placeholder="请输入邮箱地址" clearable
:disabled="dialogData.data.alterTime === null"
style="width: 200px;">
<template #prefix>
<i class="fa-solid fa-envelope"></i>
</template>
</el-input>
</el-form-item>
</div>
<div style="display: flex;justify-content: space-between;">
<el-form-item label="类别">
<el-input v-model="dialogData.data.tag" autocomplete="off"
placeholder="请输入类别" clearable
show-word-limit maxlength="10" style="width: 150px;">
</el-input>
</el-form-item>
<el-form-item label="置顶">
<el-switch v-model="dialogData.data.isTop"
:active-value="1" :inactive-value="0"
@change="changeTopEvent"
/>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="dialogData.data.priority"
placeholder="请选择优先级" style="width: 150px;"
:disabled="!!dialogData.data.isTop">
<el-option v-for="(item, index) in priorityOptionList"
:key="index" :label="item.label" :value="item.value"
/>
</el-select>
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelClick">取消</el-button>
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button v-if="planStore.planApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { planRules } from '@/utils/element/elRules.ts'
import { computed, nextTick, reactive, ref } from 'vue'
import { isBeforeTime } from '@/utils/home/planUtil.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { usePlanStore } from '@/store/plan.ts'
import { useUserStore } from '@/store/user.ts'
const formRef = ref<FormInstance>()
const rules = reactive<FormRules>(planRules)
const planStore = usePlanStore()
const userStore = useUserStore()
const dialogData = computed(() => planStore.planDialog)
const priorityOptionList = [{
value: 0,
label: '高'
},
{
value: 1,
label: '中'
},
{
value: 2,
label: '低'
}]
const disabledDate = (time: Date) => {
return time.getTime() < Date.now() - 8.64e7
}
/**
* 打开对话框事件
*/
const openDialogEvent = async () => {
await nextTick(() => {
formRef.value?.clearValidate()
})
}
const changeDateEvent = (value: string) => {
if (!value) {
dialogData.value.data.startTime = null
dialogData.value.data.endTime = null
}
}
const changeStartTimeEvent = (value: string) => {
if (!value) {
dialogData.value.data.endTime = null
}
}
const changeAlterTimeEvent = (value: string) => {
if (value) {
dialogData.value.data.email = userStore.userInfo.email
}
}
const changeTopEvent = (value: number) => {
if (value) {
dialogData.value.data.priority = 0
}
}
const validatePlan = (): boolean => {
const { startTime, endTime } = dialogData.value.data
if (startTime && endTime) {
if (!isBeforeTime(startTime, endTime)) {
ElMessage.error('请确认结束时间大于开始时间')
return false
}
}
return true
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value?.validate().then(async () => {
if (validatePlan()) {
await planStore.handlePlanApi(dialogData.value.data)
}
})
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该计划内容?').then(() => {
planStore.planApiType = 'DELETE'
planStore.handlePlanApi(dialogData.value.data)
})
}
const cancelClick = () => {
planStore.planDialog.visible = false
}
</script>
<style lang="scss">
.plan-dialog {
width:40%;
min-width: 800px;
}
</style>

View File

@@ -1,18 +0,0 @@
<template>
<div class="plan-list">
<el-empty v-if="planStore.planList.length === 0" />
<el-scrollbar v-else>
<plan-card v-for="(item, index) in planStore.planList"
:key="index" :plan="item">
</plan-card>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import PlanCard from '@/components/Plan/PlanCard.vue'
import { usePlanStore } from '@/store/plan.ts'
const planStore = usePlanStore()
</script>

View File

@@ -1,18 +1,18 @@
<template> <template>
<div class="common-query"> <div class="mobile-common-query card-item">
<el-select v-model="planStore.queryInfo.planType" <div class="query-item">
@change="planStore.refreshInfo()" <span>分类</span>
style="width: 120px;align-self: center;"> <el-segmented v-model="planStore.queryInfo.planType"
<el-option v-for="(item, index) in planTypeOptions" :key="index" :options="planTypeOptions"
:label="item.label" :value="item.value"/> @change="planStore.refreshInfo()"/>
</el-select> </div>
<el-select v-model="planStore.queryInfo.orderType" <div class="query-item">
@change="planStore.refreshInfo()" <span>排序</span>
style="width: 120px;align-self: center;"> <el-segmented v-model="planStore.queryInfo.orderType"
<el-option v-for="(item, index) in orderTypeOptions" :key="index" :options="orderTypeOptions"
:label="item.label" :value="item.value"/> @change="planStore.refreshInfo()"/>
</el-select> </div>
</div> </div>
</template> </template>
@@ -26,20 +26,20 @@ const planTypeOptions = [{
value: 'all' value: 'all'
}, },
{ {
label: '已完成计划', label: '已完成',
value: 'completed' value: 'completed'
}, },
{ {
label: '未完成计划', label: '未完成',
value: 'notCompleted' value: 'notCompleted'
}] }]
const orderTypeOptions = [{ const orderTypeOptions = [{
label: '时间排序', label: '时间',
value: 'date' value: 'date'
}, },
{ {
label: '重要性排序', label: '重要性',
value: 'priority' value: 'priority'
}] }]
</script> </script>

View File

@@ -1,26 +0,0 @@
<template>
<div class="common-statistics">
<stats-card
title="计划总数" color="#CA6C65" unit="个" icon="iconfont icon-richeng"
:value="planStore.planStats.totalCount" />
<stats-card
title="完成总数" color="#6FBB69" unit="个" icon="iconfont icon-wancheng"
:value="planStore.planStats.completedCount" />
<stats-card
title="当前计划" color="#E4BF62" unit="个" icon="iconfont icon-dangqianjihua"
:value="planStore.planStats.currentCount" />
<stats-card
title="过期计划" color="#5470C6" unit="个" icon="iconfont icon-yuqi"
:value="planStore.planStats.remainCount" />
</div>
</template>
<script setup lang="ts">
import StatsCard from '@/components/StatsCard.vue'
import { usePlanStore } from '@/store/plan.ts'
const planStore = usePlanStore()
</script>

View File

@@ -135,11 +135,12 @@ export const useBillStore = defineStore('bill', {
const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook const bookName = this.billQueryForm.billBook === 'all' ? '' : this.billQueryForm.billBook
this.getBillDateList() this.getBillDateList()
await this.queryBillSummary(bookName)
if (isStats) { if (isStats) {
await this.queryBillStats(bookName) await this.queryBillStats(bookName)
} else {
await this.queryBillSummary(bookName)
} }
this.isRefresh = !this.isRefresh this.isRefresh = !this.isRefresh
this.isLoading = false this.isLoading = false
}, },

View File

@@ -1,28 +0,0 @@
.mobile-plan-view {
display: flex;
flex-direction: column;
gap: 10px;
#planCalendar {
.fc-view {
height: calc(100vh - 210px);
.fc-timegrid-body {
// 单元格高度
tr {
height: 3.5vh;
}
}
}
}
.plan-list {
.el-scrollbar {
.el-scrollbar__view {
display: flex;
flex-direction: column;
gap: 2vh;
}
}
}
}

View File

@@ -1,13 +0,0 @@
.plan-view {
.plan-list {
height: calc(100vh - 120px);
.el-scrollbar {
.el-scrollbar__view {
display: flex;
flex-direction: column;
gap: 2vh;
}
}
}
}

View File

@@ -6,12 +6,12 @@
<el-date-picker v-if="billStore.billDateType === 'month'" <el-date-picker v-if="billStore.billDateType === 'month'"
v-model="billStore.billQueryForm.billMonth" v-model="billStore.billQueryForm.billMonth"
type="month" :editable="false" style="width: 120px;" type="month" :editable="false" style="width: 120px;"
@change="billStore.refreshInfo()"/> @change="billStore.refreshInfo(true)"/>
<el-date-picker v-if="billStore.billDateType === 'year'" <el-date-picker v-if="billStore.billDateType === 'year'"
v-model="billStore.billQueryForm.billYear" v-model="billStore.billQueryForm.billYear"
type="year" :editable="false" style="width: 120px;" type="year" :editable="false" style="width: 120px;"
@change="billStore.refreshInfo()"/> @change="billStore.refreshInfo(true)"/>
</div> </div>
<bill-stats /> <bill-stats />

View File

@@ -30,6 +30,10 @@
title="物品记录" sub-title="收纳生活琐碎" title="物品记录" sub-title="收纳生活琐碎"
router="/product/list"/> router="/product/list"/>
<module-card icon="iconfont icon-paiban" color="#9933FF"
title="日程安排" sub-title="规划每日时光"
router="/plan/calendar"/>
<module-card icon="iconfont icon-yundongjiankang" color="#FD644C" <module-card icon="iconfont icon-yundongjiankang" color="#FD644C"
title="运动健康" sub-title="乐享健康生活" title="运动健康" sub-title="乐享健康生活"
router="/health/calendar"/> router="/health/calendar"/>

View File

@@ -93,10 +93,28 @@ export const mobileHomeMeta: IRouteMetaConfig = {
title: '统计', title: '统计',
icon: '' icon: ''
}, },
'/plan': {
title: '时光日程',
icon: '',
order: 5,
redirect: '/plan/calendar'
},
'/plan/calendar': {
title: '日程安排',
icon: ''
},
'/plan/list': {
title: '日程清单',
icon: ''
},
'/plan/detail/id': {
title: '日程内容',
icon: ''
},
'/anniversary': { '/anniversary': {
title: '纪念日', title: '纪念日',
icon: 'home-anniversary', icon: 'home-anniversary',
order: 5, order: 6,
redirect: '/anniversary/list' redirect: '/anniversary/list'
}, },
'/anniversary/list': { '/anniversary/list': {
@@ -114,7 +132,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/period': { '/period': {
title: '月经记录', title: '月经记录',
icon: 'home-period', icon: 'home-period',
order: 6, order: 7,
redirect: '/period/calendar' redirect: '/period/calendar'
}, },
'/period/calendar': { '/period/calendar': {
@@ -132,7 +150,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/product': { '/product': {
title: '物品记录', title: '物品记录',
icon: 'home-product', icon: 'home-product',
order: 7, order: 8,
redirect: '/product/list' redirect: '/product/list'
}, },
'/product/list': { '/product/list': {
@@ -146,7 +164,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/journal': { '/journal': {
title: '日记', title: '日记',
icon: 'home-product', icon: 'home-product',
order: 8, order: 9,
redirect: '/journal/record' redirect: '/journal/record'
}, },
'/journal/record': { '/journal/record': {
@@ -164,7 +182,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/kpi': { '/kpi': {
title: '绩效', title: '绩效',
icon: 'home-kpi', icon: 'home-kpi',
order: 9, order: 10,
redirect: '/kpi/record' redirect: '/kpi/record'
}, },
'/kpi/record': { '/kpi/record': {
@@ -178,7 +196,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/asset': { '/asset': {
title: '资产', title: '资产',
icon: 'home-asset', icon: 'home-asset',
order: 10, order: 11,
redirect: '/asset/list' redirect: '/asset/list'
}, },
'/asset/list': { '/asset/list': {
@@ -192,7 +210,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/ledger': { '/ledger': {
title: '账本', title: '账本',
icon: 'home-ledger', icon: 'home-ledger',
order: 11, order: 12,
redirect: '/ledger/list' redirect: '/ledger/list'
}, },
'/ledger/list': { '/ledger/list': {
@@ -210,7 +228,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/password': { '/password': {
title: '密码', title: '密码',
icon: 'home-password', icon: 'home-password',
order: 12, order: 13,
redirect: '/password/list' redirect: '/password/list'
}, },
'/password/list': { '/password/list': {
@@ -220,7 +238,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/award': { '/award': {
title: '奖状证书', title: '奖状证书',
icon: 'home-award', icon: 'home-award',
order: 13, order: 14,
redirect: '/award/list' redirect: '/award/list'
}, },
'/award/list': { '/award/list': {
@@ -234,7 +252,7 @@ export const mobileHomeMeta: IRouteMetaConfig = {
'/profile': { '/profile': {
title: '个人信息', title: '个人信息',
icon: '', icon: '',
order: 14, order: 15,
hidden: true, hidden: true,
redirect: '/profile/index' redirect: '/profile/index'
}, },

View File

@@ -0,0 +1,34 @@
<template>
<div class="mobile-plan-view common-mobile-view">
<plan-calendar />
</div>
</template>
<script setup lang="ts">
import PlanCalendar from '@/components/Plan/PlanCalendar.vue'
import { usePlanStore } from '@/store/plan.ts'
import { onMounted } from 'vue'
const planStore = usePlanStore()
onMounted(() => {
planStore.refreshInfo()
})
</script>
<style lang="scss">
.mobile-plan-view {
#planCalendar {
.fc-view {
height: calc(100vh - 210px);
.fc-timegrid-body {
// 单元格高度
tr {
height: 3.5vh;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<div class="common-mobile-view">
<el-form ref="formRef" :model="planStore.currentPlan"
:rules="rules" label-width="80px" class="form-item">
<el-form-item label="计划标题" prop="title">
<el-input v-model="planStore.currentPlan.title" autocomplete="off"
placeholder="请输入标题" clearable
show-word-limit maxlength="10">
</el-input>
</el-form-item>
<el-form-item label="计划内容">
<el-input v-model="planStore.currentPlan.content" autocomplete="off"
placeholder="请输入标题" :rows="2" type="textarea" clearable
show-word-limit maxlength="20">
</el-input>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker v-model="planStore.currentPlan.date"
type="date" placeholder="请选择日期" :editable="false"
value-format="YYYY-MM-DD" style="width: 150px;"
:disabled-date="disabledDate" @change="changeDateEvent"/>
</el-form-item>
<el-form-item label="开始时间">
<el-time-picker v-model="planStore.currentPlan.startTime"
placeholder="请选择开始时间" :editable="false"
format="HH:mm" value-format="HH:mm" style="width: 150px;"
:disabled="planStore.currentPlan.date === null"
@change="changeStartTimeEvent"/>
</el-form-item>
<el-form-item label="结束时间">
<el-time-picker v-model="planStore.currentPlan.endTime"
placeholder="请选择结束时间" :editable="false"
:disabled="planStore.currentPlan.startTime === null"
format="HH:mm" value-format="HH:mm" style="width: 150px;"/>
</el-form-item>
<el-form-item label="提醒时间">
<el-date-picker v-model="planStore.currentPlan.alterTime"
type="datetime" placeholder="请选择提醒时间" :editable="false"
format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm:ss"
@change="changeAlterTimeEvent"
:disabled-date="disabledDate" style="width: 200px;"/>
</el-form-item>
<el-form-item label="邮箱地址" prop="email">
<el-input v-model="planStore.currentPlan.email"
placeholder="请输入邮箱地址" clearable
:disabled="planStore.currentPlan.alterTime === null"
style="width: 200px;">
<template #prefix>
<i class="fa-solid fa-envelope"></i>
</template>
</el-input>
</el-form-item>
<el-form-item label="类别">
<el-input v-model="planStore.currentPlan.tag" autocomplete="off"
placeholder="请输入类别" clearable
show-word-limit maxlength="10" style="width: 150px;">
</el-input>
</el-form-item>
<el-form-item label="置顶">
<el-switch v-model="planStore.currentPlan.isTop"
:active-value="1" :inactive-value="0"
@change="changeTopEvent"
/>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="planStore.currentPlan.priority"
placeholder="请选择优先级" style="width: 150px;"
:disabled="!!planStore.currentPlan.isTop">
<el-option v-for="(item, index) in priorityOptionList"
:key="index" :label="item.label" :value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<div class="form-button">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button v-if="planStore.planApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
<el-button type="info" @click="cancelClick">取消</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { planRules } from '@/utils/element/elRules.ts'
import { nextTick, onMounted, reactive, ref } from 'vue'
import { isBeforeTime } from '@/utils/home/planUtil.ts'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { usePlanStore } from '@/store/plan.ts'
import { useUserStore } from '@/store/user.ts'
import { useAppStore } from '@/store/app.ts'
const formRef = ref<FormInstance>()
const rules = reactive<FormRules>(planRules)
const planStore = usePlanStore()
const userStore = useUserStore()
const appStore = useAppStore()
const priorityOptionList = [{
value: 0,
label: '高'
},
{
value: 1,
label: '中'
},
{
value: 2,
label: '低'
}]
const disabledDate = (time: Date) => {
return time.getTime() < Date.now() - 8.64e7
}
onMounted(async () => {
appStore.isShowMobileBack = true
await nextTick(() => {
formRef.value?.clearValidate()
})
})
const changeDateEvent = (value: string) => {
if (!value) {
planStore.currentPlan.startTime = null
planStore.currentPlan.endTime = null
}
}
const changeStartTimeEvent = (value: string) => {
if (!value) {
planStore.currentPlan.endTime = null
}
}
const changeAlterTimeEvent = (value: string) => {
if (value) {
planStore.currentPlan.email = userStore.userInfo.email
}
}
const changeTopEvent = (value: number) => {
if (value) {
planStore.currentPlan.priority = 0
}
}
const validatePlan = (): boolean => {
const { startTime, endTime, alterTime, email } = planStore.currentPlan
if (startTime && endTime) {
if (!isBeforeTime(startTime, endTime)) {
ElMessage.error('请确认结束时间大于开始时间')
return false
}
}
if (alterTime) {
if (!email) {
ElMessage.error('请输入邮箱地址')
return false
}
}
return true
}
/**
* 点击确认按钮
*/
const confirmClick = () => {
formRef.value?.validate().then(async () => {
if (validatePlan()) {
await planStore.handlePlanApi(planStore.currentPlan)
history.back()
appStore.isShowMobileBack = false
}
})
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该计划内容?').then(async () => {
planStore.planApiType = 'DELETE'
await planStore.handlePlanApi(planStore.currentPlan)
history.back()
appStore.isShowMobileBack = false
})
}
const cancelClick = () => {
history.back()
appStore.isShowMobileBack = false
}
</script>

31
src/views/plan/list.vue Normal file
View File

@@ -0,0 +1,31 @@
<template>
<div class="common-mobile-view">
<plan-query />
<el-empty v-if="planStore.planList.length === 0" description="暂无日程安排"/>
<div v-else class="content-list">
<plan-card v-for="item in planStore.planList"
:key="item.id" :plan="item">
</plan-card>
</div>
<van-floating-bubble icon="plus" @click="addClick" style="top: -60px;"/>
</div>
</template>
<script setup lang="ts">
import PlanQuery from '@/components/Plan/PlanQuery.vue'
import { usePlanStore } from '@/store/plan.ts'
import { useRouter } from 'vue-router'
import PlanCard from '@/components/Plan/PlanCard.vue'
const planStore = usePlanStore()
const router = useRouter()
const addClick = () => {
planStore.planApiType = 'ADD'
planStore.currentPlan = planStore.getEmptyPlan()
router.push({ name: 'PlanDetailId', params: { id: 0 } })
}
</script>