527 lines
14 KiB
Vue
527 lines
14 KiB
Vue
<template>
|
||
<el-drawer v-model="visible" :title="isEdit ? '编辑日程' : '新建日程'" size="460px">
|
||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
|
||
<el-form-item label="日程标题" prop="title">
|
||
<el-input
|
||
v-model="form.title"
|
||
maxlength="200"
|
||
type="textarea"
|
||
:rows="4"
|
||
show-word-limit
|
||
placeholder="请输入日程标题"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="日程日期" prop="schedule_date">
|
||
<el-date-picker
|
||
v-model="form.schedule_date"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="选择日期"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="全天">
|
||
<el-switch
|
||
v-model="form.all_day"
|
||
active-text="全天"
|
||
inactive-text="按时段"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item v-if="!form.all_day" label="时间段">
|
||
<div class="time-range">
|
||
<el-time-select
|
||
v-model="form.start_time"
|
||
start="00:00"
|
||
step="00:15"
|
||
end="23:45"
|
||
placeholder="开始时间"
|
||
class="time-input"
|
||
/>
|
||
<span class="time-sep">至</span>
|
||
<el-time-select
|
||
v-model="form.end_time"
|
||
start="00:00"
|
||
step="00:15"
|
||
end="23:45"
|
||
placeholder="结束(可选)"
|
||
class="time-input"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="优先级">
|
||
<el-radio-group v-model="form.priority">
|
||
<el-radio-button :value="0">普通</el-radio-button>
|
||
<el-radio-button :value="1">重要</el-radio-button>
|
||
<el-radio-button :value="2">紧急</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="标签颜色">
|
||
<div class="color-options">
|
||
<span
|
||
v-for="c in colorOptions"
|
||
:key="c"
|
||
class="color-dot"
|
||
:class="{ active: form.color === c }"
|
||
:style="{ background: c }"
|
||
@click="form.color = c"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="form.content"
|
||
type="textarea"
|
||
:rows="4"
|
||
maxlength="2000"
|
||
show-word-limit
|
||
placeholder="补充说明(可选)"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-divider>提醒设置</el-divider>
|
||
|
||
<el-form-item label="开启提醒">
|
||
<el-switch v-model="form.reminder_enabled" @change="onReminderToggle" />
|
||
<span class="form-tip">开启后将在「提醒工具」生成一条提醒任务,到点时按渠道通知你</span>
|
||
</el-form-item>
|
||
|
||
<template v-if="form.reminder_enabled">
|
||
<el-form-item label="提醒时间" prop="reminder_time">
|
||
<el-date-picker
|
||
v-model="form.reminder_time"
|
||
type="datetime"
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
placeholder="选择提醒时间"
|
||
style="width: 100%"
|
||
/>
|
||
<span class="form-tip"
|
||
>默认取日程发生时间,可修改;提醒将在"提前提醒分钟"之前开始送达</span
|
||
>
|
||
</el-form-item>
|
||
<el-form-item label="提醒渠道" prop="remind_channels">
|
||
<el-checkbox-group v-model="form.remind_channels">
|
||
<el-checkbox value="SMS">短信</el-checkbox>
|
||
<el-checkbox value="EMAIL">邮件</el-checkbox>
|
||
<el-checkbox value="BARK">Bark</el-checkbox>
|
||
<el-checkbox value="SITE_MSG">站内信</el-checkbox>
|
||
</el-checkbox-group>
|
||
</el-form-item>
|
||
<el-form-item label="提前提醒(分钟)" prop="advance_minutes">
|
||
<el-input-number
|
||
v-model="form.advance_minutes"
|
||
:min="0"
|
||
:max="1440"
|
||
controls-position="right"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-alert
|
||
v-if="missingChannels.length"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
class="reminder-alert"
|
||
>
|
||
<template #title>
|
||
未配置「{{ missingChannels.join("、") }}」的接收方式
|
||
</template>
|
||
<div class="alert-body">
|
||
<span>请先在通知设置中配置对应账号,否则该渠道提醒无法送达。</span>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
link
|
||
@click="saveDraftAndGotoNotify"
|
||
>去设置并暂存当前日程</el-button
|
||
>
|
||
</div>
|
||
</el-alert>
|
||
</template>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button type="primary" :loading="saving" @click="handleSave"
|
||
>保存</el-button
|
||
>
|
||
</template>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, reactive, ref, watch } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import { ElMessage } from "element-plus";
|
||
import { createSchedule, updateSchedule } from "@/api/oaSchedule";
|
||
import { getNotifyConfig } from "@/api/notifyConfig";
|
||
import { getReminderDetail } from "@/api/reminder";
|
||
|
||
const props = defineProps({
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 编辑时传入的日程对象;null 表示新建
|
||
schedule: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
// 新建时预填的日期(YYYY-MM-DD)
|
||
defaultDate: {
|
||
type: String,
|
||
default: ""
|
||
},
|
||
// 从通知设置页返回时恢复的草稿(含提醒设置)
|
||
draft: {
|
||
type: Object,
|
||
default: null
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(["update:modelValue", "saved", "draft-applied"]);
|
||
|
||
const router = useRouter();
|
||
|
||
const visible = computed({
|
||
get: () => props.modelValue,
|
||
set: value => emit("update:modelValue", value)
|
||
});
|
||
|
||
const colorOptions = [
|
||
"#409EFF",
|
||
"#0AD18E",
|
||
"#F5A623",
|
||
"#F56C6C",
|
||
"#9B6DF3",
|
||
"#909399"
|
||
];
|
||
|
||
const formRef = ref(null);
|
||
const saving = ref(false);
|
||
const editingId = ref(0);
|
||
|
||
const form = reactive({
|
||
title: "",
|
||
schedule_date: "",
|
||
all_day: true,
|
||
start_time: "",
|
||
end_time: "",
|
||
priority: 0,
|
||
color: colorOptions[0],
|
||
content: "",
|
||
// 提醒绑定
|
||
reminder_enabled: false,
|
||
reminder_time: "",
|
||
remind_channels: [],
|
||
advance_minutes: 0
|
||
});
|
||
|
||
// 用户通知配置:用于判断所选渠道是否已配置接收账号
|
||
const notifyConfig = ref({});
|
||
const notifyLoading = ref(false);
|
||
|
||
const rules = {
|
||
title: [{ required: true, message: "请输入日程标题", trigger: "blur" }],
|
||
schedule_date: [
|
||
{ required: true, message: "请选择日程日期", trigger: "change" }
|
||
]
|
||
};
|
||
|
||
const isEdit = computed(() => !!props.schedule);
|
||
|
||
// 打开抽屉时根据编辑对象/默认日期/草稿重建表单。
|
||
// 用 watch 替代 el-drawer 的 @open + destroy-on-close:
|
||
// open 事件早于抽屉内容挂载,时序不稳定会导致表单控件状态错乱、点击无效。
|
||
watch(
|
||
() => props.modelValue,
|
||
open => {
|
||
if (open) {
|
||
initForm();
|
||
}
|
||
}
|
||
);
|
||
|
||
function resetReminderForm() {
|
||
form.reminder_enabled = false;
|
||
form.reminder_time = "";
|
||
form.remind_channels = [];
|
||
form.advance_minutes = 0;
|
||
}
|
||
|
||
// 由日程日期/时间推算默认提醒时间:全天取 09:00,按时段取开始时间。
|
||
function defaultReminderTime() {
|
||
if (!form.schedule_date) {
|
||
return "";
|
||
}
|
||
if (form.all_day) {
|
||
return `${form.schedule_date} 09:00:00`;
|
||
}
|
||
const t = form.start_time || "09:00";
|
||
return `${form.schedule_date} ${t}:00`;
|
||
}
|
||
|
||
// 日程时间变化时,若提醒已开启且用户未手动填写提醒时间,则同步默认值
|
||
watch(
|
||
() => [form.schedule_date, form.all_day, form.start_time],
|
||
() => {
|
||
if (form.reminder_enabled && !form.reminder_time) {
|
||
form.reminder_time = defaultReminderTime();
|
||
}
|
||
}
|
||
);
|
||
|
||
function onReminderToggle(val) {
|
||
if (val && !form.reminder_time) {
|
||
form.reminder_time = defaultReminderTime();
|
||
}
|
||
}
|
||
|
||
async function loadNotifyConfig() {
|
||
notifyLoading.value = true;
|
||
try {
|
||
const res = await getNotifyConfig();
|
||
if (res?.code === 200) {
|
||
notifyConfig.value = res.data || {};
|
||
}
|
||
} catch {
|
||
notifyConfig.value = {};
|
||
} finally {
|
||
notifyLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 当前选中渠道中,缺少对应接收账号配置的渠道
|
||
const missingChannels = computed(() => {
|
||
if (!form.reminder_enabled) {
|
||
return [];
|
||
}
|
||
const cfg = notifyConfig.value || {};
|
||
const need = [];
|
||
for (const ch of form.remind_channels) {
|
||
if (ch === "EMAIL" && !cfg.email) {
|
||
need.push("邮件");
|
||
} else if (ch === "SMS" && !cfg.phone) {
|
||
need.push("短信");
|
||
} else if (ch === "BARK" && !cfg.bark_device_key) {
|
||
need.push("Bark");
|
||
}
|
||
// SITE_MSG(站内信)无需额外配置
|
||
}
|
||
return need;
|
||
});
|
||
|
||
// 暂存当前填写内容并跳转到通知设置页
|
||
function saveDraftAndGotoNotify() {
|
||
const draft = { ...form, schedule_id: undefined };
|
||
sessionStorage.setItem("oa_schedule_draft", JSON.stringify(draft));
|
||
ElMessage.info("已暂存当前日程,去设置通知后返回即可继续");
|
||
router.push("/user/notifyConfig");
|
||
}
|
||
|
||
// 编辑已绑定提醒的日程时,回填提醒设置
|
||
async function loadReminderForEdit(reminderId) {
|
||
try {
|
||
const res = await getReminderDetail(reminderId);
|
||
if (res?.code === 200 && res.data) {
|
||
const d = res.data;
|
||
form.reminder_time = d.schedule_time || defaultReminderTime();
|
||
form.remind_channels = d.remind_channels || [];
|
||
form.advance_minutes = d.advance_minutes ?? 0;
|
||
}
|
||
} catch {
|
||
// 读取失败时不阻断编辑,退化为基础默认值
|
||
form.reminder_time = defaultReminderTime();
|
||
}
|
||
}
|
||
|
||
function initForm() {
|
||
// 优先应用从通知设置页返回的草稿
|
||
if (props.draft) {
|
||
editingId.value = 0;
|
||
Object.assign(form, props.draft);
|
||
if (!form.reminder_time) {
|
||
form.reminder_time = defaultReminderTime();
|
||
}
|
||
emit("draft-applied");
|
||
loadNotifyConfig();
|
||
return;
|
||
}
|
||
|
||
if (props.schedule) {
|
||
editingId.value = props.schedule.id;
|
||
form.title = props.schedule.title;
|
||
form.schedule_date = props.schedule.schedule_date;
|
||
form.all_day = props.schedule.all_day === 1;
|
||
form.start_time = props.schedule.start_time || "";
|
||
form.end_time = props.schedule.end_time || "";
|
||
form.priority = props.schedule.priority || 0;
|
||
form.color = props.schedule.color || colorOptions[0];
|
||
form.content = props.schedule.content || "";
|
||
// 回显已绑定的提醒任务
|
||
if (props.schedule.reminder_id > 0) {
|
||
form.reminder_enabled = true;
|
||
loadReminderForEdit(props.schedule.reminder_id);
|
||
} else {
|
||
resetReminderForm();
|
||
}
|
||
} else {
|
||
editingId.value = 0;
|
||
form.title = "";
|
||
form.schedule_date = props.defaultDate || "";
|
||
form.all_day = true;
|
||
form.start_time = "";
|
||
form.end_time = "";
|
||
form.priority = 0;
|
||
form.color = colorOptions[0];
|
||
form.content = "";
|
||
resetReminderForm();
|
||
}
|
||
loadNotifyConfig();
|
||
}
|
||
|
||
async function handleSave() {
|
||
try {
|
||
await formRef.value.validate();
|
||
} catch {
|
||
return;
|
||
}
|
||
if (!form.all_day) {
|
||
if (!form.start_time) {
|
||
ElMessage.warning("请选择开始时间");
|
||
return;
|
||
}
|
||
if (form.end_time && form.end_time < form.start_time) {
|
||
ElMessage.warning("结束时间不能早于开始时间");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 开启提醒但所选渠道缺少接收配置:阻断保存,引导先去通知设置
|
||
if (form.reminder_enabled && missingChannels.value.length) {
|
||
ElMessage.warning(
|
||
`请先配置${missingChannels.value.join("、")}的接收方式,或取消该渠道`
|
||
);
|
||
return;
|
||
}
|
||
if (form.reminder_enabled && !form.reminder_time) {
|
||
ElMessage.warning("请选择提醒时间");
|
||
return;
|
||
}
|
||
if (form.reminder_enabled && form.remind_channels.length === 0) {
|
||
ElMessage.warning("请至少选择一个提醒渠道");
|
||
return;
|
||
}
|
||
|
||
saving.value = true;
|
||
try {
|
||
const payload = {
|
||
title: form.title.trim(),
|
||
content: form.content,
|
||
schedule_date: form.schedule_date,
|
||
start_time: form.all_day ? "" : form.start_time,
|
||
end_time: form.all_day ? "" : form.end_time || "",
|
||
all_day: form.all_day ? 1 : 0,
|
||
color: form.color,
|
||
priority: form.priority,
|
||
reminder_enabled: !!form.reminder_enabled,
|
||
reminder_time: form.reminder_enabled ? form.reminder_time : "",
|
||
remind_channels: form.reminder_enabled ? form.remind_channels : [],
|
||
advance_minutes: form.reminder_enabled
|
||
? Number(form.advance_minutes || 0)
|
||
: 0
|
||
};
|
||
const res =
|
||
isEdit.value
|
||
? await updateSchedule(editingId.value, payload)
|
||
: await createSchedule(payload);
|
||
if (res?.code === 200) {
|
||
ElMessage.success(isEdit.value ? "日程已更新" : "日程已创建");
|
||
emit("saved");
|
||
visible.value = false;
|
||
} else {
|
||
ElMessage.error(res?.msg || "保存失败");
|
||
}
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.time-range {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
.time-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.time-sep {
|
||
color: #909399;
|
||
}
|
||
|
||
.color-options {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.color-dot {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
border: 2px solid transparent;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||
transition: transform 0.15s;
|
||
}
|
||
|
||
.color-dot:hover {
|
||
transform: scale(1.12);
|
||
}
|
||
|
||
.color-dot.active {
|
||
border-color: #fff;
|
||
outline: 2px solid #409eff;
|
||
}
|
||
|
||
.form-tip {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.5;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.reminder-alert {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.reminder-alert .alert-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.reminder-alert .alert-body span {
|
||
font-size: 13px;
|
||
}
|
||
// ===== 暗色主题适配 =====
|
||
html.dark & {
|
||
.time-sep {
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
.color-dot.active {
|
||
border-color: var(--el-bg-color);
|
||
outline-color: var(--el-color-primary);
|
||
}
|
||
}
|
||
</style>
|