优秀的用户体验是扩展成功的关键因素。本阶段将深入探讨VS Code扩展的UX设计原则,学习如何创建直观、高效、无障碍的用户界面,以及如何设计符合用户心理模型的交互模式。


🎯 学习目标

完成本阶段学习后,你将能够:

  • 📐 掌握UX设计原则:理解VS Code设计语言和用户期望
  • 🎨 创建一致的界面:遵循官方设计指南,保持视觉统一性
  • 🎛️ 设计直观的交互:构建符合用户习惯的操作流程
  • 实现无障碍设计:支持键盘导航和屏幕阅读器
  • 📱 优化跨平台体验:适配不同操作系统和屏幕尺寸
  • 🔔 设计有效的通知系统:平衡信息传递与用户干扰

预计学习时间:1-2周(每天1-2小时)


📐 第一部分:VS Code设计原则与指南

1.1 核心设计哲学

VS Code的设计理念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 设计原则体现在代码中的例子
export class UserExperienceManager {

// 原则1:简洁性 - 界面元素应该清晰简洁
static createSimpleStatusBar(text: string): vscode.StatusBarItem {
const statusBar = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
100
);
statusBar.text = text; // 简洁明了的文本
statusBar.tooltip = `点击查看${text}详情`; // 提供必要的提示
return statusBar;
}

// 原则2:一致性 - 使用标准的VS Code图标和样式
static createConsistentTreeItem(label: string, type: 'file' | 'folder'): vscode.TreeItem {
const item = new vscode.TreeItem(label);

// 使用VS Code内置图标保持一致性
item.iconPath = new vscode.ThemeIcon(type === 'file' ? 'file' : 'folder');
item.contextValue = type;

return item;
}

// 原则3:可预测性 - 用户能够预期操作的结果
static async performPredictableAction(action: string): Promise<void> {
// 1. 显示进度指示
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: `正在${action}...`,
cancellable: false
}, async (progress) => {

// 2. 执行操作
progress.report({ increment: 50, message: '处理中...' });
await this.executeAction();

// 3. 提供明确的完成反馈
progress.report({ increment: 100, message: '完成' });
});

// 4. 显示结果
vscode.window.showInformationMessage(`${action}完成`);
}

