From f5faa29018509f4564e2581de8ee5248168300e0 Mon Sep 17 00:00:00 2001 From: chenxing Date: Sun, 11 Oct 2026 08:50:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B0=8F=E7=BB=84=E4=BB=B6=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E5=8C=96):=20=E9=9D=A2=E6=9D=BF=E4=B8=89=E5=88=86?= =?UTF-8?q?=E6=94=AF=E6=B8=B2=E6=9F=93=20+=20=E4=B8=89=E4=B8=AA=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E7=BB=84=E4=BB=B6=20+=20=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E7=89=87=E6=AE=B5=20srcdoc=20+=20=E7=8E=BB=E7=92=83=E5=B1=82?= =?UTF-8?q?=E5=AF=B9=E6=AF=94=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 之后; 拆拼防提前闭合门户脚本;节流用 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 正序渲染) --- .gitignore | 7 + src/api/UserSettingApi.ts | 22 ++ src/assets/icons/widget-add.svg | 4 +- src/assets/icons/widget-empty.svg | 11 +- src/assets/icons/widget-sub.svg | 2 +- src/assets/styles/componets/_widget.scss | 44 ++- src/components/widgets/EvaluateWidget.vue | 121 +++++++ src/components/widgets/SyllabusWidget.vue | 144 ++++++++ src/components/widgets/WeekplanWidget.vue | 233 ++++++++++++ src/components/widgets/index.ts | 8 + src/store/index.ts | 3 +- src/utils/widgetFragment.ts | 60 ++++ src/views/Settings.vue | 15 +- src/views/index.vue | 98 ++++- src/views/widget/WidgetManage.vue | 417 ++++++++++++++++++++++ 15 files changed, 1150 insertions(+), 39 deletions(-) create mode 100644 src/components/widgets/EvaluateWidget.vue create mode 100644 src/components/widgets/SyllabusWidget.vue create mode 100644 src/components/widgets/WeekplanWidget.vue create mode 100644 src/components/widgets/index.ts create mode 100644 src/utils/widgetFragment.ts create mode 100644 src/views/widget/WidgetManage.vue diff --git a/.gitignore b/.gitignore index 4275bcc..0eeb863 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,13 @@ card-harness.html card-harness.vite.config.ts src/card-harness-main.ts +# 小组件内置化取证 harness(同一类,本地临时) +dist_widget_harness +widget-harness.html +widget-harness.vite.config.ts +src/widget-harness-main.ts +.tmp-build + yarn.lock .npmrc # Editor directories and files diff --git a/src/api/UserSettingApi.ts b/src/api/UserSettingApi.ts index 164b2e8..89f0109 100644 --- a/src/api/UserSettingApi.ts +++ b/src/api/UserSettingApi.ts @@ -92,6 +92,28 @@ class UserSettings extends Abstract { const url = path + "/sortUserDesktopApp"; return this.postRequest({ url, data: { ids: ids.join(",") } }); } + + /** 小组件统一取数:params 为 urlencoded 查询串(对齐旧 otherParams 口径) */ + getWidgetData(code: string, params: string) { + const url = path + "/getWidgetData"; + return this.getRequest({ url, params: { code: code, params: params } }); + } + + /** 仅 comp_type=2:按需取自定义片段内容 */ + getWidgetContent(code: string) { + const url = path + "/getWidgetContent"; + return this.getRequest({ url, params: { code: code } }); + } + + getWidgetCatalogList() { + const url = path + "/GetWidgetCatalogList"; + return this.getRequest({ url }); + } + + saveWidgetCatalog(catalog: Record) { + const url = path + "/SaveWidgetCatalog"; + return this.postRequest({ url, data: catalog }); + } } export default new UserSettings(); diff --git a/src/assets/icons/widget-add.svg b/src/assets/icons/widget-add.svg index ce041b3..c3ce148 100644 --- a/src/assets/icons/widget-add.svg +++ b/src/assets/icons/widget-add.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/assets/icons/widget-empty.svg b/src/assets/icons/widget-empty.svg index 233e39b..21b7d08 100644 --- a/src/assets/icons/widget-empty.svg +++ b/src/assets/icons/widget-empty.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/src/assets/icons/widget-sub.svg b/src/assets/icons/widget-sub.svg index bf15a52..fc135f7 100644 --- a/src/assets/icons/widget-sub.svg +++ b/src/assets/icons/widget-sub.svg @@ -1,3 +1,3 @@ - + diff --git a/src/assets/styles/componets/_widget.scss b/src/assets/styles/componets/_widget.scss index ac283de..8323c88 100644 --- a/src/assets/styles/componets/_widget.scss +++ b/src/assets/styles/componets/_widget.scss @@ -1,12 +1,26 @@ .widget-wrap { + // 玻璃层令牌(方案 §6.2):深底 + 模糊 + 描边 + 投影,前景走 token,不再随壁纸漂移 + --wg-glass-bg: rgba(15, 23, 42, 0.42); + --wg-glass-border: rgba(255, 255, 255, 0.18); + --wg-glass-shadow: 0 10px 36px rgba(0, 0, 0, 0.3); + --wg-fg: rgba(255, 255, 255, 0.94); + --wg-fg-dim: rgba(255, 255, 255, 0.66); + --wg-fg-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); + width: 380px; min-height: 200px; z-index: 100; border-radius: 5px; - background: rgba(0, 0, 0, 0.35); + background: var(--wg-glass-bg); + backdrop-filter: blur(18px) saturate(140%); + -webkit-backdrop-filter: blur(18px) saturate(140%); + border: 1px solid var(--wg-glass-border); + box-shadow: var(--wg-glass-shadow); .widget-header { display: flex; height: 48px; + color: var(--wg-fg-dim); + text-shadow: var(--wg-fg-shadow); .widget-header-drag { flex: 2 auto; cursor: move; @@ -18,6 +32,10 @@ cursor: pointer; width: 24px; flex-shrink: 1; + color: var(--wg-fg-dim); + } + &:hover svg { + color: var(--wg-fg); } } .operate-btn:nth-child(2) { @@ -36,6 +54,14 @@ &:first-child { margin-top: 0; } + .no-data { + min-height: 90px; + display: flex; + align-items: center; + justify-content: center; + color: #b9c2cc; + font-size: 13px; + } } .widget-app-header { @@ -59,11 +85,12 @@ } } + // 拖拽态加深 + 加强模糊(旧版翻白 → 浅灰图标压浅白,是这轮修掉的反向翻车) &__drag { - .widget-header { - background: rgba(255, 255, 255, 0.5); - } - background: rgba(255, 255, 255, 0.6); + background: rgba(15, 23, 42, 0.56); + backdrop-filter: blur(22px) saturate(150%); + -webkit-backdrop-filter: blur(22px) saturate(150%); + box-shadow: 0 14px 44px rgba(0, 0, 0, 0.38); } } @@ -73,7 +100,12 @@ align-items: center; justify-content: center; flex-direction: column; - color: #828c96; + color: var(--wg-fg); + text-shadow: var(--wg-fg-shadow); + + svg { + color: var(--wg-fg-dim); + } h4 { font-weight: 500; diff --git a/src/components/widgets/EvaluateWidget.vue b/src/components/widgets/EvaluateWidget.vue new file mode 100644 index 0000000..6bfee3b --- /dev/null +++ b/src/components/widgets/EvaluateWidget.vue @@ -0,0 +1,121 @@ + + + + + + diff --git a/src/components/widgets/SyllabusWidget.vue b/src/components/widgets/SyllabusWidget.vue new file mode 100644 index 0000000..16c3749 --- /dev/null +++ b/src/components/widgets/SyllabusWidget.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/src/components/widgets/WeekplanWidget.vue b/src/components/widgets/WeekplanWidget.vue new file mode 100644 index 0000000..d32d9f1 --- /dev/null +++ b/src/components/widgets/WeekplanWidget.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/src/components/widgets/index.ts b/src/components/widgets/index.ts new file mode 100644 index 0000000..e6d1533 --- /dev/null +++ b/src/components/widgets/index.ts @@ -0,0 +1,8 @@ +import { defineAsyncComponent } from "vue"; + +/** 组件键 → 内置组件。新增小组件只改这里 + widget_catalog 插一行。 */ +export const widgetRegistry: Record = { + evaluate: defineAsyncComponent(() => import("./EvaluateWidget.vue")), + syllabus: defineAsyncComponent(() => import("./SyllabusWidget.vue")), + weekplan: defineAsyncComponent(() => import("./WeekplanWidget.vue")), +}; diff --git a/src/store/index.ts b/src/store/index.ts index 1906272..f3e36d6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -127,7 +127,8 @@ export const useDefaultStore = defineStore("defaultStore", { getWidgetAppIds() { const { widgetAppIds: appIds, widgetAppList: appList } = this.curUserSettingInfo; let checkIds: number[] = []; - if (appIds == null) checkIds = appList.map(x => x.id); + // getWidgetAppList 的比较器按 appIds 降序渲染(新添加置顶),故未配置态需预反转,才与 widget_catalog.sort 正序一致 + if (appIds == null) checkIds = appList.map(x => x.id).reverse(); else if (appIds.length == 0) checkIds = []; else checkIds = appIds.split(",").map(x => parseInt(x)); return checkIds; diff --git a/src/utils/widgetFragment.ts b/src/utils/widgetFragment.ts new file mode 100644 index 0000000..5456aa1 --- /dev/null +++ b/src/utils/widgetFragment.ts @@ -0,0 +1,60 @@ +import conf from "@/assets/config/host.config"; + +/** + * 自定义 HTML 片段包装(方案 §5.2):门户拼两段注入,顺序不能合并成一段—— + * ① 取数桥必须在 head 且早于片段自身脚本(否则片段里 yhWidget 为 undefined、内容永远空白); + * ② 高度上报必须在片段内容之后。管理员只贴正文,不需要自己写取数和上报。 + */ + +/** 平台统一信封 {code,msg,data}:片段用原生 fetch,需自行解出 data 业务体 */ +const BRIDGE = + "window.yhWidget={getData:function(params){" + + "return fetch(YH_WIDGET_API+'?code='+YH_WIDGET_CODE+'¶ms='+encodeURIComponent(params||'')," + + "{credentials:'include',headers:{yschapirequest:'1'}})" + + ".then(function(r){return r.json()})" + + ".then(function(j){return j&&j.data!==undefined?j.data:j});}};"; + +/** + * ② 高度上报:先立即报一次(门户据此关掉骨架、把 iframe 从 display:none 放出来), + * 之后 ResizeObserver 触发节流补报。节流必须用 setTimeout 而不能用 rAF —— + * iframe 隐藏期间子文档不出动画帧,rAF 回调永不回来,会把节流标志永久占住、后续上报全被吞 + * (实测帧高卡在 min-height,内容撑出内层滚动条)。 + */ +const REPORTER = + "(function(){var timer=0;" + + "function send(){try{parent.postMessage(JSON.stringify({id:YH_WIDGET_ID," + + "height:document.documentElement.scrollHeight}),'*')}catch(e){}}" + + "function up(){if(timer)return;timer=setTimeout(function(){timer=0;send()},80)}" + + "if(window.ResizeObserver){try{new ResizeObserver(up).observe(document.body)}catch(e){}}" + + "window.addEventListener('load',up);send();setTimeout(up,300);setTimeout(up,1200);})();"; + +const RESET = + "html,body{margin:0;padding:0;background:transparent;color:#344449;font-size:13px;line-height:1.5;" + + 'font-family:"Helvetica Neue",Helvetica,"PingFang SC","Microsoft YaHei",Arial,sans-serif;}'; + +/** 闭合标签必须拆开拼:否则提前闭合门户自身的 + +