10.1 项目概述
10.1.1 项目介绍
我们将构建一个完整的任务管理应用,它包含以下功能:
- 用户认证和权限管理
- 任务的增删改查
- 项目分组管理
- 实时协作功能
- 数据同步和离线支持
- 性能监控和错误追踪
graph TD
A[用户登录] --> B[主界面]
B --> C[项目管理]
B --> D[任务管理]
B --> E[团队协作]
C --> C1[创建项目]
C --> C2[编辑项目]
C --> C3[删除项目]
D --> D1[添加任务]
D --> D2[编辑任务]
D --> D3[完成任务]
D --> D4[任务筛选]
E --> E1[邀请成员]
E --> E2[实时更新]
E --> E3[消息通知]
10.1.2 技术架构
graph TB
subgraph "前端层"
A[Sciter-JS UI]
B[组件系统]
C[状态管理]
end
subgraph "业务层"
D[用户管理]
E[项目管理]
F[任务管理]
G[协作管理]
end
subgraph "数据层"
H[本地存储]
I[网络请求]
J[数据同步]
end
subgraph "服务层"
K[认证服务]
L[API 服务]
M[WebSocket 服务]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
I --> J
J --> K
K --> L
L --> M
10.2 项目结构设计
10.2.1 目录结构
task-manager/
├── src/
│ ├── components/ # 组件目录
│ │ ├── common/ # 通用组件
│ │ ├── auth/ # 认证组件
│ │ ├── project/ # 项目组件
│ │ ├── task/ # 任务组件
│ │ └── collaboration/ # 协作组件
│ ├── services/ # 服务层
│ │ ├── auth.js
│ │ ├── project.js
│ │ ├── task.js
│ │ └── websocket.js
│ ├── store/ # 状态管理
│ │ ├── index.js
│ │ ├── auth.js
│ │ ├── project.js
│ │ └── task.js
│ ├── utils/ # 工具函数
│ │ ├── api.js
│ │ ├── storage.js
│ │ ├── validation.js
│ │ └── helpers.js
│ ├── styles/ # 样式文件
│ │ ├── global.css
│ │ ├── components.css
│ │ └── themes.css
│ ├── assets/ # 静态资源
│ │ ├── icons/
│ │ ├── images/
│ │ └── fonts/
│ └── main.js # 入口文件
├── tests/ # 测试文件
├── docs/ # 文档
├── build/ # 构建脚本
├── package.json
└── README.md
10.2.2 应用入口
<!-- main.html -->
<!DOCTYPE html>
<html>
<head>
<title>任务管理器</title>
<meta charset="utf-8">
<style src="styles/global.css"></style>
<style src="styles/components.css"></style>
<style src="styles/themes.css"></style>
</head>
<body>
<div id="app">
<div id="loading" class="loading-screen">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
// main.js
import { App } from './components/App.js';
import { Store } from './store/index.js';
import { AuthService } from './services/auth.js';
import { WebSocketService } from './services/websocket.js';
import { ErrorTracker } from './utils/error-tracker.js';
import { PerformanceMonitor } from './utils/performance-monitor.js';
class TaskManagerApp {
constructor() {
this.store = new Store();
this.authService = new AuthService(this.store);
this.wsService = new WebSocketService(this.store);
this.errorTracker = new ErrorTracker();
this.performanceMonitor = new PerformanceMonitor();
this.app = null;
this.isInitialized = false;
}
// 初始化应用
async initialize() {
try {
console.log('初始化任务管理器应用...');
// 启动性能监控
this.performanceMonitor.start();
// 初始化错误追踪
this.errorTracker.initialize();
// 检查用户认证状态
await this.checkAuthStatus();
// 创建应用实例
this.app = new App({
store: this.store,
authService: this.authService,
wsService: this.wsService
});
// 渲染应用
await this.renderApp();
// 设置全局事件监听
this.setupGlobalEventListeners();
this.isInitialized = true;
console.log('应用初始化完成');
} catch (error) {
console.error('应用初始化失败:', error);
this.errorTracker.captureError(error);
this.showErrorScreen(error);
}
}
// 检查认证状态
async checkAuthStatus() {
const token = localStorage.getItem('auth_token');
if (token) {
try {
const user = await this.authService.validateToken(token);
this.store.dispatch('auth/setUser', user);
this.store.dispatch('auth/setAuthenticated', true);
} catch (error) {
console.warn('Token 验证失败:', error.message);
localStorage.removeItem('auth_token');
}
}
}
// 渲染应用
async renderApp() {
const appContainer = document.getElementById('app');
const loadingScreen = document.getElementById('loading');
// 渲染主应用
await this.app.render(appContainer);
// 隐藏加载屏幕
loadingScreen.style.display = 'none';
}
// 设置全局事件监听
setupGlobalEventListeners() {
// 窗口关闭前保存数据
window.addEventListener('beforeunload', () => {
this.store.dispatch('app/saveState');
});
// 网络状态变化
window.addEventListener('online', () => {
this.store.dispatch('app/setOnlineStatus', true);
this.wsService.reconnect();
});
window.addEventListener('offline', () => {
this.store.dispatch('app/setOnlineStatus', false);
});
// 全局错误处理
window.addEventListener('error', (event) => {
this.errorTracker.captureError(event.error);
});
window.addEventListener('unhandledrejection', (event) => {
this.errorTracker.captureError(event.reason);
});
}
// 显示错误屏幕
showErrorScreen(error) {
const appContainer = document.getElementById('app');
appContainer.innerHTML = `
<div class="error-screen">
<h1>应用启动失败</h1>
<p>抱歉,应用无法正常启动。请刷新页面重试。</p>
<details>
<summary>错误详情</summary>
<pre>${error.stack || error.message}</pre>
</details>
<button onclick="location.reload()">刷新页面</button>
</div>
`;
}
// 销毁应用
destroy() {
if (this.app) {
this.app.destroy();
}
this.performanceMonitor.stop();
this.wsService.disconnect();
console.log('应用已销毁');
}
}
// 启动应用
const taskManager = new TaskManagerApp();
taskManager.initialize();
// 导出全局实例
window.TaskManager = taskManager;
10.3 核心组件实现
10.3.1 主应用组件
// components/App.js
import { LoginForm } from './auth/LoginForm.js';
import { Dashboard } from './Dashboard.js';
import { Router } from '../utils/router.js';
import { Component } from '../utils/component.js';
export class App extends Component {
constructor(options) {
super();
this.store = options.store;
this.authService = options.authService;
this.wsService = options.wsService;
this.router = new Router();
this.currentView = null;
this.setupRoutes();
this.setupStoreSubscriptions();
}
// 设置路由
setupRoutes() {
this.router.addRoute('/', () => this.renderDashboard());
this.router.addRoute('/login', () => this.renderLogin());
this.router.addRoute('/projects/:id', (params) => this.renderProject(params.id));
this.router.addRoute('/tasks/:id', (params) => this.renderTask(params.id));
// 路由守卫
this.router.beforeEach((to, from, next) => {
const isAuthenticated = this.store.getState().auth.isAuthenticated;
if (!isAuthenticated && to !== '/login') {
next('/login');
} else if (isAuthenticated && to === '/login') {
next('/');
} else {
next();
}
});
}
// 设置状态订阅
setupStoreSubscriptions() {
this.store.subscribe('auth/isAuthenticated', (isAuthenticated) => {
if (isAuthenticated) {
this.wsService.connect();
this.router.push('/');
} else {
this.wsService.disconnect();
this.router.push('/login');
}
});
}
// 渲染应用
async render(container) {
this.container = container;
// 创建应用结构
this.container.innerHTML = `
<div class="app">
<header id="app-header" class="app-header"></header>
<nav id="app-nav" class="app-nav"></nav>
<main id="app-main" class="app-main"></main>
<footer id="app-footer" class="app-footer"></footer>
</div>
`;
// 获取容器元素
this.headerContainer = this.container.querySelector('#app-header');
this.navContainer = this.container.querySelector('#app-nav');
this.mainContainer = this.container.querySelector('#app-main');
this.footerContainer = this.container.querySelector('#app-footer');
// 启动路由
this.router.start();
}
// 渲染登录页面
async renderLogin() {
this.hideNavigation();
if (this.currentView) {
this.currentView.destroy();
}
this.currentView = new LoginForm({
authService: this.authService,
onLogin: (user) => {
this.store.dispatch('auth/setUser', user);
this.store.dispatch('auth/setAuthenticated', true);
}
});
await this.currentView.render(this.mainContainer);
}
// 渲染仪表板
async renderDashboard() {
this.showNavigation();
if (this.currentView) {
this.currentView.destroy();
}
this.currentView = new Dashboard({
store: this.store,
router: this.router
});
await this.currentView.render(this.mainContainer);
}
// 渲染项目详情
async renderProject(projectId) {
this.showNavigation();
const { ProjectDetail } = await import('./project/ProjectDetail.js');
if (this.currentView) {
this.currentView.destroy();
}
this.currentView = new ProjectDetail({
projectId,
store: this.store,
router: this.router
});
await this.currentView.render(this.mainContainer);
}
// 渲染任务详情
async renderTask(taskId) {
this.showNavigation();
const { TaskDetail } = await import('./task/TaskDetail.js');
if (this.currentView) {
this.currentView.destroy();
}
this.currentView = new TaskDetail({
taskId,
store: this.store,
router: this.router
});
await this.currentView.render(this.mainContainer);
}
// 显示导航
showNavigation() {
this.renderHeader();
this.renderNavigation();
this.renderFooter();
}
// 隐藏导航
hideNavigation() {
this.headerContainer.innerHTML = '';
this.navContainer.innerHTML = '';
this.footerContainer.innerHTML = '';
}
// 渲染头部
renderHeader() {
const user = this.store.getState().auth.user;
this.headerContainer.innerHTML = `
<div class="header-content">
<div class="logo">
<h1>任务管理器</h1>
</div>
<div class="user-menu">
<span class="user-name">${user.name}</span>
<button class="logout-btn" onclick="this.handleLogout()">退出</button>
</div>
</div>
`;
// 绑定事件
this.headerContainer.querySelector('.logout-btn').addEventListener('click', () => {
this.handleLogout();
});
}
// 渲染导航
renderNavigation() {
this.navContainer.innerHTML = `
<ul class="nav-menu">
<li><a href="/" class="nav-link">仪表板</a></li>
<li><a href="/projects" class="nav-link">项目</a></li>
<li><a href="/tasks" class="nav-link">任务</a></li>
<li><a href="/team" class="nav-link">团队</a></li>
<li><a href="/settings" class="nav-link">设置</a></li>
</ul>
`;
// 绑定导航事件
this.navContainer.addEventListener('click', (event) => {
if (event.target.classList.contains('nav-link')) {
event.preventDefault();
const href = event.target.getAttribute('href');
this.router.push(href);
}
});
}
// 渲染底部
renderFooter() {
const appState = this.store.getState().app;
this.footerContainer.innerHTML = `
<div class="footer-content">
<div class="status-info">
<span class="online-status ${appState.isOnline ? 'online' : 'offline'}">
${appState.isOnline ? '在线' : '离线'}
</span>
<span class="sync-status">
${appState.isSyncing ? '同步中...' : '已同步'}
</span>
</div>
<div class="app-info">
<span>版本 1.0.0</span>
</div>
</div>
`;
}
// 处理退出登录
async handleLogout() {
try {
await this.authService.logout();
this.store.dispatch('auth/clearUser');
this.store.dispatch('auth/setAuthenticated', false);
} catch (error) {
console.error('退出登录失败:', error);
}
}
// 销毁组件
destroy() {
if (this.currentView) {
this.currentView.destroy();
}
this.router.destroy();
super.destroy();
}
}
10.3.2 登录组件
// components/auth/LoginForm.js
import { Component } from '../../utils/component.js';
import { Validator } from '../../utils/validation.js';
export class LoginForm extends Component {
constructor(options) {
super();
this.authService = options.authService;
this.onLogin = options.onLogin;
this.validator = new Validator();
this.isLoading = false;
this.setupValidationRules();
}
// 设置验证规则
setupValidationRules() {
this.validator.addRule('email', {
required: true,
email: true,
message: '请输入有效的邮箱地址'
});
this.validator.addRule('password', {
required: true,
minLength: 6,
message: '密码至少需要6个字符'
});
}
// 渲染组件
async render(container) {
this.container = container;
this.container.innerHTML = `
<div class="login-container">
<div class="login-form">
<div class="login-header">
<h2>登录</h2>
<p>欢迎回到任务管理器</p>
</div>
<form id="login-form" class="form">
<div class="form-group">
<label for="email">邮箱</label>
<input
type="email"
id="email"
name="email"
placeholder="请输入邮箱"
required
>
<div class="error-message" id="email-error"></div>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
name="password"
placeholder="请输入密码"
required
>
<div class="error-message" id="password-error"></div>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="remember" name="remember">
<span class="checkmark"></span>
记住我
</label>
</div>
<div class="form-actions">
<button
type="submit"
class="btn btn-primary btn-block"
id="login-btn"
>
<span class="btn-text">登录</span>
<span class="btn-spinner" style="display: none;"></span>
</button>
</div>
<div class="form-footer">
<a href="#" class="forgot-password">忘记密码?</a>
<span class="divider">|</span>
<a href="#" class="register-link">注册账号</a>
</div>
</form>
<div class="demo-account">
<p>演示账号:</p>
<button class="btn btn-secondary" id="demo-login">
使用演示账号登录
</button>
</div>
</div>
</div>
`;
this.bindEvents();
}
// 绑定事件
bindEvents() {
const form = this.container.querySelector('#login-form');
const demoBtn = this.container.querySelector('#demo-login');
form.addEventListener('submit', (event) => {
event.preventDefault();
this.handleLogin();
});
demoBtn.addEventListener('click', () => {
this.handleDemoLogin();
});
// 实时验证
const inputs = form.querySelectorAll('input[type="email"], input[type="password"]');
inputs.forEach(input => {
input.addEventListener('blur', () => {
this.validateField(input.name, input.value);
});
input.addEventListener('input', () => {
this.clearFieldError(input.name);
});
});
}
// 处理登录
async handleLogin() {
const formData = this.getFormData();
// 验证表单
const validation = this.validator.validate(formData);
if (!validation.isValid) {
this.showValidationErrors(validation.errors);
return;
}
this.setLoading(true);
try {
const user = await this.authService.login({
email: formData.email,
password: formData.password,
remember: formData.remember
});
this.onLogin(user);
} catch (error) {
this.showError(error.message || '登录失败,请重试');
} finally {
this.setLoading(false);
}
}
// 处理演示登录
async handleDemoLogin() {
this.setLoading(true);
try {
const user = await this.authService.demoLogin();
this.onLogin(user);
} catch (error) {
this.showError('演示登录失败,请重试');
} finally {
this.setLoading(false);
}
}
// 获取表单数据
getFormData() {
const form = this.container.querySelector('#login-form');
const formData = new FormData(form);
return {
email: formData.get('email'),
password: formData.get('password'),
remember: formData.has('remember')
};
}
// 验证字段
validateField(fieldName, value) {
const validation = this.validator.validateField(fieldName, value);
if (!validation.isValid) {
this.showFieldError(fieldName, validation.message);
} else {
this.clearFieldError(fieldName);
}
return validation.isValid;
}
// 显示验证错误
showValidationErrors(errors) {
Object.keys(errors).forEach(fieldName => {
this.showFieldError(fieldName, errors[fieldName]);
});
}
// 显示字段错误
showFieldError(fieldName, message) {
const errorElement = this.container.querySelector(`#${fieldName}-error`);
const inputElement = this.container.querySelector(`#${fieldName}`);
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}
if (inputElement) {
inputElement.classList.add('error');
}
}
// 清除字段错误
clearFieldError(fieldName) {
const errorElement = this.container.querySelector(`#${fieldName}-error`);
const inputElement = this.container.querySelector(`#${fieldName}`);
if (errorElement) {
errorElement.style.display = 'none';
}
if (inputElement) {
inputElement.classList.remove('error');
}
}
// 显示通用错误
showError(message) {
// 可以显示在表单顶部或使用通知组件
const form = this.container.querySelector('#login-form');
let errorContainer = form.querySelector('.form-error');
if (!errorContainer) {
errorContainer = document.createElement('div');
errorContainer.className = 'form-error';
form.insertBefore(errorContainer, form.firstChild);
}
errorContainer.innerHTML = `
<div class="alert alert-error">
<span class="alert-icon">⚠</span>
<span class="alert-message">${message}</span>
<button class="alert-close" onclick="this.parentElement.remove()">×</button>
</div>
`;
}
// 设置加载状态
setLoading(loading) {
this.isLoading = loading;
const loginBtn = this.container.querySelector('#login-btn');
const btnText = loginBtn.querySelector('.btn-text');
const btnSpinner = loginBtn.querySelector('.btn-spinner');
if (loading) {
loginBtn.disabled = true;
btnText.style.display = 'none';
btnSpinner.style.display = 'inline-block';
} else {
loginBtn.disabled = false;
btnText.style.display = 'inline';
btnSpinner.style.display = 'none';
}
}
}
10.3.3 仪表板组件
// components/Dashboard.js
import { Component } from '../utils/component.js';
import { ProjectCard } from './project/ProjectCard.js';
import { TaskList } from './task/TaskList.js';
import { StatsWidget } from './common/StatsWidget.js';
export class Dashboard extends Component {
constructor(options) {
super();
this.store = options.store;
this.router = options.router;
this.widgets = [];
this.projects = [];
this.recentTasks = [];
this.setupStoreSubscriptions();
}
// 设置状态订阅
setupStoreSubscriptions() {
this.store.subscribe('projects/list', (projects) => {
this.projects = projects;
this.renderProjects();
});
this.store.subscribe('tasks/recent', (tasks) => {
this.recentTasks = tasks;
this.renderRecentTasks();
});
this.store.subscribe('stats/overview', (stats) => {
this.renderStats(stats);
});
}
// 渲染组件
async render(container) {
this.container = container;
this.container.innerHTML = `
<div class="dashboard">
<div class="dashboard-header">
<h1>仪表板</h1>
<div class="dashboard-actions">
<button class="btn btn-primary" id="new-project-btn">
<span class="icon">+</span>
新建项目
</button>
<button class="btn btn-secondary" id="new-task-btn">
<span class="icon">+</span>
新建任务
</button>
</div>
</div>
<div class="dashboard-content">
<div class="stats-section">
<h2>概览统计</h2>
<div class="stats-grid" id="stats-grid"></div>
</div>
<div class="projects-section">
<div class="section-header">
<h2>最近项目</h2>
<a href="/projects" class="view-all-link">查看全部</a>
</div>
<div class="projects-grid" id="projects-grid"></div>
</div>
<div class="tasks-section">
<div class="section-header">
<h2>最近任务</h2>
<a href="/tasks" class="view-all-link">查看全部</a>
</div>
<div class="tasks-container" id="tasks-container"></div>
</div>
<div class="activity-section">
<h2>最近活动</h2>
<div class="activity-feed" id="activity-feed"></div>
</div>
</div>
</div>
`;
this.bindEvents();
await this.loadData();
}
// 绑定事件
bindEvents() {
const newProjectBtn = this.container.querySelector('#new-project-btn');
const newTaskBtn = this.container.querySelector('#new-task-btn');
newProjectBtn.addEventListener('click', () => {
this.handleNewProject();
});
newTaskBtn.addEventListener('click', () => {
this.handleNewTask();
});
}
// 加载数据
async loadData() {
try {
// 并行加载数据
await Promise.all([
this.store.dispatch('projects/fetchRecent'),
this.store.dispatch('tasks/fetchRecent'),
this.store.dispatch('stats/fetchOverview'),
this.store.dispatch('activity/fetchRecent')
]);
} catch (error) {
console.error('加载仪表板数据失败:', error);
this.showError('数据加载失败,请刷新页面重试');
}
}
// 渲染统计信息
renderStats(stats) {
const statsGrid = this.container.querySelector('#stats-grid');
const statsData = [
{
title: '总项目数',
value: stats.totalProjects,
icon: '📁',
color: 'blue',
trend: stats.projectsTrend
},
{
title: '总任务数',
value: stats.totalTasks,
icon: '📋',
color: 'green',
trend: stats.tasksTrend
},
{
title: '已完成',
value: stats.completedTasks,
icon: '✅',
color: 'success',
trend: stats.completedTrend
},
{
title: '进行中',
value: stats.inProgressTasks,
icon: '🔄',
color: 'warning',
trend: stats.inProgressTrend
}
];
statsGrid.innerHTML = '';
statsData.forEach(stat => {
const widget = new StatsWidget(stat);
const widgetElement = document.createElement('div');
widget.render(widgetElement);
statsGrid.appendChild(widgetElement);
this.widgets.push(widget);
});
}
// 渲染项目
renderProjects() {
const projectsGrid = this.container.querySelector('#projects-grid');
if (this.projects.length === 0) {
projectsGrid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📁</div>
<h3>暂无项目</h3>
<p>创建您的第一个项目开始管理任务</p>
<button class="btn btn-primary" onclick="this.handleNewProject()">
创建项目
</button>
</div>
`;
return;
}
projectsGrid.innerHTML = '';
this.projects.slice(0, 6).forEach(project => {
const card = new ProjectCard({
project,
onClick: (projectId) => {
this.router.push(`/projects/${projectId}`);
}
});
const cardElement = document.createElement('div');
card.render(cardElement);
projectsGrid.appendChild(cardElement);
this.widgets.push(card);
});
}
// 渲染最近任务
renderRecentTasks() {
const tasksContainer = this.container.querySelector('#tasks-container');
if (this.recentTasks.length === 0) {
tasksContainer.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📋</div>
<h3>暂无任务</h3>
<p>创建您的第一个任务开始工作</p>
<button class="btn btn-primary" onclick="this.handleNewTask()">
创建任务
</button>
</div>
`;
return;
}
const taskList = new TaskList({
tasks: this.recentTasks.slice(0, 10),
compact: true,
onTaskClick: (taskId) => {
this.router.push(`/tasks/${taskId}`);
},
onTaskUpdate: (taskId, updates) => {
this.store.dispatch('tasks/update', { id: taskId, ...updates });
}
});
taskList.render(tasksContainer);
this.widgets.push(taskList);
}
// 处理新建项目
handleNewProject() {
// 可以打开模态框或跳转到新建项目页面
this.router.push('/projects/new');
}
// 处理新建任务
handleNewTask() {
// 可以打开模态框或跳转到新建任务页面
this.router.push('/tasks/new');
}
// 显示错误
showError(message) {
// 可以使用通知组件显示错误
console.error(message);
}
// 销毁组件
destroy() {
this.widgets.forEach(widget => {
if (widget.destroy) {
widget.destroy();
}
});
this.widgets = [];
super.destroy();
}
}
10.4 状态管理实现
10.4.1 核心状态管理器
// store/index.js
import { AuthStore } from './auth.js';
import { ProjectStore } from './project.js';
import { TaskStore } from './task.js';
import { AppStore } from './app.js';
import { EventEmitter } from '../utils/event-emitter.js';
export class Store extends EventEmitter {
constructor() {
super();
// 初始化子状态管理器
this.modules = {
auth: new AuthStore(this),
projects: new ProjectStore(this),
tasks: new TaskStore(this),
app: new AppStore(this)
};
// 合并状态
this.state = {};
this.updateState();
// 设置持久化
this.setupPersistence();
}
// 更新状态
updateState() {
const newState = {};
Object.keys(this.modules).forEach(moduleName => {
newState[moduleName] = this.modules[moduleName].getState();
});
this.state = newState;
}
// 获取状态
getState() {
return this.state;
}
// 分发动作
async dispatch(action, payload) {
const [moduleName, actionName] = action.split('/');
if (!this.modules[moduleName]) {
throw new Error(`Unknown module: ${moduleName}`);
}
const module = this.modules[moduleName];
if (typeof module[actionName] !== 'function') {
throw new Error(`Unknown action: ${action}`);
}
try {
const result = await module[actionName](payload);
this.updateState();
// 触发状态变化事件
this.emit('stateChange', {
action,
payload,
state: this.state
});
// 触发特定模块的状态变化事件
this.emit(`${moduleName}/stateChange`, this.state[moduleName]);
return result;
} catch (error) {
console.error(`Action ${action} failed:`, error);
throw error;
}
}
// 订阅状态变化
subscribe(path, callback) {
const [moduleName, statePath] = path.split('/');
const handler = (moduleState) => {
if (statePath) {
const value = this.getNestedValue(moduleState, statePath);
callback(value);
} else {
callback(moduleState);
}
};
this.on(`${moduleName}/stateChange`, handler);
// 立即调用一次以获取当前值
if (this.state[moduleName]) {
handler(this.state[moduleName]);
}
// 返回取消订阅函数
return () => {
this.off(`${moduleName}/stateChange`, handler);
};
}
// 获取嵌套值
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key];
}, obj);
}
// 设置持久化
setupPersistence() {
// 从本地存储恢复状态
this.restoreState();
// 监听状态变化并保存
this.on('stateChange', () => {
this.saveState();
});
// 监听页面卸载事件
window.addEventListener('beforeunload', () => {
this.saveState();
});
}
// 保存状态到本地存储
saveState() {
try {
const persistentState = {
auth: {
user: this.state.auth.user,
isAuthenticated: this.state.auth.isAuthenticated
},
app: {
theme: this.state.app.theme,
language: this.state.app.language,
preferences: this.state.app.preferences
}
};
localStorage.setItem('app_state', JSON.stringify(persistentState));
} catch (error) {
console.warn('保存状态失败:', error);
}
}
// 从本地存储恢复状态
restoreState() {
try {
const savedState = localStorage.getItem('app_state');
if (savedState) {
const parsedState = JSON.parse(savedState);
// 恢复各模块状态
Object.keys(parsedState).forEach(moduleName => {
if (this.modules[moduleName] && this.modules[moduleName].restoreState) {
this.modules[moduleName].restoreState(parsedState[moduleName]);
}
});
this.updateState();
}
} catch (error) {
console.warn('恢复状态失败:', error);
}
}
// 清除持久化状态
clearPersistedState() {
localStorage.removeItem('app_state');
}
// 重置所有状态
reset() {
Object.values(this.modules).forEach(module => {
if (module.reset) {
module.reset();
}
});
this.updateState();
this.clearPersistedState();
}
}
10.4.2 任务状态管理
// store/task.js
import { TaskService } from '../services/task.js';
export class TaskStore {
constructor(rootStore) {
this.rootStore = rootStore;
this.taskService = new TaskService();
this.state = {
list: [],
recent: [],
current: null,
filters: {
status: 'all',
priority: 'all',
assignee: 'all',
project: 'all'
},
loading: false,
error: null
};
}
// 获取状态
getState() {
return { ...this.state };
}
// 获取任务列表
async fetchTasks(options = {}) {
this.state.loading = true;
this.state.error = null;
try {
const tasks = await this.taskService.getTasks(options);
this.state.list = tasks;
return tasks;
} catch (error) {
this.state.error = error.message;
throw error;
} finally {
this.state.loading = false;
}
}
// 获取最近任务
async fetchRecent() {
try {
const recentTasks = await this.taskService.getRecentTasks();
this.state.recent = recentTasks;
return recentTasks;
} catch (error) {
console.error('获取最近任务失败:', error);
throw error;
}
}
// 获取单个任务
async fetchTask(taskId) {
this.state.loading = true;
try {
const task = await this.taskService.getTask(taskId);
this.state.current = task;
// 更新列表中的任务
const index = this.state.list.findIndex(t => t.id === taskId);
if (index !== -1) {
this.state.list[index] = task;
}
return task;
} catch (error) {
this.state.error = error.message;
throw error;
} finally {
this.state.loading = false;
}
}
// 创建任务
async create(taskData) {
try {
const newTask = await this.taskService.createTask(taskData);
// 添加到列表开头
this.state.list.unshift(newTask);
// 添加到最近任务
this.state.recent.unshift(newTask);
if (this.state.recent.length > 10) {
this.state.recent.pop();
}
return newTask;
} catch (error) {
this.state.error = error.message;
throw error;
}
}
// 更新任务
async update(updates) {
const { id, ...data } = updates;
try {
const updatedTask = await this.taskService.updateTask(id, data);
// 更新列表中的任务
const listIndex = this.state.list.findIndex(t => t.id === id);
if (listIndex !== -1) {
this.state.list[listIndex] = updatedTask;
}
// 更新最近任务
const recentIndex = this.state.recent.findIndex(t => t.id === id);
if (recentIndex !== -1) {
this.state.recent[recentIndex] = updatedTask;
}
// 更新当前任务
if (this.state.current && this.state.current.id === id) {
this.state.current = updatedTask;
}
return updatedTask;
} catch (error) {
this.state.error = error.message;
throw error;
}
}
// 删除任务
async delete(taskId) {
try {
await this.taskService.deleteTask(taskId);
// 从列表中移除
this.state.list = this.state.list.filter(t => t.id !== taskId);
// 从最近任务中移除
this.state.recent = this.state.recent.filter(t => t.id !== taskId);
// 清除当前任务
if (this.state.current && this.state.current.id === taskId) {
this.state.current = null;
}
return true;
} catch (error) {
this.state.error = error.message;
throw error;
}
}
// 完成任务
async complete(taskId) {
return this.update({
id: taskId,
status: 'completed',
completedAt: new Date().toISOString()
});
}
// 重新打开任务
async reopen(taskId) {
return this.update({
id: taskId,
status: 'in_progress',
completedAt: null
});
}
// 分配任务
async assign(taskId, assigneeId) {
return this.update({
id: taskId,
assigneeId: assigneeId
});
}
// 设置优先级
async setPriority(taskId, priority) {
return this.update({
id: taskId,
priority: priority
});
}
// 设置截止日期
async setDueDate(taskId, dueDate) {
return this.update({
id: taskId,
dueDate: dueDate
});
}
// 添加评论
async addComment(taskId, comment) {
try {
const newComment = await this.taskService.addComment(taskId, comment);
// 更新任务的评论列表
const task = this.state.list.find(t => t.id === taskId) || this.state.current;
if (task) {
if (!task.comments) {
task.comments = [];
}
task.comments.push(newComment);
}
return newComment;
} catch (error) {
this.state.error = error.message;
throw error;
}
}
// 设置过滤器
setFilter(filterType, value) {
this.state.filters[filterType] = value;
// 重新获取任务列表
this.fetchTasks({ filters: this.state.filters });
}
// 清除过滤器
clearFilters() {
this.state.filters = {
status: 'all',
priority: 'all',
assignee: 'all',
project: 'all'
};
this.fetchTasks();
}
// 获取过滤后的任务
getFilteredTasks() {
let filteredTasks = [...this.state.list];
// 按状态过滤
if (this.state.filters.status !== 'all') {
filteredTasks = filteredTasks.filter(task =>
task.status === this.state.filters.status
);
}
// 按优先级过滤
if (this.state.filters.priority !== 'all') {
filteredTasks = filteredTasks.filter(task =>
task.priority === this.state.filters.priority
);
}
// 按分配人过滤
if (this.state.filters.assignee !== 'all') {
filteredTasks = filteredTasks.filter(task =>
task.assigneeId === this.state.filters.assignee
);
}
// 按项目过滤
if (this.state.filters.project !== 'all') {
filteredTasks = filteredTasks.filter(task =>
task.projectId === this.state.filters.project
);
}
return filteredTasks;
}
// 搜索任务
searchTasks(query) {
if (!query.trim()) {
return this.getFilteredTasks();
}
const lowercaseQuery = query.toLowerCase();
return this.getFilteredTasks().filter(task =>
task.title.toLowerCase().includes(lowercaseQuery) ||
task.description.toLowerCase().includes(lowercaseQuery) ||
(task.tags && task.tags.some(tag =>
tag.toLowerCase().includes(lowercaseQuery)
))
);
}
// 获取任务统计
getStats() {
const tasks = this.state.list;
return {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
inProgress: tasks.filter(t => t.status === 'in_progress').length,
pending: tasks.filter(t => t.status === 'pending').length,
overdue: tasks.filter(t => {
return t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'completed';
}).length
};
}
// 重置状态
reset() {
this.state = {
list: [],
recent: [],
current: null,
filters: {
status: 'all',
priority: 'all',
assignee: 'all',
project: 'all'
},
loading: false,
error: null
};
}
}
10.5 服务层实现
10.5.1 认证服务
// services/auth.js
import { ApiClient } from '../utils/api.js';
import { TokenManager } from '../utils/token-manager.js';
export class AuthService {
constructor(store) {
this.store = store;
this.apiClient = new ApiClient();
this.tokenManager = new TokenManager();
this.setupInterceptors();
}
// 设置请求拦截器
setupInterceptors() {
// 请求拦截器 - 添加认证头
this.apiClient.interceptors.request.use((config) => {
const token = this.tokenManager.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器 - 处理认证错误
this.apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.status === 401) {
this.handleAuthError();
}
return Promise.reject(error);
}
);
}
// 用户登录
async login(credentials) {
try {
const response = await this.apiClient.post('/auth/login', credentials);
const { user, token, refreshToken } = response.data;
// 保存令牌
this.tokenManager.setTokens(token, refreshToken);
// 如果选择记住我,设置持久化
if (credentials.remember) {
this.tokenManager.setPersistent(true);
}
return user;
} catch (error) {
throw new Error(error.message || '登录失败');
}
}
// 演示登录
async demoLogin() {
try {
const response = await this.apiClient.post('/auth/demo-login');
const { user, token } = response.data;
// 保存令牌(演示账号不持久化)
this.tokenManager.setTokens(token);
return user;
} catch (error) {
throw new Error('演示登录失败');
}
}
// 用户注册
async register(userData) {
try {
const response = await this.apiClient.post('/auth/register', userData);
const { user, token, refreshToken } = response.data;
// 保存令牌
this.tokenManager.setTokens(token, refreshToken);
return user;
} catch (error) {
throw new Error(error.message || '注册失败');
}
}
// 用户登出
async logout() {
try {
const refreshToken = this.tokenManager.getRefreshToken();
if (refreshToken) {
await this.apiClient.post('/auth/logout', { refreshToken });
}
} catch (error) {
console.warn('服务器登出失败:', error.message);
} finally {
// 清除本地令牌
this.tokenManager.clearTokens();
}
}
// 刷新令牌
async refreshToken() {
try {
const refreshToken = this.tokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await this.apiClient.post('/auth/refresh', {
refreshToken
});
const { token: newToken, refreshToken: newRefreshToken } = response.data;
// 更新令牌
this.tokenManager.setTokens(newToken, newRefreshToken);
return newToken;
} catch (error) {
// 刷新失败,清除令牌
this.tokenManager.clearTokens();
throw new Error('Token refresh failed');
}
}
// 验证令牌
async validateToken(token) {
try {
const response = await this.apiClient.get('/auth/validate', {
headers: { Authorization: `Bearer ${token}` }
});
return response.data.user;
} catch (error) {
throw new Error('Token validation failed');
}
}
// 获取当前用户信息
async getCurrentUser() {
try {
const response = await this.apiClient.get('/auth/me');
return response.data.user;
} catch (error) {
throw new Error('Failed to get current user');
}
}
// 更新用户信息
async updateProfile(userData) {
try {
const response = await this.apiClient.put('/auth/profile', userData);
return response.data.user;
} catch (error) {
throw new Error(error.message || '更新用户信息失败');
}
}
// 修改密码
async changePassword(passwordData) {
try {
await this.apiClient.put('/auth/password', passwordData);
return true;
} catch (error) {
throw new Error(error.message || '修改密码失败');
}
}
// 忘记密码
async forgotPassword(email) {
try {
await this.apiClient.post('/auth/forgot-password', { email });
return true;
} catch (error) {
throw new Error(error.message || '发送重置邮件失败');
}
}
// 重置密码
async resetPassword(token, newPassword) {
try {
await this.apiClient.post('/auth/reset-password', {
token,
password: newPassword
});
return true;
} catch (error) {
throw new Error(error.message || '重置密码失败');
}
}
// 处理认证错误
handleAuthError() {
// 清除令牌
this.tokenManager.clearTokens();
// 更新状态
if (this.store) {
this.store.dispatch('auth/clearUser');
this.store.dispatch('auth/setAuthenticated', false);
}
// 可以显示登录提示
console.warn('Authentication failed, please login again');
}
// 检查是否已认证
isAuthenticated() {
return this.tokenManager.hasValidToken();
}
}
10.5.2 WebSocket 服务
// services/websocket.js
import { EventEmitter } from '../utils/event-emitter.js';
export class WebSocketService extends EventEmitter {
constructor(store) {
super();
this.store = store;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.isConnecting = false;
this.isManualDisconnect = false;
this.messageHandlers = new Map();
this.setupMessageHandlers();
}
// 设置消息处理器
setupMessageHandlers() {
this.messageHandlers.set('task_updated', (data) => {
this.store.dispatch('tasks/update', data.task);
});
this.messageHandlers.set('task_created', (data) => {
this.store.dispatch('tasks/add', data.task);
});
this.messageHandlers.set('task_deleted', (data) => {
this.store.dispatch('tasks/remove', data.taskId);
});
this.messageHandlers.set('project_updated', (data) => {
this.store.dispatch('projects/update', data.project);
});
this.messageHandlers.set('user_joined', (data) => {
this.emit('userJoined', data.user);
});
this.messageHandlers.set('user_left', (data) => {
this.emit('userLeft', data.user);
});
this.messageHandlers.set('notification', (data) => {
this.emit('notification', data);
});
}
// 连接 WebSocket
async connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return;
}
if (this.isConnecting) {
return;
}
this.isConnecting = true;
this.isManualDisconnect = false;
try {
const token = localStorage.getItem('auth_token');
if (!token) {
throw new Error('No authentication token');
}
const wsUrl = `${this.getWebSocketUrl()}?token=${token}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnecting = false;
this.reconnectAttempts = 0;
this.emit('connected');
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onclose = (event) => {
console.log('WebSocket disconnected:', event.code, event.reason);
this.isConnecting = false;
this.emit('disconnected', event);
if (!this.isManualDisconnect) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.isConnecting = false;
this.emit('error', error);
};
} catch (error) {
this.isConnecting = false;
console.error('WebSocket connection failed:', error);
this.emit('error', error);
}
}
// 断开连接
disconnect() {
this.isManualDisconnect = true;
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// 重新连接
async reconnect() {
this.disconnect();
await new Promise(resolve => setTimeout(resolve, 100));
await this.connect();
}
// 安排重连
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
this.emit('maxReconnectAttemptsReached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
setTimeout(() => {
if (!this.isManualDisconnect) {
this.connect();
}
}, delay);
}
// 发送消息
send(type, data) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.warn('WebSocket is not connected');
return false;
}
try {
const message = JSON.stringify({ type, data });
this.ws.send(message);
return true;
} catch (error) {
console.error('Failed to send WebSocket message:', error);
return false;
}
}
// 处理接收到的消息
handleMessage(rawMessage) {
try {
const message = JSON.parse(rawMessage);
const { type, data } = message;
// 查找对应的处理器
const handler = this.messageHandlers.get(type);
if (handler) {
handler(data);
} else {
console.warn('Unknown message type:', type);
}
// 触发通用消息事件
this.emit('message', message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
}
// 获取 WebSocket URL
getWebSocketUrl() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
return `${protocol}//${host}/ws`;
}
// 订阅项目更新
subscribeToProject(projectId) {
this.send('subscribe_project', { projectId });
}
// 取消订阅项目更新
unsubscribeFromProject(projectId) {
this.send('unsubscribe_project', { projectId });
}
// 订阅任务更新
subscribeToTask(taskId) {
this.send('subscribe_task', { taskId });
}
// 取消订阅任务更新
unsubscribeFromTask(taskId) {
this.send('unsubscribe_task', { taskId });
}
// 发送用户活动
sendUserActivity(activity) {
this.send('user_activity', activity);
}
// 获取连接状态
getConnectionState() {
if (!this.ws) return 'disconnected';
switch (this.ws.readyState) {
case WebSocket.CONNECTING:
return 'connecting';
case WebSocket.OPEN:
return 'connected';
case WebSocket.CLOSING:
return 'closing';
case WebSocket.CLOSED:
return 'disconnected';
default:
return 'unknown';
}
}
}
10.6 实践练习
10.6.1 创建完整的任务组件
// components/task/TaskCard.js
import { Component } from '../../utils/component.js';
import { formatDate, getRelativeTime } from '../../utils/date.js';
export class TaskCard extends Component {
constructor(options) {
super();
this.task = options.task;
this.onClick = options.onClick;
this.onUpdate = options.onUpdate;
this.compact = options.compact || false;
}
// 渲染组件
async render(container) {
this.container = container;
const priorityClass = this.getPriorityClass(this.task.priority);
const statusClass = this.getStatusClass(this.task.status);
const isOverdue = this.isOverdue(this.task.dueDate, this.task.status);
this.container.innerHTML = `
<div class="task-card ${statusClass} ${priorityClass} ${this.compact ? 'compact' : ''}">
<div class="task-header">
<div class="task-checkbox">
<input
type="checkbox"
${this.task.status === 'completed' ? 'checked' : ''}
onchange="this.handleStatusChange(event)"
>
</div>
<div class="task-title">
<h3 onclick="this.handleClick()">${this.task.title}</h3>
${this.task.description ? `<p class="task-description">${this.task.description}</p>` : ''}
</div>
<div class="task-actions">
<button class="btn-icon" onclick="this.handleEdit()" title="编辑">
✏️
</button>
<button class="btn-icon" onclick="this.handleDelete()" title="删除">
🗑️
</button>
</div>
</div>
${!this.compact ? this.renderTaskDetails() : ''}
<div class="task-footer">
<div class="task-meta">
${this.task.priority ? `<span class="priority-badge ${priorityClass}">${this.getPriorityText(this.task.priority)}</span>` : ''}
${this.task.dueDate ? `<span class="due-date ${isOverdue ? 'overdue' : ''}">${formatDate(this.task.dueDate)}</span>` : ''}
${this.task.assignee ? `<span class="assignee">${this.task.assignee.name}</span>` : ''}
</div>
<div class="task-stats">
${this.task.comments ? `<span class="comment-count">💬 ${this.task.comments.length}</span>` : ''}
${this.task.attachments ? `<span class="attachment-count">📎 ${this.task.attachments.length}</span>` : ''}
</div>
</div>
</div>
`;
this.bindEvents();
}
// 渲染任务详情
renderTaskDetails() {
return `
<div class="task-details">
${this.task.tags && this.task.tags.length > 0 ? `
<div class="task-tags">
${this.task.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
` : ''}
${this.task.progress !== undefined ? `
<div class="task-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${this.task.progress}%"></div>
</div>
<span class="progress-text">${this.task.progress}%</span>
</div>
` : ''}
</div>
`;
}
// 绑定事件
bindEvents() {
const checkbox = this.container.querySelector('input[type="checkbox"]');
const title = this.container.querySelector('.task-title h3');
const editBtn = this.container.querySelector('.btn-icon[title="编辑"]');
const deleteBtn = this.container.querySelector('.btn-icon[title="删除"]');
checkbox.addEventListener('change', (event) => {
this.handleStatusChange(event);
});
title.addEventListener('click', () => {
this.handleClick();
});
editBtn.addEventListener('click', (event) => {
event.stopPropagation();
this.handleEdit();
});
deleteBtn.addEventListener('click', (event) => {
event.stopPropagation();
this.handleDelete();
});
}
// 处理状态变化
handleStatusChange(event) {
const isCompleted = event.target.checked;
const newStatus = isCompleted ? 'completed' : 'in_progress';
if (this.onUpdate) {
this.onUpdate(this.task.id, { status: newStatus });
}
}
// 处理点击
handleClick() {
if (this.onClick) {
this.onClick(this.task.id);
}
}
// 处理编辑
handleEdit() {
// 可以打开编辑模态框或跳转到编辑页面
console.log('Edit task:', this.task.id);
}
// 处理删除
handleDelete() {
if (confirm('确定要删除这个任务吗?')) {
if (this.onUpdate) {
this.onUpdate(this.task.id, { deleted: true });
}
}
}
// 获取优先级样式类
getPriorityClass(priority) {
const classes = {
high: 'priority-high',
medium: 'priority-medium',
low: 'priority-low'
};
return classes[priority] || '';
}
// 获取状态样式类
getStatusClass(status) {
const classes = {
pending: 'status-pending',
in_progress: 'status-in-progress',
completed: 'status-completed'
};
return classes[status] || '';
}
// 获取优先级文本
getPriorityText(priority) {
const texts = {
high: '高',
medium: '中',
low: '低'
};
return texts[priority] || '';
}
// 检查是否过期
isOverdue(dueDate, status) {
if (!dueDate || status === 'completed') {
return false;
}
return new Date(dueDate) < new Date();
}
// 更新任务数据
updateTask(newTask) {
this.task = newTask;
this.render(this.container);
}
}
10.6.2 样式系统实现
/* styles/global.css */
:root {
/* 颜色变量 */
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
/* 背景颜色 */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-dark: #343a40;
/* 文本颜色 */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #868e96;
/* 边框颜色 */
--border-color: #dee2e6;
--border-light: #e9ecef;
/* 阴影 */
--shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
/* 字体 */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-size-base: 14px;
--line-height-base: 1.5;
/* 间距 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 3rem;
/* 圆角 */
--border-radius: 0.375rem;
--border-radius-sm: 0.25rem;
--border-radius-lg: 0.5rem;
/* 过渡 */
--transition: all 0.15s ease-in-out;
}
/* 基础样式重置 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height-base);
color: var(--text-primary);
background-color: var(--bg-secondary);
}
/* 应用布局 */
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-header {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-md);
box-shadow: var(--shadow-sm);
}
.app-nav {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-sm) var(--spacing-md);
}
.app-main {
flex: 1;
padding: var(--spacing-lg);
overflow-y: auto;
}
.app-footer {
background: var(--bg-primary);
border-top: 1px solid var(--border-color);
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
color: var(--text-secondary);
}
/* 按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-base);
font-weight: 500;
text-decoration: none;
border: 1px solid transparent;
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--transition);
gap: var(--spacing-xs);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
color: white;
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
border-color: #0056b3;
}
.btn-secondary {
color: var(--text-primary);
background-color: var(--bg-secondary);
border-color: var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background-color: #e2e6ea;
border-color: #dae0e5;
}
.btn-icon {
padding: var(--spacing-xs);
background: none;
border: none;
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: var(--transition);
}
.btn-icon:hover {
background-color: var(--bg-secondary);
}
/* 表单样式 */
.form-group {
margin-bottom: var(--spacing-md);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-xs);
font-weight: 500;
color: var(--text-primary);
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: var(--spacing-sm);
font-size: var(--font-size-base);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
transition: var(--transition);
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-group input.error,
.form-group textarea.error,
.form-group select.error {
border-color: var(--danger-color);
}
.error-message {
color: var(--danger-color);
font-size: 0.875rem;
margin-top: var(--spacing-xs);
display: none;
}
/* 任务卡片样式 */
.task-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
box-shadow: var(--shadow-sm);
transition: var(--transition);
}
.task-card:hover {
box-shadow: var(--shadow);
transform: translateY(-1px);
}
.task-card.compact {
padding: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.task-header {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.task-checkbox input {
width: 18px;
height: 18px;
margin-top: 2px;
}
.task-title {
flex: 1;
}
.task-title h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: var(--spacing-xs);
cursor: pointer;
color: var(--text-primary);
}
.task-title h3:hover {
color: var(--primary-color);
}
.task-description {
color: var(--text-secondary);
font-size: 0.875rem;
line-height: 1.4;
}
.task-actions {
display: flex;
gap: var(--spacing-xs);
}
.task-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px solid var(--border-light);
}
.task-meta {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
.priority-badge {
padding: 2px 6px;
border-radius: var(--border-radius-sm);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.priority-high {
background-color: #fee;
color: var(--danger-color);
border-left: 3px solid var(--danger-color);
}
.priority-medium {
background-color: #fff3cd;
color: var(--warning-color);
border-left: 3px solid var(--warning-color);
}
.priority-low {
background-color: #d1ecf1;
color: var(--info-color);
border-left: 3px solid var(--info-color);
}
.status-completed {
opacity: 0.7;
}
.status-completed .task-title h3 {
text-decoration: line-through;
}
.due-date {
font-size: 0.875rem;
color: var(--text-secondary);
}
.due-date.overdue {
color: var(--danger-color);
font-weight: 600;
}
/* 仪表板样式 */
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.dashboard-header h1 {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
}
.dashboard-actions {
display: flex;
gap: var(--spacing-sm);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
}
/* 响应式设计 */
@media (max-width: 768px) {
.app-main {
padding: var(--spacing-md);
}
.dashboard-header {
flex-direction: column;
gap: var(--spacing-md);
align-items: stretch;
}
.dashboard-actions {
justify-content: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
.projects-grid {
grid-template-columns: 1fr;
}
.task-header {
flex-direction: column;
gap: var(--spacing-xs);
}
.task-actions {
align-self: flex-end;
}
}
/* 深色主题 */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-dark: #000000;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--text-muted: #666666;
--border-color: #404040;
--border-light: #333333;
}
}
10.6.3 构建和部署配置
// build/build.js
import { build } from 'esbuild';
import { copy } from 'esbuild-plugin-copy';
import { clean } from 'esbuild-plugin-clean';
import fs from 'fs';
import path from 'path';
const isDev = process.argv.includes('--dev');
const isWatch = process.argv.includes('--watch');
// 构建配置
const buildConfig = {
entryPoints: ['src/main.js'],
bundle: true,
outfile: 'dist/main.js',
format: 'esm',
target: 'es2020',
sourcemap: isDev,
minify: !isDev,
define: {
'process.env.NODE_ENV': isDev ? '"development"' : '"production"'
},
plugins: [
clean({
patterns: ['dist/*']
}),
copy({
resolveFrom: 'cwd',
assets: [
{
from: ['src/assets/**/*'],
to: ['dist/assets']
},
{
from: ['src/styles/**/*'],
to: ['dist/styles']
},
{
from: ['main.html'],
to: ['dist/index.html']
}
]
})
]
};
// 开发服务器配置
const devServerConfig = {
...buildConfig,
outdir: 'dist',
write: false,
plugins: [
...buildConfig.plugins,
{
name: 'dev-server',
setup(build) {
build.onEnd(result => {
if (result.errors.length === 0) {
console.log('✅ Build completed successfully');
} else {
console.error('❌ Build failed:', result.errors);
}
});
}
}
]
};
// 执行构建
async function runBuild() {
try {
console.log(`🚀 Starting ${isDev ? 'development' : 'production'} build...`);
if (isWatch) {
const ctx = await build({
...devServerConfig,
watch: {
onRebuild(error, result) {
if (error) {
console.error('❌ Rebuild failed:', error);
} else {
console.log('✅ Rebuild completed');
}
}
}
});
console.log('👀 Watching for changes...');
// 优雅关闭
process.on('SIGINT', () => {
console.log('\n🛑 Stopping build watcher...');
ctx.dispose();
process.exit(0);
});
} else {
await build(buildConfig);
console.log('✅ Build completed successfully!');
// 生成构建报告
generateBuildReport();
}
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}
// 生成构建报告
function generateBuildReport() {
const distPath = path.resolve('dist');
const files = getAllFiles(distPath);
const report = {
buildTime: new Date().toISOString(),
files: files.map(file => {
const stats = fs.statSync(file);
return {
path: path.relative(distPath, file),
size: stats.size,
sizeFormatted: formatBytes(stats.size)
};
}),
totalSize: files.reduce((total, file) => {
return total + fs.statSync(file).size;
}, 0)
};
report.totalSizeFormatted = formatBytes(report.totalSize);
fs.writeFileSync(
path.join(distPath, 'build-report.json'),
JSON.stringify(report, null, 2)
);
console.log('📊 Build Report:');
console.log(` Total size: ${report.totalSizeFormatted}`);
console.log(` Files: ${report.files.length}`);
}
// 获取所有文件
function getAllFiles(dir) {
const files = [];
function traverse(currentDir) {
const items = fs.readdirSync(currentDir);
for (const item of items) {
const fullPath = path.join(currentDir, item);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
traverse(fullPath);
} else {
files.push(fullPath);
}
}
}
traverse(dir);
return files;
}
// 格式化字节大小
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 启动构建
runBuild();
// package.json
{
"name": "task-manager",
"version": "1.0.0",
"description": "基于 Sciter-JS 的任务管理应用",
"main": "src/main.js",
"type": "module",
"scripts": {
"dev": "node build/build.js --dev --watch",
"build": "node build/build.js",
"build:dev": "node build/build.js --dev",
"preview": "sciter dist/index.html",
"test": "node tests/run-tests.js",
"lint": "eslint src/**/*.js",
"format": "prettier --write src/**/*.{js,css,html}"
},
"dependencies": {
"sciter-js": "^5.0.0"
},
"devDependencies": {
"esbuild": "^0.19.0",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-clean": "^1.0.1",
"eslint": "^8.50.0",
"prettier": "^3.0.0"
},
"keywords": [
"sciter-js",
"task-management",
"desktop-app",
"javascript"
],
"author": "Your Name",
"license": "MIT"
}
10.7 本章小结
10.7.1 核心概念回顾
通过本章的实战项目,我们学习了:
项目架构设计
- 模块化的目录结构
- 分层的架构设计
- 组件化的开发模式
状态管理实践
- 集中式状态管理
- 模块化的状态设计
- 状态持久化策略
服务层设计
- 认证服务的完整实现
- WebSocket 实时通信
- 错误处理和重连机制
组件开发
- 可复用组件设计
- 事件处理机制
- 生命周期管理
10.7.2 技术要点总结
应用架构
graph TB A[应用入口] --> B[路由管理] B --> C[页面组件] C --> D[业务组件] D --> E[基础组件] F[状态管理] --> G[认证状态] F --> H[业务状态] F --> I[应用状态] J[服务层] --> K[API 服务] J --> L[WebSocket 服务] J --> M[存储服务]
- 开发流程
- 需求分析和架构设计
- 组件拆分和接口定义
- 核心功能实现
- 测试和优化
- 构建和部署
- 最佳实践
模块化开发
组件复用
状态管理
错误处理
性能优化
10.7.3 扩展建议
- 功能扩展
- 添加文件上传功能
- 实现消息通知系统
- 集成第三方服务
- 添加数据可视化
- 技术优化
- 实现虚拟滚动
- 添加离线支持
- 优化包大小
- 提升加载性能
- 用户体验
添加动画效果
实现主题切换
支持键盘快捷键
优化移动端体验
10.7.4 学习建议
- 深入理解
- 仔细研究项目架构
- 理解各模块的职责
- 掌握状态管理模式
- 动手实践
- 完整实现项目功能
- 尝试添加新功能
- 优化现有代码
- 持续改进
- 收集用户反馈
- 监控应用性能
- 定期重构代码 — 恭喜! 🎉 您已经完成了 Sciter-JS 教程的全部内容。通过这个完整的实战项目,您应该已经掌握了使用 Sciter-JS 开发桌面应用的核心技能。 下一步建议:
- 继续完善这个任务管理应用
- 尝试开发其他类型的应用
- 深入学习 Sciter-JS 的高级特性
- 参与开源项目或分享您的经验 祝您在 Sciter-JS 开发之路上越走越远! 🚀