private static async executeAction(): Promise<void> {
// 模拟操作
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

1.2 视觉设计指南

颜色和主题适配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// src/themeManager.ts
export class ThemeManager {

// 使用VS Code主题变量确保颜色一致性
static getThemedColors() {
return {
// 基础颜色
background: 'var(--vscode-editor-background)',
foreground: 'var(--vscode-editor-foreground)',

// 交互元素颜色
buttonBackground: 'var(--vscode-button-background)',
buttonForeground: 'var(--vscode-button-foreground)',
buttonHover: 'var(--vscode-button-hoverBackground)',

// 状态颜色
errorColor: 'var(--vscode-errorForeground)',
warningColor: 'var(--vscode-warningForeground)',
successColor: 'var(--vscode-gitDecoration-addedResourceForeground)',

// 边框和分割线
border: 'var(--vscode-panel-border)',
separator: 'var(--vscode-menu-separatorBackground)',

// 输入框
inputBackground: 'var(--vscode-input-background)',
inputBorder: 'var(--vscode-input-border)',
inputForeground: 'var(--vscode-input-foreground)',
};
}

// 创建主题适配的CSS样式
static generateThemeCSS(): string {
return `
:root {
--app-background: var(--vscode-editor-background);
--app-foreground: var(--vscode-editor-foreground);
--app-border: var(--vscode-panel-border);
--app-accent: var(--vscode-textLink-foreground);
}

body {
background-color: var(--app-background);
color: var(--app-foreground);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
font-weight: var(--vscode-font-weight);
margin: 0;
padding: 0;
}

.button {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 6px 14px;
border-radius: 2px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
}

.button:hover {
background-color: var(--vscode-button-hoverBackground);
}

.button:disabled {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
cursor: not-allowed;
}

.input {
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
padding: 4px 6px;
border-radius: 2px;
font-family: inherit;
font-size: inherit;
}

.input:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
`;
}

// 监听主题变化
static onThemeChanged(callback: () => void): vscode.Disposable {
return vscode.window.onDidChangeActiveColorTheme(() => {
callback();
});
}
}

图标和字体规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// src/iconManager.ts
export class IconManager {

// VS Code内置图标库
static readonly ICONS = {
// 文件操作
file: 'file',
folder: 'folder',
folderOpened: 'folder-opened',

// 编辑操作
edit: 'edit',
save: 'save',
saveAll: 'save-all',

// 导航操作
search: 'search',
filter: 'filter',
refresh: 'refresh',

// 状态指示
check: 'check',
error: 'error',
warning: 'warning',
info: 'info',

// 工具操作
settings: 'settings-gear',
debug: 'debug',
run: 'play',
stop: 'stop',

// 版本控制
gitCommit: 'git-commit',
gitPullRequest: 'git-pull-request',
gitBranch: 'git-branch',

// 扩展特定
extension: 'extensions',
plugin: 'plug',
terminal: 'terminal'
} as const;

// 创建主题图标
static createThemeIcon(iconName: keyof typeof IconManager.ICONS): vscode.ThemeIcon {
return new vscode.ThemeIcon(IconManager.ICONS[iconName]);
}

// 创建带颜色的主题图标
static createColoredIcon(
iconName: keyof typeof IconManager.ICONS,
color: vscode.ThemeColor
): vscode.ThemeIcon {
return new vscode.ThemeIcon(IconManager.ICONS[iconName], color);
}

// 状态图标快捷方法
static getStatusIcon(status: 'success' | 'warning' | 'error' | 'info'): vscode.ThemeIcon {
const iconMap = {
success: this.createColoredIcon('check', new vscode.ThemeColor('gitDecoration.addedResourceForeground')),
warning: this.createColoredIcon('warning', new vscode.ThemeColor('warningForeground')),
error: this.createColoredIcon('error', new vscode.ThemeColor('errorForeground')),
info: this.createColoredIcon('info', new vscode.ThemeColor('foreground'))
};

return iconMap[status];
}

// 字体设置
static getFontSettings() {
const config = vscode.workspace.getConfiguration('editor');

return {
fontFamily: config.get<string>('fontFamily') || 'Consolas, monospace',
fontSize: config.get<number>('fontSize') || 14,
fontWeight: config.get<string>('fontWeight') || 'normal',
lineHeight: config.get<number>('lineHeight') || 1.5
};
}
}

1.3 布局和空间设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// src/layoutManager.ts
export class LayoutManager {

// 标准间距定义(基于VS Code设计系统)
static readonly SPACING = {
xs: '2px',
sm: '4px',
md: '8px',
lg: '12px',
xl: '16px',
xxl: '24px'
};

// 标准尺寸
static readonly SIZES = {
buttonHeight: '26px',
inputHeight: '24px',
statusBarHeight: '22px',
sidebarWidth: '300px',
panelHeight: '200px'
};

// 创建响应式CSS
static generateResponsiveCSS(): string {
return `
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}

.header {
flex: 0 0 auto;
padding: ${this.SPACING.md};
border-bottom: 1px solid var(--vscode-panel-border);
display: flex;
align-items: center;
justify-content: space-between;
min-height: 40px;
}

.content {
flex: 1 1 auto;
display: flex;
overflow: hidden;
}

.sidebar {
flex: 0 0 ${this.SIZES.sidebarWidth};
border-right: 1px solid var(--vscode-panel-border);
overflow-y: auto;
padding: ${this.SPACING.md};
}

.main {
flex: 1 1 auto;
padding: ${this.SPACING.md};
overflow-y: auto;
}

.footer {
flex: 0 0 auto;
padding: ${this.SPACING.sm} ${this.SPACING.md};
border-top: 1px solid var(--vscode-panel-border);
background-color: var(--vscode-statusBar-background);
color: var(--vscode-statusBar-foreground);
font-size: 12px;
}

/* 响应式设计 */
@media (max-width: 600px) {
.content {
flex-direction: column;
}

.sidebar {
flex: 0 0 auto;
border-right: none;
border-bottom: 1px solid var(--vscode-panel-border);
max-height: 200px;
}
}

/* 小屏幕隐藏侧边栏 */
@media (max-width: 400px) {
.sidebar {
display: none;
}
}
`;
}

// 创建网格布局
static createGridLayout(columns: number, gap: string = this.SPACING.md): string {
return `
.grid {
display: grid;
grid-template-columns: repeat(${columns}, 1fr);
gap: ${gap};
padding: ${this.SPACING.md};
}

.grid-item {
padding: ${this.SPACING.md};
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
background-color: var(--vscode-sideBar-background);
}

@media (max-width: 600px) {
.grid {
grid-template-columns: 1fr;
}
}
`;
}
}

🎛️ 第二部分:交互设计模式

2.1 导航和菜单设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// src/navigationManager.ts
export class NavigationManager {

// 创建面包屑导航
static createBreadcrumb(path: string[]): string {
const breadcrumbItems = path.map((item, index) => {
const isLast = index === path.length - 1;
const className = isLast ? 'breadcrumb-current' : 'breadcrumb-link';

return `<span class="${className}" data-index="${index}">${item}</span>`;
}).join('<span class="breadcrumb-separator">›</span>');

return `
<nav class="breadcrumb" aria-label="导航路径">
${breadcrumbItems}
</nav>
`;
}

// 创建标签页导航
static createTabNavigation(tabs: Array<{id: string, label: string, active?: boolean}>): string {
const tabItems = tabs.map(tab => `
<button
class="tab-item ${tab.active ? 'active' : ''}"
data-tab-id="${tab.id}"
role="tab"
aria-selected="${tab.active ? 'true' : 'false'}"
>
${tab.label}
</button>
`).join('');

return `
<div class="tab-navigation" role="tablist">
${tabItems}
</div>
`;
}

// 创建上下文菜单
static createContextMenu(items: Array<{
id: string,
label: string,
icon?: string,
disabled?: boolean,
separator?: boolean
}>): string {
const menuItems = items.map(item => {
if (item.separator) {
return '<div class="menu-separator" role="separator"></div>';
}

const iconHtml = item.icon
? `<span class="menu-icon codicon codicon-${item.icon}"></span>`
: '';

return `
<button
class="menu-item ${item.disabled ? 'disabled' : ''}"
data-action="${item.id}"
${item.disabled ? 'disabled' : ''}
role="menuitem"
>
${iconHtml}
<span class="menu-label">${item.label}</span>
</button>
`;
}).join('');

return `
<div class="context-menu" role="menu">
${menuItems}
</div>
`;
}

// 获取导航相关CSS
static getNavigationCSS(): string {
return `
/* 面包屑导航 */
.breadcrumb {
display: flex;
align-items: center;
padding: ${LayoutManager.SPACING.sm} 0;
font-size: 12px;
color: var(--vscode-breadcrumb-foreground);
}

.breadcrumb-link {
cursor: pointer;
color: var(--vscode-textLink-foreground);
text-decoration: none;
}

.breadcrumb-link:hover {
text-decoration: underline;
}

.breadcrumb-current {
color: var(--vscode-breadcrumb-activeSelectionForeground);
font-weight: bold;
}

.breadcrumb-separator {
margin: 0 ${LayoutManager.SPACING.sm};
color: var(--vscode-breadcrumb-foreground);
}

/* 标签页导航 */
.tab-navigation {
display: flex;
border-bottom: 1px solid var(--vscode-tab-border);
background-color: var(--vscode-editorGroupHeader-tabsBackground);
}

.tab-item {
background: none;
border: none;
padding: ${LayoutManager.SPACING.md} ${LayoutManager.SPACING.lg};
color: var(--vscode-tab-inactiveForeground);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}

.tab-item:hover {
color: var(--vscode-tab-activeForeground);
background-color: var(--vscode-tab-hoverBackground);
}

.tab-item.active {
color: var(--vscode-tab-activeForeground);
background-color: var(--vscode-tab-activeBackground);
border-bottom-color: var(--vscode-tab-activeBorder);
}

/* 上下文菜单 */
.context-menu {
background-color: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: ${LayoutManager.SPACING.sm} 0;
min-width: 150px;
z-index: 1000;
}

.menu-item {
display: flex;
align-items: center;
width: 100%;
padding: ${LayoutManager.SPACING.sm} ${LayoutManager.SPACING.md};
background: none;
border: none;
color: var(--vscode-menu-foreground);
cursor: pointer;
text-align: left;
}

.menu-item:hover:not(.disabled) {
background-color: var(--vscode-menu-selectionBackground);
color: var(--vscode-menu-selectionForeground);
}

.menu-item.disabled {
color: var(--vscode-disabledForeground);
cursor: not-allowed;
}

.menu-icon {
margin-right: ${LayoutManager.SPACING.sm};
width: 16px;
height: 16px;
}

.menu-separator {
height: 1px;
background-color: var(--vscode-menu-separatorBackground);
margin: ${LayoutManager.SPACING.sm} 0;
}
`;
}
}

2.2 表单和输入设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
// src/formManager.ts
export class FormManager {

// 创建标准表单字段
static createFormField(options: {
type: 'text' | 'password' | 'email' | 'number' | 'select' | 'textarea',
id: string,
label: string,
placeholder?: string,
required?: boolean,
disabled?: boolean,
options?: Array<{value: string, label: string}>, // for select
rows?: number, // for textarea
validation?: {
pattern?: string,
minLength?: number,
maxLength?: number,
min?: number,
max?: number
}
}): string {

const { type, id, label, placeholder, required, disabled, options, rows, validation } = options;

let inputHtml = '';
const commonAttrs = `
id="${id}"
name="${id}"
${placeholder ? `placeholder="${placeholder}"` : ''}
${required ? 'required' : ''}
${disabled ? 'disabled' : ''}
${validation?.pattern ? `pattern="${validation.pattern}"` : ''}
${validation?.minLength ? `minlength="${validation.minLength}"` : ''}
${validation?.maxLength ? `maxlength="${validation.maxLength}"` : ''}
${validation?.min ? `min="${validation.min}"` : ''}
${validation?.max ? `max="${validation.max}"` : ''}
`;

switch (type) {
case 'select':
const optionItems = options?.map(opt =>
`<option value="${opt.value}">${opt.label}</option>`
).join('') || '';
inputHtml = `<select class="form-select" ${commonAttrs}>${optionItems}</select>`;
break;

case 'textarea':
inputHtml = `<textarea class="form-textarea" ${commonAttrs} ${rows ? `rows="${rows}"` : ''}></textarea>`;
break;

default:
inputHtml = `<input type="${type}" class="form-input" ${commonAttrs}>`;
}

return `
<div class="form-field">
<label class="form-label" for="${id}">
${label}
${required ? '<span class="required-indicator">*</span>' : ''}
</label>
${inputHtml}
<div class="form-error" id="${id}-error"></div>
</div>
`;
}

// 创建表单组
static createFormGroup(title: string, fields: string[]): string {
return `
<fieldset class="form-group">
<legend class="form-group-title">${title}</legend>
${fields.join('')}
</fieldset>
`;
}

// 创建表单按钮组
static createFormActions(actions: Array<{
type: 'submit' | 'button' | 'reset',
label: string,
variant?: 'primary' | 'secondary' | 'danger',
disabled?: boolean,
onclick?: string
}>): string {
const buttons = actions.map(action => `
<button
type="${action.type}"
class="form-button ${action.variant || 'secondary'}"
${action.disabled ? 'disabled' : ''}
${action.onclick ? `onclick="${action.onclick}"` : ''}
>
${action.label}
</button>
`).join('');

return `
<div class="form-actions">
${buttons}
</div>
`;
}

// 表单验证JavaScript
static getFormValidationScript(): string {
return `
function validateForm(formId) {
const form = document.getElementById(formId);
const fields = form.querySelectorAll('.form-input, .form-select, .form-textarea');
let isValid = true;

fields.forEach(field => {
const error = validateField(field);
if (error) {
showFieldError(field.id, error);
isValid = false;
} else {
clearFieldError(field.id);
}
});

return isValid;
}

function validateField(field) {
const value = field.value.trim();
const type = field.type;

// 必填验证
if (field.required && !value) {
return '此字段为必填项';
}

// 邮箱验证
if (type === 'email' && value && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {
return '请输入有效的邮箱地址';
}

// 长度验证
if (field.minLength && value.length < field.minLength) {
return \`最少需要 \${field.minLength} 个字符\`;
}

if (field.maxLength && value.length > field.maxLength) {
return \`最多允许 \${field.maxLength} 个字符\`;
}

// 数字范围验证
if (type === 'number' && value) {
const num = parseFloat(value);
if (field.min && num < field.min) {
return \`值不能小于 \${field.min}\`;
}
if (field.max && num > field.max) {
return \`值不能大于 \${field.max}\`;
}
}

return null;
}

function showFieldError(fieldId, message) {
const errorElement = document.getElementById(fieldId + '-error');
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = 'block';
}

const field = document.getElementById(fieldId);
if (field) {
field.classList.add('error');
}
}

function clearFieldError(fieldId) {
const errorElement = document.getElementById(fieldId + '-error');
if (errorElement) {
errorElement.style.display = 'none';
}

const field = document.getElementById(fieldId);
if (field) {
field.classList.remove('error');
}
}

// 实时验证
document.addEventListener('DOMContentLoaded', function() {
const fields = document.querySelectorAll('.form-input, .form-select, .form-textarea');
fields.forEach(field => {
field.addEventListener('blur', function() {
const error = validateField(this);
if (error) {
showFieldError(this.id, error);
} else {
clearFieldError(this.id);
}
});

field.addEventListener('input', function() {
if (this.classList.contains('error')) {
const error = validateField(this);
if (!error) {
clearFieldError(this.id);
}
}
});
});
});
`;
}

// 获取表单CSS样式
static getFormCSS(): string {
return `
.form-field {
margin-bottom: ${LayoutManager.SPACING.lg};
}

.form-label {
display: block;
margin-bottom: ${LayoutManager.SPACING.sm};
font-weight: 600;
color: var(--vscode-foreground);
}

.required-indicator {
color: var(--vscode-errorForeground);
margin-left: 2px;
}

.form-input,
.form-select,
.form-textarea {
width: 100%;
padding: ${LayoutManager.SPACING.sm} ${LayoutManager.SPACING.md};
border: 1px solid var(--vscode-input-border);
border-radius: 2px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: inherit;
font-size: inherit;
box-sizing: border-box;
transition: border-color 0.2s ease;
}

.form-input:focus,
.form-select:focus,
.form-textarea:focus {
outline: none;
border-color: var(--vscode-focusBorder);
box-shadow: 0 0 0 1px var(--vscode-focusBorder);
}

.form-input.error,
.form-select.error,
.form-textarea.error {
border-color: var(--vscode-errorBorder);
}

.form-input:disabled,
.form-select:disabled,
.form-textarea:disabled {
background-color: var(--vscode-input-background);
color: var(--vscode-disabledForeground);
cursor: not-allowed;
opacity: 0.6;
}

.form-textarea {
resize: vertical;
min-height: 80px;
}

.form-error {
display: none;
margin-top: ${LayoutManager.SPACING.sm};
font-size: 12px;
color: var(--vscode-errorForeground);
}

.form-group {
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
padding: ${LayoutManager.SPACING.lg};
margin-bottom: ${LayoutManager.SPACING.xl};
}

.form-group-title {
font-size: 16px;
font-weight: bold;
color: var(--vscode-textLink-foreground);
margin-bottom: ${LayoutManager.SPACING.lg};
padding: 0 ${LayoutManager.SPACING.sm};
}

.form-actions {
display: flex;
gap: ${LayoutManager.SPACING.md};
justify-content: flex-end;
margin-top: ${LayoutManager.SPACING.xl};
padding-top: ${LayoutManager.SPACING.lg};
border-top: 1px solid var(--vscode-panel-border);
}

.form-button {
padding: ${LayoutManager.SPACING.sm} ${LayoutManager.SPACING.lg};
border: none;
border-radius: 2px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
font-weight: 500;
transition: background-color 0.2s ease;
}

.form-button.primary {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}

.form-button.primary:hover {
background-color: var(--vscode-button-hoverBackground);
}

.form-button.secondary {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}

.form-button.secondary:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}

.form-button.danger {
background-color: var(--vscode-errorBadge-background);
color: var(--vscode-errorBadge-foreground);
}

.form-button.danger:hover {
opacity: 0.9;
}

.form-button:disabled {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-disabledForeground);
cursor: not-allowed;
opacity: 0.6;
}
`;
}
}

2.3 状态和反馈设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
// src/feedbackManager.ts
export class FeedbackManager {

// 创建加载状态指示器
static createLoadingIndicator(type: 'spinner' | 'progress' | 'skeleton'): string {
switch (type) {
case 'spinner':
return `
<div class="loading-spinner" role="status" aria-label="加载中">
<div class="spinner"></div>
<span class="loading-text">加载中...</span>
</div>
`;

case 'progress':
return `
<div class="loading-progress" role="progressbar" aria-label="加载进度">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-text" id="progressText">0%</div>
</div>
`;

case 'skeleton':
return `
<div class="loading-skeleton" aria-label="加载中">
<div class="skeleton-line short"></div>
<div class="skeleton-line medium"></div>
<div class="skeleton-line long"></div>
<div class="skeleton-line medium"></div>
</div>
`;
}
}

// 创建状态徽章
static createStatusBadge(
status: 'success' | 'warning' | 'error' | 'info' | 'pending',
text: string
): string {
const iconMap = {
success: 'check',
warning: 'warning',
error: 'error',
info: 'info',
pending: 'clock'
};

return `
<span class="status-badge ${status}" role="status">
<span class="status-icon codicon codicon-${iconMap[status]}"></span>
<span class="status-text">${text}</span>
</span>
`;
}

// 创建通知消息
static createNotification(options: {
type: 'success' | 'warning' | 'error' | 'info',
title: string,
message: string,
actions?: Array<{label: string, action: string}>,
dismissible?: boolean,
autoClose?: number
}): string {
const { type, title, message, actions, dismissible = true, autoClose } = options;

const actionsHtml = actions ? actions.map(action =>
`<button class="notification-action" data-action="${action.action}">${action.label}</button>`
).join('') : '';

const dismissButton = dismissible
? '<button class="notification-dismiss" aria-label="关闭" onclick="this.parentElement.remove()">×</button>'
: '';

return `
<div class="notification ${type}" role="alert" ${autoClose ? `data-auto-close="${autoClose}"` : ''}>
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon codicon codicon-${IconManager.ICONS[type === 'success' ? 'check' : type]}"></span>
<strong class="notification-title">${title}</strong>
${dismissButton}
</div>
<div class="notification-message">${message}</div>
${actionsHtml ? `<div class="notification-actions">${actionsHtml}</div>` : ''}
</div>
</div>
`;
}

// 创建空状态
static createEmptyState(options: {
icon: string,
title: string,
description: string,
action?: {label: string, onclick: string}
}): string {
const actionHtml = options.action
? `<button class="empty-action primary" onclick="${options.action.onclick}">${options.action.label}</button>`
: '';

return `
<div class="empty-state">
<div class="empty-icon codicon codicon-${options.icon}"></div>
<h3 class="empty-title">${options.title}</h3>
<p class="empty-description">${options.description}</p>
${actionHtml}
</div>
`;
}

// 获取反馈相关CSS
static getFeedbackCSS(): string {
return `
/* 加载指示器 */
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: ${LayoutManager.SPACING.xl};
}

.spinner {
width: 24px;
height: 24px;
border: 2px solid var(--vscode-progressBar-background);
border-top: 2px solid var(--vscode-button-background);
border-radius: 50%;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.loading-text {
margin-top: ${LayoutManager.SPACING.md};
color: var(--vscode-foreground);
font-size: 14px;
}

.loading-progress {
padding: ${LayoutManager.SPACING.lg};
}

.progress-bar {
width: 100%;
height: 8px;
background-color: var(--vscode-progressBar-background);
border-radius: 4px;
overflow: hidden;
}

.progress-fill {
height: 100%;
background-color: var(--vscode-button-background);
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}

.progress-text {
text-align: center;
margin-top: ${LayoutManager.SPACING.sm};
color: var(--vscode-foreground);
font-size: 12px;
}

.loading-skeleton .skeleton-line {
height: 16px;
background: linear-gradient(
90deg,
var(--vscode-sideBar-background) 25%,
var(--vscode-panel-border) 50%,
var(--vscode-sideBar-background) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 2s infinite;
border-radius: 2px;
margin-bottom: ${LayoutManager.SPACING.sm};
}

.skeleton-line.short { width: 60%; }
.skeleton-line.medium { width: 80%; }
.skeleton-line.long { width: 100%; }

@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}

/* 状态徽章 */
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px ${LayoutManager.SPACING.sm};
border-radius: 12px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.status-badge.success {
background-color: rgba(0, 255, 0, 0.1);
color: var(--vscode-gitDecoration-addedResourceForeground);
border: 1px solid var(--vscode-gitDecoration-addedResourceForeground);
}

.status-badge.warning {
background-color: rgba(255, 165, 0, 0.1);
color: var(--vscode-warningForeground);
border: 1px solid var(--vscode-warningForeground);
}

.status-badge.error {
background-color: rgba(255, 0, 0, 0.1);
color: var(--vscode-errorForeground);
border: 1px solid var(--vscode-errorForeground);
}

.status-badge.info {
background-color: rgba(0, 123, 255, 0.1);
color: var(--vscode-textLink-foreground);
border: 1px solid var(--vscode-textLink-foreground);
}

.status-badge.pending {
background-color: rgba(108, 117, 125, 0.1);
color: var(--vscode-disabledForeground);
border: 1px solid var(--vscode-disabledForeground);
}

.status-icon {
margin-right: 4px;
}

/* 通知消息 */
.notification {
border: 1px solid;
border-radius: 4px;
margin-bottom: ${LayoutManager.SPACING.md};
overflow: hidden;
}

.notification.success {
border-color: var(--vscode-gitDecoration-addedResourceForeground);
background-color: rgba(0, 255, 0, 0.05);
}

.notification.warning {
border-color: var(--vscode-warningForeground);
background-color: rgba(255, 165, 0, 0.05);
}

.notification.error {
border-color: var(--vscode-errorForeground);
background-color: rgba(255, 0, 0, 0.05);
}

.notification.info {
border-color: var(--vscode-textLink-foreground);
background-color: rgba(0, 123, 255, 0.05);
}

.notification-content {
padding: ${LayoutManager.SPACING.md};
}

.notification-header {
display: flex;
align-items: center;
margin-bottom: ${LayoutManager.SPACING.sm};
}

.notification-icon {
margin-right: ${LayoutManager.SPACING.sm};
font-size: 16px;
}

.notification-title {
flex: 1;
font-weight: 600;
color: var(--vscode-foreground);
}

.notification-dismiss {
background: none;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: 0;
margin-left: ${LayoutManager.SPACING.sm};
}

.notification-message {
color: var(--vscode-foreground);
margin-bottom: ${LayoutManager.SPACING.sm};
}

.notification-actions {
display: flex;
gap: ${LayoutManager.SPACING.sm};
}

.notification-action {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: 4px ${LayoutManager.SPACING.md};
border-radius: 2px;
cursor: pointer;
font-size: 12px;
}

/* 空状态 */
.empty-state {
text-align: center;
padding: ${LayoutManager.SPACING.xxl};
color: var(--vscode-foreground);
}

.empty-icon {
font-size: 48px;
color: var(--vscode-disabledForeground);
margin-bottom: ${LayoutManager.SPACING.lg};
}

.empty-title {
font-size: 18px;
font-weight: 600;
margin-bottom: ${LayoutManager.SPACING.md};
color: var(--vscode-foreground);
}

.empty-description {
color: var(--vscode-disabledForeground);
margin-bottom: ${LayoutManager.SPACING.lg};
max-width: 400px;
margin-left: auto;
margin-right: auto;
}

.empty-action {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
padding: ${LayoutManager.SPACING.md} ${LayoutManager.SPACING.xl};
border-radius: 2px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
`;
}

// JavaScript辅助函数
static getFeedbackScript(): string {
return `
// 自动关闭通知
document.addEventListener('DOMContentLoaded', function() {
const autoCloseNotifications = document.querySelectorAll('[data-auto-close]');
autoCloseNotifications.forEach(notification => {
const delay = parseInt(notification.getAttribute('data-auto-close'));
setTimeout(() => {
notification.remove();
}, delay);
});
});

// 更新进度条
function updateProgress(percentage) {
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');

if (progressFill) {
progressFill.style.width = percentage + '%';
}

if (progressText) {
progressText.textContent = Math.round(percentage) + '%';
}
}

// 显示通知
function showNotification(type, title, message, actions = [], autoClose = 5000) {
const notification = createNotificationElement(type, title, message, actions, autoClose);
document.body.appendChild(notification);

if (autoClose > 0) {
setTimeout(() => {
notification.remove();
}, autoClose);
}
}

function createNotificationElement(type, title, message, actions, autoClose) {
const div = document.createElement('div');
div.className = \`notification \${type}\`;
div.setAttribute('role', 'alert');

if (autoClose > 0) {
div.setAttribute('data-auto-close', autoClose);
}

const actionsHtml = actions.map(action =>
\`<button class="notification-action" onclick="\${action.action}">\${action.label}</button>\`
).join('');

div.innerHTML = \`
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon codicon codicon-\${type === 'success' ? 'check' : type}"></span>
<strong class="notification-title">\${title}</strong>
<button class="notification-dismiss" onclick="this.parentElement.parentElement.parentElement.remove()">×</button>
</div>
<div class="notification-message">\${message}</div>
\${actions.length ? \`<div class="notification-actions">\${actionsHtml}</div>\` : ''}
</div>
\`;

return div;
}
`;
}
}

♿ 第三部分:无障碍设计

3.1 键盘导航支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
// src/accessibilityManager.ts
export class AccessibilityManager {

// 键盘导航管理器
static setupKeyboardNavigation(container: HTMLElement): void {
const focusableElements = container.querySelectorAll(`
button:not([disabled]),
[href],
input:not([disabled]),
select:not([disabled]),
textarea:not([disabled]),
[tabindex]:not([tabindex="-1"]):not([disabled]),
[contenteditable="true"]
`);

let currentIndex = 0;

container.addEventListener('keydown', (event) => {
const elementsArray = Array.from(focusableElements) as HTMLElement[];

switch (event.key) {
case 'Tab':
// Tab键导航已由浏览器处理,但我们可以添加额外逻辑
if (event.shiftKey) {
// Shift+Tab 向后导航
currentIndex = Math.max(0, currentIndex - 1);
} else {
// Tab 向前导航
currentIndex = Math.min(elementsArray.length - 1, currentIndex + 1);
}
break;

case 'ArrowDown':
case 'ArrowRight':
event.preventDefault();
currentIndex = (currentIndex + 1) % elementsArray.length;
elementsArray[currentIndex]?.focus();
break;

case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault();
currentIndex = currentIndex === 0 ? elementsArray.length - 1 : currentIndex - 1;
elementsArray[currentIndex]?.focus();
break;

case 'Home':
event.preventDefault();
currentIndex = 0;
elementsArray[currentIndex]?.focus();
break;

case 'End':
event.preventDefault();
currentIndex = elementsArray.length - 1;
elementsArray[currentIndex]?.focus();
break;

case 'Enter':
case ' ':
const activeElement = document.activeElement as HTMLElement;
if (activeElement && (activeElement.tagName === 'BUTTON' || activeElement.getAttribute('role') === 'button')) {
event.preventDefault();
activeElement.click();
}
break;

case 'Escape':
// 关闭对话框或取消操作
const modal = container.closest('.modal, .dialog');
if (modal) {
this.closeModal(modal as HTMLElement);
}
break;
}
});
}

// 焦点管理
static manageFocus(options: {
container: HTMLElement,
autoFocus?: boolean,
trapFocus?: boolean,
restoreFocus?: HTMLElement
}): () => void {
const { container, autoFocus = true, trapFocus = true, restoreFocus } = options;

// 保存之前的焦点元素
const previousFocus = document.activeElement as HTMLElement;

// 自动聚焦到第一个可聚焦元素
if (autoFocus) {
const firstFocusable = container.querySelector(`
button:not([disabled]),
[href],
input:not([disabled]),
select:not([disabled]),
textarea:not([disabled]),
[tabindex]:not([tabindex="-1"]):not([disabled])
`) as HTMLElement;

firstFocusable?.focus();
}

let focusTrapHandler: ((event: KeyboardEvent) => void) | null = null;

// 焦点陷阱
if (trapFocus) {
focusTrapHandler = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
const focusableElements = container.querySelectorAll(`
button:not([disabled]),
[href],
input:not([disabled]),
select:not([disabled]),
textarea:not([disabled]),
[tabindex]:not([tabindex="-1"]):not([disabled])
`);

const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
};

document.addEventListener('keydown', focusTrapHandler);
}

// 返回清理函数
return () => {
if (focusTrapHandler) {
document.removeEventListener('keydown', focusTrapHandler);
}

// 恢复焦点
if (restoreFocus) {
restoreFocus.focus();
} else if (previousFocus) {
previousFocus.focus();
}
};
}

// ARIA属性管理
static setAriaAttributes(element: HTMLElement, attributes: {
label?: string,
labelledBy?: string,
describedBy?: string,
expanded?: boolean,
selected?: boolean,
checked?: boolean,
hidden?: boolean,
live?: 'off' | 'polite' | 'assertive',
atomic?: boolean,
busy?: boolean,
controls?: string,
owns?: string,
role?: string
}): void {
Object.entries(attributes).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
const ariaKey = key === 'role' ? 'role' : `aria-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
element.setAttribute(ariaKey, String(value));
}
});
}

// 屏幕阅读器公告
static announceToScreenReader(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.style.position = 'absolute';
announcement.style.left = '-9999px';
announcement.style.width = '1px';
announcement.style.height = '1px';
announcement.style.overflow = 'hidden';

document.body.appendChild(announcement);

// 延迟添加内容确保屏幕阅读器能够读取
setTimeout(() => {
announcement.textContent = message;
}, 100);

// 清理元素
setTimeout(() => {
document.body.removeChild(announcement);
}, 2000);
}

// 高对比度模式检测
static isHighContrastMode(): boolean {
// 创建测试元素检测高对比度模式
const testElement = document.createElement('div');
testElement.style.position = 'absolute';
testElement.style.left = '-9999px';
testElement.style.color = 'rgb(31, 31, 31)';
testElement.style.backgroundColor = 'rgb(31, 31, 31)';

document.body.appendChild(testElement);

const computedStyle = window.getComputedStyle(testElement);
const isHighContrast = computedStyle.color !== computedStyle.backgroundColor;

document.body.removeChild(testElement);

return isHighContrast;
}

// 动效减少检测
static prefersReducedMotion(): boolean {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

// 创建跳转链接
static createSkipLinks(targets: Array<{label: string, target: string}>): string {
const links = targets.map(({label, target}) =>
`<a href="#${target}" class="skip-link">${label}</a>`
).join('');

return `<nav class="skip-navigation" aria-label="跳转导航">${links}</nav>`;
}

private static closeModal(modal: HTMLElement): void {
// 模拟关闭模态框的逻辑
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
}

// 获取无障碍CSS
static getAccessibilityCSS(): string {
return `
/* 跳转链接 */
.skip-navigation {
position: absolute;
top: 0;
left: 0;
z-index: 9999;
}

.skip-link {
position: absolute;
left: -9999px;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
padding: ${LayoutManager.SPACING.md};
text-decoration: none;
border-radius: 0 0 4px 0;
}

.skip-link:focus {
left: 0;
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}

/* 焦点指示器 */
*:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 1px;
}

/* 高对比度模式适配 */
@media (prefers-contrast: high) {
.button,
.form-input,
.form-select,
.form-textarea {
border-width: 2px;
}

.status-badge {
border-width: 2px;
}
}

/* 减少动画 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}

.spinner {
animation: none;
}

.skeleton-line {
animation: none;
background: var(--vscode-panel-border);
}
}

/* 屏幕阅读器专用文本 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

/* 确保最小触摸目标尺寸 */
button,
.button,
a,
input[type="checkbox"],
input[type="radio"] {
min-height: 44px;
min-width: 44px;
}

/* 提高文字对比度 */
.low-contrast-fix {
color: var(--vscode-foreground) !important;
background-color: var(--vscode-editor-background) !important;
}
`;
}
}

3.2 语义化HTML和ARIA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
// src/semanticHTML.ts
export class SemanticHTMLManager {

// 创建语义化表格
static createAccessibleTable(options: {
headers: string[],
rows: string[][],
caption?: string,
sortable?: boolean
}): string {
const { headers, rows, caption, sortable } = options;

const captionHtml = caption ? `<caption>${caption}</caption>` : '';

const headerHtml = headers.map((header, index) => `
<th scope="col"
${sortable ? `role="columnheader" aria-sort="none" tabindex="0" data-column="${index}"` : ''}
>
${header}
${sortable ? '<span class="sort-indicator" aria-hidden="true"></span>' : ''}
</th>
`).join('');

const rowsHtml = rows.map((row, rowIndex) => `
<tr>
${row.map((cell, cellIndex) => `
<td ${cellIndex === 0 ? 'scope="row"' : ''}>${cell}</td>
`).join('')}
</tr>
`).join('');

return `
<table role="table" aria-label="${caption || '数据表格'}">
${captionHtml}
<thead>
<tr role="row">
${headerHtml}
</tr>
</thead>
<tbody>
${rowsHtml}
</tbody>
</table>
`;
}

// 创建手风琴组件
static createAccessibleAccordion(items: Array<{
id: string,
title: string,
content: string,
expanded?: boolean
}>): string {
const accordionItems = items.map(item => `
<div class="accordion-item">
<h3>
<button
class="accordion-trigger"
aria-expanded="${item.expanded || false}"
aria-controls="${item.id}-content"
id="${item.id}-trigger"
>
<span class="accordion-title">${item.title}</span>
<span class="accordion-icon" aria-hidden="true">▼</span>
</button>
</h3>
<div
class="accordion-content ${item.expanded ? 'expanded' : ''}"
id="${item.id}-content"
aria-labelledby="${item.id}-trigger"
role="region"
>
<div class="accordion-content-inner">
${item.content}
</div>
</div>
</div>
`).join('');

return `
<div class="accordion" role="group" aria-label="手风琴组件">
${accordionItems}
</div>
`;
}

// 创建模态对话框
static createAccessibleModal(options: {
id: string,
title: string,
content: string,
actions?: Array<{label: string, action: string, variant?: string}>,
closeButton?: boolean
}): string {
const { id, title, content, actions, closeButton = true } = options;

const actionsHtml = actions ? actions.map(action => `
<button
class="modal-action ${action.variant || 'secondary'}"
onclick="${action.action}"
>
${action.label}
</button>
`).join('') : '';

const closeButtonHtml = closeButton ? `
<button
class="modal-close"
aria-label="关闭对话框"
onclick="closeModal('${id}')"
>
<span aria-hidden="true">×</span>
</button>
` : '';

return `
<div
class="modal-overlay"
id="${id}"
role="dialog"
aria-modal="true"
aria-labelledby="${id}-title"
aria-describedby="${id}-content"
aria-hidden="true"
>
<div class="modal-container">
<div class="modal-header">
<h2 id="${id}-title" class="modal-title">${title}</h2>
${closeButtonHtml}
</div>
<div class="modal-body">
<div id="${id}-content">
${content}
</div>
</div>
${actions ? `
<div class="modal-footer">
${actionsHtml}
</div>
` : ''}
</div>
</div>
`;
}

// 创建列表组件
static createAccessibleList(options: {
items: Array<{
id: string,
content: string,
selected?: boolean,
disabled?: boolean
}>,
selectable?: boolean,
multiSelect?: boolean,
role?: 'list' | 'listbox' | 'menu'
}): string {
const { items, selectable, multiSelect, role = 'list' } = options;

const listItems = items.map(item => `
<li
class="list-item ${item.selected ? 'selected' : ''} ${item.disabled ? 'disabled' : ''}"
${selectable ? `
role="${role === 'listbox' ? 'option' : 'menuitem'}"
aria-selected="${item.selected || false}"
${item.disabled ? 'aria-disabled="true"' : 'tabindex="0"'}
data-item-id="${item.id}"
` : ''}
>
${item.content}
${item.selected ? '<span class="sr-only">已选中</span>' : ''}
</li>
`).join('');

return `
<ul
class="accessible-list"
role="${role}"
${selectable ? `
aria-multiselectable="${multiSelect || false}"
aria-label="可选择列表"
` : ''}
>
${listItems}
</ul>
`;
}

// 获取语义化组件CSS
static getSemanticCSS(): string {
return `
/* 表格样式 */
table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--vscode-panel-border);
}

