mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-08 14:34:13 +08:00
320 lines
8.3 KiB
Vue
320 lines
8.3 KiB
Vue
<template>
|
|
<q-card class="quickcommand-ui q-dialog-plugin" @mousewheel="mouseHandler">
|
|
<q-virtual-scroll
|
|
ref="scrollBar"
|
|
@scroll="scrollHandler"
|
|
:style="{ maxHeight: listMaxHeight + 'px', height: '100vh' }"
|
|
:virtual-scroll-slice-size="lazyItemSize"
|
|
:virtual-scroll-item-size="itemHeight"
|
|
:items="matchedItems"
|
|
>
|
|
<template v-slot="{ item, index }">
|
|
<div
|
|
:key="index"
|
|
v-ripple
|
|
@mousemove="currentIndex = index"
|
|
@click="clickOK"
|
|
:class="{
|
|
'item-selected': index === currentIndex,
|
|
item: true,
|
|
}"
|
|
:style="{
|
|
height: itemHeight + 'px',
|
|
paddingRight: shortCutWidth - 16 + 'px',
|
|
cursor: 'pointer',
|
|
}"
|
|
>
|
|
<!-- 纯文本 -->
|
|
<q-item v-if="is.text">
|
|
<q-item-section>
|
|
<q-item-label
|
|
lines="1"
|
|
v-html="highlightText(item)"
|
|
></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<!-- json -->
|
|
<q-item v-else-if="is.json">
|
|
<q-item-section avatar v-if="item.icon">
|
|
<q-avatar size="34px">
|
|
<q-img :src="item.icon" />
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label
|
|
lines="1"
|
|
v-html="highlightText(item.title)"
|
|
></q-item-label>
|
|
<q-item-label
|
|
lines="1"
|
|
caption
|
|
v-html="highlightText(item.description)"
|
|
></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<!-- html -->
|
|
<q-item v-else-if="is.html">
|
|
<q-item-section>
|
|
<div v-html="item" class="full-width"></div>
|
|
</q-item-section>
|
|
</q-item>
|
|
</div>
|
|
</template>
|
|
</q-virtual-scroll>
|
|
<div
|
|
:style="{ top: 0, bottom: 0, width: shortCutWidth + 'px' }"
|
|
class="fixed-right shortcut"
|
|
>
|
|
<div
|
|
:style="{ height: itemHeight + 'px' }"
|
|
class="flex content-center justify-start q-pa-xs"
|
|
v-for="count in countsPerPage"
|
|
:key="count"
|
|
>
|
|
{{ $q.platform.is.mac ? "⌘" : "Alt" }}+{{ count % countsPerPage }}
|
|
</div>
|
|
</div>
|
|
<q-btn
|
|
v-if="options.options.showCancelButton"
|
|
class="close-btn"
|
|
round
|
|
flat
|
|
icon="close"
|
|
@click="hide"
|
|
/>
|
|
</q-card>
|
|
</template>
|
|
|
|
<script>
|
|
import pinyinMatch from "pinyin-match";
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
items: this.options.initItems,
|
|
listMaxHeight: 500,
|
|
currentIndex: 0,
|
|
itemHeight: 50,
|
|
lazyItemSize: 100,
|
|
shortCutWidth: 50,
|
|
searchWords: "",
|
|
topIndex: 0,
|
|
timeoutId: null,
|
|
};
|
|
},
|
|
mounted() {
|
|
this.options.options.enableSearch && this.setSubInput();
|
|
this.addListeners();
|
|
this.setUtoolsHeight(this.itemHeight * this.matchedItemsSize);
|
|
},
|
|
unmounted() {
|
|
this.clear();
|
|
},
|
|
computed: {
|
|
matchedItems() {
|
|
if (!this.searchWords) return this.items;
|
|
|
|
return this.items.filter((x) => {
|
|
if (this.is.json) {
|
|
const titleMatch = pinyinMatch.match(x.title, this.searchWords);
|
|
const descMatch =
|
|
x.description && pinyinMatch.match(x.description, this.searchWords);
|
|
return titleMatch || descMatch;
|
|
} else {
|
|
return pinyinMatch.match(x, this.searchWords);
|
|
}
|
|
});
|
|
},
|
|
matchedItemsSize() {
|
|
return this.matchedItems.length;
|
|
},
|
|
countsPerPage() {
|
|
return this.listMaxHeight / this.itemHeight;
|
|
},
|
|
bottomIndex() {
|
|
return this.topIndex + this.countsPerPage - 1;
|
|
},
|
|
is() {
|
|
return {
|
|
json: this.options.options.optionType === "json",
|
|
html: this.options.options.optionType === "html",
|
|
text: this.options.options.optionType === "plaintext",
|
|
};
|
|
},
|
|
},
|
|
props: {
|
|
options: Object,
|
|
},
|
|
methods: {
|
|
hide() {
|
|
this.clear();
|
|
this.$emit("hide");
|
|
},
|
|
|
|
clickOK() {
|
|
let selected = this.is.json
|
|
? this.matchedItems[this.currentIndex]
|
|
: {
|
|
id: this.currentIndex,
|
|
text: this.matchedItems[this.currentIndex],
|
|
};
|
|
this.is.json && selected.clickFn
|
|
? selected.clickFn(this.matchedItems[this.currentIndex].id)
|
|
: this.$emit("clickOK", selected);
|
|
this.options.options.closeOnSelect && this.hide();
|
|
},
|
|
|
|
keyHandler(e) {
|
|
e.preventDefault();
|
|
if (this.$q.platform.is.mac ? e.metaKey : e.altKey && !isNaN(e.key)) {
|
|
let index = e.key === "0" ? 10 : parseInt(e.key);
|
|
this.currentIndex = this.topIndex + index - 1;
|
|
return this.clickOK();
|
|
}
|
|
switch (e.keyCode) {
|
|
case 38: // 上
|
|
this.currentIndex = Math.max(0, this.currentIndex - 1);
|
|
break;
|
|
case 40: // 下
|
|
this.currentIndex = Math.min(
|
|
this.matchedItemsSize - 1,
|
|
this.currentIndex + 1
|
|
);
|
|
break;
|
|
case 13: // 回车
|
|
return this.clickOK();
|
|
}
|
|
this.$nextTick(() => {
|
|
document.querySelector(".item-selected").scrollIntoViewIfNeeded(0);
|
|
});
|
|
},
|
|
|
|
scrollHandler(e) {
|
|
this.topIndex = parseInt(e.target.scrollTop / this.itemHeight);
|
|
clearTimeout(this.timeoutId);
|
|
this.timeoutId = setTimeout(() => {
|
|
if (this.currentIndex < this.topIndex)
|
|
this.currentIndex = this.topIndex;
|
|
if (this.currentIndex > this.bottomIndex)
|
|
this.currentIndex = this.bottomIndex;
|
|
this.timeoutId = null;
|
|
}, 200);
|
|
},
|
|
|
|
mouseHandler(e) {
|
|
e.preventDefault();
|
|
this.$refs.scrollBar.$el.scrollBy(
|
|
0,
|
|
(e.deltaY / this.itemHeight).toFixed() * this.itemHeight
|
|
);
|
|
},
|
|
|
|
setSubInput() {
|
|
utools.setSubInput(({ text }) => {
|
|
this.searchWords = text;
|
|
if (this.matchedItems.length < this.currentIndex + 1)
|
|
this.currentIndex = 0;
|
|
}, this.options.options.placeholder);
|
|
},
|
|
|
|
setUtoolsHeight(height) {
|
|
utools.setExpendHeight(Math.min(height, this.listMaxHeight));
|
|
},
|
|
|
|
clear() {
|
|
utools.removeSubInput();
|
|
document.removeEventListener("keydown", this.keyHandler, true);
|
|
this.setUtoolsHeight(this.listMaxHeight);
|
|
},
|
|
|
|
addListeners() {
|
|
// 如果之前启用了子输入框,先移除监听
|
|
if (this.$root.subInputEvent)
|
|
document.removeEventListener(...this.$root.subInputEvent);
|
|
document.addEventListener("keydown", this.keyHandler, true);
|
|
},
|
|
|
|
highlightText(text) {
|
|
if (!text || !this.searchWords) return text;
|
|
|
|
const matchInfo = pinyinMatch.match(text, this.searchWords);
|
|
if (!matchInfo) return text;
|
|
|
|
let result = text;
|
|
// 从后往前替换,避免位置变化
|
|
for (let i = matchInfo.length - 2; i >= 0; i -= 2) {
|
|
const start = matchInfo[i];
|
|
const end = matchInfo[i + 1] + 1; // 增加1来包含最后一个字符
|
|
const highlightedText = `<span class="select-list-highlight">${text.slice(
|
|
start,
|
|
end
|
|
)}</span>`;
|
|
result = result.slice(0, start) + highlightedText + result.slice(end);
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.shortcut {
|
|
user-select: none;
|
|
transition: 0;
|
|
}
|
|
|
|
.item {
|
|
transform: scale(1);
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
border-radius: 8px;
|
|
height: 50px;
|
|
}
|
|
|
|
.item-selected {
|
|
transform: scale(0.996);
|
|
background-color: rgba(13, 110, 253, 0.1);
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.q-item .q-avatar {
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.item-selected .q-avatar {
|
|
transform: scale(1.01);
|
|
filter: brightness(1.1);
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.q-card--dark .item-selected {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
}
|
|
|
|
:deep(.select-list-highlight) {
|
|
color: var(--q-primary);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.close-btn {
|
|
position: absolute;
|
|
bottom: 12px;
|
|
right: 12px;
|
|
backdrop-filter: blur(8px);
|
|
background: rgba(255, 255, 255, 0.2);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.close-btn:hover {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.q-card--dark .close-btn {
|
|
background: rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.q-card--dark .close-btn:hover {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
}
|
|
</style>
|