feat(一周安排小组件): 标题/日期精简 + 日常详情加重 + 外跳「查看概览」+ 备注两行折叠
build / build (push) Successful in 2m44s

- 标题改合成简称「10月12日~10月18日 第N周」,接口全称留 hover(title 属性)
- 日期条只留「日」(10.12→12),今日改蓝底白字方块(原浅蓝底)
- 「日常详情」12px/400/#6e7c87 → 14px/700/#1f2329,同排右侧新增「查看概览」外跳(新窗口)
- 外跳地址由后端下发(props.overviewUrl);缺配置则不渲染入口
- 备注 line-clamp:2 + 溢出才出「展开/收起」,支持含换行的多行文本
- index.vue 透传 overview-url;WidgetEditModal 配置页加「外跳入口」行,超管保存/校管覆盖/清除覆盖三链路带上
This commit is contained in:
chenxing
2026-10-15 20:26:07 +08:00
parent 8305129c73
commit 8af636641f
4 changed files with 181 additions and 19 deletions
+126 -17
View File
@@ -13,7 +13,8 @@
也必须保留标题行 + 翻页箭头,否则用户翻进空周后没有出口 --> 也必须保留标题行 + 翻页箭头,否则用户翻进空周后没有出口 -->
<template v-else-if="hasWeek"> <template v-else-if="hasWeek">
<div class="wk-title"> <div class="wk-title">
<span :title="titleStr">{{ titleStr }}</span> <!-- 显示用简称(日期区间 + 第N周);接口全称(如「学校工作安排」)留在 title 里 hover 可见 -->
<span :title="titleFull">{{ titleStr }}</span>
<!-- 翻页:边界由响应 minWeek/maxWeek 驱动(缺省按 [0,0] 禁用两侧);越界 oapi 返回 code:2,前端先拦 --> <!-- 翻页:边界由响应 minWeek/maxWeek 驱动(缺省按 [0,0] 禁用两侧);越界 oapi 返回 code:2,前端先拦 -->
<span class="pager"> <span class="pager">
<i :class="{ disabled: !canPrev }" @click="onPrev">‹</i> <i :class="{ disabled: !canPrev }" @click="onPrev">‹</i>
@@ -33,7 +34,17 @@
</div> </div>
</div> </div>
<div class="wk-tips">日常详情</div> <div class="wk-tips-row">
<span class="wk-tips">日常详情</span>
<!-- 外跳地址由后端按「子应用 code + 路由」解析下发(应用注册表 pc_url),前端不写死域名 -->
<a
v-if="overviewUrl"
class="wk-overview"
:href="overviewUrl"
target="_blank"
rel="noopener"
>查看概览 ›</a>
</div>
<ul v-if="currentList.length > 0" class="wk-list"> <ul v-if="currentList.length > 0" class="wk-list">
<li v-for="(item, index) in currentList" :key="index"> <li v-for="(item, index) in currentList" :key="index">
<div class="line"> <div class="line">
@@ -45,7 +56,11 @@
</ul> </ul>
<div v-else class="no-data">{{ currentDayLabel }}</div> <div v-else class="no-data">{{ currentDayLabel }}</div>
<div v-if="remarkText" class="wk-remark" :title="remarkText">{{ remarkText }}</div> <!-- 备注:默认 2 行截断,超出才给「展开/收起」;空备注整块不渲染 -->
<div v-if="remarkText" class="wk-remark">
<div ref="refRemark" class="rk-txt" :class="{ expanded: remarkExpanded }">{{ remarkText }}</div>
<span v-if="remarkOverflow" class="rk-more" @click="toggleRemark">{{ remarkExpanded ? "收起" : "展开" }}</span>
</div>
</template> </template>
<!-- 空态:模板内联 SVG,不依赖外部图片 / CSS 背景 --> <!-- 空态:模板内联 SVG,不依赖外部图片 / CSS 背景 -->
@@ -72,13 +87,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue"; import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
import UserSettingApi from "@/api/UserSettingApi"; import UserSettingApi from "@/api/UserSettingApi";
defineProps<{ widgetId: number }>(); defineProps<{ widgetId: number; overviewUrl?: string }>();
const emit = defineEmits(["loaded"]); const emit = defineEmits(["loaded"]);
const refRoot = ref<HTMLElement | null>(null); const refRoot = ref<HTMLElement | null>(null);
const refRemark = ref<HTMLElement | null>(null);
const loading = ref(true); const loading = ref(true);
/** /**
* 接口 data:单个周对象(title/year/term/week/monday/days[]/remark/hasData/minWeek/maxWeek),非数组。 * 接口 data:单个周对象(title/year/term/week/monday/days[]/remark/hasData/minWeek/maxWeek),非数组。
@@ -92,10 +108,28 @@ const weekOffset = ref(0);
const minWeek = ref(0); const minWeek = ref(0);
const maxWeek = ref(0); const maxWeek = ref(0);
const currentIndex = ref(0); const currentIndex = ref(0);
/** 备注展开态;remarkOverflow 只在折叠态测量(溢出才显示按钮) */
const remarkExpanded = ref(false);
const remarkOverflow = ref(false);
let observer: ResizeObserver | null = null; let observer: ResizeObserver | null = null;
const days = computed<any[]>(() => (Array.isArray(weekData.value.days) ? weekData.value.days : [])); const days = computed<any[]>(() => (Array.isArray(weekData.value.days) ? weekData.value.days : []));
const titleStr = computed<string>(() => weekData.value.title || ""); /** 标题简称:日期区间 + 第N周(接口 title 往往是「××学校工作安排」这类长名,放到 hover) */
const titleStr = computed<string>(() => {
const list = days.value;
const week = Number(weekData.value.week);
const weekPart = Number.isFinite(week) && week > 0 ? `第${week}周` : "";
if (list.length >= 2) {
const range = `${monthDay(list[0])}~${monthDay(list[list.length - 1])}`;
return weekPart ? `${range} ${weekPart}` : range;
}
return weekPart || weekData.value.title || "";
});
/** hover 全称:接口 title,与简称相同则不挂(避免重复提示) */
const titleFull = computed<string>(() => {
const full = weekData.value.title || "";
return full === titleStr.value ? "" : full;
});
/** /**
* 有周对象 = titleStr 非空(含 hasData=false 的空周:后端仍回 title/days,只是 items 全空)。 * 有周对象 = titleStr 非空(含 hasData=false 的空周:后端仍回 title/days,只是 items 全空)。
* 翻页箭头必须随之保留,避免「翻进空周 → 箭头消失 → 无法返回」的死角; * 翻页箭头必须随之保留,避免「翻进空周 → 箭头消失 → 无法返回」的死角;
@@ -146,6 +180,9 @@ async function loadWeek(week: number) {
} finally { } finally {
// finally 收尾:即使 catch 内再抛,loading 也必须落 false(v1 骨架卡死的直接机制) // finally 收尾:即使 catch 内再抛,loading 也必须落 false(v1 骨架卡死的直接机制)
loading.value = false; loading.value = false;
// 换周/换数据后重置备注折叠态并重测溢出
remarkExpanded.value = false;
nextTick(measureRemark);
reportHeight(); reportHeight();
} }
} }
@@ -187,10 +224,23 @@ function hasWork(index: number) {
return !!day && Array.isArray(day.items) && day.items.length > 0; return !!day && Array.isArray(day.items) && day.items.length > 0;
} }
/** 日期条显示:优先接口 dateText(MM.dd),兜底 date 切片 */ /** 日期条只显示「日」,不带月份:优先 date(yyyy-MM-dd),退化到 dateText 尾部数字 */
function dayText(day: any) { function dayText(day: any) {
if (!day) return ""; if (!day) return "";
return day.dateText || (day.date || "").slice(8, 10); const date = typeof day.date === "string" ? day.date : "";
if (date.length >= 10) return date.slice(8, 10);
const text = String(day.dateText || "");
const hit = text.match(/(\d{1,2})\s*$/);
return hit ? hit[1].padStart(2, "0") : text;
}
/** M月D日(标题区间用;月/日去前导零) */
function monthDay(day: any) {
const date = day && typeof day.date === "string" ? day.date : "";
if (date.length >= 10) {
return `${Number(date.slice(5, 7))}月${Number(date.slice(8, 10))}日`;
}
return String((day && day.dateText) || "");
} }
/** 次要信息行:地点 / 负责人 / 参与人员,全部为空则不显示 */ /** 次要信息行:地点 / 负责人 / 参与人员,全部为空则不显示 */
@@ -203,6 +253,19 @@ function metaOf(item: any) {
return parts.join(" · "); return parts.join(" · ");
} }
function toggleRemark() {
remarkExpanded.value = !remarkExpanded.value;
if (!remarkExpanded.value) nextTick(measureRemark);
reportHeight();
}
/** 备注溢出判定:折叠态下内容高度超过可视高度才算溢出(展开态保持按钮可见,不重测) */
function measureRemark() {
const el = refRemark.value;
if (!el || remarkExpanded.value) return;
remarkOverflow.value = el.scrollHeight > el.clientHeight + 2;
}
function reportHeight() { function reportHeight() {
if (refRoot.value) emit("loaded", refRoot.value.offsetHeight); if (refRoot.value) emit("loaded", refRoot.value.offsetHeight);
} }
@@ -259,10 +322,6 @@ function reportHeight() {
margin-top: 2px; margin-top: 2px;
font-weight: 600; font-weight: 600;
} }
&.is-on {
background: #eef4ff;
color: #1d4ed8;
}
&.has b::after { &.has b::after {
content: ""; content: "";
display: block; display: block;
@@ -280,12 +339,41 @@ function reportHeight() {
&.holiday b { &.holiday b {
color: #e64545; color: #e64545;
} }
// 今日:蓝底白字方块(置最后,压过 rest / holiday 的日期字色)
&.is-on {
color: #1d4ed8;
b {
width: 26px;
margin: 2px auto 0;
padding: 1px 0;
background: #2d8cf0;
color: #fff;
border-radius: 4px;
}
}
} }
} }
.wk-tips { .wk-tips-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 8px 15px 0; margin: 8px 15px 0;
}
.wk-tips {
font-size: 14px;
font-weight: 700;
color: #1f2329;
}
.wk-overview {
flex: 0 0 auto;
font-size: 12px; font-size: 12px;
color: #6e7c87; color: #2d8cf0;
text-decoration: none;
cursor: pointer;
&:hover {
color: #1d6fd0;
}
} }
.wk-list { .wk-list {
list-style: none; list-style: none;
@@ -329,9 +417,30 @@ function reportHeight() {
margin: 8px 15px 0; margin: 8px 15px 0;
font-size: 12px; font-size: 12px;
color: #6e7c87; color: #6e7c87;
overflow: hidden; .rk-txt {
text-overflow: ellipsis; line-height: 1.6;
white-space: nowrap; // 备注可能是含换行的多行文本:pre-line 保留换行;折叠态截断 2 行
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
&.expanded {
display: block;
overflow: visible;
}
}
.rk-more {
display: inline-block;
margin-top: 2px;
font-size: 12px;
color: #2d8cf0;
cursor: pointer;
user-select: none;
&:hover {
color: #1d6fd0;
}
}
} }
.no-data { .no-data {
min-height: 90px; min-height: 90px;
+4
View File
@@ -76,6 +76,10 @@ export interface WidgetCatalog {
originalName?: string | null; originalName?: string | null;
/** 校管视角:本校是否已有覆盖行 */ /** 校管视角:本校是否已有覆盖行 */
covered?: boolean; covered?: boolean;
/** 外跳入口:目标子应用 code(应用注册表 app_code),NULL/空 = 不显示入口 */
linkAppCode?: string | null;
/** 外跳入口:子应用内路由(如 /weekplan);绝对地址由后端按注册表 pc_url 解析 */
linkPath?: string | null;
sort?: number; sort?: number;
enabled?: number; enabled?: number;
updateTime?: string | null; updateTime?: string | null;
+1
View File
@@ -118,6 +118,7 @@
v-if="item.compType === 1 && widgetRegistry[item.code]" v-if="item.compType === 1 && widgetRegistry[item.code]"
:is="widgetRegistry[item.code]" :is="widgetRegistry[item.code]"
:widget-id="item.id" :widget-id="item.id"
:overview-url="item.overviewUrl"
@loaded="onWidgetLoaded(item.id, $event)" @loaded="onWidgetLoaded(item.id, $event)"
/> />
<template v-else> <template v-else>
+50 -2
View File
@@ -68,6 +68,27 @@
</div> </div>
</div> </div>
<div class="form-row">
<span class="lb">外跳入口</span>
<div class="ctl">
<div class="link-cfg">
<input
v-model="form.linkAppCode"
class="inp"
maxlength="32"
placeholder="子应用 code,如 work-week-plan"
/>
<input
v-model="form.linkPath"
class="inp"
maxlength="128"
placeholder="子应用内路由,如 /weekplan"
/>
</div>
<div class="field-msg hint">{{ linkMsg }}</div>
</div>
</div>
<div v-if="isSuperView" class="form-row"> <div v-if="isSuperView" class="form-row">
<span class="lb">可见范围</span> <span class="lb">可见范围</span>
<div class="ctl"> <div class="ctl">
@@ -333,6 +354,8 @@ const form = reactive({
roleAliasMap: {} as Record<string, string>, roleAliasMap: {} as Record<string, string>,
sort: 0, sort: 0,
enabled: 1, enabled: 1,
linkAppCode: "",
linkPath: "",
}); });
const extensions = [html(), EditorView.lineWrapping]; const extensions = [html(), EditorView.lineWrapping];
@@ -361,8 +384,12 @@ const dataMsg = computed(() => {
if (!props.isSuperView) return "取数键由超管在后端白名单里维护,不可改"; if (!props.isSuperView) return "取数键由超管在后端白名单里维护,不可改";
return form.compType === 1 ? "内置组件的数据由前端组件自身取,不在此配置" : "只能从后端白名单里选;留空=静态片段"; return form.compType === 1 ? "内置组件的数据由前端组件自身取,不在此配置" : "只能从后端白名单里选;留空=静态片段";
}); });
const scopeHint = computed(() => { /** 外跳入口说明:库里只存「子应用 code + 路由」,绝对地址由后端按应用注册表解析 */
if (form.scopeType === 0) return "所有学校都下发。"; const linkMsg = computed(() => {
const scope = props.isSuperView ? "" : "留空=沿用超管配置;";
return `${scope}只填「子应用 code + 路由」,绝对地址由后端按应用注册表 pc_url 解析后下发(不落库域名);两列需成对填写,都留空=不显示入口。`;
});
const scopeHint = computed(() => { if (form.scopeType === 0) return "所有学校都下发。";
return form.scopeType === 1 return form.scopeType === 1
? "白名单:只有选中的学校下发(未选中的学校看不到)。" ? "白名单:只有选中的学校下发(未选中的学校看不到)。"
: "黑名单:除选中的学校外都下发(选中的学校看不到)。"; : "黑名单:除选中的学校外都下发(选中的学校看不到)。";
@@ -448,6 +475,8 @@ function fillForm() {
form.roleAliasMap = parseRoleAlias(src?.roleAlias); form.roleAliasMap = parseRoleAlias(src?.roleAlias);
form.sort = src?.sort ?? 0; form.sort = src?.sort ?? 0;
form.enabled = src?.enabled === 0 ? 0 : 1; form.enabled = src?.enabled === 0 ? 0 : 1;
form.linkAppCode = src?.linkAppCode ?? "";
form.linkPath = src?.linkPath ?? "";
if (!props.isSuperView && src?.id) loadSchoolOverride(src.id); if (!props.isSuperView && src?.id) loadSchoolOverride(src.id);
} }
@@ -460,6 +489,9 @@ async function loadSchoolOverride(widgetId: number) {
form.roleList = parseRoles(setting.roleTypes as string); form.roleList = parseRoles(setting.roleTypes as string);
form.sort = setting.sortNo ?? props.item?.sort ?? 0; form.sort = setting.sortNo ?? props.item?.sort ?? 0;
form.alias = setting.alias ?? ""; form.alias = setting.alias ?? "";
// 外跳同样认「NULL=沿用全局」:本校未覆盖则清空输入框,由全局值兜底
form.linkAppCode = setting.linkAppCode ?? "";
form.linkPath = setting.linkPath ?? "";
} catch (e) { } catch (e) {
console.warn("[widget] 本校覆盖读取失败", e); console.warn("[widget] 本校覆盖读取失败", e);
} }
@@ -558,6 +590,8 @@ async function save() {
roleAlias: Object.keys(aliasMap).length ? JSON.stringify(aliasMap) : null, roleAlias: Object.keys(aliasMap).length ? JSON.stringify(aliasMap) : null,
sort: Number(form.sort) || 0, sort: Number(form.sort) || 0,
enabled: form.enabled, enabled: form.enabled,
linkAppCode: form.linkAppCode.trim() || null,
linkPath: form.linkPath.trim() || null,
}); });
Message.success("保存成功"); Message.success("保存成功");
emit("saved"); emit("saved");
@@ -594,6 +628,8 @@ async function clearOverride() {
roleTypes: null, roleTypes: null,
sortNo: null, sortNo: null,
alias: null, alias: null,
linkAppCode: null,
linkPath: null,
}); });
Message.success("已清除本校覆盖,恢复沿用超管配置"); Message.success("已清除本校覆盖,恢复沿用超管配置");
emit("saved"); emit("saved");
@@ -611,6 +647,8 @@ function buildSchoolSetting() {
roleTypes: form.roleList.length ? form.roleList.join(",") : null, roleTypes: form.roleList.length ? form.roleList.join(",") : null,
sortNo: Number(form.sort) || 0, sortNo: Number(form.sort) || 0,
alias: form.alias.trim() || null, alias: form.alias.trim() || null,
linkAppCode: form.linkAppCode.trim() || null,
linkPath: form.linkPath.trim() || null,
}; };
} }
</script> </script>
@@ -836,6 +874,16 @@ $wm-brand-line: color.mix(#ffffff, $wm-brand, 78%);
max-width: 150px; max-width: 150px;
} }
} }
/* 外跳入口:子应用 code + 路由 并排(两列需成对,故放同一行) */
.link-cfg {
display: flex;
gap: 8px;
max-width: 340px;
.inp {
flex: 1 1 0;
min-width: 0;
}
}
.hint { .hint {
font-size: 12px; font-size: 12px;
color: var(--wm-t4); color: var(--wm-t4);