编排新增文件/文件夹操作,获取文件图标

This commit is contained in:
fofolee
2025-01-03 15:31:56 +08:00
parent 75b9fdff80
commit dcaa00823b
15 changed files with 1232 additions and 27 deletions

View File

@@ -238,4 +238,14 @@ export default defineComponent({
.command-composer :deep(.q-checkbox__inner) {
font-size: 24px;
}
/* 暗黑模式下的标签栏背景颜色 */
.body--dark .command-composer :deep(.q-tab),
.body--dark .command-composer :deep(.q-tab-panel) {
background-color: #303133;
}
.body--dark .command-composer :deep(.q-tab--inactive) {
opacity: 2;
}
</style>

View File

@@ -56,7 +56,7 @@
</div>
<q-item-section>
<q-item-label
class="text-weight-medium"
class="text-weight-medium q-px-lg"
v-html="highlightText(command.label)"
/>
</q-item-section>

View File

@@ -415,10 +415,6 @@ export default defineComponent({
}
}
.body--dark .q-tab,
.body--dark .q-tab-panel {
background-color: #303133;
}
/* 确保下拉按钮内容垂直居中 */
.codec-dropdown :deep(.q-btn__content) {

View File

@@ -448,11 +448,6 @@ export default defineComponent({
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.body--dark .q-tab,
.body--dark .q-tab-panel {
background-color: #303133;
}
/* 确保下拉按钮内容垂直居中 */
.codec-dropdown :deep(.q-btn__content) {
min-height: unset;

View File

@@ -0,0 +1,699 @@
<template>
<div class="file-operation-editor">
<!-- 操作类型选择 -->
<div class="tabs-container">
<q-tabs
v-model="operation"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
inline-label
>
<q-tab name="read" no-caps>
<div class="row items-center no-wrap">
<q-icon name="file_open" size="16px" />
<div class="q-ml-xs">读取</div>
</div>
</q-tab>
<q-tab name="write" no-caps>
<div class="row items-center no-wrap">
<q-icon name="save" size="16px" />
<div class="q-ml-xs">写入</div>
</div>
</q-tab>
<q-tab name="list" no-caps>
<div class="row items-center no-wrap">
<q-icon name="folder_open" size="16px" />
<div class="q-ml-xs">列目录</div>
</div>
</q-tab>
<q-tab name="stat" no-caps>
<div class="row items-center no-wrap">
<q-icon name="info" size="16px" />
<div class="q-ml-xs">状态</div>
</div>
</q-tab>
<q-tab name="delete" no-caps>
<div class="row items-center no-wrap">
<q-icon name="delete" size="16px" />
<div class="q-ml-xs">删除</div>
</div>
</q-tab>
<q-tab name="manage" no-caps>
<div class="row items-center no-wrap">
<q-icon name="settings" size="16px" />
<div class="q-ml-xs">管理</div>
</div>
</q-tab>
</q-tabs>
<q-separator />
</div>
<!-- 文件路径输入 -->
<div class="row q-gutter-sm">
<VariableInput
v-model="filePath"
label="文件路径"
:command="{ icon: 'folder' }"
class="col-grow"
@update:model-value="updateConfig"
/>
<q-btn
flat
dense
round
icon="folder_open"
class="self-center"
@click="selectFile"
>
<q-tooltip>选择文件</q-tooltip>
</q-btn>
</div>
<!-- 读取操作配置 -->
<template v-if="operation === 'read'">
<div class="row q-gutter-sm">
<q-select
v-model="encoding"
:options="encodingOptions"
label="编码"
dense
filled
class="col-grow"
emit-value
map-options
/>
<q-select
v-model="readMode"
:options="readModeOptions"
label="读取模式"
dense
filled
class="col-grow"
emit-value
map-options
/>
<q-select
v-model="readFlag"
:options="readFlagOptions"
label="读取标志"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
<VariableInput
v-if="readMode === 'start'"
v-model="start"
label="起始位置"
:command="{ icon: 'first_page', inputType: 'number' }"
class="col-grow"
/>
<VariableInput
v-if="readMode === 'start'"
v-model="length"
label="读取长度"
:command="{ icon: 'last_page', inputType: 'number' }"
class="col-grow"
/>
</div>
</template>
<!-- 写入操作配置 -->
<template v-if="operation === 'write'">
<div class="row q-gutter-sm">
<q-select
v-model="encoding"
:options="encodingOptions"
label="编码"
dense
filled
class="col-grow"
emit-value
map-options
/>
<q-select
v-model="writeMode"
:options="writeModeOptions"
label="写入模式"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
<q-select
v-model="writeFlag"
:options="writeFlagOptions"
label="文件权限"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
</div>
<div class="row q-gutter-sm">
<VariableInput
v-model="content"
label="写入内容"
:command="{ icon: 'edit' }"
class="col-12"
@update:model-value="updateConfig"
/>
</div>
</template>
<!-- 删除操作配置 -->
<template v-if="operation === 'delete'">
<div class="row q-gutter-sm">
<q-select
v-model="targetType"
:options="targetTypeOptions"
label="目标类型"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
<q-checkbox
v-model="recursive"
label="递归删除"
v-if="targetType === 'directory'"
dense
class="col-grow"
/>
<q-checkbox v-model="force" label="强制删除" dense class="col-grow" />
</div>
</template>
<!-- 管理操作配置 -->
<template v-if="operation === 'manage'">
<div class="row q-gutter-sm">
<q-select
v-model="targetType"
:options="targetTypeOptions"
label="目标类型"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
<q-select
v-model="manageOperation"
:options="manageOperationOptions"
label="操作类型"
dense
filled
class="col-grow"
emit-value
map-options
/>
</div>
<!-- 重命名操作 -->
<template v-if="manageOperation === 'rename'">
<div class="row q-gutter-sm">
<VariableInput
v-model="newPath"
label="新路径"
:command="{ icon: 'drive_file_rename_outline' }"
class="col-grow"
@update:model-value="updateConfig"
/>
</div>
</template>
<!-- 修改权限操作 -->
<template v-if="manageOperation === 'chmod'">
<div class="row q-col-gutter-md">
<div class="col-12">
<div class="row q-col-gutter-sm">
<div class="col-4">
<div class="text-caption q-mb-xs">所有者</div>
<q-option-group
v-model="ownerMode"
:options="permissionOptions"
type="checkbox"
inline
dense
@update:model-value="updateMode"
/>
</div>
<div class="col-4">
<div class="text-caption q-mb-xs">用户组</div>
<q-option-group
v-model="groupMode"
:options="permissionOptions"
type="checkbox"
inline
dense
@update:model-value="updateMode"
/>
</div>
<div class="col-4">
<div class="text-caption q-mb-xs">其他用户</div>
<q-option-group
v-model="otherMode"
:options="permissionOptions"
type="checkbox"
inline
dense
@update:model-value="updateMode"
/>
</div>
</div>
<div class="row q-col-gutter-md items-center q-mt-xs">
<div style="font-size: 12px">权限值: {{ mode }}</div>
<q-checkbox
v-model="recursive"
label="递归修改"
v-if="targetType === 'directory'"
dense
class="col-grow"
/>
</div>
</div>
</div>
</template>
<!-- 修改所有者操作 -->
<template v-if="manageOperation === 'chown'">
<div class="row q-col-gutter-sm">
<VariableInput
v-model="uid"
label="用户ID"
:command="{ icon: 'person', inputType: 'number' }"
class="col-grow"
/>
<VariableInput
v-model="gid"
label="组ID"
:command="{ icon: 'group', inputType: 'number' }"
class="col-grow"
/>
<q-checkbox
v-model="recursive"
label="递归修改"
v-if="targetType === 'directory'"
dense
class="col-grow"
/>
</div>
</template>
</template>
<!-- 列目录操作配置 -->
<template v-if="operation === 'list'">
<div class="row q-gutter-sm q-px-xs">
<q-checkbox
v-model="recursive"
label="递归列出子目录"
dense
class="col-grow"
/>
<q-checkbox
v-model="showHidden"
label="显示隐藏文件"
dense
class="col-grow"
/>
</div>
</template>
<!-- 状态操作配置 -->
<template v-if="operation === 'stat'">
<div class="row q-gutter-sm">
<q-select
v-model="targetType"
:options="targetTypeOptions"
label="目标类型"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
<q-select
v-model="statMode"
:options="statModeOptions"
label="检查类型"
dense
filled
class="col-grow"
emit-value
map-options
@update:model-value="updateConfig"
/>
<q-checkbox
v-model="followSymlinks"
label="跟随符号链接"
v-if="statMode === 'status'"
dense
class="col-grow"
/>
</div>
</template>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "../ui/VariableInput.vue";
import { formatJsonVariables } from "js/composer/formatString";
export default defineComponent({
name: "FileOperationEditor",
components: { VariableInput },
data() {
return {
operation: "read",
filePath: "",
encoding: "utf8",
readMode: "all",
writeMode: "w",
content: "",
start: 0,
length: 1024,
recursive: false,
force: false,
showHidden: false,
followSymlinks: false,
manageOperation: "rename",
newPath: "",
mode: "644",
uid: "",
gid: "",
readFlag: "r",
writeFlag: "666",
ownerMode: ["read", "write"],
groupMode: ["read"],
otherMode: ["read"],
permissionOptions: [
{ label: "读取(r)", value: "read" },
{ label: "写入(w)", value: "write" },
{ label: "执行(x)", value: "execute" },
],
encodingOptions: [
{ label: "UTF-8", value: "utf8" },
{ label: "ASCII", value: "ascii" },
{ label: "Base64", value: "base64" },
{ label: "二进制", value: "binary" },
{ label: "十六进制", value: "hex" },
],
readModeOptions: [
{ label: "全部读取", value: "all" },
{ label: "指定位置读取", value: "start" },
],
writeModeOptions: [
{ label: "覆盖写入", value: "w" },
{ label: "追加写入", value: "a" },
{ label: "读写", value: "w+" },
{ label: "追加读写", value: "a+" },
],
manageOperationOptions: [
{ label: "重命名/移动", value: "rename" },
{ label: "修改权限", value: "chmod" },
{ label: "修改所有者", value: "chown" },
],
readFlagOptions: [
{ label: "只读", value: "r" },
{ label: "读写", value: "r+" },
{ label: "同步读取", value: "rs" },
{ label: "同步读写", value: "rs+" },
],
writeFlagOptions: [
{ label: "默认权限", value: "666" },
{ label: "只读", value: "444" },
{ label: "可执行", value: "755" },
{ label: "完全控制", value: "777" },
],
targetType: "",
targetTypeOptions: [
{ label: "文件", value: "file" },
{ label: "文件夹", value: "directory" },
],
statMode: "exists",
statModeOptions: [
{ label: "检查是否存在", value: "exists" },
{ label: "获取详细状态", value: "status" },
],
};
},
created() {
switch (this.operation) {
case "list":
this.targetType = "directory";
break;
case "stat":
case "delete":
case "manage":
this.targetType = "file";
break;
}
},
methods: {
updateConfig() {
let config = {
operation: this.operation,
filePath: this.filePath,
};
// 定义需要处理的变量字段
const variableFields = [
"filePath",
...(this.operation === "read" && this.readMode === "start"
? ["start", "length"]
: []),
...(this.operation === "write" ? ["content"] : []),
...(this.operation === "manage" && this.manageOperation === "rename"
? ["newPath"]
: []),
...(this.operation === "manage" && this.manageOperation === "chmod"
? ["mode"]
: []),
...(this.operation === "manage" && this.manageOperation === "chown"
? ["uid", "gid"]
: []),
];
let code = "";
switch (this.operation) {
case "read":
config = {
...config,
encoding: this.encoding,
readMode: this.readMode,
flag: this.readFlag,
...(this.readMode === "start" && {
start: this.start,
length: this.length,
}),
};
code = `quickcomposer.file.operation(${formatJsonVariables(
config,
variableFields
)})`;
break;
case "write":
config = {
...config,
encoding: this.encoding,
flag: this.writeMode,
mode: this.writeFlag,
content: this.content,
};
code = `quickcomposer.file.operation(${formatJsonVariables(
config,
variableFields
)})`;
break;
case "delete":
config = {
...config,
recursive: this.recursive,
force: this.force,
};
code = `quickcomposer.file.operation(${formatJsonVariables(
config,
variableFields
)})`;
break;
case "manage":
config = {
...config,
manageOperation: this.manageOperation,
...(this.manageOperation === "rename" && { newPath: this.newPath }),
...(this.manageOperation === "chmod" && {
mode: this.mode,
recursive: this.recursive,
}),
...(this.manageOperation === "chown" && {
uid: this.uid,
gid: this.gid,
recursive: this.recursive,
}),
};
code = `quickcomposer.file.operation(${formatJsonVariables(
config,
variableFields
)})`;
break;
case "stat":
config = {
...config,
targetType: this.targetType,
statMode: this.statMode,
...(this.statMode === "status" && {
followSymlinks: this.followSymlinks,
}),
};
code = `quickcomposer.file.operation(${formatJsonVariables(
config,
variableFields
)})`;
break;
}
this.$emit("update:modelValue", code);
},
async selectFile() {
const result = window.utools.showOpenDialog({
title: "选择文件",
properties: [
this.operation === "list" ||
(["delete", "manage", "stat"].includes(this.operation) &&
this.targetType === "directory")
? "openDirectory"
: "openFile",
"showHiddenFiles",
],
buttonLabel: "选择",
});
console.log(result);
if (result && result[0]) {
this.filePath = result[0];
this.updateConfig();
}
},
updateMode() {
const modeMap = {
read: 4,
write: 2,
execute: 1,
};
const calculateMode = (perms) => {
return perms.reduce((sum, perm) => sum + modeMap[perm], 0);
};
const ownerValue = calculateMode(this.ownerMode);
const groupValue = calculateMode(this.groupMode);
const otherValue = calculateMode(this.otherMode);
this.mode = `${ownerValue}${groupValue}${otherValue}`;
this.updateConfig();
},
},
watch: {
operation() {
this.updateConfig();
},
encoding() {
this.updateConfig();
},
readMode() {
this.updateConfig();
},
writeMode() {
this.updateConfig();
},
recursive() {
this.updateConfig();
},
force() {
this.updateConfig();
},
manageOperation() {
this.updateConfig();
},
start() {
this.updateConfig();
},
length() {
this.updateConfig();
},
mode() {
this.updateConfig();
},
uid() {
this.updateConfig();
},
gid() {
this.updateConfig();
},
targetType: {
handler(newType) {
this.filePath = "";
this.updateConfig();
},
},
},
});
</script>
<style scoped>
.file-operation-editor {
display: flex;
flex-direction: column;
gap: 8px;
}
.tabs-container {
position: relative;
}
.tabs-container .q-tabs {
min-height: 32px;
}
.tabs-container .q-tab {
min-height: 32px;
padding: 0 12px;
}
.col-grow {
flex: 1 1 0;
min-width: 150px;
}
@media (max-width: 600px) {
.col-grow {
flex: 1 1 calc(50% - 8px);
max-width: none;
}
}
</style>

View File

@@ -467,9 +467,3 @@ export default defineComponent({
});
</script>
<style scoped>
.body--dark .q-tab,
.body--dark .q-tab-panel {
background-color: #303133;
}
</style>

View File

@@ -131,11 +131,6 @@ export default defineComponent({
flex-direction: column;
}
/* 暗色模式 */
.body--dark .q-tab,
.body--dark .q-tab-panel {
background-color: #303133;
}
/* 调整面板内边距和布局 */
.ubrowser-panels :deep(.q-tab-panel) {

View File

@@ -49,3 +49,8 @@ export const SymmetricCryptoEditor = defineAsyncComponent(() =>
export const AsymmetricCryptoEditor = defineAsyncComponent(() =>
import("components/composer/crypto/AsymmetricCryptoEditor.vue")
);
// File Components
export const FileOperationEditor = defineAsyncComponent(() =>
import("components/composer/file/FileOperationEditor.vue")
);

View File

@@ -4,8 +4,15 @@ export const fileCommands = {
defaultOpened: true,
commands: [
{
value: "open",
label: "打开文件/文件夹/软件",
value: "quickcomposer.file.operation",
label: "文件/文件夹操作",
component: "FileOperationEditor",
desc: "文件和文件夹的读写、删除、重命名等操作",
isAsync: true,
},
{
value: "utools.shellOpenItem",
label: "默认程序打开",
config: [
{
key: "path",
@@ -17,8 +24,8 @@ export const fileCommands = {
],
},
{
value: "locate",
label: "文件管理器中定位文件",
value: "utools.shellShowItemInFolder",
label: "文件管理器中显示",
config: [
{
key: "path",
@@ -29,5 +36,18 @@ export const fileCommands = {
},
],
},
{
value: "utools.getFileIcon",
label: "获取文件图标",
config: [
{
key: "path",
label: "文件或软件的绝对路径",
type: "input",
defaultValue: "",
icon: "folder_open",
},
],
},
],
};

View File

@@ -5,7 +5,7 @@ export const networkCommands = {
commands: [
{
value: "visit",
label: "默认浏览器打开网址",
label: "默认浏览器打开网址",
config: [
{
key: "url",
@@ -18,7 +18,7 @@ export const networkCommands = {
},
{
value: "utools.ubrowser.goto",
label: "ubrowser打开网址",
label: "ubrowser打开网址",
config: [
{
key: "url",

View File

@@ -0,0 +1,133 @@
/**
* Custom Component Creation Guide
* 自定义组件创建指南
*/
const customComponentGuide = {
description: "创建自定义命令组件的完整流程",
steps: {
"1. Backend Interface": {
location: "plugin/lib/quickcomposer/xxx/yyy.js",
description: "创建具体功能实现",
requirements: {
functionDefinition: "使用独立函数而非对象方法",
asyncHandling: "使用 async/await 处理异步操作",
errorHandling: "合理的错误捕获和提示",
paramValidation: "检查必要参数是否存在",
},
},
"2. Interface Export": {
location: "plugin/lib/quickcomposer/xxx/index.js",
description: "导出接口给quickcomposer使用",
examples: {
singleFunction: "module.exports = { operation }",
multipleFunctions: "module.exports = { ...encoder, ...hash }",
},
},
"3. Interface Registration": {
location: "plugin/lib/quickcomposer.js",
description: "将接口注册到quickcomposer对象",
format: "quickcomposer.xxx = require('./quickcomposer/xxx')",
},
"4. Component Development": {
location: "src/components/composer/xxx/YourComponent.vue",
basicStructure: {
template: "组件模板使用quasar组件库",
script: "组件逻辑使用Vue3 defineComponent",
style: "组件样式建议使用scoped",
},
keyPoints: {
variableInput: {
scenarios: [
"需要支持变量输入的文本框",
"数字输入框设置inputType='number'",
"需要自动处理引号的输入",
],
props: {
vModel: "双向绑定值",
command: "配置图标和输入类型",
label: "输入框标签",
},
events: {
description: "需要监听的事件",
list: ["@update:model-value='updateConfig' - 监听值变化并更新代码"],
},
},
selectInput: {
description: "选择框组件",
component: "q-select",
props: {
"v-model": "双向绑定值",
options: "选项列表",
label: "标签文本",
"emit-value": "true - 使用选项的value作为值",
"map-options": "true - 启用选项映射",
},
events: {
"@update:model-value": "必须监听此事件以触发代码更新",
},
tips: "所有影响代码生成的输入组件都必须在值变化时触发updateConfig",
},
codeGeneration: {
tool: "使用formatJsonVariables处理变量",
params: {
config: "完整的配置对象",
variableFields: "需要处理的字段列表",
},
formats: {
objectParams: {
description: "当参数是对象时的处理方式",
example:
"`quickcomposer.xxx.yyy(${formatJsonVariables(config, variableFields)})`",
reference: "参考 AxiosConfigEditor.vue",
},
simpleParams: {
description: "当参数是简单值时的处理方式",
example: "`${functionName}(${args.join(',')})`",
reference: "参考 MultiParams.vue",
},
},
},
},
},
"5. Component Registration": {
location: "src/js/composer/cardComponents.js",
description: "使用defineAsyncComponent注册组件",
format:
"export const YourComponent = defineAsyncComponent(() => import('path/to/component'))",
},
"6. Command Configuration": {
location: "src/js/composer/commands/xxxCommands.js",
requiredFields: {
value: "quickcomposer.xxx.yyy",
label: "显示名称",
component: "组件名称",
},
optionalFields: {
desc: "命令描述",
isAsync: "是否异步命令",
isControlFlow: "是否控制流命令",
allowEmptyArgv: "是否允许空参数",
},
},
},
notes: {
variableHandling: {
description: "VariableInput 值的处理方式取决于参数类型",
cases: {
objectCase:
"当值在对象中时,使用 formatJsonVariables 处理,如 AxiosConfigEditor",
simpleCase: "当值是直接参数时,直接使用值本身,如 MultiParams",
},
tips: "formatJsonVariables 主要用于处理对象中的变量,避免对简单参数使用,以免产生不必要的引号",
},
asyncCommand: "后端使用异步函数时命令配置需要设置isAsync: true",
componentStructure: "参考现有组件的实现方式,保持一致的代码风格",
errorHandling: "前后端都需要适当的错误处理和提示",
typeChecking: "确保所有参数都有适当的类型检查",
},
examples: {
simpleComponent: "RegexEditor - 单一功能的组件",
complexComponent: "AxiosConfigEditor - 多功能、多配置的组件",
controlComponent: "ConditionalJudgment - 流程控制组件",
},
};