caption {
padding: ${LayoutManager.SPACING.md};
font-weight: bold;
text-align: left;
color: var(--vscode-foreground);
}

th, td {
padding: ${LayoutManager.SPACING.md};
text-align: left;
border-bottom: 1px solid var(--vscode-panel-border);
}

th {
background-color: var(--vscode-sideBar-background);
font-weight: 600;
color: var(--vscode-foreground);
}

th[role="columnheader"] {
cursor: pointer;
user-select: none;
}

th[role="columnheader"]:hover {
background-color: var(--vscode-list-hoverBackground);
}

th[role="columnheader"]:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: -2px;
}

.sort-indicator {
margin-left: ${LayoutManager.SPACING.sm};
opacity: 0.5;
}

th[aria-sort="ascending"] .sort-indicator::before {
content: "▲";
}

th[aria-sort="descending"] .sort-indicator::before {
content: "▼";
}

/* 手风琴样式 */
.accordion {
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
}

.accordion-item {
border-bottom: 1px solid var(--vscode-panel-border);
}

.accordion-item:last-child {
border-bottom: none;
}

.accordion-trigger {
width: 100%;
padding: ${LayoutManager.SPACING.md};
background: var(--vscode-sideBar-background);
border: none;
text-align: left;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--vscode-foreground);
}

