Skip to content

Commit 6fe2a30

Browse files
committed
更新侧栏
1 parent 6b9b442 commit 6fe2a30

8 files changed

Lines changed: 311 additions & 61 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<script setup lang="ts">
2+
import { storeToRefs } from "pinia";
3+
import { useStore } from "../store";
4+
5+
const props = defineProps<{
6+
isOpen: boolean;
7+
}>();
8+
9+
const emit = defineEmits(["update:isOpen"]);
10+
11+
const toggleSidebar = () => {
12+
emit("update:isOpen", !props.isOpen);
13+
};
14+
15+
const store = useStore();
16+
const { settings } = storeToRefs(store);
17+
18+
const rangeConfigs = {
19+
targetSpeed: { name: "目标速度", unit: "WPM", min: 10, max: 120 },
20+
targetAccuracy: { name: "目标准确率", unit: "%", min: 80, max: 100 },
21+
};
22+
</script>
23+
24+
<template>
25+
<aside :class="['sidebar', { 'is-open': isOpen }]">
26+
<button
27+
class="toggle-btn"
28+
@click="toggleSidebar"
29+
:title="isOpen ? '收起' : '展开设置'"
30+
>
31+
<div :class="['arrow-icon', { 'is-active': isOpen }]"></div>
32+
</button>
33+
34+
<div class="sidebar-inner" v-if="isOpen">
35+
<h3 class="sidebar-title">训练设置</h3>
36+
37+
<div class="range-setting-container">
38+
<div
39+
v-for="(config, key) in rangeConfigs"
40+
:key="key"
41+
class="range-item"
42+
>
43+
<div class="range-header">
44+
<span class="setting-name">{{ config.name }}</span>
45+
<span class="setting-value"
46+
>{{ settings[key] }}{{ config.unit }}</span
47+
>
48+
</div>
49+
<input
50+
type="range"
51+
class="custom-slider"
52+
:min="config.min"
53+
:max="config.max"
54+
v-model.number="settings[key]"
55+
/>
56+
</div>
57+
</div>
58+
</div>
59+
</aside>
60+
</template>
61+
62+
<style lang="less" scoped>
63+
@import "../styles/color.less";
64+
@import "../styles/animation.less";
65+
66+
.sidebar:not(.is-open) {
67+
.toggle-btn {
68+
opacity: 0.8;
69+
&:hover {
70+
opacity: 1;
71+
}
72+
}
73+
}
74+
75+
.sidebar {
76+
position: absolute;
77+
right: 0;
78+
top: 50%;
79+
transform: translate(100%, -50%);
80+
width: 280px;
81+
background: var(--white, #fff);
82+
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.05);
83+
border-radius: 20px 0 0 20px;
84+
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
85+
z-index: 1000;
86+
padding: 32px 24px;
87+
border: 1px solid rgba(0, 0, 0, 0.05);
88+
89+
&.is-open {
90+
transform: translate(0, -50%);
91+
}
92+
93+
.toggle-btn {
94+
position: absolute;
95+
left: -36px;
96+
top: 50%;
97+
transform: translateY(-50%);
98+
width: 36px;
99+
height: 80px;
100+
101+
background: rgba(255, 255, 255, 0.9);
102+
backdrop-filter: blur(12px);
103+
104+
border: 1px solid rgba(0, 0, 0, 0.08);
105+
border-right: none;
106+
border-radius: 18px 0 0 18px;
107+
108+
cursor: pointer;
109+
display: flex;
110+
align-items: center;
111+
justify-content: center;
112+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
113+
box-shadow: -6px 0 15px rgba(0, 0, 0, 0.05);
114+
115+
&:hover {
116+
background: #fff;
117+
left: -40px;
118+
width: 40px;
119+
box-shadow: -8px 0 20px rgba(0, 0, 0, 0.08);
120+
121+
.arrow-icon {
122+
border-color: @primary-color;
123+
transform: translateX(0px) rotate(-45deg) scale(1.1);
124+
}
125+
}
126+
127+
.arrow-icon {
128+
width: 9px;
129+
height: 9px;
130+
border-top: 2.5px solid rgba(0, 0, 0, 0.4);
131+
border-left: 2.5px solid rgba(0, 0, 0, 0.4);
132+
transform: translateX(3px) rotate(-45deg);
133+
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
134+
135+
&.is-active {
136+
transform: translateX(-1px) rotate(135deg);
137+
}
138+
}
139+
}
140+
141+
.sidebar-inner {
142+
display: flex;
143+
flex-direction: column;
144+
gap: 28px;
145+
}
146+
147+
.sidebar-title {
148+
margin: 0;
149+
font-size: 1.1rem;
150+
font-weight: bold;
151+
color: var(--black);
152+
opacity: 0.9;
153+
border-left: 4px solid @primary-color;
154+
padding-left: 12px;
155+
}
156+
157+
.range-setting-container {
158+
display: flex;
159+
flex-direction: column;
160+
gap: 24px;
161+
}
162+
163+
.range-item {
164+
display: flex;
165+
flex-direction: column;
166+
167+
.range-header {
168+
display: flex;
169+
justify-content: space-between;
170+
margin-bottom: 10px;
171+
font-weight: bold;
172+
font-size: 0.9rem;
173+
174+
.setting-name {
175+
color: @primary-color;
176+
}
177+
.setting-value {
178+
color: var(--black);
179+
opacity: 0.7;
180+
}
181+
}
182+
}
183+
184+
.custom-slider {
185+
appearance: none;
186+
-webkit-appearance: none;
187+
width: 100%;
188+
height: 4px;
189+
background: rgba(0, 0, 0, 0.06);
190+
border-radius: 2px;
191+
outline: none;
192+
193+
&::-webkit-slider-thumb {
194+
appearance: none;
195+
-webkit-appearance: none;
196+
width: 16px;
197+
height: 16px;
198+
background: @primary-color;
199+
border-radius: 50%;
200+
cursor: pointer;
201+
border: 2px solid #fff;
202+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
203+
transition: transform 0.2s;
204+
205+
&:hover {
206+
transform: scale(1.1);
207+
}
208+
}
209+
}
210+
}
211+
</style>

