概述
本 Demo 展示了如何使用 SpreadJS Designer 的主题系统,通过自定义右侧面板实现设计器主题和运行时主题的动态切换。Demo 中实现了三种设计器主题(Classic、Light、Dark)和两种运行时主题(Excel2013 White、Excel2016 Black)的切换,并演示了如何在切换深色主题时自动关联对应的运行时主题。
实现思路
初始化默认配置:使用 GC.Spread.Sheets.Designer.DefaultConfig 获取设计器默认配置
创建主题选择面板:定义模板和命令,包含设计器主题和运行时主题两个选择列表
注册模板和命令:使用 registerTemplate 注册面板模板,在 commandMap 中配置命令逻辑
实现动态切换:通过动态替换 CSS 文件的 href 属性实现主题切换,使用 Promise 处理异步加载
主题关联逻辑:切换到深色主题时自动切换运行时主题为 Excel2016 Black,保持视觉一致性
代码解析
1. 定义主题选择面板模板
创建模板结构
这段代码定义了右侧面板的 UI 结构。templateName 用于注册模板,content 数组包含两个 LabelContainer,分别对应设计器主题和运行时主题的选择列表。bindingPath 属性用于数据绑定,将选择结果与命令的状态管理关联。
2. 实现主题切换命令
命令执行逻辑
execute 方法处理主题切换逻辑。当 propertyName 为 'designerTheme' 时,切换设计器主题,并在切换到深色主题时自动将运行时主题切换为 Excel2016 Black。getState 方法返回当前主题状态,用于面板的初始显示。
3. 动态加载 CSS 文件
CSS 文件替换核心函数
changeCSSScript 函数通过查找当前 CSS 文件的 link 标签,替换 href 为新主题的 CSS 文件路径。addLink 函数返回一个 Promise,在 CSS 文件加载完成后 resolve,确保主题切换的异步流程可控。加载过程中显示遮罩层提示用户。
4. 配置侧边栏面板
将面板添加到设计器配置
将定义的面板配置添加到 config.sidePanels 数组,并在 commandMap 中注册命令。position: "right" 将面板显示在设计器右侧,width: "250px" 设置面板宽度。
运行效果
页面加载后,设计器右侧显示一个主题选择面板
设计器预设主题列表:Classic、Light、Dark 三个选项
SpreadJS 运行时预设主题列表:Excel2013 White、Excel2016 Black 两个选项
点击任意主题选项,会显示"正在加载 CSS 文件…"提示,加载完成后主题立即生效
切换到 Dark 主题时,运行时主题自动切换为 Excel2016 Black,保持视觉一致性
所有主题切换无需刷新页面,提供流畅的用户体验
API 参考
registerTemplate 方法
templateName:模板名称,字符串类型
template:模板对象,包含 content 数组定义 UI 结构
Workbook.refresh 方法
手动刷新 Workbook 对象的布局和渲染。在主题切换后调用,确保运行时样式正确应用。
sidePanels 配置
用于配置设计器的侧边栏面板。
let spreadNS = GC.Spread.Sheets;
let config = GC.Spread.Sheets.Designer.DefaultConfig;
let designer, spread;
window.onload = function () {
initDesignerConfig();
};
const THEME_SETTING_PANEL_CMD = 'theme-setting-panel-command';
const THEME_SETTING_PANEL_TEMPLATE = 'theme-setting-panel-template';
const i18n = {
designerPresetThemes: "设计器预设主题",
runtimePresetThemes: "SpreadJS 运行时预设主题",
loading: "正在加载 CSS 文件...",
}
function initDesignerConfig() {
let designerTheme = 'light';
let runtimeTheme = 'excel2013white';
const themeSettingCMD = {
commandName: THEME_SETTING_PANEL_CMD,
execute: async (designer, propertyName, value) => {
if (propertyName === 'designerTheme') {
await changeCSSScript(getDesignerCSSHref(designerTheme), getDesignerCSSHref(value));
designerTheme = value;
if (value === 'dark') {
await changeCSSScript(getRuntimeCSSHref(runtimeTheme), getRuntimeCSSHref('excel2016black'));
designer.getWorkbook().refresh();
runtimeTheme = 'excel2016black';
}
} else if (propertyName === 'runtimeTheme') {
await changeCSSScript(getRuntimeCSSHref(runtimeTheme), getRuntimeCSSHref(value));
designer.getWorkbook().refresh();
runtimeTheme = value;
}
},
getState: (designer) => {
return { designerTheme, runtimeTheme };
}
}
const themeSettingTemplate = {
templateName: THEME_SETTING_PANEL_TEMPLATE,
content: [{
type: "Column",
margin: "10px",
children: [{
type: "LabelContainer",
text: i18n.designerPresetThemes,
children: [{
type: "List",
bindingPath: "designerTheme",
items: [{
text: "Classic",
value: "classic"
},{
text: "Light",
value: "light"
},{
text: "Dark",
value: "dark"
},]
},]
},{
type: "LabelContainer",
text: i18n.runtimePresetThemes,
margin: "10px 0 0 0",
children: [{
type: "List",
bindingPath: "runtimeTheme",
items: [{
text: "Excel2013 White",
value: "excel2013white"
},{
text: "Excel2016 Black",
value: "excel2016black"
},]
},]
}]
}]
}
GC.Spread.Sheets.Designer.registerTemplate(THEME_SETTING_PANEL_TEMPLATE, themeSettingTemplate);
const themeSettingPanel = {
command: THEME_SETTING_PANEL_CMD,
uiTemplate: THEME_SETTING_PANEL_TEMPLATE,
position: "right",
width: "250px",
classList: ['theme-setting-demo']
}
config.sidePanels.push(themeSettingPanel);
config.commandMap = {
[THEME_SETTING_PANEL_CMD]: themeSettingCMD
}
new spreadNS.Designer.Designer(document.getElementById('ribbonHost'), config);
}
function getDesignerCSSHref(themeName) {
return 'gc.spread.sheets.designer' + (themeName === 'classic' ? '' : ('.' + themeName));
}
function getRuntimeCSSHref(themeName) {
return 'gc.spread.sheets.' + themeName;
}
function showLoadingMask() {
let mask = document.createElement('div');
mask.id = 'loading-mask';
mask.innerText = i18n.loading;
let target = document.querySelector('.theme-setting-demo');
if (getComputedStyle(target).position === "static") {
target.style.position = "relative";
}
mask.setAttribute('data-mask', '1');
target.appendChild(mask);
}
function hideLoadingMask() {
let mask = document.getElementById('loading-mask');
if (mask) mask.remove();
}
function withLoadingMask(promise) {
showLoadingMask();
return promise.finally(hideLoadingMask);
}
function changeCSSScript (current, target) {
let currentLink = document.querySelector(`link[href*="${current}"]`);
let href = currentLink.href;
return withLoadingMask(addLink(href.replace(current, target)).then(() => {
currentLink.remove();
}));
}
function addLink (href) {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.type = "text/css";
link.rel = "stylesheet";
link.href = href;
link.onload = function () {
resolve();
};
const header = document.getElementsByTagName('head')[0];
header.appendChild(link);
})
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta name="spreadjs culture" content="zh-cn" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-designer/styles/gc.spread.sheets.designer.light.min.css">
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-shapes/dist/gc.spread.sheets.shapes.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-charts/dist/gc.spread.sheets.charts.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-print/dist/gc.spread.sheets.print.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-barcode/dist/gc.spread.sheets.barcode.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-pdf/dist/gc.spread.sheets.pdf.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-slicers/dist/gc.spread.sheets.slicers.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-pivot-addon/dist/gc.spread.pivot.pivottables.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-tablesheet/dist/gc.spread.sheets.tablesheet.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-reportsheet-addon/dist/gc.spread.report.reportsheet.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-datacharts-addon/dist/gc.spread.sheets.datacharts.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-ganttsheet/dist/gc.spread.sheets.ganttsheet.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-formula-panel/dist/gc.spread.sheets.formulapanel.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-io/dist/gc.spread.sheets.io.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-resources-zh/dist/gc.spread.sheets.resources.zh.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-designer-resources-cn/dist/gc.spread.sheets.designer.resource.cn.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-designer/dist/gc.spread.sheets.designer.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/designer/license.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="container">
<div class="spreadSheet">
<div id="ribbonHost"></div>
<div id="ss"></div>
</div>
</div>
</body>
</html>
.sample-tutorial {
position: relative;
height: 100%;
overflow: hidden;
}
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.container {
height: 100%;
}
.spreadSheet {
height: 100%;
}
#ribbonHost {
height: 100%;
}
.description {
margin: 10px;
width: 40%;
}
.gc-designer-label-container {
background-color: var(--sjs-color-background);
}
#loading-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--sjs-color-background, #fff);
color: var(--sjs,color-foreground, #000);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
z-index: 99;
opacity: 0.8;
}