uTools-quickcommand/src/pages/ConfigurationPage.vue
2024-12-21 20:25:32 +08:00

869 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="config-page-container">
<div
class="background-layer"
:style="{
background: $q.dark.isActive
? $root.profile.backgroundImgDark
? `url('${$root.profile.backgroundImgDark}')`
: 'none'
: $root.profile.backgroundImgLight
? `url('${$root.profile.backgroundImgLight}')`
: 'none',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundAttachment: 'fixed',
}"
></div>
<!-- 标签栏 -->
<!-- 面板视图不显示标签栏 -->
<q-scroll-area
v-show="commandCardStyle !== 'mini'"
class="absolute-left tag-bar"
:thumb-style="{
width: '2px',
}"
:style="{
width: tabBarWidth,
zIndex: 1,
}"
>
<q-tabs
v-model="currentTag"
vertical
switch-indicator
:key="$root.profile.denseTagBar"
:dense="$root.profile.denseTagBar"
>
<!-- 所有标签 -->
<q-tab
v-for="tag in allQuickCommandTags"
:key="tag"
:name="tag"
:data-search-result="tag === '搜索结果'"
:data-active-panel="activatedQuickPanels.includes(tag)"
>
{{ tag }}
<q-tooltip v-if="tag === '未分类'">
所有没有添加标签的命令都会归在未分类 <br />
可以在新建命令时在标签一栏选择或直接键入标签名来添加标签
</q-tooltip>
</q-tab>
</q-tabs>
</q-scroll-area>
<!-- 面板栏 -->
<q-tab-panels
animated
class="absolute-right"
:style="{
bottom: footerBarHeight,
left: tabBarWidth,
}"
v-model="currentTag"
transition-prev="jump-down"
transition-next="jump-up"
>
<q-tab-panel
style="padding: 0"
v-for="tag in allQuickCommandTags"
:key="tag"
:name="tag"
>
<q-scroll-area
style="height: 100%"
:thumb-style="{
background: 'grey',
width: '6px',
opacity: 0.5,
}"
>
<div class="row center q-pa-xs">
<CommandCard
v-for="commandInfo in currentTagQuickCommands"
:key="commandInfo.features.code"
:commandInfo="commandInfo"
:isCommandActivated="
activatedQuickCommandFeatureCodes.includes(
commandInfo.features.code
)
"
:cardStyle="commandCardStyleSheet[commandCardStyle]"
@commandChanged="commandChanged"
:style="{
width: commandCardStyleSheet[commandCardStyle].width,
}"
class="relative-position q-pa-sm"
></CommandCard>
</div>
</q-scroll-area>
</q-tab-panel>
</q-tab-panels>
<!-- 底栏 -->
<div
class="absolute-bottom footer-bar"
:style="{
height: footerBarHeight,
left: tabBarWidth,
}"
>
<div class="row">
<!-- 搜索栏 -->
<div class="col">
<q-input
v-model="commandSearchKeyword"
debounce="200"
filled
dense
:autofocus="$root.profile.autofocusSearch"
@update:model-value="updateSearch"
placeholder="搜索,支持拼音首字母"
>
<template v-slot:prepend>
<q-icon name="search" />
</template>
</q-input>
</div>
<!-- 按钮组 -->
<div class="col-auto justify-end flex">
<q-btn-group>
<!-- 添加暗色模式切换按钮 -->
<q-btn
flat
v-if="isDev"
:color="$q.dark.isActive ? 'amber' : 'blue-9'"
:icon="$q.dark.isActive ? 'dark_mode' : 'light_mode'"
@click="$q.dark.toggle()"
>
<q-tooltip>
{{ $q.dark.isActive ? "切换到亮色模式" : "切换到暗色模式" }}
</q-tooltip>
</q-btn>
<!-- 切换视图 -->
<q-btn-toggle
v-model="commandCardStyle"
@click="$root.profile.commandCardStyle = commandCardStyle"
toggle-color="primary"
flat
:options="[
{ slot: 'normal', value: 'normal' },
{ slot: 'dense', value: 'dense' },
{ slot: 'mini', value: 'mini' },
]"
>
<template v-slot:normal>
<q-icon name="dashboard" />
<q-tooltip>双列视图</q-tooltip>
</template>
<template v-slot:dense>
<q-icon name="apps" />
<q-tooltip>三列视图</q-tooltip>
</template>
<template v-slot:mini>
<q-icon name="view_comfy" />
<q-tooltip
>五列面板视图<br />
未启用匹配类型为窗口的命令在此视图下不显示</q-tooltip
>
</template>
</q-btn-toggle>
<!-- <q-separator vertical /> -->
<q-btn
split
flat
@click="goShareCenter"
color="primary"
label="分享中心"
icon="groups"
class="share-btn"
/>
<!-- <q-separator vertical /> -->
<!-- 新建按钮 -->
<q-btn
split
flat
@click="addNewCommand"
color="primary"
label="新建"
icon="add"
class="new-btn"
/>
<!-- <q-separator vertical /> -->
<q-btn
stretch
color="primary"
flat
size="xs"
id="menuBtn"
:style="{
height: footerBarHeight,
}"
><q-spinner-bars color="primary" size="1.5em" />
<!-- 菜单 -->
<ConfigurationMenu
:isTagStared="activatedQuickPanels.includes(currentTag)"
:currentTag="currentTag"
>
</ConfigurationMenu>
</q-btn>
</q-btn-group>
</div>
</div>
</div>
<!-- 命令编辑界面 -->
<q-dialog
v-model="isCommandEditorShow"
persistent
maximized
:transition-show="fromNewCommand ? '' : 'slide-up'"
transition-hide="slide-down"
style="overflow: hidden"
>
<CommandEditor
ref="commandEditor"
:action="commandEditorAction"
@editorEvent="editorEvent"
></CommandEditor>
</q-dialog>
<CommandRunResult
:action="{ type: 'config' }"
ref="result"
></CommandRunResult>
</div>
</template>
<script>
import { defineAsyncComponent } from "vue";
import quickcommandParser from "js/common/quickcommandParser.js";
import importAll from "js/common/importAll.js";
import changeLog from "js/options/changeLog.js";
import pinyinMatch from "pinyin-match";
import CommandCard from "components/CommandCard";
import CommandEditor from "components/CommandEditor";
import ConfigurationMenu from "components/menu";
const CommandRunResult = defineAsyncComponent(() =>
import("components/CommandRunResult.vue")
);
// Performance Rendering > 300ms
// 默认命令
let defaultCommands = importAll(require.context("../json/", false, /\.json$/));
export default {
components: {
CommandCard,
CommandEditor,
ConfigurationMenu,
CommandRunResult,
},
data() {
return {
currentTag: "默认",
lastTag: "",
activatedQuickCommandFeatureCodes: [],
activatedQuickPanels: [],
allQuickCommands: {},
allQuickCommandTags: [],
commandSearchKeyword: "",
isCommandEditorShow: false,
commandEditorAction: {},
footerBarHeight: "40px",
commandCardStyle: this.$root.profile.commandCardStyle,
isDev: window.utools.isDev(),
commandCardStyleSheet: {
mini: {
width: "20%",
code: 1,
},
dense: {
width: "33%",
code: 2,
},
normal: {
width: "50%",
code: 3,
},
large: {
width: "100%",
code: 4,
},
},
};
},
computed: {
// 当前标签下的所有快捷命令
currentTagQuickCommands() {
let commands = Object.values(_.cloneDeep(this.allQuickCommands));
switch (this.currentTag) {
case "未分类":
return commands.filter((cmd) => !cmd.tags || cmd.tags.length === 0);
// case "来自分享":
// return commands.filter((cmd) => cmd.fromShare);
case "搜索结果":
if (this.commandSearchKeyword?.length < 2) return;
let searchResult = [];
commands.forEach((cmd) => {
// 拼音搜索
let explain = cmd.features.explain;
let matchedWordPositions = pinyinMatch.match(
explain,
this.commandSearchKeyword
);
if (!matchedWordPositions) return;
let matchedWords = explain.slice(
matchedWordPositions[0],
matchedWordPositions[1] + 1
);
// 高亮
cmd.features.explain = explain.replace(
matchedWords,
`<strong style="color:#ed6237">${matchedWords}</strong>`
);
searchResult.push(cmd);
});
return searchResult;
default:
return commands.filter((cmd) => cmd.tags?.includes(this.currentTag));
}
},
// 标签栏宽度
tabBarWidth() {
return this.commandCardStyle === "mini" ? "0" : "80px";
},
fromNewCommand() {
return this.$route.name === "newcommand";
},
fromImportCommand() {
return this.$route.name === "importcommand";
},
},
mounted() {
this.initPage();
},
methods: {
// 初始化
initPage() {
// newcommand 直接新建命令
if (this.fromNewCommand) this.addNewCommand();
// importcommand 导入命令
else if (this.fromImportCommand)
this.importCommand(this.$root.enterData.payload);
this.$router.push("/configuration");
if (this.$route.params.tags) {
this.changeCurrentTag(window.hexDecode(this.$route.params.tags));
this.commandCardStyle = "mini";
}
this.showChangeLog();
// 异步读取
setTimeout(this.getActivatedFeatures, 0);
setTimeout(this.getAllQuickCommands, 0);
},
// 获取所有已启用的命令的 features 以及面板名称
getActivatedFeatures() {
let features = utools.getFeatures();
let currentFts = [];
let quickpanels = [];
features.forEach((x) =>
x.code.slice(0, 6) == "panel_"
? quickpanels.push(window.hexDecode(x.code.slice(6)))
: currentFts.push(x)
);
this.activatedQuickCommandFeatureCodes = currentFts.map((f) => f.code);
// 已启用的面板
this.activatedQuickPanels = quickpanels;
},
// 获取所有的命令(导出的格式)
getAllQuickCommands() {
this.allQuickCommands = _.cloneDeep(defaultCommands);
this.$root.utools.getAll("qc_").forEach((x) => {
if (x.data.features.code.includes("default_")) return;
this.allQuickCommands[x.data.features.code] = x.data;
});
this.getAllQuickCommandTags();
},
getAllQuickCommandTags() {
this.allQuickCommandTags = _.union(
...Object.values(this.allQuickCommands).map((x) => x.tags)
)
.concat([
"未分类",
// "来自分享"
])
.filter((x) => x);
},
// 监听命令变更事件
commandChanged(event) {
switch (event.type) {
case "remove":
this.removeCommand(event.data);
return;
case "enable":
this.enableCommand(event.data);
return;
case "disable":
this.disableCommand(event.data);
return;
case "edit":
this.editCommand(event.data);
return;
case "run":
this.runCommand(event.data);
return;
default:
return;
}
},
runCommand(code) {
this.$refs.result.runCurrentCommand(
_.cloneDeep(this.allQuickCommands[code])
);
},
// 启用命令
enableCommand(code) {
this.$root.utools.whole.setFeature(
_.cloneDeep(this.allQuickCommands[code].features)
);
this.activatedQuickCommandFeatureCodes.push(code);
},
// 禁用命令
disableCommand(code) {
this.$root.utools.whole.removeFeature(code);
this.activatedQuickCommandFeatureCodes =
this.activatedQuickCommandFeatureCodes.filter((x) => x !== code);
},
// 删除命令
removeCommand(code) {
utools.copyText(JSON.stringify(this.allQuickCommands[code], null, 4));
delete this.allQuickCommands[code];
this.$root.utools.delDB("qc_" + code);
this.removeCommandFromHistory(code);
this.disableCommand(code);
this.getAllQuickCommandTags();
if (!this.allQuickCommandTags.includes(this.currentTag))
this.changeCurrentTag("默认");
quickcommand.showMessageBox(
"删除成功,为防止误操作,已将删除的命令复制到剪贴板",
"success",
1000,
"bottom-right"
);
},
removeCommandFromHistory(code) {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith("editor_history_" + code)) {
localStorage.removeItem(key);
}
}
},
// 编辑命令
editCommand(command) {
// 即可传入 code也可直接传入 command
if (typeof command === "string") command = this.allQuickCommands[command];
this.commandEditorAction = {
type: "edit",
data: _.cloneDeep(command),
};
this.isCommandEditorShow = true;
},
// 是否为默认命令
isDefaultCommand(code) {
return code.slice(0, 8) === "default_";
},
// 导入命令
async importCommand(quickCommandInfo) {
if (!quickCommandInfo)
return quickcommand.showMessageBox("导入未完成!", "warning");
let parsedData = await quickcommandParser(quickCommandInfo);
if (!parsedData) return quickcommand.showMessageBox("格式错误", "error");
// 单个命令导入
let dataToPushed = {};
if (parsedData.single) {
if (this.isDefaultCommand(parsedData.qc.features.code))
return quickcommand.showMessageBox("默认命令不能导入!", "error");
dataToPushed[parsedData.qc.features.code] = parsedData.qc;
// 多个
} else {
dataToPushed = parsedData.qc;
}
for (var code of Object.keys(dataToPushed)) {
if (this.isDefaultCommand(code)) continue;
this.$root.utools.putDB(dataToPushed[code], "qc_" + code);
}
Object.assign(this.allQuickCommands, dataToPushed);
this.getAllQuickCommandTags();
quickcommand.showMessageBox("导入成功!");
this.locateToCommand(parsedData.qc.tags, parsedData.qc.features?.code);
},
// 定位命令, 包含changeCurrentTag
locateToCommand(tags = ["默认"], code) {
this.currentTag = !tags || !tags.length ? "未分类" : tags[0];
if (!code) return;
// 等待 dom 渲染
this.$nextTick(() => {
let el = document.getElementById(code);
el.scrollIntoViewIfNeeded();
el.querySelector(".q-card").style.boxShadow =
"0px 1px var(--q-primary)";
setTimeout(() => {
el.querySelector(".q-card").style.boxShadow = "";
}, 5000);
// 跳转标签
document
.querySelector(".q-tab--active")
.scrollIntoView({ behavior: "smooth" });
});
},
// 修改并定位当前标签事件
changeCurrentTag(tagName) {
this.currentTag = tagName;
this.$nextTick(() => {
document
.querySelector(".q-tab--active")
.scrollIntoView({ behavior: "smooth" });
});
},
// 全部导出
exportAllCommands(saveAsFile = true) {
let options = {
title: "选择保存位置",
defaultPath: "quickCommand",
filters: [
{
name: "json",
extensions: ["json"],
},
],
};
let commandsToExport = _.cloneDeep(this.allQuickCommands);
// 不导出默认命令
Object.keys(commandsToExport).forEach((code) => {
if (this.isDefaultCommand(code)) delete commandsToExport[code];
});
let stringifyCommands = JSON.stringify(commandsToExport);
if (saveAsFile) {
window.saveFile(stringifyCommands, options) &&
quickcommand.showMessageBox("导出成功!");
} else {
utools.copyText(stringifyCommands);
}
},
// 清空
clearAllCommands() {
quickcommand
.showConfirmBox("将会清空所有自定义命令,停用所有实用功能,请确认!")
.then((isConfirmed) => {
if (!isConfirmed)
return quickcommand.showMessageBox("取消操作", "info");
this.exportAllCommands(false);
this.$root.utools.delAll("qc_");
this.clearAllFeatures();
this.allQuickCommands = _.cloneDeep(defaultCommands);
this.getAllQuickCommandTags();
this.changeCurrentTag("默认");
quickcommand.showMessageBox(
"清空完毕,为防止误操作,已将所有命令复制到剪贴板,可通过导入命令恢复",
"success",
2000,
"bottom-right"
);
});
},
// 删除所有 features
clearAllFeatures() {
for (var feature of utools.getFeatures()) {
if (feature.code.slice(0, 8) === "feature_") continue;
this.$root.utools.whole.removeFeature(feature.code);
}
this.activatedQuickCommandFeatureCodes = [];
},
// 搜索
updateSearch() {
// 记录当前标签页
let searchTagName = "搜索结果";
if (this.currentTag !== searchTagName) this.lastTag = this.currentTag;
if (this.commandSearchKeyword?.length > 1) {
if (!this.allQuickCommandTags.includes(searchTagName))
this.allQuickCommandTags.push(searchTagName);
// 搜索时跳转到搜索结果标签
this.changeCurrentTag(searchTagName);
} else {
// 清空搜索回跳到之前标签
if (this.allQuickCommandTags.slice(-1)[0] === searchTagName)
this.allQuickCommandTags.pop();
if (this.currentTag !== this.lastTag)
this.changeCurrentTag(this.lastTag);
}
},
// 新建命令
addNewCommand() {
this.commandEditorAction = {
type: "new",
data: {},
};
this.isCommandEditorShow = true;
},
saveCommand(command) {
let code = command.features.code;
this.allQuickCommands[code] = command;
//无论禁用还是启用都启用
if (!this.activatedQuickCommandFeatureCodes.includes(code))
this.activatedQuickCommandFeatureCodes.push(code);
// 先删除再添加,强制刷新
this.$root.utools.whole.removeFeature(code);
this.$root.utools.whole.setFeature(command.features);
this.getAllQuickCommandTags();
this.locateToCommand(command.tags, code);
},
editorEvent(event) {
switch (event.type) {
case "save":
this.saveCommand(event.data);
// this.isCommandEditorShow = false;
default:
return;
}
},
showChangeLog() {
let lastNeedLogEvent = changeLog[changeLog.length - 1];
let loggedVersion =
this.$root.utools.whole.dbStorage.getItem("cfg_loggedVersion") ||
"0.0.0";
if (loggedVersion < lastNeedLogEvent.version) {
quickcommand.showConfirmBox(
'<pre style="white-space: pre-wrap;word-wrap: break-word;">' +
lastNeedLogEvent.log +
"</pre>",
"更新日志",
true,
700
);
this.$root.utools.whole.dbStorage.setItem(
"cfg_loggedVersion",
lastNeedLogEvent.version
);
}
},
goShareCenter() {
utools.shellOpenExternal("https://qc.qaz.ink/");
},
},
};
</script>
<style scoped>
.config-page-container {
position: relative;
height: 100vh;
width: 100%;
overflow: hidden;
}
.background-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
/* 标签栏容器样式 */
.q-tabs {
height: 100vh !important;
background: transparent !important;
}
/* 标签栏和底栏内的按钮悬浮效果 */
.q-tabs .q-tab:hover,
.absolute-bottom .q-btn:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.body--dark .q-tabs .q-tab:hover,
.body--dark .absolute-bottom .q-btn:hover {
background: rgba(255, 255, 255, 0.05) !important;
}
.q-tab {
word-break: break-all;
white-space: normal;
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--q-blue-grey) !important;
opacity: 0.7;
transition: all 0.3s ease;
}
/* 激活标签样式 */
.q-tab--active {
color: var(--q-primary) !important;
font-weight: 600 !important;
opacity: 1;
}
.q-tab:hover::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
background: var(--q-primary);
opacity: 0.1;
border-radius: 4px;
transition: opacity 0.3s;
}
/* 特殊标签样式(搜索结果和激活面板) */
.q-tab[data-search-result="true"],
.q-tab[data-active-panel="true"] {
color: #4286de !important;
font-weight: 600 !important;
}
/* 标签悬浮效果 */
.q-tab:hover {
opacity: 0.9;
transform: translateX(4px);
}
/* 标签内容过渡效果 */
.q-tab__content {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 面板过渡效果 */
.q-tab-panels {
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
background: none !important;
}
.q-tab-panel {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 暗色模式适配 */
:deep(.body--dark) .q-tab:hover::after {
opacity: 0.15;
}
/* 标签滚动区域样式 */
.q-scroll-area {
background: transparent;
}
:deep(.body--dark) .q-scroll-area {
background: transparent;
}
/* 底栏输入框样式 */
.absolute-bottom .q-field__control {
background: rgba(255, 255, 255, 0.15) !important;
border-radius: 4px;
}
.body--dark .absolute-bottom .q-field__control {
background: rgba(0, 0, 0, 0.2) !important;
}
/* 底栏毛玻璃效果 */
body.glass-effect-menu .absolute-bottom {
background: rgba(
255,
255,
255,
calc(0.15 + var(--glass-effect-strength) * 0.01)
) !important;
backdrop-filter: blur(calc(var(--glass-effect-strength) * 1px)) !important;
-webkit-backdrop-filter: blur(
calc(var(--glass-effect-strength) * 1px)
) !important;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
body.body--dark.glass-effect-menu .absolute-bottom {
background: rgba(
0,
0,
0,
calc(0.2 + var(--glass-effect-strength) * 0.02)
) !important;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
/* 调整内容层级 */
.q-scroll-area,
.q-tab-panels,
.absolute-bottom {
position: absolute;
z-index: 1;
}
/* 底栏过渡动画 */
.footer-bar {
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
will-change: left;
}
/* 底栏输入框样式 */
.absolute-bottom .q-field__control {
background: rgba(255, 255, 255, 0.15) !important;
border-radius: 4px;
}
.body--dark .absolute-bottom .q-field__control {
background: rgba(0, 0, 0, 0.2) !important;
}
/* 标签栏过渡动画 */
.tag-bar {
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
will-change: width, opacity;
opacity: 1;
}
/* 标签栏隐藏时的样式 */
.tag-bar[style*="display: none"] {
opacity: 0;
width: 0 !important;
}
/* 面板过渡效果 */
.q-tab-panels {
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
background: none !important;
}
/* 基础按钮动画 */
.share-btn,
.new-btn {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.share-btn:hover,
.new-btn:hover {
transform: translateY(-2px);
background: rgba(var(--q-primary-rgb), 0.1) !important;
}
/* 搜索框动画 */
@keyframes searchIconShake {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-10deg);
}
75% {
transform: rotate(10deg);
}
}
.q-field--focused .q-field__prepend .q-icon {
animation: searchIconShake 0.8s ease;
color: var(--q-primary);
}
</style>