src/components/SingleMode.vue

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Hanzi from "../components/Hanzi.vue";
44
import Pinyin from "../components/Pinyin.vue";
55
import TypeSummary from "../components/TypeSummary.vue";
66
import MenuList from "../components/MenuList.vue";
7-
7+
import Sidebar from "../components/ProgressiveSidebar.vue";
88
import {
99
onActivated,
1010
onDeactivated,
@@ -176,20 +176,17 @@ watchPostEffect(() => {
176176
}
177177
});
178178
179-
180-
watch([() => props.mode, () => menuIndex.value], () => {
181-
refreshPartialSequence();
182-
}, { immediate: false });
183-
179+
watch(
180+
[() => props.mode, () => menuIndex.value],
181+
() => {
182+
refreshPartialSequence();
183+
},
184+
{ immediate: false },
185+
);
184186
185187
function refreshPartialSequence() {
186188
const currentChar = hanziSeq.value.at(-1) || getNextChar();
187-
hanziSeq.value = [
188-
getNextChar(),
189-
getNextChar(),
190-
getNextChar(),
191-
currentChar
192-
];
189+
hanziSeq.value = [getNextChar(), getNextChar(), getNextChar(), currentChar];
193190
isValid.value = false;
194191
}
195192
@@ -199,6 +196,16 @@ defineExpose({
199196
summary.value = new TypingSummary();
200197
},
201198
});
199+
200+
const isSidebarOpen = ref(false);
201+
watch(
202+
() => props.mode,
203+
(newMode) => {
204+
if (newMode !== "Progressive") {
205+
isSidebarOpen.value = false;
206+
}
207+
},
208+
);
202209
</script>
203210

