Skip to content

Commit 4ea2872

Browse files
committed
feat: Input / Textarea 通用组件,统一 X 清除按钮交互
新增 src/ui/components/Input.vue 与 Textarea.vue,hover 时框线变 accent,有内容自动显示 X 清除按钮,X mousedown.prevent 触发清空并保持焦点;search input 也通过 ::-webkit-search-cancel-button 隐藏浏览器原生 X,统一外观。Popup 搜索框 + Settings 全部 input / textarea(4 input + 5 textarea)改用新组件,Enter 通过 keydown 事件透传保留搜索快捷键。将版本号提升到 1.3.0。
1 parent 756e0dd commit 4ea2872

5 files changed

Lines changed: 338 additions & 21 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stackprism",
33
"private": true,
4-
"version": "1.2.99",
4+
"version": "1.3.0",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/ui/components/Input.vue

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<template>
2+
<span class="sp-input" :class="{ disabled, focused, clearable: showClearButton }">
3+
<input
4+
ref="inputRef"
5+
class="sp-input-inner"
6+
:type="type"
7+
:value="modelValue"
8+
:placeholder="placeholder"
9+
:disabled="disabled"
10+
:spellcheck="spellcheck"
11+
:name="name"
12+
:autocomplete="autocomplete"
13+
@input="onInput"
14+
@keydown="emit('keydown', $event)"
15+
@focus="onFocus"
16+
@blur="onBlur"
17+
/>
18+
<button
19+
v-if="showClearButton"
20+
type="button"
21+
class="sp-input-clear"
22+
tabindex="-1"
23+
:title="clearTitle"
24+
:aria-label="clearTitle"
25+
@mousedown.prevent="clear"
26+
>
27+
<X :size="12" :stroke-width="2" />
28+
</button>
29+
</span>
30+
</template>
31+
32+
<script setup lang="ts">
33+
import { computed, ref } from 'vue'
34+
import { X } from 'lucide-vue-next'
35+
36+
const props = withDefaults(
37+
defineProps<{
38+
modelValue: string
39+
type?: string
40+
placeholder?: string
41+
disabled?: boolean
42+
clearable?: boolean
43+
spellcheck?: boolean
44+
clearTitle?: string
45+
name?: string
46+
autocomplete?: string
47+
}>(),
48+
{
49+
type: 'text',
50+
clearable: true,
51+
spellcheck: true,
52+
clearTitle: '清除'
53+
}
54+
)
55+
56+
const emit = defineEmits<{
57+
'update:modelValue': [value: string]
58+
keydown: [event: KeyboardEvent]
59+
focus: [event: FocusEvent]
60+
blur: [event: FocusEvent]
61+
}>()
62+
63+
const inputRef = ref<HTMLInputElement | null>(null)
64+
const focused = ref(false)
65+
66+
const showClearButton = computed(() => props.clearable && !props.disabled && Boolean(props.modelValue))
67+
68+
const onInput = (event: Event) => {
69+
emit('update:modelValue', (event.target as HTMLInputElement).value)
70+
}
71+
72+
const onFocus = (event: FocusEvent) => {
73+
focused.value = true
74+
emit('focus', event)
75+
}
76+
77+
const onBlur = (event: FocusEvent) => {
78+
focused.value = false
79+
emit('blur', event)
80+
}
81+
82+
const clear = () => {
83+
if (props.disabled) return
84+
emit('update:modelValue', '')
85+
inputRef.value?.focus()
86+
}
87+
</script>
88+
89+
<style lang="scss" scoped>
90+
.sp-input {
91+
align-items: center;
92+
background: var(--panel);
93+
border: 1px solid var(--line);
94+
border-radius: 6px;
95+
display: inline-flex;
96+
font-size: 13px;
97+
gap: 4px;
98+
padding: 0 6px 0 10px;
99+
transition: border-color 0.15s ease;
100+
width: 100%;
101+
102+
&:hover:not(.disabled) {
103+
border-color: var(--accent);
104+
}
105+
106+
&.focused {
107+
border-color: var(--accent);
108+
outline: none;
109+
}
110+
111+
&.disabled {
112+
cursor: not-allowed;
113+
opacity: 0.6;
114+
}
115+
}
116+
117+
.sp-input-inner {
118+
background: transparent;
119+
border: 0;
120+
color: var(--text);
121+
flex: 1 1 auto;
122+
font: inherit;
123+
min-width: 0;
124+
outline: none;
125+
padding: 7px 0;
126+
127+
&::placeholder {
128+
color: var(--muted);
129+
}
130+
131+
&::-webkit-search-cancel-button,
132+
&::-webkit-search-decoration {
133+
display: none;
134+
-webkit-appearance: none;
135+
}
136+
}
137+
138+
.sp-input-clear {
139+
align-items: center;
140+
background: transparent;
141+
border: 0;
142+
border-radius: 4px;
143+
color: var(--muted);
144+
cursor: pointer;
145+
display: inline-flex;
146+
flex-shrink: 0;
147+
height: 18px;
148+
justify-content: center;
149+
padding: 0;
150+
transition:
151+
background 0.15s ease,
152+
color 0.15s ease;
153+
width: 18px;
154+
155+
&:hover {
156+
background: var(--accent-soft);
157+
color: var(--accent);
158+
}
159+
}
160+
</style>

