概述
本 Demo 展示了如何自定义 SpreadJS Designer 的主题外观,包括预设主题切换和自定义主题设置。Demo 创建了一个右侧面板,提供了设计器预设主题、运行时预设主题的切换功能,以及主题颜色、边框圆角和阴影的自定义能力。
实现思路
定义主题配置结构,包括颜色、渐变色、圆角等主题令牌
创建自定义命令处理主题切换和自定义设置
使用 registerTemplate 注册自定义模板,构建主题设置面板 UI
将自定义面板添加到设计器配置中
实现动态加载 CSS 文件和主题切换逻辑
实现渐变色自动生成和前景色自动计算功能
代码解析
定义主题设置命令
这段代码定义了主题设置命令的核心逻辑。execute 方法处理主题切换,当切换设计器主题时,先使用 setTheme(null) 重置自定义主题,然后动态替换 CSS 文件。getState 方法返回当前的主题状态,包括预设主题和自定义主题设置。
注册自定义模板
这段代码定义了主题设置面板的 UI 结构。面板包含预设主题选择列表和自定义颜色编辑器,使用 registerTemplate 方法将模板注册到设计器中。
应用自定义主题
这段代码展示了如何应用自定义主题。当用户修改颜色时,会自动生成相关的渐变色(如悬停色、选中色等),并根据背景色自动计算合适的前景色,最后通过 setTheme 方法应用自定义主题。
运行效果
页面右侧显示主题设置面板
可以在 Light 和 Dark 预设主题之间切换
可以在 Excel2013 White 和 Excel2016 Black 运行时主题之间切换
可以自定义背景色、前景色、品牌色、描边色等多个主题令牌
修改基础颜色时,相关的渐变色会自动生成
修改背景色时,前景色会自动计算为合适的颜色(黑色或白色)
API 参考
setTheme 方法
theme: 主题配置对象,包含颜色、圆角、阴影等属性
传入 null 可重置为预设主题的默认值
getTheme 方法
返回当前的主题配置对象,包含所有主题属性的当前值。
registerTemplate 方法
templateName: 模板名称
template: 模板配置对象,定义 UI 结构和布局
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: "运行时预设主题",
loading: "正在加载 CSS 文件...",
gradientVariable: "Gradient Token",
colorForeground: "前景色",
colorForegroundDisabled: "禁用状态前景色",
colorBackground: "背景色",
colorBackgroundHover: "悬停状态背景色",
colorBackgroundSelected: "选中状态背景色",
colorBackgroundDisabled: "禁用状态背景色",
colorBackground2: "页面背景色",
colorBackground2Hover: "页面悬停状态背景色",
colorBackground2Selected: "页面选中状态背景色",
colorBrandBackground: "品牌背景色",
colorBrandForeground: "品牌前景色",
colorBrandBackgroundHover: "品牌悬停状态背景色",
colorBrandBackgroundSelected: "品牌选中状态背景色",
colorStroke: "描边色",
colorStrokeHover: "悬停状态描边色",
colorStrokeSelected: "选中状态描边色",
}
function initDesignerConfig() {
const colorSettings = [{
name: "colorForeground",
gradient: [{
name: "colorForegroundDisabled",
offset: 2
}]
}, {
name: "colorBackground",
gradient: [{
name: "colorBackgroundHover",
offset: 1
}, {
name: "colorBackgroundSelected",
offset: 2
}, {
name: "colorBackgroundDisabled",
offset: 3
}]
}, {
name: "colorBackground2",
gradient: [{
name: "colorBackground2Hover",
offset: 1
}, {
name: "colorBackground2Selected",
offset: 2
}]
}, {
name: "colorBrandBackground",
gradient: [{
name: "colorBrandForeground"
}, {
name: "colorBrandBackgroundHover",
offset: 1
}, {
name: "colorBrandBackgroundSelected",
offset: 2
}]
}, {
name: "colorStroke",
gradient: [{
name: "colorStrokeHover",
offset: 1
}, {
name: "colorStrokeSelected",
offset: 2
}]
}]
let designerTheme = 'light';
let runtimeTheme = 'excel2013white';
const themeSettingCMD = {
commandName: THEME_SETTING_PANEL_CMD,
execute: async (designer, propertyName, value) => {
if (propertyName === 'designerTheme') {
GC.Spread.Sheets.Designer.setTheme(null);
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;
} else {
let customTheme = {
[propertyName]: value
};
// Automatically generate gradient colors
if (colorSettings.find(item => item.name === propertyName)) {
colorSettings.find(item => item.name === propertyName).gradient.forEach(item => {
if (item.offset !== undefined) {
customTheme[item.name] = ColorTool.generate(value, {theme: value === '#000000' ? 'dark' : 'default'})[5 + item.offset];
}
});
}
// Auto foreground color
if (propertyName === 'colorBackground') {
customTheme["colorForeground"] = calcForegroundColor(value);
}
GC.Spread.Sheets.Designer.setTheme(customTheme);
}
},
getState: (designer) => {
return { designerTheme, runtimeTheme, ...GC.Spread.Sheets.Designer.getTheme() };
}
}
const generateColorEditor = (colorSetting) => {
return {
type: "LabelContainer",
text: i18n[colorSetting.name],
margin: "5px 0 0 0",
padding: "10px 10px",
children: [{
type: "FlexContainer",
direction: "horizontal",
className: "custom-theme-demo",
children: [{
type: "TextBlock",
text: i18n[colorSetting.name],
margin: "0 0 0 10px"
},{
type: "ColorComboEditor",
margin: "0 0 0 30px",
bindingPath: colorSetting.name
}]
}, {
type: "CollapsePanel",
text: i18n.gradientVariable,
active: false,
children: colorSetting.gradient.map(item => ({
type: "FlexContainer",
direction: "horizontal",
className: "custom-theme-demo",
children: [{
type: "TextBlock",
margin: "0 0 0 5px",
text: i18n[item.name],
},{
type: "ColorComboEditor",
margin: "0 0 0 10px",
bindingPath: item.name
}]
}))
}]
}
}
const themeSettingTemplate = {
templateName: THEME_SETTING_PANEL_TEMPLATE,
content: [{
type: "Column",
margin: "10px",
children: [{
type: "ColumnSet",
children: [{
type: "Column",
width: "stretch",
children: [{
type: "LabelContainer",
text: i18n.designerPresetThemes,
padding: "5px 5px",
children: [{
type: "List",
bindingPath: "designerTheme",
items: [{
text: "Light",
value: "light"
},{
text: "Dark",
value: "dark"
},]
},]
}]
},{
type: "Column",
width: "stretch",
margin: "0 0 0 5px",
children: [{
type: "LabelContainer",
text: i18n.runtimePresetThemes,
padding: "5px 5px",
children: [{
type: "List",
bindingPath: "runtimeTheme",
items: [{
text: "Excel2013 White",
value: "excel2013white"
},{
text: "Excel2016 Black",
value: "excel2016black"
},]
},]
}]
},]
}, ...colorSettings.map(generateColorEditor)]
}]
}
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: "400px",
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 calcForegroundColor (backgroundColor) {
const [r, g, b] = (s => s.startsWith('#') ? s.slice(1).match(/\w\w/g).map(x=>parseInt(x,16)) : s.match(/\d+/g).map(Number))(backgroundColor);
const grayLevel = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return grayLevel > 0.5 ? "#000000" : "#ffffff";
}
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.head;
header.insertBefore(link, header.firstElementChild);
})
}
<!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>
<script src="color-tool.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);
}
.custom-theme-demo .gc-combo-editor-container {
width: 120px;
margin-right: 10px;
}
.custom-theme-demo {
justify-content: space-between
}
#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;
}