204211
<template>
@@ -230,6 +237,10 @@ defineExpose({
230237
:avgpress="summary.pressPerHanzi"
231238
/>
232239
</div>
240+
<Sidebar
241+
v-show="props.mode === 'Progressive'"
242+
v-model:is-open="isSidebarOpen"
243+
/>
233244
</div>
234245
</template>
235246

src/env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ interface Settings {
7575
enablePinyinHint: boolean; // 拼音提示
7676
enableAutoClear: boolean; // 自动清空
7777
shuangpinMode: ShuangpinType;
78+
targetSpeed: number; // 目标速度 (WPM/KPM)
79+
targetAccuracy: number; // 目标准确率 (0-100)
7880
}
7981

8082
type Theme = "auto" | "dark" | "light";

src/pages/ParagraphMode.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import {
88
watchPostEffect,
99
onActivated,
1010
onDeactivated,
11-
watchEffect,computed
11+
watchEffect,
12+
computed,
1213
} from "vue";
1314
import { useStore } from "../store";
1415
import { storeToRefs } from "pinia";
1516
1617
import rawArticles from "../utils/article.json";
17-
import { getPinyinOf,isValidHanzi } from "../utils/hanzi";
18+
import { getPinyinOf, isValidHanzi } from "../utils/hanzi";
1819
import { matchSpToPinyin } from "../utils/keyboard";
1920
import { TypingSummary } from "../utils/summary";
2021
import MenuList from "../components/MenuList.vue";
@@ -45,11 +46,11 @@ onDeactivated(() => {
4546
4647
rawNames.forEach((v) => {
4748
const name = v as RawArticleName;
48-
const progress: Progress = {
49+
const progress: Progress = {
4950
currentIndex: 0,
5051
total: rawArticles[name].length,
51-
history: [],
52-
correctSum: 0,
52+
history: [],
53+
correctSum: 0,
5354
};
5455
5556
articles.value.push({ progress, type: name });
@@ -333,11 +334,10 @@ function shortPinyin(pinyins: string[]) {
333334
</div>
334335

335336
<Keyboard v-if="!isEditing" :valid-seq="onSeq" :hints="article.spHints" />
336-
337337
<div v-if="!isEditing" class="summary">
338338
<TypeSummary
339339
:speed="summary.hanziPerMinutes"
340-
:accuracy="summary.accuracy"
340+
:accuracy="summary.totalAccuracy"
341341
:avgpress="summary.pressPerHanzi"
342342
/>
343343
</div>

src/pages/Settings.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ const buildBooleanOption = (name: string): SettingOption<boolean> => ({
3131
name,
3232
});
3333
34-
type SettingKeys = keyof Omit<Settings, "shuangpinMode">;
35-
34+
type SettingKeys = keyof Omit<
35+
Settings,
36+
"shuangpinMode" | "targetSpeed" | "targetAccuracy"
37+
>;
3638
const settingOptions: {
3739
[_ in SettingKeys]: SettingOption<boolean | Theme>;
3840
} = {
@@ -61,7 +63,7 @@ function nextOption(name: SettingKeys) {
6163
function getOptionName(name: SettingKeys) {
6264
const setting = settingOptions[name];
6365
const index = setting.options.findIndex(
64-
(v) => v.option === settings.value[name]
66+
(v) => v.option === settings.value[name],
6567
);
6668
const safeIndex = Math.max(index, 0);
6769
return setting.options[safeIndex].name;
@@ -97,7 +99,7 @@ function deleteMode() {
9799
if (confirm("确认删除?")) {
98100
store.deleteConfig(spName.value);
99101
settings.value.shuangpinMode = store.modes.at(
100-
Math.max(currentIndex.value, -1)
102+
Math.max(currentIndex.value, -1),
101103
) as ShuangpinType;
102104
spName.value = settings.value.shuangpinMode;
103105
}

src/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const useStore = defineStore("app", {
2626
enablePinyinHint: true,
2727
theme: "auto",
2828
shuangpinMode: "小鹤双拼",
29+
targetSpeed: 35,
30+
targetAccuracy: 95,
2931
},
3032
};
3133
},

src/utils/hanzi.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ export function getPinyinOf(hanzi: string): string[] {
3333
const backup = pinyin(char, {
3434
multiple: true,
3535
type: 'array',
36-
toneType: 'none'
36+
toneType: 'none'
3737
});
38-
return backup.length > 0 && backup[0] !== char ? backup : [];
38+
39+
if (backup.length > 0 && backup[0] !== char) {
40+
return backup.map(p => p.replaceAll('ü', 'v'));
41+
}
42+
43+
return [];
3944
}
4045

4146
export function isValidHanzi(char: string): boolean {

0 commit comments

Comments
 (0)