.accordion-trigger:hover {
background-color: var(--vscode-list-hoverBackground);
}

.accordion-trigger:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: -2px;
}

.accordion-trigger[aria-expanded="true"] .accordion-icon {
transform: rotate(180deg);
}

.accordion-icon {
transition: transform 0.3s ease;
font-size: 12px;
}

.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}

.accordion-content.expanded {
max-height: 1000px;
}

.accordion-content-inner {
padding: ${LayoutManager.SPACING.md};
color: var(--vscode-foreground);
}

/* 模态对话框样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}

.modal-overlay[aria-hidden="false"] {
opacity: 1;
visibility: visible;
}

.modal-container {
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
min-width: 400px;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}

.modal-header {
padding: ${LayoutManager.SPACING.lg};
border-bottom: 1px solid var(--vscode-panel-border);
display: flex;
justify-content: space-between;
align-items: center;
}

.modal-title {
margin: 0;
color: var(--vscode-foreground);
font-size: 18px;
font-weight: 600;
}

.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--vscode-foreground);
padding: 0;
line-height: 1;
}

.modal-body {
padding: ${LayoutManager.SPACING.lg};
flex: 1;
overflow-y: auto;
color: var(--vscode-foreground);
}

.modal-footer {
padding: ${LayoutManager.SPACING.lg};
border-top: 1px solid var(--vscode-panel-border);
display: flex;
gap: ${LayoutManager.SPACING.md};
justify-content: flex-end;
}

.modal-action {
padding: ${LayoutManager.SPACING.sm} ${LayoutManager.SPACING.lg};
border: none;
border-radius: 2px;
cursor: pointer;
font-weight: 500;
}

.modal-action.primary {
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}

.modal-action.secondary {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}

/* 列表样式 */
.accessible-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
}

