第07章 Options 选项页面
12/3/25About 27 min
第7章:Options 选项页面
学习目标
- 理解Options页面的作用和特点
- 掌握复杂设置界面的开发技巧
- 学会数据验证和设置同步机制
- 实现用户友好的配置体验
7.1 Options页面概述
Options页面是Chrome扩展的设置中心,提供比Popup更丰富的配置功能和更大的操作空间。
7.1.1 特点与作用
核心特点
- 独立的完整网页,拥有更大空间
- 支持复杂的表单和交互
- 可以包含多个配置分类
- 设置变更会影响扩展行为
- 支持导入/导出配置
7.1.2 Manifest配置
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"options_page": "options.html",
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"permissions": [
"storage"
]
}7.2 完整的Options页面设计
7.2.1 HTML结构
<!-- options.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>扩展设置</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="container">
<!-- 头部导航 -->
<header class="header">
<div class="header-content">
<div class="logo-section">
<img src="icons/icon48.png" alt="Logo" class="logo">
<div class="title-section">
<h1>扩展设置</h1>
<p class="version">版本 1.0.0</p>
</div>
</div>
<div class="header-actions">
<button class="btn btn-secondary" id="exportBtn">导出配置</button>
<button class="btn btn-secondary" id="importBtn">导入配置</button>
<input type="file" id="importFile" accept=".json" style="display: none;">
</div>
</div>
</header>
<div class="main-wrapper">
<!-- 侧边导航 -->
<nav class="sidebar">
<ul class="nav-list">
<li class="nav-item active" data-tab="general">
<span class="nav-icon">⚙️</span>
<span class="nav-text">常规设置</span>
</li>
<li class="nav-item" data-tab="features">
<span class="nav-icon">🛠️</span>
<span class="nav-text">功能配置</span>
</li>
<li class="nav-item" data-tab="appearance">
<span class="nav-icon">🎨</span>
<span class="nav-text">外观设置</span>
</li>
<li class="nav-item" data-tab="advanced">
<span class="nav-icon">🔧</span>
<span class="nav-text">高级设置</span>
</li>
<li class="nav-item" data-tab="about">
<span class="nav-icon">ℹ️</span>
<span class="nav-text">关于</span>
</li>
</ul>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<!-- 常规设置 -->
<section class="tab-content active" id="general">
<div class="content-header">
<h2>常规设置</h2>
<p>配置扩展的基本行为和功能</p>
</div>
<div class="settings-group">
<h3>基本功能</h3>
<div class="setting-item">
<div class="setting-label">
<label for="enableExtension">启用扩展</label>
<p class="setting-description">控制扩展的全局开关</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="enableExtension">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="autoStart">开机自动启动</label>
<p class="setting-description">浏览器启动时自动激活扩展</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="autoStart">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="updateInterval">更新间隔</label>
<p class="setting-description">自动更新数据的时间间隔</p>
</div>
<div class="setting-control">
<select id="updateInterval" class="setting-select">
<option value="1">1分钟</option>
<option value="5">5分钟</option>
<option value="10">10分钟</option>
<option value="30">30分钟</option>
<option value="60">1小时</option>
</select>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="maxCacheSize">缓存大小限制</label>
<p class="setting-description">最大缓存数据大小(MB)</p>
</div>
<div class="setting-control">
<div class="input-group">
<input type="range" id="cacheSlider" min="10" max="500" value="100" class="range-input">
<input type="number" id="maxCacheSize" min="10" max="500" value="100" class="number-input">
<span class="input-unit">MB</span>
</div>
</div>
</div>
</div>
<div class="settings-group">
<h3>通知设置</h3>
<div class="setting-item">
<div class="setting-label">
<label for="enableNotifications">启用通知</label>
<p class="setting-description">允许扩展显示桌面通知</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="enableNotifications">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item notification-dependent" style="display: none;">
<div class="setting-label">
<label for="notificationSound">通知声音</label>
<p class="setting-description">通知时播放声音提醒</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="notificationSound">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</section>
<!-- 功能配置 -->
<section class="tab-content" id="features">
<div class="content-header">
<h2>功能配置</h2>
<p>启用或禁用特定功能模块</p>
</div>
<div class="settings-group">
<h3>核心功能</h3>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-header">
<div class="feature-icon">📊</div>
<div class="feature-info">
<h4>数据分析</h4>
<p>分析网页数据和用户行为</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="featureAnalytics">
<span class="toggle-slider"></span>
</label>
</div>
<div class="feature-settings" id="analyticsSettings" style="display: none;">
<div class="sub-setting">
<label>
<input type="checkbox" id="trackClicks"> 跟踪点击事件
</label>
</div>
<div class="sub-setting">
<label>
<input type="checkbox" id="trackPageViews"> 跟踪页面访问
</label>
</div>
</div>
</div>
<div class="feature-card">
<div class="feature-header">
<div class="feature-icon">🔒</div>
<div class="feature-info">
<h4>隐私保护</h4>
<p>阻止跟踪和保护隐私</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="featurePrivacy">
<span class="toggle-slider"></span>
</label>
</div>
<div class="feature-settings" id="privacySettings" style="display: none;">
<div class="sub-setting">
<label>
<input type="checkbox" id="blockTrackers"> 阻止跟踪器
</label>
</div>
<div class="sub-setting">
<label>
<input type="checkbox" id="blockAds"> 阻止广告
</label>
</div>
</div>
</div>
</div>
</div>
<div class="settings-group">
<h3>网站白名单</h3>
<div class="whitelist-section">
<div class="input-group">
<input type="url" id="whitelistInput" placeholder="输入网站域名,如:example.com">
<button type="button" id="addWhitelistBtn" class="btn btn-primary">添加</button>
</div>
<ul class="whitelist-list" id="whitelistList">
<!-- 动态生成的白名单项目 -->
</ul>
</div>
</div>
</section>
<!-- 外观设置 -->
<section class="tab-content" id="appearance">
<div class="content-header">
<h2>外观设置</h2>
<p>自定义界面外观和主题</p>
</div>
<div class="settings-group">
<h3>主题设置</h3>
<div class="theme-selector">
<div class="theme-option" data-theme="light">
<div class="theme-preview light-preview">
<div class="preview-header"></div>
<div class="preview-content">
<div class="preview-line"></div>
<div class="preview-line short"></div>
</div>
</div>
<div class="theme-info">
<h4>浅色主题</h4>
<p>经典的浅色界面</p>
</div>
<input type="radio" name="theme" value="light" id="themeLight">
</div>
<div class="theme-option" data-theme="dark">
<div class="theme-preview dark-preview">
<div class="preview-header"></div>
<div class="preview-content">
<div class="preview-line"></div>
<div class="preview-line short"></div>
</div>
</div>
<div class="theme-info">
<h4>深色主题</h4>
<p>护眼的深色界面</p>
</div>
<input type="radio" name="theme" value="dark" id="themeDark">
</div>
<div class="theme-option" data-theme="auto">
<div class="theme-preview auto-preview">
<div class="preview-header"></div>
<div class="preview-content">
<div class="preview-line"></div>
<div class="preview-line short"></div>
</div>
</div>
<div class="theme-info">
<h4>跟随系统</h4>
<p>自动切换主题</p>
</div>
<input type="radio" name="theme" value="auto" id="themeAuto">
</div>
</div>
</div>
<div class="settings-group">
<h3>字体设置</h3>
<div class="setting-item">
<div class="setting-label">
<label for="fontSize">字体大小</label>
<p class="setting-description">调整界面字体大小</p>
</div>
<div class="setting-control">
<div class="input-group">
<input type="range" id="fontSizeSlider" min="12" max="18" value="14" class="range-input">
<span class="font-size-display">14px</span>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="fontFamily">字体系列</label>
<p class="setting-description">选择界面字体</p>
</div>
<div class="setting-control">
<select id="fontFamily" class="setting-select">
<option value="system">系统默认</option>
<option value="sans-serif">无衬线字体</option>
<option value="serif">衬线字体</option>
<option value="monospace">等宽字体</option>
</select>
</div>
</div>
</div>
</section>
<!-- 高级设置 -->
<section class="tab-content" id="advanced">
<div class="content-header">
<h2>高级设置</h2>
<p>面向高级用户的配置选项</p>
</div>
<div class="settings-group">
<h3>开发者选项</h3>
<div class="setting-item">
<div class="setting-label">
<label for="debugMode">调试模式</label>
<p class="setting-description">启用详细的调试信息</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="debugMode">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label for="logLevel">日志级别</label>
<p class="setting-description">控制日志输出的详细程度</p>
</div>
<div class="setting-control">
<select id="logLevel" class="setting-select">
<option value="error">仅错误</option>
<option value="warn">警告及以上</option>
<option value="info">信息及以上</option>
<option value="debug">调试及以上</option>
</select>
</div>
</div>
</div>
<div class="settings-group">
<h3>数据管理</h3>
<div class="setting-item">
<div class="setting-label">
<label>清理缓存</label>
<p class="setting-description">删除所有缓存数据</p>
</div>
<div class="setting-control">
<button type="button" id="clearCacheBtn" class="btn btn-secondary">清理缓存</button>
</div>
</div>
<div class="setting-item">
<div class="setting-label">
<label>重置设置</label>
<p class="setting-description">恢复所有设置到默认值</p>
</div>
<div class="setting-control">
<button type="button" id="resetSettingsBtn" class="btn btn-danger">重置设置</button>
</div>
</div>
</div>
</section>
<!-- 关于页面 -->
<section class="tab-content" id="about">
<div class="content-header">
<h2>关于扩展</h2>
<p>版本信息和相关链接</p>
</div>
<div class="about-content">
<div class="about-card">
<div class="about-icon">
<img src="icons/icon128.png" alt="扩展图标">
</div>
<div class="about-info">
<h3>我的Chrome扩展</h3>
<p class="version-info">版本 1.0.0</p>
<p class="description">这是一个功能强大的Chrome扩展,帮助用户提高浏览效率。</p>
</div>
</div>
<div class="links-section">
<h4>相关链接</h4>
<ul class="links-list">
<li><a href="#" id="homepageLink" target="_blank">官方网站</a></li>
<li><a href="#" id="supportLink" target="_blank">技术支持</a></li>
<li><a href="#" id="feedbackLink" target="_blank">意见反馈</a></li>
<li><a href="#" id="privacyLink" target="_blank">隐私政策</a></li>
</ul>
</div>
<div class="changelog-section">
<h4>更新日志</h4>
<div class="changelog-list">
<div class="changelog-item">
<div class="changelog-version">v1.0.0</div>
<div class="changelog-date">2025-12-03</div>
<div class="changelog-content">
<ul>
<li>初始版本发布</li>
<li>基础功能完成</li>
<li>用户界面优化</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- 保存状态提示 -->
<div class="save-status" id="saveStatus" style="display: none;">
<span class="status-icon">✓</span>
<span class="status-text">设置已保存</span>
</div>
</div>
<!-- 确认对话框 -->
<div class="modal-overlay" id="confirmModal" style="display: none;">
<div class="modal">
<div class="modal-header">
<h3 id="modalTitle">确认操作</h3>
</div>
<div class="modal-body">
<p id="modalMessage">确定要执行此操作吗?</p>
</div>
<div class="modal-footer">
<button type="button" id="modalCancel" class="btn btn-secondary">取消</button>
<button type="button" id="modalConfirm" class="btn btn-primary">确认</button>
</div>
</div>
</div>
<script src="options.js"></script>
</body>
</html>7.2.2 Python模拟Options页面生成器
# options_generator.py - 模拟Chrome扩展Options页面生成器
from typing import Dict, List, Any, Optional
import json
from datetime import datetime
class OptionsGenerator:
"""Chrome扩展Options页面生成器"""
def __init__(self):
self.title = "扩展设置"
self.version = "1.0.0"
self.tabs = []
self.settings_structure = {}
def add_tab(self, tab_id: str, icon: str, title: str, description: str):
"""添加设置标签页"""
tab = {
'id': tab_id,
'icon': icon,
'title': title,
'description': description,
'groups': []
}
self.tabs.append(tab)
def add_settings_group(self, tab_id: str, group_title: str, settings: List[Dict]):
"""添加设置组"""
tab = next((t for t in self.tabs if t['id'] == tab_id), None)
if tab:
group = {
'title': group_title,
'settings': settings
}
tab['groups'].append(group)
def generate_setting_html(self, setting: Dict[str, Any]) -> str:
"""生成单个设置项的HTML"""
setting_type = setting.get('type', 'checkbox')
setting_id = setting.get('id', '')
label = setting.get('label', '')
description = setting.get('description', '')
if setting_type == 'checkbox':
return f'''
<div class="setting-item">
<div class="setting-label">
<label for="{setting_id}">{label}</label>
<p class="setting-description">{description}</p>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="{setting_id}">
<span class="toggle-slider"></span>
</label>
</div>
</div>'''
elif setting_type == 'select':
options_html = []
for option in setting.get('options', []):
options_html.append(f'<option value="{option["value"]}">{option["text"]}</option>')
return f'''
<div class="setting-item">
<div class="setting-label">
<label for="{setting_id}">{label}</label>
<p class="setting-description">{description}</p>
</div>
<div class="setting-control">
<select id="{setting_id}" class="setting-select">
{"".join(options_html)}
</select>
</div>
</div>'''
elif setting_type == 'range':
min_val = setting.get('min', 0)
max_val = setting.get('max', 100)
default_val = setting.get('default', 50)
unit = setting.get('unit', '')
return f'''
<div class="setting-item">
<div class="setting-label">
<label for="{setting_id}">{label}</label>
<p class="setting-description">{description}</p>
</div>
<div class="setting-control">
<div class="input-group">
<input type="range" id="{setting_id}Slider" min="{min_val}"
max="{max_val}" value="{default_val}" class="range-input">
<input type="number" id="{setting_id}" min="{min_val}"
max="{max_val}" value="{default_val}" class="number-input">
<span class="input-unit">{unit}</span>
</div>
</div>
</div>'''
return ''
def generate_tab_content(self, tab: Dict[str, Any]) -> str:
"""生成标签页内容HTML"""
groups_html = []
for group in tab['groups']:
settings_html = []
for setting in group['settings']:
settings_html.append(self.generate_setting_html(setting))
groups_html.append(f'''
<div class="settings-group">
<h3>{group['title']}</h3>
{"".join(settings_html)}
</div>''')
return f'''
<section class="tab-content" id="{tab['id']}">
<div class="content-header">
<h2>{tab['title']}</h2>
<p>{tab['description']}</p>
</div>
{"".join(groups_html)}
</section>'''
def generate_navigation(self) -> str:
"""生成导航HTML"""
nav_items = []
for i, tab in enumerate(self.tabs):
active_class = ' active' if i == 0 else ''
nav_items.append(f'''
<li class="nav-item{active_class}" data-tab="{tab['id']}">
<span class="nav-icon">{tab['icon']}</span>
<span class="nav-text">{tab['title']}</span>
</li>''')
return f'''
<nav class="sidebar">
<ul class="nav-list">
{"".join(nav_items)}
</ul>
</nav>'''
def generate_complete_html(self) -> str:
"""生成完整的Options页面HTML"""
navigation_html = self.generate_navigation()
content_html = []
for i, tab in enumerate(self.tabs):
active_class = ' active' if i == 0 else ''
tab_html = self.generate_tab_content(tab)
tab_html = tab_html.replace('class="tab-content"',
f'class="tab-content{active_class}"')
content_html.append(tab_html)
return f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{self.title}</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="container">
<header class="header">
<div class="header-content">
<div class="logo-section">
<img src="icons/icon48.png" alt="Logo" class="logo">
<div class="title-section">
<h1>{self.title}</h1>
<p class="version">版本 {self.version}</p>
</div>
</div>
</div>
</header>
<div class="main-wrapper">
{navigation_html}
<main class="main-content">
{"".join(content_html)}
</main>
</div>
</div>
<script src="options.js"></script>
</body>
</html>'''
# 使用示例
def create_sample_options():
"""创建示例Options页面"""
generator = OptionsGenerator()
# 添加常规设置标签页
generator.add_tab('general', '⚙️', '常规设置', '配置扩展的基本行为和功能')
generator.add_settings_group('general', '基本功能', [
{
'type': 'checkbox',
'id': 'enableExtension',
'label': '启用扩展',
'description': '控制扩展的全局开关'
},
{
'type': 'select',
'id': 'updateInterval',
'label': '更新间隔',
'description': '自动更新数据的时间间隔',
'options': [
{'value': '1', 'text': '1分钟'},
{'value': '5', 'text': '5分钟'},
{'value': '10', 'text': '10分钟'}
]
},
{
'type': 'range',
'id': 'maxCacheSize',
'label': '缓存大小限制',
'description': '最大缓存数据大小',
'min': 10,
'max': 500,
'default': 100,
'unit': 'MB'
}
])
# 添加外观设置标签页
generator.add_tab('appearance', '🎨', '外观设置', '自定义界面外观和主题')
generator.add_settings_group('appearance', '主题设置', [
{
'type': 'select',
'id': 'theme',
'label': '主题',
'description': '选择界面主题',
'options': [
{'value': 'light', 'text': '浅色主题'},
{'value': 'dark', 'text': '深色主题'},
{'value': 'auto', 'text': '跟随系统'}
]
},
{
'type': 'range',
'id': 'fontSize',
'label': '字体大小',
'description': '调整界面字体大小',
'min': 12,
'max': 18,
'default': 14,
'unit': 'px'
}
])
return generator.generate_complete_html()
# 生成示例页面
sample_html = create_sample_options()
print("生成的Options页面HTML:")
print(sample_html[:1000] + "...") # 显示前1000字符7.3 CSS样式设计
7.3.1 现代化布局样式
/* options.css */
:root {
--primary-color: #4285f4;
--secondary-color: #34a853;
--danger-color: #ea4335;
--warning-color: #fbbc05;
--text-primary: #202124;
--text-secondary: #5f6368;
--text-muted: #9aa0a6;
--background-primary: #ffffff;
--background-secondary: #f8f9fa;
--background-tertiary: #e8f0fe;
--border-color: #dadce0;
--border-light: #f1f3f4;
--shadow-light: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-heavy: 0 8px 16px rgba(0, 0, 0, 0.15);
--border-radius: 8px;
--border-radius-large: 12px;
--transition: all 0.2s ease;
--sidebar-width: 240px;
}
/* 深色主题 */
.dark-theme {
--text-primary: #e8eaed;
--text-secondary: #9aa0a6;
--text-muted: #5f6368;
--background-primary: #202124;
--background-secondary: #303134;
--background-tertiary: #1a73e8;
--border-color: #5f6368;
--border-light: #3c4043;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--background-primary);
overflow-x: hidden;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.header {
background-color: var(--background-primary);
border-bottom: 1px solid var(--border-color);
padding: 20px 24px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-light);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
}
.logo-section {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
width: 40px;
height: 40px;
border-radius: var(--border-radius);
}
.title-section h1 {
font-size: 24px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 2px;
}
.version {
font-size: 12px;
color: var(--text-secondary);
}
.header-actions {
display: flex;
gap: 12px;
}
/* 主体布局 */
.main-wrapper {
flex: 1;
display: flex;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* 侧边导航 */
.sidebar {
width: var(--sidebar-width);
background-color: var(--background-secondary);
border-right: 1px solid var(--border-color);
padding: 24px 0;
position: sticky;
top: 81px;
height: calc(100vh - 81px);
overflow-y: auto;
}
.nav-list {
list-style: none;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
cursor: pointer;
transition: var(--transition);
color: var(--text-secondary);
}
.nav-item:hover {
background-color: var(--background-tertiary);
color: var(--primary-color);
}
.nav-item.active {
background-color: var(--background-tertiary);
color: var(--primary-color);
border-right: 3px solid var(--primary-color);
}
.nav-icon {
font-size: 18px;
width: 24px;
text-align: center;
}
.nav-text {
font-weight: 500;
}
/* 主内容区 */
.main-content {
flex: 1;
padding: 24px;
overflow-y: auto;
max-height: calc(100vh - 81px);
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
.content-header {
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-light);
}
.content-header h2 {
font-size: 28px;
font-weight: 400;
color: var(--text-primary);
margin-bottom: 8px;
}
.content-header p {
font-size: 16px;
color: var(--text-secondary);
}
/* 设置组 */
.settings-group {
margin-bottom: 40px;
background-color: var(--background-primary);
border: 1px solid var(--border-light);
border-radius: var(--border-radius-large);
overflow: hidden;
box-shadow: var(--shadow-light);
}
.settings-group h3 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
padding: 16px 20px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--border-light);
margin: 0;
}
/* 设置项 */
.setting-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--border-light);
}
.setting-item:last-child {
border-bottom: none;
}
.setting-label {
flex: 1;
margin-right: 20px;
}
.setting-label label {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
display: block;
margin-bottom: 4px;
cursor: pointer;
}
.setting-description {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
}
.setting-control {
flex-shrink: 0;
display: flex;
align-items: center;
}
/* 开关样式 */
.toggle-switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: var(--transition);
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: var(--transition);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--primary-color);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
}
/* 选择框样式 */
.setting-select {
padding: 8px 32px 8px 12px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--background-primary);
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 8px center;
background-repeat: no-repeat;
background-size: 16px;
min-width: 120px;
}
.setting-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
}
/* 输入组样式 */
.input-group {
display: flex;
align-items: center;
gap: 12px;
}
.range-input {
width: 120px;
height: 6px;
border-radius: 3px;
background: var(--border-color);
outline: none;
appearance: none;
}
.range-input::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
box-shadow: var(--shadow-light);
}
.range-input::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: none;
box-shadow: var(--shadow-light);
}
.number-input {
width: 60px;
padding: 6px 8px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--background-primary);
color: var(--text-primary);
font-size: 13px;
text-align: center;
}
.input-unit {
font-size: 12px;
color: var(--text-secondary);
min-width: 20px;
}
/* 按钮样式 */
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--border-radius);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
display: inline-flex;
align-items: center;
gap: 6px;
text-decoration: none;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #3367d6;
}
.btn-secondary {
background-color: var(--background-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background-color: var(--border-color);
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #d33b2c;
}
/* 功能卡片 */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.feature-card {
border: 1px solid var(--border-light);
border-radius: var(--border-radius-large);
padding: 20px;
background-color: var(--background-primary);
box-shadow: var(--shadow-light);
transition: var(--transition);
}
.feature-card:hover {
box-shadow: var(--shadow-medium);
}
.feature-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.feature-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--background-tertiary);
border-radius: var(--border-radius);
}
.feature-info {
flex: 1;
}
.feature-info h4 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.feature-info p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
}
.feature-settings {
padding-top: 12px;
border-top: 1px solid var(--border-light);
}
.sub-setting {
padding: 8px 0;
}
.sub-setting label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
}
/* 主题选择器 */
.theme-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.theme-option {
border: 2px solid var(--border-light);
border-radius: var(--border-radius-large);
padding: 16px;
cursor: pointer;
transition: var(--transition);
position: relative;
}
.theme-option:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-medium);
}
.theme-option input[type="radio"] {
position: absolute;
top: 12px;
right: 12px;
}
.theme-preview {
width: 100%;
height: 80px;
border-radius: var(--border-radius);
margin-bottom: 12px;
overflow: hidden;
border: 1px solid var(--border-light);
}
.light-preview {
background: linear-gradient(to bottom, #f8f9fa 30%, #ffffff 30%);
}
.dark-preview {
background: linear-gradient(to bottom, #303134 30%, #202124 30%);
}
.auto-preview {
background: linear-gradient(to right, #f8f9fa 50%, #303134 50%);
}
.preview-header {
height: 30%;
background-color: rgba(0, 0, 0, 0.1);
}
.preview-content {
padding: 8px;
}
.preview-line {
height: 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 2px;
margin-bottom: 4px;
}
.preview-line.short {
width: 60%;
}
.theme-info h4 {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 2px;
}
.theme-info p {
font-size: 12px;
color: var(--text-secondary);
}
/* 白名单样式 */
.whitelist-section .input-group {
margin-bottom: 16px;
}
.whitelist-section input[type="url"] {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 14px;
}
.whitelist-list {
list-style: none;
border: 1px solid var(--border-light);
border-radius: var(--border-radius);
max-height: 200px;
overflow-y: auto;
}
.whitelist-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
}
.whitelist-item:last-child {
border-bottom: none;
}
.whitelist-url {
font-size: 14px;
color: var(--text-primary);
}
.remove-btn {
color: var(--danger-color);
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius);
font-size: 12px;
}
.remove-btn:hover {
background-color: var(--danger-color);
color: white;
}
/* 关于页面样式 */
.about-content {
display: flex;
flex-direction: column;
gap: 32px;
}
.about-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background-color: var(--background-secondary);
border-radius: var(--border-radius-large);
border: 1px solid var(--border-light);
}
.about-icon img {
width: 64px;
height: 64px;
border-radius: var(--border-radius-large);
}
.about-info h3 {
font-size: 20px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.version-info {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.description {
font-size: 14px;
color: var(--text-primary);
line-height: 1.5;
}
.links-section h4,
.changelog-section h4 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 12px;
}
.links-list {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.links-list a {
color: var(--primary-color);
text-decoration: none;
font-size: 14px;
padding: 6px 12px;
border-radius: var(--border-radius);
border: 1px solid var(--primary-color);
transition: var(--transition);
}
.links-list a:hover {
background-color: var(--primary-color);
color: white;
}
.changelog-list {
background-color: var(--background-secondary);
border-radius: var(--border-radius-large);
overflow: hidden;
border: 1px solid var(--border-light);
}
.changelog-item {
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
}
.changelog-item:last-child {
border-bottom: none;
}
.changelog-version {
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
}
.changelog-date {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.changelog-content ul {
margin-left: 16px;
}
.changelog-content li {
font-size: 13px;
color: var(--text-primary);
line-height: 1.4;
margin-bottom: 2px;
}
/* 保存状态提示 */
.save-status {
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 16px;
background-color: var(--secondary-color);
color: white;
border-radius: var(--border-radius);
display: flex;
align-items: center;
gap: 8px;
box-shadow: var(--shadow-heavy);
z-index: 1000;
animation: slideInRight 0.3s ease;
}
.status-icon {
font-weight: bold;
}
/* 模态框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal {
background-color: var(--background-primary);
border-radius: var(--border-radius-large);
box-shadow: var(--shadow-heavy);
max-width: 400px;
width: 90%;
overflow: hidden;
}
.modal-header {
padding: 16px 20px;
background-color: var(--background-secondary);
border-bottom: 1px solid var(--border-light);
}
.modal-header h3 {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
}
.modal-body {
padding: 20px;
}
.modal-body p {
font-size: 14px;
color: var(--text-primary);
line-height: 1.5;
}
.modal-footer {
padding: 16px 20px;
background-color: var(--background-secondary);
border-top: 1px solid var(--border-light);
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInRight {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-wrapper {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
position: static;
border-right: none;
border-bottom: 1px solid var(--border-color);
}
.nav-list {
display: flex;
overflow-x: auto;
padding: 0 12px;
}
.nav-item {
white-space: nowrap;
padding: 12px 16px;
border-right: none;
border-bottom: 3px solid transparent;
}
.nav-item.active {
border-right: none;
border-bottom-color: var(--primary-color);
}
.main-content {
max-height: none;
}
.header-content {
flex-direction: column;
gap: 16px;
text-align: center;
}
.feature-grid {
grid-template-columns: 1fr;
}
.theme-selector {
grid-template-columns: 1fr;
}
.about-card {
flex-direction: column;
text-align: center;
}
}
@media (max-width: 480px) {
.header {
padding: 16px;
}
.main-content {
padding: 16px;
}
.setting-item {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.setting-control {
align-self: flex-end;
}
}
/* 打印样式 */
@media print {
.sidebar,
.header-actions,
.save-status,
.modal-overlay {
display: none;
}
.main-wrapper {
flex-direction: column;
}
.main-content {
max-height: none;
}
.tab-content {
display: block !important;
page-break-before: always;
}
.tab-content:first-child {
page-break-before: auto;
}
}7.4 JavaScript交互逻辑
7.4.1 完整的Options脚本
// options.js
class OptionsManager {
constructor() {
this.settings = {};
this.defaults = {
enableExtension: true,
autoStart: false,
updateInterval: 5,
maxCacheSize: 100,
enableNotifications: true,
notificationSound: false,
featureAnalytics: false,
featurePrivacy: true,
theme: 'light',
fontSize: 14,
fontFamily: 'system',
debugMode: false,
logLevel: 'error',
whitelist: []
};
this.init();
}
async init() {
try {
await this.loadSettings();
this.setupNavigation();
this.bindEvents();
this.populateUI();
this.setupDependencies();
console.log('Options页面初始化完成');
} catch (error) {
console.error('初始化失败:', error);
this.showToast('初始化失败,请刷新页面', 'error');
}
}
async loadSettings() {
try {
const result = await chrome.storage.sync.get(null);
this.settings = { ...this.defaults, ...result };
} catch (error) {
console.error('加载设置失败:', error);
this.settings = { ...this.defaults };
}
}
setupNavigation() {
const navItems = document.querySelectorAll('.nav-item');
const tabContents = document.querySelectorAll('.tab-content');
navItems.forEach(item => {
item.addEventListener('click', () => {
const tabId = item.dataset.tab;
// 更新导航状态
navItems.forEach(nav => nav.classList.remove('active'));
item.classList.add('active');
// 显示对应标签页内容
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === tabId) {
content.classList.add('active');
}
});
});
});
}
bindEvents() {
// 通用设置项事件绑定
this.bindCheckboxes();
this.bindSelects();
this.bindRanges();
this.bindButtons();
this.bindSpecialControls();
}
bindCheckboxes() {
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const key = e.target.id;
const value = e.target.checked;
this.updateSetting(key, value);
});
});
}
bindSelects() {
const selects = document.querySelectorAll('select');
selects.forEach(select => {
select.addEventListener('change', (e) => {
const key = e.target.id;
const value = e.target.value;
if (key === 'updateInterval') {
this.updateSetting(key, parseInt(value));
} else {
this.updateSetting(key, value);
}
});
});
}
bindRanges() {
// 缓存大小滑块
const cacheSlider = document.getElementById('cacheSlider');
const cacheInput = document.getElementById('maxCacheSize');
if (cacheSlider && cacheInput) {
const updateCacheSize = (value) => {
cacheSlider.value = value;
cacheInput.value = value;
this.updateSetting('maxCacheSize', parseInt(value));
};
cacheSlider.addEventListener('input', (e) => {
updateCacheSize(e.target.value);
});
cacheInput.addEventListener('change', (e) => {
updateCacheSize(e.target.value);
});
}
// 字体大小滑块
const fontSizeSlider = document.getElementById('fontSizeSlider');
const fontSizeDisplay = document.querySelector('.font-size-display');
if (fontSizeSlider && fontSizeDisplay) {
fontSizeSlider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
fontSizeDisplay.textContent = `${value}px`;
this.updateSetting('fontSize', value);
this.applyFontSize(value);
});
}
}
bindButtons() {
// 导出配置
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportSettings());
}
// 导入配置
const importBtn = document.getElementById('importBtn');
const importFile = document.getElementById('importFile');
if (importBtn && importFile) {
importBtn.addEventListener('click', () => importFile.click());
importFile.addEventListener('change', (e) => this.importSettings(e));
}
// 清理缓存
const clearCacheBtn = document.getElementById('clearCacheBtn');
if (clearCacheBtn) {
clearCacheBtn.addEventListener('click', () => this.clearCache());
}
// 重置设置
const resetBtn = document.getElementById('resetSettingsBtn');
if (resetBtn) {
resetBtn.addEventListener('click', () => this.resetSettings());
}
// 白名单管理
const addWhitelistBtn = document.getElementById('addWhitelistBtn');
if (addWhitelistBtn) {
addWhitelistBtn.addEventListener('click', () => this.addToWhitelist());
}
// 功能卡片切换
this.bindFeatureCards();
}
bindSpecialControls() {
// 主题选择
const themeRadios = document.querySelectorAll('input[name="theme"]');
themeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
this.updateSetting('theme', e.target.value);
this.applyTheme(e.target.value);
}
});
});
// 通知设置依赖
const notificationToggle = document.getElementById('enableNotifications');
if (notificationToggle) {
notificationToggle.addEventListener('change', (e) => {
this.toggleNotificationDependencies(e.target.checked);
});
}
}
bindFeatureCards() {
const featureToggles = {
'featureAnalytics': 'analyticsSettings',
'featurePrivacy': 'privacySettings'
};
Object.entries(featureToggles).forEach(([toggleId, settingsId]) => {
const toggle = document.getElementById(toggleId);
const settings = document.getElementById(settingsId);
if (toggle && settings) {
toggle.addEventListener('change', (e) => {
settings.style.display = e.target.checked ? 'block' : 'none';
});
}
});
}
populateUI() {
// 填充复选框
Object.keys(this.settings).forEach(key => {
const element = document.getElementById(key);
if (element) {
if (element.type === 'checkbox') {
element.checked = this.settings[key];
} else if (element.tagName === 'SELECT') {
element.value = this.settings[key];
} else if (element.type === 'number' || element.type === 'range') {
element.value = this.settings[key];
}
}
});
// 设置主题单选按钮
const themeRadio = document.getElementById(`theme${this.settings.theme.charAt(0).toUpperCase() + this.settings.theme.slice(1)}`);
if (themeRadio) {
themeRadio.checked = true;
}
// 同步滑块和输入框
this.syncRangeInputs();
// 填充白名单
this.populateWhitelist();
// 应用当前主题
this.applyTheme(this.settings.theme);
// 应用字体大小
this.applyFontSize(this.settings.fontSize);
// 更新功能卡片状态
this.updateFeatureCards();
}
syncRangeInputs() {
// 缓存大小
const cacheSlider = document.getElementById('cacheSlider');
const cacheInput = document.getElementById('maxCacheSize');
if (cacheSlider && cacheInput) {
const value = this.settings.maxCacheSize;
cacheSlider.value = value;
cacheInput.value = value;
}
// 字体大小
const fontSizeSlider = document.getElementById('fontSizeSlider');
const fontSizeDisplay = document.querySelector('.font-size-display');
if (fontSizeSlider && fontSizeDisplay) {
const value = this.settings.fontSize;
fontSizeSlider.value = value;
fontSizeDisplay.textContent = `${value}px`;
}
}
setupDependencies() {
// 通知设置依赖
this.toggleNotificationDependencies(this.settings.enableNotifications);
}
toggleNotificationDependencies(enabled) {
const dependentElements = document.querySelectorAll('.notification-dependent');
dependentElements.forEach(el => {
el.style.display = enabled ? 'flex' : 'none';
});
}
updateFeatureCards() {
const featureToggles = {
'featureAnalytics': 'analyticsSettings',
'featurePrivacy': 'privacySettings'
};
Object.entries(featureToggles).forEach(([toggleId, settingsId]) => {
const toggle = document.getElementById(toggleId);
const settings = document.getElementById(settingsId);
if (toggle && settings) {
settings.style.display = toggle.checked ? 'block' : 'none';
}
});
}
async updateSetting(key, value) {
this.settings[key] = value;
try {
await chrome.storage.sync.set({ [key]: value });
this.showSaveStatus();
// 通知background script设置变更
chrome.runtime.sendMessage({
action: 'settingChanged',
key: key,
value: value
});
} catch (error) {
console.error('保存设置失败:', error);
this.showToast('保存设置失败', 'error');
}
}
applyTheme(theme) {
document.body.className = `${theme}-theme`;
// 如果是自动主题,根据系统设置决定
if (theme === 'auto') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.body.className = `${isDark ? 'dark' : 'light'}-theme`;
}
}
applyFontSize(size) {
document.documentElement.style.setProperty('--base-font-size', `${size}px`);
}
// 白名单管理
populateWhitelist() {
const whitelistList = document.getElementById('whitelistList');
if (!whitelistList) return;
whitelistList.innerHTML = '';
this.settings.whitelist.forEach((domain, index) => {
const listItem = document.createElement('li');
listItem.className = 'whitelist-item';
listItem.innerHTML = `
<span class="whitelist-url">${domain}</span>
<button type="button" class="remove-btn" data-index="${index}">移除</button>
`;
listItem.querySelector('.remove-btn').addEventListener('click', () => {
this.removeFromWhitelist(index);
});
whitelistList.appendChild(listItem);
});
}
addToWhitelist() {
const input = document.getElementById('whitelistInput');
if (!input) return;
const domain = input.value.trim();
if (!domain) {
this.showToast('请输入有效的域名', 'warning');
return;
}
// 简单的域名验证
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) {
this.showToast('请输入有效的域名格式', 'warning');
return;
}
if (this.settings.whitelist.includes(domain)) {
this.showToast('该域名已在白名单中', 'warning');
return;
}
this.settings.whitelist.push(domain);
this.updateSetting('whitelist', this.settings.whitelist);
this.populateWhitelist();
input.value = '';
this.showToast('域名已添加到白名单', 'success');
}
removeFromWhitelist(index) {
this.settings.whitelist.splice(index, 1);
this.updateSetting('whitelist', this.settings.whitelist);
this.populateWhitelist();
this.showToast('域名已从白名单移除', 'success');
}
// 配置导入导出
exportSettings() {
const dataStr = JSON.stringify(this.settings, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = `extension-settings-${new Date().toISOString().split('T')[0]}.json`;
link.click();
this.showToast('配置已导出', 'success');
}
async importSettings(event) {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
const importedSettings = JSON.parse(text);
// 验证导入的设置
const validSettings = {};
Object.keys(this.defaults).forEach(key => {
if (importedSettings.hasOwnProperty(key)) {
validSettings[key] = importedSettings[key];
}
});
this.showConfirmDialog(
'确认导入配置',
'导入配置将覆盖当前设置,确定继续吗?',
async () => {
this.settings = { ...this.defaults, ...validSettings };
await chrome.storage.sync.set(this.settings);
this.populateUI();
this.showToast('配置已导入', 'success');
}
);
} catch (error) {
console.error('导入失败:', error);
this.showToast('配置文件格式错误', 'error');
}
// 清空文件选择
event.target.value = '';
}
// 数据管理
async clearCache() {
this.showConfirmDialog(
'清理缓存',
'确定要清理所有缓存数据吗?此操作不可恢复。',
async () => {
try {
await chrome.storage.local.clear();
this.showToast('缓存已清理', 'success');
} catch (error) {
console.error('清理缓存失败:', error);
this.showToast('清理缓存失败', 'error');
}
}
);
}
async resetSettings() {
this.showConfirmDialog(
'重置设置',
'确定要恢复所有设置到默认值吗?此操作不可恢复。',
async () => {
try {
this.settings = { ...this.defaults };
await chrome.storage.sync.set(this.settings);
this.populateUI();
this.showToast('设置已重置', 'success');
} catch (error) {
console.error('重置设置失败:', error);
this.showToast('重置设置失败', 'error');
}
}
);
}
// UI反馈方法
showSaveStatus() {
const saveStatus = document.getElementById('saveStatus');
if (saveStatus) {
saveStatus.style.display = 'flex';
setTimeout(() => {
saveStatus.style.display = 'none';
}, 2000);
}
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
border-radius: 6px;
color: white;
font-size: 14px;
z-index: 10000;
animation: slideInRight 0.3s ease;
background-color: ${
type === 'success' ? '#34a853' :
type === 'error' ? '#ea4335' :
type === 'warning' ? '#fbbc05' : '#4285f4'
};
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 3000);
}
showConfirmDialog(title, message, onConfirm) {
const modal = document.getElementById('confirmModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalConfirm = document.getElementById('modalConfirm');
const modalCancel = document.getElementById('modalCancel');
if (modal && modalTitle && modalMessage && modalConfirm && modalCancel) {
modalTitle.textContent = title;
modalMessage.textContent = message;
const hideModal = () => {
modal.style.display = 'none';
};
modalConfirm.onclick = () => {
hideModal();
onConfirm();
};
modalCancel.onclick = hideModal;
modal.onclick = (e) => {
if (e.target === modal) hideModal();
};
modal.style.display = 'flex';
}
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
new OptionsManager();
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + S: 快速保存
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
// 触发保存状态显示
const event = new Event('change');
document.querySelector('input[type="checkbox"]')?.dispatchEvent(event);
}
// Escape: 关闭模态框
if (e.key === 'Escape') {
const modal = document.getElementById('confirmModal');
if (modal && modal.style.display === 'flex') {
modal.style.display = 'none';
}
}
});
// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const currentTheme = document.querySelector('input[name="theme"]:checked')?.value;
if (currentTheme === 'auto') {
document.body.className = `${e.matches ? 'dark' : 'light'}-theme`;
}
});7.4.2 Python模拟设置管理器
# settings_manager.py - 模拟Chrome扩展设置管理器
import json
import os
from datetime import datetime
from typing import Dict, Any, Optional, List
class SettingsManager:
"""Chrome扩展设置管理器"""
def __init__(self, config_file: str = "extension_settings.json"):
self.config_file = config_file
self.defaults = {
'enableExtension': True,
'autoStart': False,
'updateInterval': 5,
'maxCacheSize': 100,
'enableNotifications': True,
'notificationSound': False,
'featureAnalytics': False,
'featurePrivacy': True,
'theme': 'light',
'fontSize': 14,
'fontFamily': 'system',
'debugMode': False,
'logLevel': 'error',
'whitelist': []
}
self.settings = {}
self.load_settings()
def load_settings(self):
"""加载设置"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r', encoding='utf-8') as f:
saved_settings = json.load(f)
self.settings = {**self.defaults, **saved_settings}
else:
self.settings = self.defaults.copy()
print("设置加载完成")
except Exception as e:
print(f"设置加载失败: {e}")
self.settings = self.defaults.copy()
def save_settings(self):
"""保存设置"""
try:
with open(self.config_file, 'w', encoding='utf-8') as f:
json.dump(self.settings, f, indent=2, ensure_ascii=False)
print("设置保存完成")
return True
except Exception as e:
print(f"设置保存失败: {e}")
return False
def get_setting(self, key: str, default=None):
"""获取单个设置"""
return self.settings.get(key, default)
def update_setting(self, key: str, value: Any):
"""更新单个设置"""
if key in self.defaults:
old_value = self.settings.get(key)
self.settings[key] = value
if self.save_settings():
print(f"设置更新: {key}: {old_value} -> {value}")
self.on_setting_changed(key, value, old_value)
return True
else:
# 回滚更改
self.settings[key] = old_value
return False
else:
print(f"未知设置键: {key}")
return False
def on_setting_changed(self, key: str, new_value: Any, old_value: Any):
"""设置变更回调"""
if key == 'theme':
print(f"主题已更改为: {new_value}")
elif key == 'enableExtension':
print(f"扩展{'已启用' if new_value else '已禁用'}")
elif key == 'whitelist':
if len(new_value) > len(old_value):
print("添加了新的白名单域名")
elif len(new_value) < len(old_value):
print("移除了白名单域名")
def add_to_whitelist(self, domain: str) -> bool:
"""添加域名到白名单"""
if not self.is_valid_domain(domain):
print(f"无效域名: {domain}")
return False
whitelist = self.settings.get('whitelist', [])
if domain in whitelist:
print(f"域名已存在: {domain}")
return False
whitelist.append(domain)
return self.update_setting('whitelist', whitelist)
def remove_from_whitelist(self, domain: str) -> bool:
"""从白名单移除域名"""
whitelist = self.settings.get('whitelist', [])
if domain not in whitelist:
print(f"域名不在白名单中: {domain}")
return False
whitelist.remove(domain)
return self.update_setting('whitelist', whitelist)
def is_valid_domain(self, domain: str) -> bool:
"""验证域名格式"""
import re
pattern = r'^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$'
return re.match(pattern, domain) is not None
def export_settings(self, filename: Optional[str] = None) -> str:
"""导出设置到文件"""
if not filename:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"extension_settings_export_{timestamp}.json"
try:
export_data = {
'exported_at': datetime.now().isoformat(),
'version': '1.0.0',
'settings': self.settings
}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(export_data, f, indent=2, ensure_ascii=False)
print(f"设置已导出到: {filename}")
return filename
except Exception as e:
print(f"导出设置失败: {e}")
return ""
def import_settings(self, filename: str) -> bool:
"""从文件导入设置"""
try:
with open(filename, 'r', encoding='utf-8') as f:
import_data = json.load(f)
if 'settings' in import_data:
imported_settings = import_data['settings']
else:
imported_settings = import_data
# 验证并导入设置
valid_settings = {}
for key in self.defaults.keys():
if key in imported_settings:
valid_settings[key] = imported_settings[key]
self.settings = {**self.defaults, **valid_settings}
if self.save_settings():
print(f"设置已从 {filename} 导入")
return True
else:
return False
except Exception as e:
print(f"导入设置失败: {e}")
return False
def reset_settings(self) -> bool:
"""重置所有设置到默认值"""
self.settings = self.defaults.copy()
if self.save_settings():
print("设置已重置到默认值")
return True
return False
def get_all_settings(self) -> Dict[str, Any]:
"""获取所有设置"""
return self.settings.copy()
def validate_settings(self) -> List[str]:
"""验证设置的有效性"""
errors = []
# 验证更新间隔
interval = self.settings.get('updateInterval', 5)
if not isinstance(interval, int) or interval < 1 or interval > 60:
errors.append("更新间隔必须是1-60之间的整数")
# 验证缓存大小
cache_size = self.settings.get('maxCacheSize', 100)
if not isinstance(cache_size, int) or cache_size < 10 or cache_size > 500:
errors.append("缓存大小必须是10-500之间的整数")
# 验证字体大小
font_size = self.settings.get('fontSize', 14)
if not isinstance(font_size, int) or font_size < 12 or font_size > 18:
errors.append("字体大小必须是12-18之间的整数")
# 验证主题
theme = self.settings.get('theme', 'light')
if theme not in ['light', 'dark', 'auto']:
errors.append("主题必须是light、dark或auto之一")
# 验证白名单
whitelist = self.settings.get('whitelist', [])
if not isinstance(whitelist, list):
errors.append("白名单必须是数组格式")
else:
for domain in whitelist:
if not self.is_valid_domain(domain):
errors.append(f"无效的白名单域名: {domain}")
return errors
def print_settings_summary(self):
"""打印设置摘要"""
print("\n=== 扩展设置摘要 ===")
print(f"扩展状态: {'启用' if self.settings.get('enableExtension') else '禁用'}")
print(f"主题: {self.settings.get('theme', 'light')}")
print(f"更新间隔: {self.settings.get('updateInterval', 5)}分钟")
print(f"缓存大小: {self.settings.get('maxCacheSize', 100)}MB")
print(f"通知: {'启用' if self.settings.get('enableNotifications') else '禁用'}")
print(f"白名单域名: {len(self.settings.get('whitelist', []))}个")
print(f"调试模式: {'启用' if self.settings.get('debugMode') else '禁用'}")
print("==================\n")
# 使用示例
def demo_settings_manager():
"""演示设置管理器功能"""
manager = SettingsManager()
# 显示当前设置摘要
manager.print_settings_summary()
# 更新一些设置
manager.update_setting('theme', 'dark')
manager.update_setting('updateInterval', 10)
manager.update_setting('enableNotifications', False)
# 添加白名单域名
manager.add_to_whitelist('example.com')
manager.add_to_whitelist('google.com')
# 验证设置
errors = manager.validate_settings()
if errors:
print("设置验证错误:")
for error in errors:
print(f" - {error}")
else:
print("所有设置验证通过")
# 导出设置
export_file = manager.export_settings()
# 显示最终设置摘要
manager.print_settings_summary()
return manager
# 运行演示(注释掉实际执行)
# demo_manager = demo_settings_manager()最佳实践
- 数据验证: 始终验证用户输入的设置值
- 默认值: 为所有设置项提供合理的默认值
- 错误处理: 优雅处理存储失败的情况
- 用户体验: 提供即时的视觉反馈
- 性能优化: 避免频繁的存储操作
扩展功能
- 支持设置分组和搜索
- 实现设置项依赖关系
- 添加设置验证规则
- 支持批量操作
- 提供高级用户模式
总结
本章介绍了Chrome扩展Options页面的完整开发流程,包括复杂的HTML结构、现代化的CSS样式、完整的JavaScript交互逻辑,以及设置管理的最佳实践。一个优秀的Options页面能够显著提升用户体验和扩展的可用性。下一章我们将学习消息传递机制,实现扩展组件间的高效通信。
