feat(小组件内置化): 面板三分支渲染 + 三个内置组件 + 自定义片段 srcdoc + 玻璃层对比度
build / build (push) Successful in 42s

- components/widgets:widgetRegistry(defineAsyncComponent 按需加载)+ EvaluateWidget/SyllabusWidget/WeekplanWidget,props widgetId、emit loaded、内部自行取数、ResizeObserver 自报真实高度;呈现只保留语义不逐像素复刻旧 html
- views/index.vue:iframe 单分支改三分支(内置组件 / compType=2 走 :srcdoc + sandbox="allow-scripts allow-same-origin allow-forms"(不给 allow-top-navigation)/ 兜底「该小工具暂不可用」不白屏);:src="item.url" 与候选池 url 依赖删除;postMessage 身份下发只服务片段分支,高度统一收口 find.height
- utils/widgetFragment.ts:wrapFragment 两段注入——取数桥在 head(早于片段自身脚本,否则 yhWidget undefined)、高度上报在 body 之后;</script> 拆拼防提前闭合门户脚本;节流用 setTimeout 尾沿 + 300/1200ms 兜底(iframe 隐藏期子文档不出动画帧,rAF 会把上报永久吞掉、帧高卡 min-height 撑出内层滚动);桥 URL 走 serviceHost 且自行解平台信封 data
- views/Settings.vue:新增 admin pane「widget」(ADMIN_PANES/ALL_PANES/「系统」组导航项/v-else-if 链分支),普通用户导航隐藏且 URL 直达落 no-perm 黄条
- views/widget/WidgetManage.vue:双卡管理页(内置/自定义分组列表 + 名称/键/类型/data_code/排序/启用/sch_id 表单 + CodeMirror + 上传 .html 走 FileReader 填编辑器,不落地文件不产生 URL;内置条目 content 只读)
- _widget.scss:--wg-* 六令牌 + 深玻璃 blur(18px) saturate(140%) + 描边 + 投影;拖拽态加深 blur(22px) 不再翻白;空态与 +/− 前景改走 token(配套 widget-add/sub/empty.svg 由硬编码色改 currentColor,widget-empty 原为内嵌 PNG 无法着色故换线性版);.widget-item 浅底深字保持不动
- UserSettingApi.ts:getWidgetData / getWidgetContent / GetWidgetCatalogList / SaveWidgetCatalog
- 另含 src/store/index.ts 的 getWidgetAppIds 未配置态 .reverse()(非本会话所写,按用户确认一并提交:与 getWidgetAppList 降序比较器对齐,使未配置态按 widget_catalog.sort 正序渲染)
This commit is contained in:
chenxing
2026-10-11 08:50:49 +08:00
parent be0dac6bfa
commit f5faa29018
15 changed files with 1150 additions and 39 deletions
+144
View File
@@ -0,0 +1,144 @@
<template>
<div class="wg-syllabus" ref="refRoot">
<Skeleton
v-if="loading"
style="padding: 15px"
:loading="true"
:title="false"
:paragraph="{ rows: 4 }"
animated
/>
<div v-else-if="grid.length > 0" class="table-wrap">
<table class="my-table">
<tr>
<th>节</th>
<th v-for="week in weekCols" :key="week.Weeks">{{ week.weekName }}</th>
</tr>
<tr v-for="row in grid" :key="row.label">
<td class="sec">{{ row.label }}</td>
<td v-for="(cell, index) in row.cells" :key="index">
<template v-if="cell">
<div class="cla">{{ cell.ClassName }}</div>
<div class="sub">{{ cell.SubName }}</div>
</template>
</td>
</tr>
</table>
</div>
<div v-else class="no-data">暂无数据</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
import UserSettingApi from "@/api/UserSettingApi";
defineProps<{ widgetId: number }>();
const emit = defineEmits(["loaded"]);
const refRoot = ref<HTMLElement | null>(null);
const loading = ref(true);
const list = ref<any[]>([]);
let observer: ResizeObserver | null = null;
/** 表头星期(按 Weeks 去重升序),口径同旧 syllabus_index.js weekArr */
const weekCols = computed(() => {
const nameByKey: Record<number, string> = {};
list.value.forEach(x => {
if (nameByKey[x.Weeks] === undefined) nameByKey[x.Weeks] = x.weekName;
});
return Object.keys(nameByKey)
.map(key => ({ Weeks: Number(key), weekName: nameByKey[Number(key)] }))
.sort((a, b) => a.Weeks - b.Weeks);
});
/** 行节次(按 CourseTimes 去重升序),口径同旧 courseArr */
const courseRows = computed(() => {
const nameByKey: Record<number, string> = {};
list.value.forEach(x => {
if (nameByKey[x.CourseTimes] === undefined) nameByKey[x.CourseTimes] = x.CourseName;
});
return Object.keys(nameByKey)
.map(key => ({ CourseTimes: Number(key), CourseName: nameByKey[Number(key)] }))
.sort((a, b) => a.CourseTimes - b.CourseTimes);
});
const grid = computed(() => {
const cellByKey: Record<string, any> = {};
list.value.forEach(x => {
cellByKey[`${x.CourseTimes}-${x.Weeks}`] = x;
});
return courseRows.value.map(course => ({
label: (course.CourseName || "").replace("第", "").replace("节", ""),
cells: weekCols.value.map(week => cellByKey[`${course.CourseTimes}-${week.Weeks}`]),
}));
});
onMounted(async () => {
if (window.ResizeObserver && refRoot.value) {
observer = new ResizeObserver(reportHeight);
observer.observe(refRoot.value);
}
try {
const data: any = await UserSettingApi.getWidgetData("syllabus", "");
list.value = Array.isArray(data) ? data : [];
} catch (e) {
console.warn("[widget] syllabus 取数失败", e);
}
loading.value = false;
reportHeight();
});
onUnmounted(() => {
if (observer) observer.disconnect();
});
function reportHeight() {
if (refRoot.value) emit("loaded", refRoot.value.offsetHeight);
}
</script>
<style lang="scss" scoped>
.wg-syllabus {
padding: 4px 0 10px;
}
.table-wrap {
padding: 0 15px;
}
.my-table {
width: 100%;
border-collapse: collapse;
th {
line-height: 20px;
padding: 8px 0;
font-weight: 700;
font-size: 12px;
color: #6e7c87;
}
td {
border-top: 1px solid #f7f7f7;
line-height: 15px;
padding: 8px 0;
text-align: center;
font-size: 12px;
}
td.sec {
color: #6e7c87;
font-weight: 700;
}
.cla {
color: #344449;
}
.sub {
color: #98a2b0;
}
}
.no-data {
min-height: 90px;
display: flex;
align-items: center;
justify-content: center;
color: #b9c2cc;
font-size: 13px;
}
</style>