.list-item {
padding: ${LayoutManager.SPACING.md};
border-bottom: 1px solid var(--vscode-panel-border);
color: var(--vscode-foreground);
cursor: pointer;
}

.list-item:last-child {
border-bottom: none;
}

.list-item:hover:not(.disabled) {
background-color: var(--vscode-list-hoverBackground);
}

.list-item:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: -2px;
}

.list-item.selected {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
}

.list-item.disabled {
color: var(--vscode-disabledForeground);
cursor: not-allowed;
opacity: 0.6;
}
`;
}

// JavaScript辅助函数
static getSemanticScript(): string {
return `
// 手风琴交互
document.addEventListener('DOMContentLoaded', function() {
const accordionTriggers = document.querySelectorAll('.accordion-trigger');

accordionTriggers.forEach(trigger => {
trigger.addEventListener('click', function() {
const expanded = this.getAttribute('aria-expanded') === 'true';
const content = document.getElementById(this.getAttribute('aria-controls'));

this.setAttribute('aria-expanded', !expanded);
content.classList.toggle('expanded', !expanded);
});
});

// 列表交互
const selectableLists = document.querySelectorAll('[role="listbox"], [role="menu"]');

selectableLists.forEach(list => {
const items = list.querySelectorAll('[role="option"], [role="menuitem"]');
const multiSelect = list.getAttribute('aria-multiselectable') === 'true';

items.forEach(item => {
item.addEventListener('click', function() {
if (this.getAttribute('aria-disabled') === 'true') return;

const selected = this.getAttribute('aria-selected') === 'true';

if (!multiSelect) {
// 单选:清除其他选择
items.forEach(otherItem => {
otherItem.setAttribute('aria-selected', 'false');
otherItem.classList.remove('selected');
});
}

this.setAttribute('aria-selected', !selected);
this.classList.toggle('selected', !selected);
});

item.addEventListener('keydown', function(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.click();
}
});
});
});

// 表格排序
const sortableHeaders = document.querySelectorAll('th[role="columnheader"]');

sortableHeaders.forEach(header => {
header.addEventListener('click', function() {
const table = this.closest('table');
const column = parseInt(this.getAttribute('data-column'));
const currentSort = this.getAttribute('aria-sort');

// 重置其他列的排序状态
sortableHeaders.forEach(h => h.setAttribute('aria-sort', 'none'));

// 设置当前列的排序状态
const newSort = currentSort === 'ascending' ? 'descending' : 'ascending';
this.setAttribute('aria-sort', newSort);

// 执行排序逻辑(这里需要根据实际数据结构实现)
sortTable(table, column, newSort === 'ascending');
});
});
});

