@wsxjs/wsx-base-components 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +28 -28
- package/dist/index.cjs +14 -2
- package/dist/index.js +4971 -2032
- package/dist/style.css +1 -1
- package/package.json +16 -7
- package/src/{XyButton.css → Button.css} +1 -1
- package/src/{XyButton.wsx → Button.wsx} +18 -9
- package/src/ButtonGroup.css +30 -0
- package/src/{XyButtonGroup.wsx → ButtonGroup.wsx} +26 -14
- package/src/CodeBlock.css +275 -0
- package/src/CodeBlock.types.ts +25 -0
- package/src/CodeBlock.wsx +296 -0
- package/src/ColorPicker.wsx +6 -5
- package/src/Combobox.css +254 -0
- package/src/Combobox.types.ts +32 -0
- package/src/Combobox.wsx +352 -0
- package/src/Dropdown.css +178 -0
- package/src/Dropdown.types.ts +28 -0
- package/src/Dropdown.wsx +221 -0
- package/src/LanguageSwitcher.css +148 -0
- package/src/LanguageSwitcher.wsx +190 -0
- package/src/OverflowDetector.ts +169 -0
- package/src/ResponsiveNav.css +555 -0
- package/src/ResponsiveNav.types.ts +30 -0
- package/src/ResponsiveNav.wsx +450 -0
- package/src/SvgIcon.wsx +2 -2
- package/src/index.ts +17 -9
- package/src/types/wsx.d.ts +4 -3
- package/src/ReactiveCounter.css +0 -304
- package/src/ReactiveCounter.wsx +0 -231
- package/src/SimpleReactiveDemo.wsx +0 -59
- package/src/SvgDemo.wsx +0 -241
- package/src/TodoList.css +0 -197
- package/src/TodoList.wsx +0 -264
- package/src/TodoListLight.css +0 -198
- package/src/TodoListLight.wsx +0 -263
- package/src/UserProfile.css +0 -146
- package/src/UserProfile.wsx +0 -247
- package/src/UserProfileLight.css +0 -146
- package/src/UserProfileLight.wsx +0 -256
- package/src/XyButtonGroup.css +0 -30
package/src/Dropdown.wsx
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* Dropdown Component
|
|
4
|
+
* 通用下拉菜单组件,用于构建各种下拉选择器
|
|
5
|
+
*/
|
|
6
|
+
import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
|
|
7
|
+
import styles from "./Dropdown.css?inline";
|
|
8
|
+
import type { DropdownOption, DropdownConfig } from "./Dropdown.types";
|
|
9
|
+
export type { DropdownOption, DropdownConfig };
|
|
10
|
+
|
|
11
|
+
@autoRegister({ tagName: "wsx-dropdown" })
|
|
12
|
+
export default class Dropdown extends WebComponent {
|
|
13
|
+
/** 选项列表 */
|
|
14
|
+
private options: DropdownOption[] = [];
|
|
15
|
+
/** 当前选中的值 */
|
|
16
|
+
@state private selectedValue = "";
|
|
17
|
+
/** 下拉菜单是否打开 */
|
|
18
|
+
@state private isOpen: boolean = false;
|
|
19
|
+
/** 配置选项 */
|
|
20
|
+
private config: DropdownConfig = {};
|
|
21
|
+
|
|
22
|
+
/** 元素引用 */
|
|
23
|
+
private buttonElement?: HTMLElement;
|
|
24
|
+
private dropdownElement?: HTMLElement;
|
|
25
|
+
private outsideClickHandler?: (event: Event) => void;
|
|
26
|
+
|
|
27
|
+
constructor(config: DropdownConfig = {}) {
|
|
28
|
+
super({
|
|
29
|
+
styles,
|
|
30
|
+
styleName: "wsx-dropdown",
|
|
31
|
+
});
|
|
32
|
+
this.config = {
|
|
33
|
+
placeholder: "Select...",
|
|
34
|
+
align: "left",
|
|
35
|
+
position: "bottom",
|
|
36
|
+
trigger: "click",
|
|
37
|
+
...config,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 设置选项列表
|
|
43
|
+
*/
|
|
44
|
+
public setOptions(options: DropdownOption[]): void {
|
|
45
|
+
this.options = options;
|
|
46
|
+
this.rerender();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 设置选中的值
|
|
51
|
+
*/
|
|
52
|
+
public setValue(value: string): void {
|
|
53
|
+
this.selectedValue = value;
|
|
54
|
+
this.rerender();
|
|
55
|
+
this.dispatchEvent(
|
|
56
|
+
new CustomEvent("change", {
|
|
57
|
+
detail: { value },
|
|
58
|
+
bubbles: true,
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 获取选中的值
|
|
65
|
+
*/
|
|
66
|
+
public getValue(): string | undefined {
|
|
67
|
+
return this.selectedValue || undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
render(): HTMLElement {
|
|
71
|
+
const selectedOption = this.options.find(
|
|
72
|
+
(opt) => opt.value === this.selectedValue && this.selectedValue !== ""
|
|
73
|
+
);
|
|
74
|
+
const displayText = selectedOption?.label || this.config.placeholder || "Select...";
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div class="dropdown-container">
|
|
78
|
+
<button
|
|
79
|
+
ref={(el) => (this.buttonElement = el)}
|
|
80
|
+
class={`dropdown-button ${this.isOpen ? "open" : ""} ${
|
|
81
|
+
this.config.disabled ? "disabled" : ""
|
|
82
|
+
}`}
|
|
83
|
+
onClick={this.config.trigger === "click" ? this.toggleDropdown : undefined}
|
|
84
|
+
onMouseEnter={this.config.trigger === "hover" ? this.openDropdown : undefined}
|
|
85
|
+
onMouseLeave={this.config.trigger === "hover" ? this.closeDropdown : undefined}
|
|
86
|
+
disabled={this.config.disabled}
|
|
87
|
+
aria-expanded={this.isOpen}
|
|
88
|
+
aria-haspopup="listbox"
|
|
89
|
+
>
|
|
90
|
+
<span class="dropdown-text">{displayText}</span>
|
|
91
|
+
<span class="dropdown-arrow">{this.isOpen ? "▲" : "▼"}</span>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
{this.isOpen && (
|
|
95
|
+
<div
|
|
96
|
+
ref={(el) => (this.dropdownElement = el)}
|
|
97
|
+
class={`dropdown-menu dropdown-${this.config.position} dropdown-${this.config.align}`}
|
|
98
|
+
role="listbox"
|
|
99
|
+
onMouseEnter={
|
|
100
|
+
this.config.trigger === "hover" ? this.openDropdown : undefined
|
|
101
|
+
}
|
|
102
|
+
onMouseLeave={
|
|
103
|
+
this.config.trigger === "hover" ? this.closeDropdown : undefined
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
{this.options.length === 0 ? (
|
|
107
|
+
<div class="dropdown-empty">No options</div>
|
|
108
|
+
) : (
|
|
109
|
+
this.options.map((option) => (
|
|
110
|
+
<button
|
|
111
|
+
key={option.value}
|
|
112
|
+
class={`dropdown-option ${
|
|
113
|
+
option.value === this.selectedValue &&
|
|
114
|
+
this.selectedValue !== ""
|
|
115
|
+
? "selected"
|
|
116
|
+
: ""
|
|
117
|
+
} ${option.disabled ? "disabled" : ""}`}
|
|
118
|
+
onClick={() =>
|
|
119
|
+
!option.disabled && this.selectOption(option.value)
|
|
120
|
+
}
|
|
121
|
+
role="option"
|
|
122
|
+
aria-selected={
|
|
123
|
+
option.value === this.selectedValue &&
|
|
124
|
+
this.selectedValue !== ""
|
|
125
|
+
}
|
|
126
|
+
disabled={option.disabled}
|
|
127
|
+
>
|
|
128
|
+
{option.render ? option.render() : <span>{option.label}</span>}
|
|
129
|
+
</button>
|
|
130
|
+
))
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 切换下拉菜单
|
|
140
|
+
*/
|
|
141
|
+
private toggleDropdown = (): void => {
|
|
142
|
+
if (this.config.disabled) return;
|
|
143
|
+
this.isOpen = !this.isOpen;
|
|
144
|
+
this.rerender();
|
|
145
|
+
|
|
146
|
+
if (this.isOpen) {
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
this.attachOutsideClickHandler();
|
|
149
|
+
}, 0);
|
|
150
|
+
} else {
|
|
151
|
+
this.detachOutsideClickHandler();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 打开下拉菜单
|
|
157
|
+
*/
|
|
158
|
+
private openDropdown = (): void => {
|
|
159
|
+
if (this.config.disabled || this.isOpen) return;
|
|
160
|
+
this.isOpen = true;
|
|
161
|
+
this.rerender();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 关闭下拉菜单
|
|
166
|
+
*/
|
|
167
|
+
private closeDropdown = (): void => {
|
|
168
|
+
if (!this.isOpen) return;
|
|
169
|
+
this.isOpen = false;
|
|
170
|
+
this.rerender();
|
|
171
|
+
this.detachOutsideClickHandler();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 选择选项
|
|
176
|
+
*/
|
|
177
|
+
private selectOption = (value: string): void => {
|
|
178
|
+
this.setValue(value);
|
|
179
|
+
if (this.config.trigger === "click") {
|
|
180
|
+
this.closeDropdown();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 附加外部点击处理器
|
|
186
|
+
*/
|
|
187
|
+
private attachOutsideClickHandler = (): void => {
|
|
188
|
+
if (this.config.trigger === "hover") return;
|
|
189
|
+
|
|
190
|
+
this.outsideClickHandler = (event: Event) => {
|
|
191
|
+
const target = event.target as Node;
|
|
192
|
+
if (
|
|
193
|
+
this.dropdownElement &&
|
|
194
|
+
this.buttonElement &&
|
|
195
|
+
!this.dropdownElement.contains(target) &&
|
|
196
|
+
!this.buttonElement.contains(target)
|
|
197
|
+
) {
|
|
198
|
+
this.closeDropdown();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
document.addEventListener("click", this.outsideClickHandler, true);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 移除外部点击处理器
|
|
207
|
+
*/
|
|
208
|
+
private detachOutsideClickHandler = (): void => {
|
|
209
|
+
if (this.outsideClickHandler) {
|
|
210
|
+
document.removeEventListener("click", this.outsideClickHandler, true);
|
|
211
|
+
this.outsideClickHandler = undefined;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 组件断开连接时清理
|
|
217
|
+
*/
|
|
218
|
+
protected onDisconnected(): void {
|
|
219
|
+
this.detachOutsideClickHandler();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/* LanguageSwitcher Component Styles */
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.language-switcher-container {
|
|
9
|
+
position: relative;
|
|
10
|
+
display: inline-block;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.language-switcher-btn {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
padding: 0.5rem 0.75rem;
|
|
18
|
+
background: var(--language-switcher-bg, rgba(255, 255, 255, 0.1));
|
|
19
|
+
border: 1px solid var(--language-switcher-border, rgba(255, 255, 255, 0.2));
|
|
20
|
+
border-radius: var(--language-switcher-border-radius, 0.5rem);
|
|
21
|
+
color: var(--language-switcher-color, currentColor);
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
font-size: 0.875rem;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
transition: all 0.2s ease;
|
|
26
|
+
min-width: 100px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.language-switcher-btn:hover {
|
|
30
|
+
background: var(--language-switcher-hover-bg, rgba(255, 255, 255, 0.15));
|
|
31
|
+
border-color: var(--language-switcher-hover-border, rgba(255, 255, 255, 0.3));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.language-switcher-btn:focus {
|
|
35
|
+
outline: 2px solid var(--language-switcher-focus-color, var(--focus-color, #dc2626));
|
|
36
|
+
outline-offset: 2px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.language-switcher-icon {
|
|
40
|
+
font-size: 1rem;
|
|
41
|
+
line-height: 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.language-switcher-text {
|
|
45
|
+
flex: 1;
|
|
46
|
+
text-align: left;
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.language-switcher-arrow {
|
|
51
|
+
font-size: 0.625rem;
|
|
52
|
+
opacity: 0.7;
|
|
53
|
+
transition: transform 0.2s ease;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.language-switcher-dropdown {
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: calc(100% + 0.25rem);
|
|
59
|
+
right: 0;
|
|
60
|
+
min-width: 160px;
|
|
61
|
+
max-height: 300px;
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
background: var(--language-switcher-dropdown-bg, #ffffff);
|
|
64
|
+
border: 1px solid var(--language-switcher-dropdown-border, #e5e7eb);
|
|
65
|
+
border-radius: var(--language-switcher-border-radius, 0.5rem);
|
|
66
|
+
box-shadow:
|
|
67
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
68
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
69
|
+
z-index: 1000;
|
|
70
|
+
padding: 0.25rem;
|
|
71
|
+
margin: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
[data-theme="dark"] .language-switcher-dropdown {
|
|
75
|
+
background: var(--language-switcher-dropdown-bg-dark, #1f2937);
|
|
76
|
+
border-color: var(--language-switcher-dropdown-border-dark, #374151);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.language-switcher-option {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
width: 100%;
|
|
84
|
+
padding: 0.5rem 0.75rem;
|
|
85
|
+
background: transparent;
|
|
86
|
+
border: none;
|
|
87
|
+
border-radius: 0.375rem;
|
|
88
|
+
color: var(--language-switcher-option-color, #1f2937);
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
font-size: 0.875rem;
|
|
91
|
+
text-align: left;
|
|
92
|
+
transition: background-color 0.15s ease;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
[data-theme="dark"] .language-switcher-option {
|
|
96
|
+
color: var(--language-switcher-option-color-dark, #f3f4f6);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.language-switcher-option:hover {
|
|
100
|
+
background: var(--language-switcher-option-hover-bg, #f3f4f6);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
[data-theme="dark"] .language-switcher-option:hover {
|
|
104
|
+
background: var(--language-switcher-option-hover-bg-dark, #374151);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.language-switcher-option.active {
|
|
108
|
+
background: var(--language-switcher-option-active-bg, #dc2626);
|
|
109
|
+
color: var(--language-switcher-option-active-color, #ffffff);
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.language-switcher-option.active:hover {
|
|
114
|
+
background: var(--language-switcher-option-active-hover-bg, #b91c1c);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.language-name {
|
|
118
|
+
flex: 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.language-code {
|
|
122
|
+
font-size: 0.75rem;
|
|
123
|
+
opacity: 0.7;
|
|
124
|
+
margin-left: 0.5rem;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.language-switcher-option.active .language-code {
|
|
129
|
+
opacity: 0.9;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Responsive design */
|
|
133
|
+
@media (max-width: 768px) {
|
|
134
|
+
.language-switcher-btn {
|
|
135
|
+
min-width: 80px;
|
|
136
|
+
padding: 0.375rem 0.5rem;
|
|
137
|
+
font-size: 0.8125rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.language-switcher-text {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.language-switcher-dropdown {
|
|
145
|
+
right: 0;
|
|
146
|
+
min-width: 140px;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
+
/**
|
|
3
|
+
* LanguageSwitcher Component
|
|
4
|
+
* 语言切换器组件,用于切换 i18next 的语言
|
|
5
|
+
* 不使用国旗图标,使用语言名称显示
|
|
6
|
+
*/
|
|
7
|
+
import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
|
|
8
|
+
import { i18nInstance } from "@wsxjs/wsx-i18next";
|
|
9
|
+
import styles from "./LanguageSwitcher.css?inline";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 语言选项接口
|
|
13
|
+
*/
|
|
14
|
+
export interface LanguageOption {
|
|
15
|
+
/** 语言代码(如 'en', 'zh') */
|
|
16
|
+
code: string;
|
|
17
|
+
/** 语言显示名称(如 'English', '中文') */
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@autoRegister({ tagName: "language-switcher" })
|
|
22
|
+
export default class LanguageSwitcher extends WebComponent {
|
|
23
|
+
/** 支持的语言列表 */
|
|
24
|
+
private languages: LanguageOption[] = [
|
|
25
|
+
{ code: "en", name: "English" },
|
|
26
|
+
{ code: "zh", name: "中文" },
|
|
27
|
+
{ code: "es", name: "Español" },
|
|
28
|
+
{ code: "fr", name: "Français" },
|
|
29
|
+
{ code: "de", name: "Deutsch" },
|
|
30
|
+
{ code: "ja", name: "日本語" },
|
|
31
|
+
{ code: "ko", name: "한국어" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/** 当前选中的语言代码 */
|
|
35
|
+
@state private currentLanguage: string = "en";
|
|
36
|
+
|
|
37
|
+
/** 下拉菜单是否打开 */
|
|
38
|
+
@state private isOpen: boolean = false;
|
|
39
|
+
|
|
40
|
+
/** 下拉菜单元素引用 */
|
|
41
|
+
private dropdownElement?: HTMLElement;
|
|
42
|
+
private buttonElement?: HTMLElement;
|
|
43
|
+
|
|
44
|
+
constructor() {
|
|
45
|
+
super({
|
|
46
|
+
styles,
|
|
47
|
+
styleName: "language-switcher",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render(): HTMLElement {
|
|
52
|
+
const currentLang = this.languages.find((lang) => lang.code === this.currentLanguage);
|
|
53
|
+
const displayName = currentLang?.name || this.currentLanguage.toUpperCase();
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div class="language-switcher-container">
|
|
57
|
+
<button
|
|
58
|
+
ref={(el) => (this.buttonElement = el)}
|
|
59
|
+
class="language-switcher-btn"
|
|
60
|
+
onClick={this.toggleDropdown}
|
|
61
|
+
aria-label={`Current language: ${displayName}`}
|
|
62
|
+
aria-expanded={this.isOpen}
|
|
63
|
+
aria-haspopup="listbox"
|
|
64
|
+
>
|
|
65
|
+
<span class="language-switcher-icon">🌐</span>
|
|
66
|
+
<span class="language-switcher-text">{displayName}</span>
|
|
67
|
+
<span class="language-switcher-arrow">{this.isOpen ? "▲" : "▼"}</span>
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
{this.isOpen && (
|
|
71
|
+
<div
|
|
72
|
+
ref={(el) => (this.dropdownElement = el)}
|
|
73
|
+
class="language-switcher-dropdown"
|
|
74
|
+
role="listbox"
|
|
75
|
+
>
|
|
76
|
+
{this.languages.map((lang) => (
|
|
77
|
+
<button
|
|
78
|
+
key={lang.code}
|
|
79
|
+
class={`language-switcher-option ${
|
|
80
|
+
lang.code === this.currentLanguage ? "active" : ""
|
|
81
|
+
}`}
|
|
82
|
+
onClick={() => this.selectLanguage(lang.code)}
|
|
83
|
+
role="option"
|
|
84
|
+
aria-selected={lang.code === this.currentLanguage}
|
|
85
|
+
>
|
|
86
|
+
<span class="language-name">{lang.name}</span>
|
|
87
|
+
<span class="language-code">{lang.code.toUpperCase()}</span>
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 切换下拉菜单
|
|
98
|
+
*/
|
|
99
|
+
private toggleDropdown = (): void => {
|
|
100
|
+
this.isOpen = !this.isOpen;
|
|
101
|
+
this.rerender();
|
|
102
|
+
|
|
103
|
+
if (this.isOpen) {
|
|
104
|
+
// 点击外部关闭下拉菜单
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
document.addEventListener("click", this.handleOutsideClick, true);
|
|
107
|
+
}, 0);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 处理点击外部区域
|
|
113
|
+
*/
|
|
114
|
+
private handleOutsideClick = (event: Event): void => {
|
|
115
|
+
const target = event.target as Node;
|
|
116
|
+
if (
|
|
117
|
+
this.dropdownElement &&
|
|
118
|
+
this.buttonElement &&
|
|
119
|
+
!this.dropdownElement.contains(target) &&
|
|
120
|
+
!this.buttonElement.contains(target)
|
|
121
|
+
) {
|
|
122
|
+
this.isOpen = false;
|
|
123
|
+
this.rerender();
|
|
124
|
+
document.removeEventListener("click", this.handleOutsideClick, true);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 选择语言
|
|
130
|
+
*/
|
|
131
|
+
private selectLanguage = (languageCode: string): void => {
|
|
132
|
+
if (languageCode === this.currentLanguage) {
|
|
133
|
+
this.isOpen = false;
|
|
134
|
+
this.rerender();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 更改 i18next 语言
|
|
139
|
+
i18nInstance.changeLanguage(languageCode).then(() => {
|
|
140
|
+
this.currentLanguage = languageCode;
|
|
141
|
+
this.isOpen = false;
|
|
142
|
+
this.rerender();
|
|
143
|
+
|
|
144
|
+
// 保存到 localStorage
|
|
145
|
+
localStorage.setItem("wsx-language", languageCode);
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 组件连接时初始化
|
|
151
|
+
*/
|
|
152
|
+
protected onConnected(): void {
|
|
153
|
+
// 从 localStorage 或 i18next 获取当前语言
|
|
154
|
+
const savedLanguage = localStorage.getItem("wsx-language");
|
|
155
|
+
const i18nLanguage = i18nInstance.language || i18nInstance.options.fallbackLng || "en";
|
|
156
|
+
|
|
157
|
+
// 获取基础语言代码(处理 'en-US' -> 'en' 的情况)
|
|
158
|
+
const baseLanguage = (savedLanguage || i18nLanguage).split("-")[0];
|
|
159
|
+
|
|
160
|
+
if (baseLanguage !== this.currentLanguage) {
|
|
161
|
+
this.currentLanguage = baseLanguage;
|
|
162
|
+
// 如果 localStorage 中的语言与 i18next 不一致,更新 i18next
|
|
163
|
+
if (savedLanguage && savedLanguage !== i18nLanguage) {
|
|
164
|
+
i18nInstance.changeLanguage(savedLanguage);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 监听 i18next 语言变化事件
|
|
169
|
+
i18nInstance.on("languageChanged", this.handleLanguageChanged);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 组件断开连接时清理
|
|
174
|
+
*/
|
|
175
|
+
protected onDisconnected(): void {
|
|
176
|
+
i18nInstance.off("languageChanged", this.handleLanguageChanged);
|
|
177
|
+
document.removeEventListener("click", this.handleOutsideClick, true);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 处理 i18next 语言变化事件
|
|
182
|
+
*/
|
|
183
|
+
private handleLanguageChanged = (lng: string): void => {
|
|
184
|
+
const baseLanguage = lng.split("-")[0];
|
|
185
|
+
if (baseLanguage !== this.currentLanguage) {
|
|
186
|
+
this.currentLanguage = baseLanguage;
|
|
187
|
+
this.rerender();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|