This is an automated email from the ASF dual-hosted git repository. ovilia pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/echarts-theme-builder.git
commit 3450c84489c433f83a14550b8121b444144600e2 Author: Ovilia <[email protected]> AuthorDate: Tue Sep 2 10:16:37 2025 +0800 feat: download and import and export --- src/components/ThemePanel.vue | 302 +++++++++++++++++++++++++++++++++++++++--- src/utils/download.ts | 114 ++++++++++++++++ 2 files changed, 398 insertions(+), 18 deletions(-) diff --git a/src/components/ThemePanel.vue b/src/components/ThemePanel.vue index 8db099e..5d8f067 100644 --- a/src/components/ThemePanel.vue +++ b/src/components/ThemePanel.vue @@ -18,6 +18,10 @@ <van-icon name="share" /> 导出配置 </van-button> + <van-button size="small" @click="showThemeCode"> + <van-icon name="eye-o" /> + 使用主题 + </van-button> </div> <div class="action-buttons"> @@ -413,6 +417,8 @@ import { PRE_DEFINED_THEMES } from '../stores/theme' import ColorPicker from './ColorPicker.vue' import ColorList from './ColorList.vue' import type ChartPreviewPanel from './ChartPreviewPanel.vue' +import { downloadJsonFile, downloadJsFile, copyToClipboard } from '../utils/download' +import { showToast, showDialog } from 'vant' // Props interface Props { @@ -432,28 +438,201 @@ const { theme, themeName } = themeStore const preDefinedThemes = PRE_DEFINED_THEMES // Methods -const downloadTheme = () => { - // TODO: Implement theme download +const downloadTheme = async () => { + try { + const themeConfig = themeStore.getEChartsTheme(true) + const jsContent = themeStore.getThemeJsFile() + const filename = themeName.value || 'customized' + + // Show format selection dialog using action sheet style + try { + await showDialog({ + title: '选择下载格式', + message: '请选择要下载的主题文件格式:', + showCancelButton: true, + confirmButtonText: 'JavaScript 文件', + cancelButtonText: 'JSON 文件' + }) + + // User chose JavaScript + downloadJsFile(jsContent, filename) + showUsageInstructions('js', filename) + } catch { + // User chose JSON (clicked cancel button) + downloadJsonFile(themeConfig, filename) + showUsageInstructions('json', filename) + } + } catch (error) { + console.error('Download failed:', error) + showToast({ + message: '下载失败,请重试', + type: 'fail' + }) + } +} + +const showUsageInstructions = (format: 'js' | 'json', filename: string) => { + const themeNameDisplay = themeName.value || 'customized' + + if (format === 'js') { + showDialog({ + title: 'JavaScript 主题文件使用方法', + message: `<div style="text-align: left; padding: 5px 0;"> + <ol style="margin: 0; line-height: 1"> + <li>将下载的 <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-family: Monaco, monospace;">${filename}.js</code> 文件保存到项目中</li> + <li>在 HTML 中引入此文件:<br/><code style="background: #f0f0f0; padding: 4px 8px; border-radius: 3px; font-family: Monaco, monospace; display: inline-block; margin-top: 6px;"><script src="${filename}.js"></script></code></li> + <li>创建图表时使用主题:<br/><code style="background: #f0f0f0; padding: 4px 8px; border-radius: 3px; font-family: Monaco, monospace; display: inline-block; margin-top: 6px;">echarts.init(dom, '${themeNameDisplay}')</code></li> + </ol> + <p style="margin: 0; color: #666; font-size: 14px; line-height: 1; background: #f8f9fa; padding: 10px; border-radius: 4px; border-left: 3px solid #1989fa;">💡 第二个参数是在 JS 文件中注册的主题名称。</p> + </div>`, + allowHtml: true, + confirmButtonText: '好的' + }) + } else { + showDialog({ + title: 'JSON 主题文件使用方法', + message: `<div style="text-align: left; padding: 5px 0;"> + <ol style="margin: 0; line-height: 1"> + <li>将下载的 <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-family: Monaco, monospace;">${filename}.json</code> 文件保存到项目中</li> + <li>读取 JSON 文件并解析:<br/><code style="background: #f0f0f0; padding: 4px 8px; border-radius: 3px; font-family: Monaco, monospace; display: inline-block; margin-top: 6px;">const obj = JSON.parse(data)</code></li> + <li>注册主题:<br/><code style="background: #f0f0f0; padding: 4px 8px; border-radius: 3px; font-family: Monaco, monospace; display: inline-block; margin-top: 6px;">echarts.registerTheme('${themeNameDisplay}', obj)</code></li> + <li>创建图表时使用主题:<br/><code style="background: #f0f0f0; padding: 4px 8px; border-radius: 3px; font-family: Monaco, monospace; display: inline-block; margin-top: 6px;">echarts.init(dom, '${themeNameDisplay}')</code></li> + </ol> + <p style="margin: 0; color: #666; font-size: 14px; line-height: 1; background: #f8f9fa; padding: 10px; border-radius: 4px; border-left: 3px solid #1989fa;">💡 第二个参数是注册时使用的主题名称。</p> + </div>`, + allowHtml: true, + confirmButtonText: '好的' + }) + } } const importConfig = () => { fileInput.value?.click() } -const exportConfig = () => { - // TODO: Implement config export +const exportConfig = async () => { + try { + const configData = themeStore.getThemeConfigForDownload() + const filename = `${themeName.value || 'customized'}.project` + + downloadJsonFile(configData, filename) + + showToast({ + message: '配置导出成功!', + type: 'success' + }) + } catch (error) { + console.error('Export failed:', error) + showToast({ + message: '导出失败,请重试', + type: 'fail' + }) + } } const refreshCharts = () => { - // TODO: Implement chart refresh + if (props.chartPreviewRef?.updateCharts) { + props.chartPreviewRef.updateCharts() + showToast({ + message: '图表已刷新', + type: 'success' + }) + } } -const resetTheme = () => { - themeStore.resetTheme() +const resetTheme = async () => { + try { + await showDialog({ + title: '确认重置', + message: '确定要重置为默认主题吗?此操作不可撤销。', + }) + + themeStore.resetTheme() + showToast({ + message: '主题已重置', + type: 'success' + }) + } catch { + // User cancelled + } +} + +const showThemeCode = async () => { + try { + const themeConfig = themeStore.getEChartsTheme(true) + const jsContent = themeStore.getThemeJsFile() + + // Show format selection dialog + try { + await showDialog({ + title: '主题代码预览', + message: '选择要查看的代码格式:', + showCancelButton: true, + confirmButtonText: 'JavaScript 格式', + cancelButtonText: 'JSON 格式' + }) + + // User chose JavaScript format + showCodeDialog('JavaScript 主题文件', jsContent) + } catch { + // User chose JSON format + const jsonCode = JSON.stringify(themeConfig, null, 4) + showCodeDialog('JSON 主题配置', jsonCode) + } + } catch (error) { + console.error('Failed to show theme code:', error) + showToast({ + message: '代码生成失败', + type: 'fail' + }) + } +} + +const showCodeDialog = async (title: string, code: string) => { + try { + await showDialog({ + title, + message: `<pre style="text-align: left; white-space: pre-wrap; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 11px; max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 6px; border: 1px solid #e9ecef; line-height: 1.4; margin: 0;">${code}</pre>`, + allowHtml: true, + confirmButtonText: '复制代码', + cancelButtonText: '关闭' + }) + + // User wants to copy + const success = await copyToClipboard(code) + if (success) { + showToast({ + message: '代码已复制到剪贴板', + type: 'success' + }) + } else { + showToast({ + message: '复制失败,请手动复制', + type: 'fail' + }) + } + } catch { + // User closed dialog + } } const showHelp = () => { - // TODO: Implement help dialog + showDialog({ + title: '使用帮助', + message: `ECharts 主题构建工具 + +• 基本配置:设置主题的基本颜色和样式 +• 预定义主题:选择内置的主题方案 +• 导入配置:导入之前导出的配置文件 +• 导出配置:导出当前配置供后续使用 +• 下载主题:下载可用于 ECharts 的主题文件 +• 使用主题:查看和复制生成的主题代码 + +支持的格式: +• JSON:ECharts 主题配置文件 +• JavaScript:可直接引入的 JS 文件`, + confirmButtonText: '知道了' + }) } const selectPreDefinedTheme = async (index: number) => { @@ -473,23 +652,86 @@ const onAxisSettingChange = () => { themeStore.updateAxisSetting() } -const handleFileImport = (event: Event) => { +const handleFileImport = async (event: Event) => { const target = event.target as HTMLInputElement const file = target.files?.[0] if (!file) return - const reader = new FileReader() - reader.onload = (e) => { - try { - const result = e.target?.result as string - themeStore.importTheme(result) - } catch (error) { - console.error('Failed to import theme:', error) - // TODO: Show error message + // Check file extension + const extension = file.name.slice(file.name.lastIndexOf('.')) + if (extension !== '.json') { + showToast({ + message: '请选择 JSON 格式的配置文件!', + type: 'fail' + }) + target.value = '' + return + } + + try { + const reader = new FileReader() + reader.onload = async (e) => { + try { + const result = e.target?.result as string + const data = JSON.parse(result) + + // Validate imported data + if (!data.themeName && !data.version && !data.theme) { + showToast({ + message: '请使用从本网站导出的 JSON 配置文件!', + type: 'fail' + }) + return + } + + // Check version compatibility + if (data.version && data.version < 1) { + try { + await showDialog({ + title: '版本兼容性警告', + message: '导入的主题版本较低,某些属性可能无法正确设置。是否继续导入?', + }) + } catch { + return // User cancelled + } + } + + themeStore.importTheme(result) + + // Update charts if reference is available + if (props.chartPreviewRef?.updateCharts) { + props.chartPreviewRef.updateCharts() + } + + showToast({ + message: '主题导入成功!', + type: 'success' + }) + } catch (error) { + console.error('Import error:', error) + showToast({ + message: '配置文件格式错误,请使用从本网站导出的 JSON 文件!', + type: 'fail' + }) + } } + + reader.onerror = () => { + showToast({ + message: '文件读取失败,请重试', + type: 'fail' + }) + } + + reader.readAsText(file) + } catch (error) { + console.error('File import failed:', error) + showToast({ + message: '文件导入失败', + type: 'fail' + }) } - reader.readAsText(file) // Clear input target.value = '' @@ -675,4 +917,28 @@ const handleFileImport = (event: Event) => { border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } + +/* Dialog width adjustments */ +:global(.van-dialog) { + width: 800px; + max-width: 90vw; +} + +:global(.van-dialog__content) { + max-height: 70vh !important; + overflow-y: auto !important; +} + +:global(.van-dialog__message) { + text-align: left !important; + line-height: 1 !important; +} + +/* Code dialog specific styles */ +:global(.van-dialog__message pre) { + white-space: pre-wrap !important; + word-wrap: break-word !important; + font-size: 12px !important; + line-height: 1 !important; +} </style> diff --git a/src/utils/download.ts b/src/utils/download.ts new file mode 100644 index 0000000..d84937a --- /dev/null +++ b/src/utils/download.ts @@ -0,0 +1,114 @@ +/** + * Download utilities for theme builder + */ + +/** + * Check if the browser is Safari + */ +function isSafari(): boolean { + return navigator.userAgent.indexOf('Safari') > 0 && + navigator.userAgent.indexOf('Chrome') < 0 +} + +/** + * Check if the browser is Internet Explorer + */ +function isIE(): boolean { + return navigator.userAgent.indexOf('MSIE') > 0 || + navigator.userAgent.indexOf('Trident') > 0 +} + +/** + * Check if the browser is Edge (legacy) + */ +function isEdge(): boolean { + return navigator.userAgent.indexOf('Edge') > 0 +} + +/** + * Save a file with the given content and filename + * @param data - File content + * @param filename - Name of the file + * @param type - MIME type of the file + */ +function saveFile(data: string, filename: string, type: string = 'text/plain'): void { + if (isSafari()) { + // Safari doesn't support Blob downloads well, use data URL + window.open('data:text/plain;charset=utf-8,' + encodeURIComponent(data)) + } else { + try { + const file = new Blob([data], { type }) + const url = URL.createObjectURL(file) + + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } catch (e) { + console.error('Download failed:', e) + // Fallback to data URL + window.open('data:text/plain;charset=utf-8,' + encodeURIComponent(data)) + } + } +} + +/** + * Download a JSON file + * @param data - JSON data object + * @param filename - Name of the file (without extension) + */ +export function downloadJsonFile(data: any, filename: string): void { + const jsonString = JSON.stringify(data, null, 4) + saveFile(jsonString, `${filename}.json`, 'application/json') +} + +/** + * Download a JavaScript file + * @param content - JavaScript code content + * @param filename - Name of the file (without extension) + */ +export function downloadJsFile(content: string, filename: string): void { + saveFile(content, `${filename}.js`, 'application/javascript') +} + +/** + * Check if the browser supports downloads + */ +export function isDownloadSupported(): boolean { + return !isIE() && !isEdge() +} + +/** + * Copy text to clipboard + * @param text - Text to copy + * @returns Promise that resolves when copy is successful + */ +export async function copyToClipboard(text: string): Promise<boolean> { + try { + // Modern clipboard API + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + return true + } + + // Fallback for older browsers + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + const result = document.execCommand('copy') + document.body.removeChild(textArea) + return result + } catch (error) { + console.error('Failed to copy to clipboard:', error) + return false + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