// 模态对话框控制
function openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.setAttribute('aria-hidden', 'false');
const firstFocusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (firstFocusable) {
firstFocusable.focus();
}
}
}

function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.setAttribute('aria-hidden', 'true');
}
}

// 表格排序函数(示例)
function sortTable(table, column, ascending) {
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));

rows.sort((a, b) => {
const aVal = a.children[column].textContent.trim();
const bVal = b.children[column].textContent.trim();

if (ascending) {
return aVal.localeCompare(bVal, undefined, { numeric: true });
} else {
return bVal.localeCompare(aVal, undefined, { numeric: true });
}
});

rows.forEach(row => tbody.appendChild(row));
}
`;
}
}

📚 本阶段总结

通过本阶段学习,你已经掌握了:

UX设计原则:VS Code设计哲学、视觉一致性、用户期望管理
界面设计技能:颜色主题适配、图标规范、响应式布局
交互设计模式:导航系统、表单设计、状态反馈
无障碍设计能力:键盘导航、ARIA属性、屏幕阅读器支持
语义化HTML:结构化内容、可访问组件、交互逻辑

核心设计原则回顾

  • 简洁性:界面简洁明了,避免视觉混乱
  • 一致性:遵循VS Code设计规范,保持视觉统一
  • 可预测性:用户能够预期操作结果
  • 可访问性:支持所有用户,包括残障用户
  • 响应性:适配不同设备和屏幕尺寸

🚀 下一步学习方向

在下一阶段(语言扩展开发),我们将学习:

  • 📝 语法高亮:TextMate语法、语义高亮
  • 🔍 智能感知:代码补全、参数提示、悬停信息
  • 🛠️ Language Server Protocol:LSP架构、服务器开发
  • 🔧 代码操作:格式化、重构、快速修复

继续深入学习之前,建议:

  1. 实践本阶段的无障碍设计技术
  2. 测试扩展在不同主题和对比度下的表现
  3. 使用屏幕阅读器测试扩展可访问性
  4. 收集用户反馈优化用户体验

相关资源

恭喜你完成了VS Code扩展开发的用户体验设计阶段!现在你已经具备了创建专业、易用、无障碍扩展的设计能力。