更新角色管理相关内容

This commit is contained in:
2026-09-09 15:04:34 +08:00
parent de7225b61e
commit fef0233cc4
24 changed files with 1442 additions and 116 deletions
@@ -434,7 +434,7 @@ async function removeScheme(row) {
onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadDashboard(), loadPayrolls()]) })
</script>
<style scoped>
<style lang="less" scoped>
.compensation-page { padding: 16px 20px 28px; }
.page-header, .header-actions, .filter-bar, .dashboard-toolbar, .scheme-toolbar, .section-title, .detail-head, .detail-head > div, .custom-item { display: flex; align-items: center; }
.page-header { justify-content: space-between; margin-bottom: 16px; }
@@ -457,4 +457,64 @@ onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadD
.scheme-toolbar { justify-content: flex-end; margin-bottom: 12px; }.scheme-form { margin-top: 18px; padding-top: 18px; border-top: 1px solid #ebeef5; }.scheme-actions { display: flex; justify-content: flex-end; gap: 10px; }
@media (max-width: 1000px) { .stats-row { grid-template-columns: repeat(2, 1fr); }.salary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) { .page-header { align-items: flex-start; gap: 12px; flex-direction: column; }.stats-row, .form-grid, .salary-grid { grid-template-columns: 1fr; }.custom-item { flex-wrap: wrap; }.amount-summary { grid-template-columns: 1fr; gap: 10px; }.amount-summary div { border: none; }.detail-summary { grid-template-columns: 1fr; } }
// ===== 暗色主题适配 =====
html.dark & {
.page-header p {
color: var(--el-text-color-secondary);
}
.dashboard-toolbar {
color: var(--el-text-color-regular);
}
.stat-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
.stat-label,
.stat-sub {
color: var(--el-text-color-secondary);
}
.stat-value {
color: var(--el-text-color-primary);
}
}
.table-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.section-title {
color: var(--el-text-color-primary);
}
.salary-section {
border-top-color: var(--el-border-color-lighter);
}
.scheme-form {
border-top-color: var(--el-border-color-lighter);
}
.detail-head {
border-bottom-color: var(--el-border-color-lighter);
span {
color: var(--el-text-color-secondary);
}
}
.detail-summary {
span {
color: var(--el-text-color-secondary);
}
strong {
color: var(--el-text-color-primary);
}
}
.detail-remark {
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
}
.amount-summary {
background: var(--el-fill-color-light);
div {
border-right-color: var(--el-border-color-lighter);
}
}
}
</style>
@@ -631,4 +631,63 @@ onMounted(() => {
flex-wrap: wrap;
}
}
// ===== 暗色主题适配 =====
html.dark & {
.contact-page {
background: var(--el-bg-color-page);
}
.page-header {
h2 {
color: var(--el-text-color-primary);
}
p {
color: var(--el-text-color-secondary);
}
}
.org-tree-panel {
background: var(--el-bg-color);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
.tree-header {
border-bottom-color: var(--el-border-color-light);
color: var(--el-text-color-primary);
}
.node-count {
color: var(--el-text-color-secondary);
background: var(--el-fill-color);
}
}
.contact-main {
background: var(--el-bg-color);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
.filter-bar {
border-bottom-color: var(--el-border-color-light);
}
}
.pagination-wrap {
border-top-color: var(--el-border-color-light);
}
.contact-name {
color: var(--el-color-primary);
}
.star-icon {
color: var(--el-text-color-placeholder);
&:hover {
color: #e6a23c;
}
&.starred {
color: #e6a23c;
}
}
}
</style>
+186 -5
View File
@@ -170,6 +170,7 @@
class="schedule-item-title"
:class="{ done: item.status === 1 }"
>
<i v-if="hasImages(item)" class="fa-solid fa-image schedule-image-icon"></i>
{{ item.title }}
<el-tag
v-if="item.priority > 0"
@@ -295,14 +296,19 @@ const statCards = computed(() => [
// 趋势图
const trendChartRef = ref(null);
let trendChart = null;
let themeObserver = null;
const renderTrendChart = () => {
if (!trendChartRef.value) return;
if (!trendChart) trendChart = echarts.init(trendChartRef.value);
const dark = document.documentElement.classList.contains("dark");
const axisLineColor = dark ? "#3a3a3a" : "#E0E6ED";
const labelColor = dark ? "#A3A6AD" : "#606266";
const splitColor = dark ? "#2a2a2a" : "#F2F6FC";
const months = dashboard.trend.map((item) => item.month.slice(5) + "月");
trendChart.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
legend: { data: ["已报销", "未报销"], top: 0 },
legend: { data: ["已报销", "未报销"], top: 0, textStyle: { color: labelColor } },
grid: {
left: "3%",
right: "4%",
@@ -313,15 +319,15 @@ const renderTrendChart = () => {
xAxis: {
type: "category",
data: months,
axisLine: { lineStyle: { color: "#E0E6ED" } },
axisLabel: { color: "#606266", fontSize: 11 },
axisLine: { lineStyle: { color: axisLineColor } },
axisLabel: { color: labelColor, fontSize: 11 },
},
yAxis: {
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#F2F6FC" } },
axisLabel: { color: "#606266", fontSize: 11 },
splitLine: { lineStyle: { color: splitColor } },
axisLabel: { color: labelColor, fontSize: 11 },
},
series: [
{
@@ -476,6 +482,22 @@ const scheduleDateTag = (item) => {
return `${m}/${d} 周${week}`;
};
// 判断日程是否关联了图片(与详情页 parseImages 语义一致),用于列表标题前展示图片图标
function hasImages(item) {
const imgs = item?.images;
if (!imgs) return false;
if (Array.isArray(imgs)) return imgs.some(img => img && img.url);
if (typeof imgs === "string" && imgs.trim()) {
try {
const parsed = JSON.parse(imgs);
return Array.isArray(parsed) && parsed.some(img => img && img.url);
} catch {
return false;
}
}
return false;
}
const scheduleTimeText = (item) =>
item.all_day === 1
? "全天"
@@ -609,10 +631,18 @@ const statusType = (value) =>
onMounted(async () => {
await loadDashboard();
window.addEventListener("resize", handleResize);
// 主题切换时重绘图表,使坐标轴/文字颜色跟随暗色
themeObserver = new MutationObserver(() => renderTrendChart());
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
});
onBeforeUnmount(() => {
window.removeEventListener("resize", handleResize);
themeObserver?.disconnect();
themeObserver = null;
trendChart?.dispose();
trendChart = null;
});
@@ -969,6 +999,12 @@ onBeforeUnmount(() => {
}
}
.schedule-image-icon {
flex-shrink: 0;
margin-right: 4px;
color: var(--el-text-color-secondary);
}
.schedule-item-time {
display: flex;
gap: 10px;
@@ -1128,4 +1164,149 @@ onBeforeUnmount(() => {
width: auto;
}
}
// ===== 暗色主题适配 =====
html.dark & {
.oa-dashboard {
background: var(--el-bg-color-page);
}
.page-header {
h2 {
color: var(--el-text-color-primary);
}
p {
color: var(--el-text-color-secondary);
}
}
.stat-card {
background: var(--el-bg-color);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
.stat-title {
color: var(--el-text-color-secondary);
}
.stat-amount {
.currency,
.number {
color: var(--el-text-color-primary);
}
}
.stat-meta {
color: var(--el-text-color-secondary);
}
}
.chart-card {
background: var(--el-bg-color);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
.chart-title {
color: var(--el-text-color-primary);
border-bottom-color: var(--el-border-color-lighter);
}
}
.notice-item {
background: var(--el-fill-color-light);
&:hover {
background: var(--el-fill-color);
}
.notice-text {
color: var(--el-text-color-primary);
}
.notice-time {
color: var(--el-text-color-placeholder);
}
}
.notice-detail-meta {
border-bottom-color: var(--el-border-color-lighter);
}
.notice-detail-text {
color: var(--el-text-color-secondary);
}
.notice-detail-content {
color: var(--el-text-color-primary);
}
.tab-count {
background: var(--el-fill-color);
color: var(--el-text-color-secondary);
}
.schedule-item {
background: var(--el-fill-color-light);
&:hover {
background: var(--el-fill-color);
}
&.done {
background: var(--el-bg-color-page);
border-left-color: var(--el-border-color);
}
.item-status-dot.done {
background: var(--el-text-color-placeholder);
}
.schedule-item-title {
color: var(--el-text-color-primary);
&.done {
color: var(--el-text-color-placeholder);
}
}
.schedule-item-time {
color: var(--el-text-color-secondary);
&.done {
color: var(--el-text-color-placeholder);
}
}
}
.schedule-action-title {
color: var(--el-text-color-primary);
}
.schedule-action-meta {
color: var(--el-text-color-regular);
}
.schedule-action-tip {
color: var(--el-text-color-secondary);
}
.dist-bar-wrap,
.type-bar-wrap {
background: var(--el-fill-color);
}
.dist-count {
color: var(--el-text-color-secondary);
}
.type-name {
color: var(--el-text-color-regular);
}
.type-count {
color: var(--el-text-color-secondary);
}
.type-amount {
color: var(--el-text-color-primary);
}
}
</style>
@@ -340,4 +340,36 @@ onMounted(() => {
justify-content: flex-end;
margin-top: 14px;
}
// ===== 暗色主题适配 =====
html.dark & {
.page-header {
h2 {
color: var(--el-text-color-primary);
}
p {
color: var(--el-text-color-secondary);
}
}
.stat-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
.stat-value {
color: var(--el-text-color-primary);
}
.stat-label {
color: var(--el-text-color-secondary);
}
&.info .stat-value {
color: var(--el-text-color-secondary);
}
}
.name-link {
color: var(--el-color-primary);
}
}
</style>
+28 -1
View File
@@ -298,7 +298,7 @@ function formatDateTime(value) {
onMounted(loadList);
</script>
<style scoped>
<style lang="less" scoped>
.notice-page {
padding: 16px 20px 24px;
}
@@ -386,4 +386,31 @@ onMounted(loadList);
overflow-y: auto;
}
}
// ===== 暗色主题适配 =====
html.dark & {
.page-header p {
color: var(--el-text-color-secondary);
}
.table-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.title-text:hover {
color: var(--el-color-primary);
}
.notice-detail {
.detail-meta {
border-bottom-color: var(--el-border-color-lighter);
}
.meta-text {
color: var(--el-text-color-secondary);
}
.detail-content {
color: var(--el-text-color-primary);
}
}
}
</style>
@@ -511,4 +511,25 @@ onBeforeUnmount(() => {
margin-top: 12px;
}
}
// ===== 暗色主题适配 =====
html.dark & {
.reimburse-page {
background: var(--el-bg-color-page);
}
.page-header {
h2 {
color: var(--el-text-color-primary);
}
p {
color: var(--el-text-color-secondary);
}
}
.filter-card,
.table-card {
background: var(--el-bg-color);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
}
</style>
@@ -43,6 +43,36 @@
<span class="meta-label">延期</span>
<span>{{ carryText }}</span>
</div>
<div v-if="schedule.reminder_id > 0" class="meta-row">
<span class="meta-label">提醒</span>
<span class="reminder-status">
<el-tag v-if="reminderInfo?.is_finished" type="info" size="small"
>已关闭</el-tag
>
<el-tag v-else type="success" size="small">提醒中</el-tag>
<el-tag
v-for="ch in reminderInfo?.remind_channels || []"
:key="ch"
size="small"
effect="plain"
class="reminder-ch"
>{{ channelText(ch) }}</el-tag
>
<span v-if="reminderInfo?.advance_minutes" class="reminder-adv"
>提前 {{ reminderInfo.advance_minutes }} 分</span
>
<el-button
v-if="!reminderInfo?.is_finished"
size="small"
link
type="warning"
:loading="reminderLoading"
@click="endReminder"
>结束提醒</el-button
>
<span v-if="reminderLoading" class="reminder-loading">加载中…</span>
</span>
</div>
<div v-if="schedule.content" class="meta-row">
<span class="meta-label">备注</span>
<span class="meta-content">{{ schedule.content }}</span>
@@ -144,6 +174,7 @@ import {
} from "@element-plus/icons-vue";
import { uploadFile } from "@/api/file";
import { updateScheduleImages } from "@/api/oaSchedule";
import { getReminderDetail, finishReminder } from "@/api/reminder";
const props = defineProps({
modelValue: {
@@ -270,10 +301,63 @@ watch(
() => props.schedule,
schedule => {
imageList.value = parseImages(schedule?.images);
loadReminderInfo(schedule?.reminder_id);
},
{ immediate: true }
);
// ---------- 绑定提醒的状态回显与反馈 ----------
const reminderInfo = ref(null);
const reminderLoading = ref(false);
const channelTextMap = {
SMS: "短信",
EMAIL: "邮件",
BARK: "Bark",
SITE_MSG: "站内信"
};
function channelText(ch) {
return channelTextMap[ch] || ch;
}
async function loadReminderInfo(reminderId) {
reminderInfo.value = null;
if (!reminderId) {
return;
}
reminderLoading.value = true;
try {
const res = await getReminderDetail(reminderId);
if (res?.code === 200) {
reminderInfo.value = res.data || null;
}
} catch {
reminderInfo.value = null;
} finally {
reminderLoading.value = false;
}
}
// 在日程详情直接关闭绑定的提醒任务;关闭后下方标签会回显"已关闭"
async function endReminder() {
const id = props.schedule?.reminder_id;
if (!id) {
return;
}
try {
const res = await finishReminder(id);
if (res?.code === 200) {
ElMessage.success("提醒已关闭");
await loadReminderInfo(id);
emit("changed", props.schedule.id);
} else {
ElMessage.error(res?.msg || "操作失败");
}
} catch {
ElMessage.error("操作失败");
}
}
function validateImage(file) {
if (file.type && !file.type.startsWith("image/")) {
ElMessage.error("只支持上传图片文件");
@@ -418,7 +502,7 @@ function formatDateTime(value) {
}
</script>
<style scoped>
<style lang="less" scoped>
.detail-title-row {
display: flex;
align-items: center;
@@ -469,6 +553,27 @@ function formatDateTime(value) {
word-break: break-all;
}
.reminder-status {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.reminder-ch {
margin: 0;
}
.reminder-adv {
font-size: 12px;
color: #909399;
}
.reminder-loading {
font-size: 12px;
color: #c0c4cc;
}
/* 相关图片 */
.detail-images {
margin-top: 20px;
@@ -529,4 +634,31 @@ function formatDateTime(value) {
color: #c0c4cc;
text-align: center;
}
// ===== 暗色主题适配 =====
html.dark & {
.detail-title.done {
color: var(--el-text-color-placeholder);
}
.meta-row {
color: var(--el-text-color-primary);
}
.meta-label {
color: var(--el-text-color-secondary);
}
.detail-images {
border-top-color: var(--el-border-color-lighter);
}
.images-title {
color: var(--el-text-color-primary);
}
.images-tip {
color: var(--el-text-color-placeholder);
}
.image-thumb {
background: var(--el-fill-color);
}
.image-empty {
color: var(--el-text-color-placeholder);
}
}
</style>
@@ -77,6 +77,68 @@
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>
@@ -89,8 +151,11 @@
<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: {
@@ -106,10 +171,17 @@ const props = defineProps({
defaultDate: {
type: String,
default: ""
},
// 从通知设置页返回时恢复的草稿(含提醒设置)
draft: {
type: Object,
default: null
}
});
const emit = defineEmits(["update:modelValue", "saved"]);
const emit = defineEmits(["update:modelValue", "saved", "draft-applied"]);
const router = useRouter();
const visible = computed({
get: () => props.modelValue,
@@ -137,9 +209,18 @@ const form = reactive({
end_time: "",
priority: 0,
color: colorOptions[0],
content: ""
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: [
@@ -149,7 +230,7 @@ const rules = {
const isEdit = computed(() => !!props.schedule);
// 打开抽屉时根据编辑对象/默认日期重建表单。
// 打开抽屉时根据编辑对象/默认日期/草稿重建表单。
// 用 watch 替代 el-drawer 的 @open + destroy-on-close:
// open 事件早于抽屉内容挂载,时序不稳定会导致表单控件状态错乱、点击无效。
watch(
@@ -161,7 +242,112 @@ watch(
}
);
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;
@@ -172,6 +358,13 @@ function initForm() {
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 = "";
@@ -182,7 +375,9 @@ function initForm() {
form.priority = 0;
form.color = colorOptions[0];
form.content = "";
resetReminderForm();
}
loadNotifyConfig();
}
async function handleSave() {
@@ -202,6 +397,22 @@ async function handleSave() {
}
}
// 开启提醒但所选渠道缺少接收配置:阻断保存,引导先去通知设置
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 = {
@@ -212,7 +423,13 @@ async function handleSave() {
end_time: form.all_day ? "" : form.end_time || "",
all_day: form.all_day ? 1 : 0,
color: form.color,
priority: form.priority
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
@@ -231,7 +448,7 @@ async function handleSave() {
}
</script>
<style scoped>
<style lang="less" scoped>
.time-range {
display: flex;
align-items: center;
@@ -272,4 +489,38 @@ async function handleSave() {
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>
@@ -163,7 +163,7 @@ async function submit() {
}
</script>
<style scoped>
<style lang="less" scoped>
.postpone-tip {
margin: 0 0 12px;
font-size: 13px;
@@ -184,4 +184,14 @@ async function submit() {
align-items: center;
gap: 8px;
}
// ===== 暗色主题适配 =====
html.dark & {
.postpone-tip {
color: var(--el-text-color-regular);
background: var(--el-fill-color-light);
}
.postpone-overdue {
color: #f56c6c;
}
}
</style>
+169 -2
View File
@@ -108,6 +108,12 @@
<span v-if="item.all_day === 0" class="item-time">{{
item.start_time
}}</span>
<i v-if="hasImages(item)" class="fa-solid fa-image item-image-icon"></i>
<i
v-if="item.reminder_id > 0"
class="fa-solid fa-bell item-bell"
title="已开启提醒"
></i>
<span class="item-title" :class="{ done: item.status === 1 }">{{
item.title
}}</span>
@@ -172,6 +178,12 @@
/>
<div class="day-item-main" @click="openDetail(item)">
<div class="day-item-title" :class="{ done: item.status === 1 }">
<i v-if="hasImages(item)" class="fa-solid fa-image item-image-icon"></i>
<i
v-if="item.reminder_id > 0"
class="fa-solid fa-bell item-bell"
title="已开启提醒"
></i>
{{ item.title }}
<el-tag
v-if="item.priority > 0"
@@ -276,7 +288,11 @@
<span
class="color-chip"
:style="{ background: row.color }"
/>{{ row.title }}
/><i v-if="hasImages(row)" class="fa-solid fa-image item-image-icon"></i><i
v-if="row.reminder_id > 0"
class="fa-solid fa-bell item-image-icon"
title="已开启提醒"
></i>{{ row.title }}
</span>
</template>
</el-table-column>
@@ -341,7 +357,9 @@
v-model="editVisible"
:schedule="editingItem"
:default-date="selectedDate"
:draft="pendingDraft"
@saved="reloadAll"
@draft-applied="pendingDraft = null"
/>
<!-- 日程详情抽屉 -->
@@ -648,6 +666,8 @@ const editVisible = ref(false);
const editingItem = ref(null);
const detailVisible = ref(false);
const detailItem = ref(null);
// 从通知设置页返回时恢复的草稿(含提醒设置)
const pendingDraft = ref(null);
function openCreate(date) {
editingItem.value = null;
@@ -842,6 +862,22 @@ function priorityTagType(priority) {
return { 1: "warning", 2: "danger" }[priority] || "info";
}
// 判断日程是否关联了图片(与详情页 parseImages 语义一致),用于列表标题前展示图片图标
function hasImages(item) {
const imgs = item?.images;
if (!imgs) return false;
if (Array.isArray(imgs)) return imgs.some(img => img && img.url);
if (typeof imgs === "string" && imgs.trim()) {
try {
const parsed = JSON.parse(imgs);
return Array.isArray(parsed) && parsed.some(img => img && img.url);
} catch {
return false;
}
}
return false;
}
// 排序权重:紧急(2) > 重要(1) > 普通(0)
function priorityWeight(priority) {
return { 2: 3, 1: 2 }[priority] || 1;
@@ -864,10 +900,23 @@ function compareByPriority(a, b) {
onMounted(() => {
loadStats();
loadCalendarData();
// 从通知设置页返回:恢复上次暂存的新建日程草稿
const raw = sessionStorage.getItem("oa_schedule_draft");
if (raw) {
try {
pendingDraft.value = JSON.parse(raw);
sessionStorage.removeItem("oa_schedule_draft");
editingItem.value = null;
editVisible.value = true;
ElMessage.info("已从暂存恢复未提交的新建日程");
} catch {
sessionStorage.removeItem("oa_schedule_draft");
}
}
});
</script>
<style scoped>
<style lang="less" scoped>
.schedule-page {
padding: 16px 20px 24px;
}
@@ -1113,6 +1162,18 @@ onMounted(() => {
text-overflow: ellipsis;
}
.item-image-icon {
flex-shrink: 0;
margin-right: 4px;
color: var(--el-text-color-secondary);
}
.item-bell {
flex-shrink: 0;
margin-right: 4px;
color: #e6a23c;
}
.item-title.done {
text-decoration: line-through;
color: #c0c4cc;
@@ -1290,4 +1351,110 @@ onMounted(() => {
background: #409eff;
}
}
// ===== 暗色主题适配 =====
html.dark & {
.page-header p {
color: var(--el-text-color-secondary);
}
.stat-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
.stat-label {
color: var(--el-text-color-secondary);
}
.stat-sub {
color: var(--el-text-color-placeholder);
}
}
.category-tabs {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.tab-count {
background: var(--el-fill-color);
color: var(--el-text-color-secondary);
}
.calendar-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.calendar-week-header {
border-bottom-color: var(--el-border-color-lighter);
}
.week-name {
color: var(--el-text-color-secondary);
}
.calendar-cell {
border-right-color: var(--el-border-color-lighter);
border-bottom-color: var(--el-border-color-lighter);
&:hover {
background: var(--el-fill-color-light);
}
&.other-month {
color: var(--el-text-color-placeholder);
background: var(--el-fill-color-light);
}
&.selected {
background: var(--el-color-primary-light-9);
box-shadow: inset 0 0 0 2px var(--el-color-primary);
}
}
.today-tag,
.month-tag {
color: var(--el-color-primary);
}
.cell-item {
background: var(--el-fill-color-light);
&:hover {
background: var(--el-fill-color);
}
}
.item-time {
color: var(--el-text-color-secondary);
}
.item-title.done {
color: var(--el-text-color-placeholder);
}
.cell-more {
color: var(--el-text-color-secondary);
&:hover {
color: var(--el-color-primary);
}
}
.day-panel {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.day-panel-header {
border-bottom-color: var(--el-border-color-lighter);
}
.day-sub {
color: var(--el-text-color-secondary);
}
.day-item {
background: var(--el-fill-color-light);
}
.day-item-title.done {
color: var(--el-text-color-placeholder);
}
.day-item-time {
color: var(--el-text-color-secondary);
}
.table-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
}
.list-title.done {
color: var(--el-text-color-placeholder);
}
}
</style>