src/ui/components/Textarea.vue

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<template>
2+
<span class="sp-textarea" :class="{ disabled, focused, clearable: showClearButton }">
3+
<textarea
4+
ref="textareaRef"
5+
class="sp-textarea-inner"
6+
:value="modelValue"
7+
:placeholder="placeholder"
8+
:disabled="disabled"
9+
:rows="rows"
10+
:spellcheck="spellcheck"
11+
:name="name"
12+
@input="onInput"
13+
@keydown="emit('keydown', $event)"
14+
@focus="onFocus"
15+
@blur="onBlur"
16+
/>
17+
<button
18+
v-if="showClearButton"
19+
type="button"
20+
class="sp-textarea-clear"
21+
tabindex="-1"
22+
:title="clearTitle"
23+
:aria-label="clearTitle"
24+
@mousedown.prevent="clear"
25+
>
26+
<X :size="12" :stroke-width="2" />
27+
</button>
28+
</span>
29+
</template>
30+
31+
<script setup lang="ts">
32+
import { computed, ref } from 'vue'
33+
import { X } from 'lucide-vue-next'
34+
35+
const props = withDefaults(
36+
defineProps<{
37+
modelValue: string
38+
placeholder?: string
39+
disabled?: boolean
40+
clearable?: boolean
41+
spellcheck?: boolean
42+
rows?: number | string
43+
clearTitle?: string
44+
name?: string
45+
}>(),
46+
{
47+
clearable: true,
48+
spellcheck: false,
49+
rows: 6,
50+
clearTitle: '清除'
51+
}
52+
)
53+
54+
const emit = defineEmits<{
55+
'update:modelValue': [value: string]
56+
keydown: [event: KeyboardEvent]
57+
focus: [event: FocusEvent]
58+
blur: [event: FocusEvent]
59+
}>()
60+
61+
const textareaRef = ref<HTMLTextAreaElement | null>(null)
62+
const focused = ref(false)
63+
64+
const showClearButton = computed(() => props.clearable && !props.disabled && Boolean(props.modelValue))
65+
66+
const onInput = (event: Event) => {
67+
emit('update:modelValue', (event.target as HTMLTextAreaElement).value)
68+
}
69+
70+
const onFocus = (event: FocusEvent) => {
71+
focused.value = true
72+
emit('focus', event)
73+
}
74+
75+
const onBlur = (event: FocusEvent) => {
76+
focused.value = false
77+
emit('blur', event)
78+
}
79+
80+
const clear = () => {
81+
if (props.disabled) return
82+
emit('update:modelValue', '')
83+
textareaRef.value?.focus()
84+
}
85+
</script>
86+
87+
<style lang="scss" scoped>
88+
.sp-textarea {
89+
background: var(--panel);
90+
border: 1px solid var(--line);
91+
border-radius: 6px;
92+
display: block;
93+
font-size: 12px;
94+
position: relative;
95+
transition: border-color 0.15s ease;
96+
width: 100%;
97+
98+
&:hover:not(.disabled) {
99+
border-color: var(--accent);
100+
}
101+
102+
&.focused {
103+
border-color: var(--accent);
104+
outline: none;
105+
}
106+
107+
&.disabled {
108+
cursor: not-allowed;
109+
opacity: 0.6;
110+
}
111+
}
112+
113+
.sp-textarea-inner {
114+
background: transparent;
115+
border: 0;
116+
color: var(--text);
117+
display: block;
118+
font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace;
119+
font-size: 12px;
120+
line-height: 1.5;
121+
min-height: 60px;
122+
outline: none;
123+
padding: 7px 10px;
124+
resize: vertical;
125+
width: 100%;
126+
127+
&::placeholder {
128+
color: var(--muted);
129+
}
130+
}
131+
132+
.sp-textarea-clear {
133+
align-items: center;
134+
background: var(--panel);
135+
border: 0;
136+
border-radius: 4px;
137+
color: var(--muted);
138+
cursor: pointer;
139+
display: inline-flex;
140+
height: 22px;
141+
justify-content: center;
142+
padding: 0;
143+
position: absolute;
144+
right: 6px;
145+
top: 6px;
146+
transition:
147+
background 0.15s ease,
148+
color 0.15s ease;
149+
width: 22px;
150+
z-index: 1;
151+
152+
&:hover {
153+
background: var(--accent-soft);
154+
color: var(--accent);
155+
}
156+
}
157+
</style>

src/ui/popup/Popup.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
</header>
193193
<div v-if="footerPanel === 'search'" class="footer-panel-body">
194194
<div class="search-row">
195-
<input v-model="search.query" type="search" placeholder="输入关键词或正则表达式" @keydown.enter="searchPageSourceFromPopup" />
195+
<Input v-model="search.query" type="search" placeholder="输入关键词或正则表达式" @keydown="onSearchKeydown" />
196196
<RippleButton @click="searchPageSourceFromPopup">搜索</RippleButton>
197197
</div>
198198
<div class="search-options">
@@ -286,6 +286,7 @@
286286
} from 'lucide-vue-next'
287287
import Select from '@/ui/components/Select.vue'
288288
import Checkbox from '@/ui/components/Checkbox.vue'
289+
import Input from '@/ui/components/Input.vue'
289290
import RippleButton from '@/ui/components/RippleButton.vue'
290291
import { categoryIndex, confidenceClass, confidenceRank } from '@/utils/category-order'
291292
import { applyCustomCss } from '@/utils/apply-custom-css'
@@ -921,6 +922,13 @@
921922
}
922923
}
923924
925+
const onSearchKeydown = (event: KeyboardEvent) => {
926+
if (event.key === 'Enter') {
927+
event.preventDefault()
928+
searchPageSourceFromPopup()
929+
}
930+
}
931+
924932
const searchPageSourceFromPopup = async () => {
925933
const query = search.query
926934
if (!query) {

0 commit comments

Comments
 (0)