编排添加正则提取/替换

This commit is contained in:
fofolee 2024-12-31 15:32:31 +08:00
parent c163bdd9d6
commit b532630ab9
15 changed files with 1862 additions and 58 deletions

View File

@ -103,10 +103,14 @@ const textProcessing = {
substring: function (text, start, end) {
return text.substring(start, end);
},
// 正则提取
regexExtract: function (text, regex) {
const match = text.match(regex);
return match ? match[0] : "";
// 正则处理
regexTransform: function (text, regex, replace) {
try {
if (replace === undefined) return text.match(regex);
return text.replace(regex, replace);
} catch (e) {
throw "正则表达式格式错误";
}
},
// 非对称加解密
asymmetricCrypto: function (config) {

View File

@ -188,13 +188,12 @@ export default defineComponent({
/* 输入框标签字体大小,占位时的位置 */
.command-composer :deep(.q-field--filled .q-field__label) {
font-size: 11px;
top: 50%;
transform: translateY(-50%);
top: 11px;
}
/* 输入框标签悬浮的位置 */
.command-composer :deep(.q-field--filled.q-field--float .q-field__label) {
transform: translateY(-90%) scale(0.7);
.command-composer :deep(.q-field--filled .q-field--float .q_field__label) {
transform: translateY(-35%) scale(0.7);
}
/* 去除filled输入框边框 */

View File

@ -122,6 +122,7 @@ import AxiosConfigEditor from "components/composer/http/AxiosConfigEditor.vue";
import SymmetricCryptoEditor from "components/composer/crypto/SymmetricCryptoEditor.vue";
import AsymmetricCryptoEditor from "components/composer/crypto/AsymmetricCryptoEditor.vue";
import FunctionSelector from "components/composer/ui/FunctionSelector.vue";
import RegexEditor from "components/composer/regex/RegexEditor.vue";
import { validateVariableName } from "js/common/variableValidator";
export default defineComponent({
@ -135,6 +136,7 @@ export default defineComponent({
SymmetricCryptoEditor,
AsymmetricCryptoEditor,
FunctionSelector,
RegexEditor,
},
props: {
command: {
@ -321,6 +323,7 @@ export default defineComponent({
.command-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
/* 拖拽和放置样式 */
@ -424,34 +427,31 @@ export default defineComponent({
}
/* 暗色模式适配 */
.body--dark {
.command-item {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.body--dark .command-item {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.command-item:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.body--dark .command-item:hover {
box-shadow: 0 4px 8px rgba(58, 58, 58, 0.3);
}
.can-drop {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.body--dark .can-drop {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.output-section :deep(.q-field) {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .output-section :deep(.q-field) {
background: rgba(255, 255, 255, 0.03);
}
.output-section :deep(.q-field--focused) {
background: #1d1d1d;
}
.body--dark .output-section :deep(.q-field--focused) {
background: #1d1d1d;
}
.output-btn {
border-color: rgba(255, 255, 255, 0.1);
}
.body--dark .output-btn {
border-color: rgba(255, 255, 255, 0.1);
}
.output-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
.body--dark .output-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
</style>

View File

@ -198,9 +198,14 @@ export default defineComponent({
],
};
},
props: {
command: {
type: Object,
},
},
methods: {
updateConfig() {
const code = `quickcomposer.textProcessing.asymmetricCrypto(${formatJsonVariables(
const code = `${this.command.value}(${formatJsonVariables(
{
text: this.text,
algorithm: this.algorithm,

View File

@ -164,6 +164,9 @@ export default defineComponent({
type: [String, Object],
default: "",
},
command: {
type: Object,
},
},
data() {
return {
@ -243,7 +246,7 @@ export default defineComponent({
},
methods: {
updateConfig() {
const code = `quickcomposer.textProcessing.symmetricCrypto(${formatJsonVariables(
const code = `${this.command.value}(${formatJsonVariables(
{
text: this.text,
algorithm: this.algorithm,

View File

@ -240,6 +240,9 @@ export default defineComponent({
type: [Object, String],
default: () => ({}),
},
command: {
type: Object,
},
},
emits: ["update:modelValue"],
data() {
@ -316,9 +319,9 @@ export default defineComponent({
? `, ${formatJsonVariables(restConfig, null, excludeFields)}`
: "";
const code = `axios.${method.toLowerCase()}(${url}${
const code = `${this.command.value}(${url}${
this.hasRequestData ? `, ${formatJsonVariables(data)}` : ""
}${configStr})?.data`;
}${configStr})`;
this.$emit("update:modelValue", code);
},
@ -344,7 +347,7 @@ export default defineComponent({
deep: true,
handler(newValue) {
if (typeof newValue === "string") {
//
//
try {
const config = JSON.parse(newValue);
this.localConfig = {

View File

@ -0,0 +1,520 @@
<template>
<div class="regex-builder">
<!-- 基础字符类和量词 -->
<div class="row q-col-gutter-sm">
<!-- 字符类 -->
<div class="col-6">
<div class="pattern-section">
<div class="section-title q-mb-xs">字符类</div>
<div class="patterns-grid">
<q-btn
v-for="pattern in characterClasses"
:key="pattern.value"
flat
no-caps
dense
class="pattern-btn"
@click="insertPattern(pattern.value)"
>
<q-tooltip class="pattern-tooltip">
<div
v-for="(line, index) in pattern.desc.split('\n')"
:key="index"
>
{{ line }}
</div>
</q-tooltip>
{{ pattern.label }}
</q-btn>
</div>
</div>
</div>
<!-- 量词 -->
<div class="col-6">
<div class="pattern-section">
<div class="section-title q-mb-xs">量词</div>
<div class="patterns-grid">
<q-btn
v-for="pattern in quantifiers"
:key="pattern.value"
flat
no-caps
dense
class="pattern-btn"
@click="insertPattern(pattern.value)"
>
<q-tooltip class="pattern-tooltip">
<div
v-for="(line, index) in pattern.desc.split('\n')"
:key="index"
>
{{ line }}
</div>
</q-tooltip>
{{ pattern.label }}
</q-btn>
</div>
</div>
</div>
<!-- 锚点和断言 -->
<div class="col-6">
<div class="pattern-section">
<div class="section-title q-mb-xs">锚点和断言</div>
<div class="patterns-grid">
<q-btn
v-for="pattern in anchors"
:key="pattern.value"
flat
no-caps
dense
class="pattern-btn"
@click="insertPattern(pattern.value)"
>
<q-tooltip class="pattern-tooltip">
<div
v-for="(line, index) in pattern.desc.split('\n')"
:key="index"
>
{{ line }}
</div>
</q-tooltip>
{{ pattern.label }}
</q-btn>
</div>
</div>
</div>
<!-- 分组和引用 -->
<div class="col-6">
<div class="pattern-section">
<div class="section-title q-mb-xs">分组和引用</div>
<div class="patterns-grid">
<q-btn
v-for="pattern in groups"
:key="pattern.value"
flat
no-caps
dense
class="pattern-btn"
@click="insertPattern(pattern.value)"
>
<q-tooltip class="pattern-tooltip">
<div
v-for="(line, index) in pattern.desc.split('\n')"
:key="index"
>
{{ line }}
</div>
</q-tooltip>
{{ pattern.label }}
</q-btn>
</div>
</div>
</div>
<!-- 常用模式 -->
<div class="col-12">
<div class="pattern-section">
<div class="section-title q-mb-xs">常用模式</div>
<div class="patterns-grid">
<q-btn
v-for="pattern in commonPatterns"
:key="pattern.value"
flat
no-caps
dense
class="pattern-btn"
@click="insertPattern(pattern.value)"
>
<q-tooltip class="pattern-tooltip">
<div
v-for="(line, index) in pattern.desc.split('\n')"
:key="index"
>
{{ line }}
</div>
</q-tooltip>
{{ pattern.label }}
</q-btn>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "RegexBuilder",
props: {
selection: {
type: Object,
default: () => ({ start: 0, end: 0 }),
},
},
emits: ["insert"],
data() {
return {
commonPatterns: [
{
label: "字母",
value: "[a-zA-Z]+",
desc: "匹配一个或多个字母\n示例[a-zA-Z]+ 匹配'Hello123'中的'Hello'",
},
{
label: "字母数字",
value: "[a-zA-Z0-9]+",
desc: "匹配一个或多个字母或数字\n示例[a-zA-Z0-9]+ 匹配'Test_123!'中的'Test'和'123'",
},
{
label: "中文",
value: "[\\u4e00-\\u9fa5]+",
desc: "匹配一个或多个中文字符\n示例[\\u4e00-\\u9fa5]+ 匹配'你好hello世界'中的'你好'和'世界'",
},
{
label: "非中文",
value: "[^\\u4e00-\\u9fa5]+",
desc: "匹配一个或多个非中文字符\n示例[^\\u4e00-\\u9fa5]+ 匹配'Hello世界'中的'Hello'",
},
{
label: "键盘字符",
value: "[\\x20-\\x7e]+",
desc: "匹配一个或多个可打印的键盘字符(包含空格)\n示例[\\x20-\\x7e]+ 匹配'Hello 世界@123'中的'Hello '和'@123'",
},
{
label: "真·任意字符",
value: "[\\s\\S]*",
desc: "匹配包含换行的任意字符\n示例[\\s\\S]* 匹配整个文本,包括换行符",
},
{
label: "强口令",
value: "^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$",
desc: "至少8位包含大小写字母和数字\n示例Test123456 匹配test123不匹配",
},
{
label: "IP地址",
value: "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}",
desc: "匹配IP地址格式\n示例192.168.1.1 匹配256.1.2.3 不匹配",
},
{
label: "手机号",
value: "1[3-9]\\d{9}",
desc: "匹配中国大陆手机号\n示例13812345678 匹配12345678901 不匹配",
},
{
label: "邮箱",
value: "[\\w.-]+@[\\w.-]+\\.\\w+",
desc: "匹配电子邮箱地址\n示例test@example.com 匹配,@test.com 不匹配",
},
{
label: "域名",
value:
"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]",
desc: "匹配域名格式\n示例example.com 匹配example..com 不匹配",
},
{
label: "网址",
value: "https?://[\\w.-]+\\.\\w+",
desc: "匹配网址\n示例https://example.com 匹配ftp://test.com 不匹配",
},
{
label: "日期",
value: "\\d{4}-\\d{2}-\\d{2}",
desc: "匹配日期格式 YYYY-MM-DD\n示例2024-03-15 匹配2024/03/15 不匹配",
},
{
label: "时间",
value: "(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d",
desc: "匹配时间格式 HH:MM:SS\n示例23:59:59 匹配24:00:00 不匹配",
},
{
label: "身份证",
value: "\\d{17}[\\dXx]",
desc: "匹配18位身份证号\n示例110101199001011234 匹配1234567890 不匹配",
},
{
label: "MAC地址",
value: "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})",
desc: "匹配MAC地址(xx:xx:xx:xx:xx:xx格式)\n示例00:1B:44:11:3A:B7 匹配00-1B-44-11-3A-B7 也匹配",
},
{
label: "车牌号",
value:
"[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]",
desc: "匹配中国车牌号\n示例京A12345 匹配京123456 不匹配",
},
{
label: "IPv6",
value: "([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}",
desc: "匹配IPv6地址\n示例2001:0db8:85a3:0000:0000:8a2e:0370:7334 匹配",
},
],
characterClasses: [
{
label: "数字",
value: "\\d",
desc: "匹配任意数字字符 (0-9)\n示例\\d 匹配'5'\\d+ 匹配'123'",
},
{
label: "非数字",
value: "\\D",
desc: "匹配任意非数字字符\n示例\\D 匹配'a'、'@'等非数字字符",
},
{
label: "单词字符",
value: "\\w",
desc: "匹配单个英文字母、数字或下划线字符\n示例\\w 匹配'a'、'1'、'_'\\w+ 匹配'abc123'",
},
{
label: "非单词字符",
value: "\\W",
desc: "匹配除英文字母、数字和下划线以外的字符\n示例\\W 匹配'@'、'#'、空格等特殊字符",
},
{
label: "空白",
value: "\\s",
desc: "匹配任意空白字符(包括空格、制表符、换行符等)\n示例\\s+ 匹配连续的空白字符",
},
{
label: "非空白",
value: "\\S",
desc: "匹配非空白字符\n示例\\S+ 匹配一串连续的非空白字符",
},
{
label: "任意字符",
value: ".",
desc: "匹配除换行符外的任意字符\n示例.+ 匹配除换行外的所有字符a.c 匹配'abc'、'adc'等",
},
{
label: "字符集",
value: "[]",
desc: "匹配字符集中的任意一个字符\n示例[aeiou] 匹配任意一个元音字母,[0-9a-f] 匹配一个十六进制数字",
},
{
label: "排除字符",
value: "[^]",
desc: "匹配不在字符集中的任意字符\n示例[^0-9] 匹配任意非数字字符,[^aeiou] 匹配任意非元音字母",
},
{
label: "范围",
value: "[a-z]",
desc: "匹配指定范围内的任意字符\n示例[a-z] 匹配小写字母,[A-Z0-9] 匹配大写字母和数字",
},
{
label: "Unicode",
value: "\\u",
desc: "匹配Unicode字符\n示例\\u4e00-\\u9fa5 匹配中文字符,\\u0041 匹配'A'",
},
],
quantifiers: [
{
label: "零或一",
value: "?",
desc: "匹配零次或一次(可选项)\n示例colou?r 匹配'color'或'colour'",
},
{
label: "零或多",
value: "*",
desc: "匹配零次或多次(任意次数)\n示例ab* 匹配'a'、'ab'、'abb'等",
},
{
label: "一或多",
value: "+",
desc: "匹配一次或多次(至少一次)\n示例ab+ 匹配'ab'、'abb'等,但不匹配'a'",
},
{
label: "非贪婪",
value: "?",
desc: "在量词后添加?使其变为非贪婪模式(最小匹配)\n示例a.*?b 匹配'acb'中的'acb',而不是'acb...b'中的全部",
},
{
label: "恰好n次",
value: "{n}",
desc: "匹配恰好n次\n示例a{3} 只匹配恰好3个连续的'a'",
},
{
label: "至少n次",
value: "{n,}",
desc: "匹配至少n次\n示例a{2,} 匹配2个或更多连续的'a'",
},
{
label: "n到m次",
value: "{n,m}",
desc: "匹配n到m次\n示例a{2,4} 匹配2到4个连续的'a'",
},
],
anchors: [
{
label: "行首",
value: "^",
desc: "匹配行的开始位置\n示例^abc 只匹配以'abc'开头的行",
},
{
label: "行尾",
value: "$",
desc: "匹配行的结束位置\n示例abc$ 只匹配以'abc'结尾的行",
},
{
label: "单词边界",
value: "\\b",
desc: "匹配单词的边界\n示例\\bword\\b 只匹配独立的'word',不匹配'password'",
},
{
label: "非单词边界",
value: "\\B",
desc: "匹配非单词边界\n示例\\Bword\\B 匹配'keyword'中的'word',但不匹配独立的'word'",
},
{
label: "断言(?=)",
value: "(?=)",
desc: "正向先行断言,匹配后面是指定内容的位置\n示例foo(?=bar) 匹配后面是'bar'的'foo'",
},
{
label: "断言(?!)",
value: "(?!)",
desc: "负向先行断言,匹配后面不是指定内容的位置\n示例foo(?!bar) 匹配后面不是'bar'的'foo'",
},
{
label: "断言(?<=)",
value: "(?<=)",
desc: "正向后行断言,匹配前面是指定内容的位置\n示例(?<=foo)bar 匹配前面是'foo'的'bar'",
},
{
label: "断言(?<!)",
value: "(?<!)",
desc: "负向后行断言,匹配前面不是指定内容的位置\n示例(?<!foo)bar 匹配前面不是'foo'的'bar'",
},
],
groups: [
{
label: "捕获组",
value: "()",
desc: "创建一个捕获组,可以被后面引用\n示例(\\w+)\\s+\\1 匹配重复的单词,如'hello hello'",
},
{
label: "非捕获组",
value: "(?:)",
desc: "创建一个非捕获组,仅分组不捕获\n示例(?:ab|cd)+ 匹配'ababcd'等,但不保存匹配结果",
},
{
label: "命名组",
value: "(?<name>)",
desc: "创建一个命名捕获组\n示例(?<year>\\d{4})-(?<month>\\d{2}) 可通过名称引用匹配组",
},
{
label: "或",
value: "|",
desc: "匹配多个模式之一(或关系)\n示例cat|dog 匹配'cat'或'dog'",
},
{
label: "反向引用",
value: "\\1",
desc: "引用第一个捕获组的内容\n示例(\\w+)=\\1 匹配'foo=foo'这样的重复模式",
},
{
label: "命名引用",
value: "\\k<name>",
desc: "引用命名捕获组的内容\n示例(?<tag>\\w+)<\\k<tag>> 匹配HTML标签对",
},
{
label: "条件组",
value: "(?(1)yes|no)",
desc: "根据捕获组是否匹配选择不同的模式\n示例(\\d)?[a-z](?(1)\\d|[a-z]) 根据是否有数字选择不同的匹配",
},
],
};
},
methods: {
insertPattern(pattern) {
this.$emit("insert", pattern);
},
},
});
</script>
<style scoped>
.pattern-section {
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
padding: 6px;
height: 100%;
}
.section-title {
font-size: 12px;
font-weight: 500;
color: var(--q-primary);
opacity: 0.8;
padding-left: 4px;
}
.patterns-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.pattern-btn {
font-size: 12px;
height: 24px;
background: white;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
transition: all 0.3s ease;
min-height: 24px;
padding: 0 4px;
}
.pattern-btn:hover {
background: rgba(var(--q-primary-rgb), 0.05);
border-color: rgba(var(--q-primary-rgb), 0.2);
transform: translateY(-1px);
}
/* 暗色模式适配 */
.body--dark .pattern-section {
background: rgba(255, 255, 255, 0.03);
}
.body--dark .pattern-btn {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
}
.body--dark .pattern-btn:hover {
background: rgba(var(--q-primary-rgb), 0.15);
border-color: rgba(var(--q-primary-rgb), 0.3);
}
/* Tooltip 样式优化 */
:deep(.q-tooltip) {
font-size: 12px;
padding: 4px 8px;
}
/* 常用模式使用四列布局 */
.col-12 .patterns-grid {
grid-template-columns: repeat(6, 1fr);
}
/* Tooltip 样式优化 */
.regex-builder :deep(.pattern-tooltip) {
max-width: 300px;
padding: 8px 12px;
white-space: pre-line;
}
.regex-builder :deep(.pattern-tooltip div) {
line-height: 1.4;
}
.regex-builder :deep(.pattern-tooltip div + div) {
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
</style>

View File

@ -0,0 +1,273 @@
<template>
<div class="regex-editor">
<div class="row q-col-gutter-xs">
<div :class="mode === 'replace' ? 'col-6' : 'col-12'">
<div class="row items-center q-col-gutter-xs">
<div class="col-auto">
<q-btn
flat
dense
color="grey"
:icon="mode === 'extract' ? 'content_cut' : 'find_replace'"
@click="mode = mode === 'extract' ? 'replace' : 'extract'"
>
<q-tooltip>{{
mode === "extract" ? "提取模式" : "替换模式"
}}</q-tooltip>
</q-btn>
</div>
<div class="col">
<!-- 输入文本区域 -->
<VariableInput
v-model="textValue"
label="要处理的文本"
:command="command"
@update:model-value="updateValue"
/>
</div>
</div>
</div>
<div class="col-6" v-if="mode === 'replace'">
<!-- 替换文本区域 -->
<VariableInput
v-model="replaceValue"
label="替换为"
:command="command"
@update:model-value="updateValue"
/>
</div>
</div>
<!-- 正则表达式编辑器 -->
<div class="regex-input-section">
<div class="row items-center q-col-gutter-xs">
<div class="col-auto">
<q-btn
flat
dense
icon="auto_fix_high"
@click="showBuilder = !showBuilder"
:color="showBuilder ? 'primary' : 'grey'"
>
<q-tooltip>正则表达式构建工具</q-tooltip>
</q-btn>
</div>
<div class="col">
<RegexInput
v-model="regexValue"
@update:model-value="updateValue"
:flags="flags"
@update:flags="updateFlags"
ref="regexInput"
/>
</div>
</div>
</div>
<!-- 测试预览 -->
<RegexTester
:text="textValue"
:regex="regexValue"
:flags="flags"
:replace="replaceValue"
/>
<!-- 正则表达式构建工具 -->
<transition
name="builder"
@enter="el => el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })"
>
<div v-if="showBuilder" class="builder-container">
<RegexBuilder :selection="selection" @insert="insertPattern" />
</div>
</transition>
</div>
</template>
<script>
import { defineComponent } from "vue";
import VariableInput from "../ui/VariableInput.vue";
import RegexInput from "./RegexInput.vue";
import RegexBuilder from "./RegexBuilder.vue";
import RegexTester from "./RegexTester.vue";
export default defineComponent({
name: "RegexEditor",
components: {
VariableInput,
RegexInput,
RegexBuilder,
RegexTester,
},
props: {
modelValue: {
type: String,
default: "",
},
command: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
data() {
return {
textValue: "",
regexValue: "",
replaceValue: "",
mode: "extract",
showBuilder: false,
flags: {
ignoreCase: false,
multiline: false,
global: true,
},
selection: {
start: 0,
end: 0,
},
};
},
methods: {
updateValue() {
const flagStr = Object.entries(this.flags)
.filter(([_, value]) => value)
.map(([key]) => key.charAt(0))
.join("");
const output = [this.textValue, `/${this.regexValue}/${flagStr}`];
if (this.mode === "replace") {
output.push(this.replaceValue || '""');
}
this.$emit(
"update:modelValue",
`${this.command.value}(${output.join(",")})`
);
},
updateFlags(newFlags) {
this.flags = newFlags;
this.updateValue();
},
insertPattern(pattern) {
// RegexInput
const regexInput = this.$refs.regexInput;
if (regexInput) {
//
const editor = regexInput.$refs.editor;
editor.focus();
//
const selection = window.getSelection();
const range = selection.getRangeAt(0);
//
if (!editor.contains(range.startContainer)) {
//
range.selectNodeContents(editor);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
//
const start = range.startOffset;
const content = editor.textContent;
//
const newContent =
content.slice(0, start) + pattern + content.slice(start);
editor.textContent = newContent;
this.regexValue = newContent;
//
this.$nextTick(() => {
const newRange = document.createRange();
const textNode = editor.firstChild || editor;
const newPosition = start + pattern.length;
try {
newRange.setStart(textNode, newPosition);
newRange.setEnd(textNode, newPosition);
selection.removeAllRanges();
selection.addRange(newRange);
} catch (error) {
console.warn("Failed to set cursor position:", error);
}
});
//
this.updateValue();
}
},
},
watch: {
modelValue: {
immediate: true,
handler(val) {
if (val) {
//
const match = val.match(/^.*?\((.*)\)$/);
if (!match) return;
const params = match[1];
const parts = params.split(",");
const text = parts[0];
const regexPart = parts[1];
const replace = parts[2];
if (regexPart) {
const [_, pattern, flags] = regexPart.match(/\/(.*?)\/(.*)/) || [];
this.textValue = text;
this.regexValue = pattern;
this.replaceValue = replace || "";
this.flags = {
ignoreCase: flags.includes("i"),
multiline: flags.includes("m"),
global: flags.includes("g"),
};
}
}
},
},
mode() {
//
if (this.mode === "extract") {
this.replaceValue = "";
}
this.updateValue();
},
},
});
</script>
<style scoped>
.regex-editor {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.builder-container {
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
padding: 8px;
overflow: hidden;
}
.builder-enter-active,
.builder-leave-active {
transition: all 0.3s ease;
max-height: 500px;
}
.builder-enter-from,
.builder-leave-to {
max-height: 0;
opacity: 0;
padding: 0 8px;
}
/* 暗色模式适配 */
.body--dark .builder-container {
background: rgba(255, 255, 255, 0.03);
}
</style>

View File

@ -0,0 +1,786 @@
<template>
<div class="regex-input">
<div class="input-container">
<!-- 语法提示 -->
<div class="syntax-tooltip" v-if="selectedPattern && isFocused">
<div class="tooltip-content">
<div class="text-weight-medium">{{ syntaxHelp.title }}</div>
<div class="text-caption q-mt-xs">{{ syntaxHelp.description }}</div>
<div
class="text-caption text-grey-5 q-mt-xs"
v-if="syntaxHelp.example"
>
示例: {{ syntaxHelp.example }}
</div>
</div>
</div>
<!-- 前置图标 -->
<div class="input-icon">
<q-icon name="rule" size="18px" class="text-grey-7" />
</div>
<!-- 真实的输入框 -->
<div
ref="editor"
class="editor"
contenteditable="true"
spellcheck="false"
@input="handleInput"
@keydown="handleKeydown"
@select="handleSelect"
@blur="handleBlur"
@click="handleClick"
:data-placeholder="placeholder"
></div>
<!-- 高亮层 -->
<div class="highlight" ref="highlight" @mousedown.stop @click.stop></div>
<!-- 错误提示 -->
<div class="error-message" v-if="error">
{{ error }}
<q-btn
flat
dense
round
size="xs"
icon="close"
class="close-error"
@click="error = ''"
/>
</div>
<!-- 工具栏 -->
<div class="input-toolbar">
<q-btn
flat
dense
size="sm"
icon="clear"
@click="clearInput"
v-show="content"
>
<q-tooltip>清空</q-tooltip>
</q-btn>
<q-btn
flat
dense
size="sm"
icon="text_format"
:color="flags.ignoreCase ? 'primary' : ''"
@click="
$emit('update:flags', { ...flags, ignoreCase: !flags.ignoreCase })
"
>
<q-tooltip>忽略大小写 (i)</q-tooltip>
</q-btn>
<q-btn
flat
dense
size="sm"
icon="wrap_text"
:color="flags.multiline ? 'primary' : ''"
@click="
$emit('update:flags', { ...flags, multiline: !flags.multiline })
"
>
<q-tooltip>多行模式 (m)</q-tooltip>
</q-btn>
<q-btn
flat
dense
size="sm"
icon="repeat"
:color="flags.global ? 'primary' : ''"
@click="$emit('update:flags', { ...flags, global: !flags.global })"
>
<q-tooltip>全局匹配 (g)</q-tooltip>
</q-btn>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "RegexInput",
props: {
modelValue: {
type: String,
default: "",
},
flags: {
type: Object,
required: true,
},
placeholder: {
type: String,
default: "输入正则表达式...",
},
},
emits: ["update:modelValue", "update:flags"],
data() {
return {
content: "",
error: "",
selectedPattern: "",
syntaxHelp: {
title: "",
description: "",
example: "",
},
isFocused: false,
};
},
methods: {
handleInput(e) {
this.content = e.target.innerText;
//
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const start = range.startOffset;
const end = range.endOffset;
//
const cursorChar = this.content.charAt(start - 1);
this.updateSyntaxHelp(cursorChar);
//
this.$emit("update:modelValue", this.content);
this.highlightSyntax();
//
this.$nextTick(() => {
const newRange = document.createRange();
const textNode = this.$refs.editor.firstChild || this.$refs.editor;
newRange.setStart(textNode, start);
newRange.setEnd(textNode, end);
selection.removeAllRanges();
selection.addRange(newRange);
});
},
updateSyntaxHelp(char) {
//
//
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const cursorPos = range.startOffset;
const prevChar = this.content.charAt(cursorPos - 2);
const currentChar = this.content.charAt(cursorPos - 1);
if (currentChar === "\\") {
this.selectedPattern = "\\"; //
this.syntaxHelp = {
title: "转义字符",
description: "用于转义特殊字符,使其失去特殊含义",
example: "\\. 匹配普通的点号",
};
} else if (currentChar === "[") {
this.selectedPattern = "["; //
this.syntaxHelp = {
title: "字符集",
description: "匹配方括号中的任意一个字符",
example: "[aeiou] 匹配任意一个元音字母",
};
} else if (currentChar === "(") {
this.selectedPattern = "("; //
this.syntaxHelp = {
title: "分组",
description: "创建一个捕获组,可以被后面引用",
example: "(\\w+) \\1 匹配重复的单词",
};
} else if ("?+*{".includes(currentChar)) {
this.selectedPattern = currentChar; //
//
if (currentChar === "?" && "*+}".includes(prevChar)) {
this.syntaxHelp = {
title: "非贪婪量词",
description: "使前面的量词变成非贪婪模式,尽可能少的匹配",
example: ".*? 懒惰匹配,<.+?> 最小匹配HTML标签",
};
} else if (currentChar === "?") {
this.syntaxHelp = {
title: "可选量词",
description: "匹配前面的模式零次或一次",
example: "colou?r 匹配 color 或 colour",
};
} else {
this.syntaxHelp = {
title: "量词",
description:
currentChar === "?"
? "非贪婪匹配,尽可能少的匹配"
: currentChar === "+"
? "一次或多次"
: currentChar === "*"
? "零次或多次"
: "指定次数",
example:
currentChar === "?"
? ".*? 懒惰匹配,<.+?>匹配HTML标签"
: currentChar === "+"
? "\\d+ 匹配一个或多个数字"
: currentChar === "*"
? "\\w* 匹配零个或多个字符"
: "{2,3} 匹配2到3次",
};
}
} else if ("|^$".includes(currentChar)) {
this.selectedPattern = currentChar; //
this.syntaxHelp = {
title:
currentChar === "|" ? "或" : currentChar === "^" ? "行首" : "行尾",
description:
currentChar === "|"
? "匹配多个模式之一"
: currentChar === "^"
? "匹配行的开始"
: "匹配行的结束",
example:
currentChar === "|"
? "cat|dog 匹配cat或dog"
: currentChar === "^"
? "^\\w+ 匹配行首的单词"
: "\\w+$ 匹配行尾的单词",
};
} else {
//
if (prevChar === "\\") {
this.selectedPattern = "\\" + currentChar; //
this.syntaxHelp = {
title: "转义序列",
description: this.getEscapeSequenceHelp(currentChar),
example: this.getEscapeSequenceExample(currentChar),
};
} else {
this.selectedPattern = ""; //
this.syntaxHelp = {};
}
}
},
getEscapeSequenceHelp(char) {
const helpMap = {
d: "匹配任意数字字符 (0-9)",
D: "匹配任意非数字字符",
w: "匹配字母、数字、下划线",
W: "匹配非单词字符",
s: "匹配任意空白字符",
S: "匹配非空白字符",
b: "匹配单词边界",
B: "匹配非单词边界",
};
return helpMap[char] || "特殊字符转义";
},
getEscapeSequenceExample(char) {
const exampleMap = {
d: "\\d+ 匹配一个或多个数字",
D: "\\D+ 匹配一个或多个非数字字符",
w: "\\w+ 匹配一个或多个单词字符",
W: "\\W+ 匹配一个或多个非单词字符",
s: "\\s+ 匹配一个或多个空白字符",
S: "\\S+ 匹配一个或多个非空白字符",
b: "\\bword\\b 精确匹配单词",
B: "\\Bword\\B 匹配被其他字符包围的word",
};
return exampleMap[char] || `\\${char} 匹配字符 ${char}`;
},
handleKeydown(e) {
// Tab
if (e.key === "Tab") {
e.preventDefault();
document.execCommand("insertText", false, " ");
}
//
else if (e.key === "[") {
e.preventDefault();
this.insertWithCursor("[]");
} else if (e.key === "(") {
e.preventDefault();
this.insertWithCursor("()");
} else if (e.key === "{") {
e.preventDefault();
this.insertWithCursor("{}");
}
},
insertWithCursor(text) {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
//
const cursorPos = range.startOffset;
//
const currentText = this.$refs.editor.innerText;
//
const newText =
currentText.slice(0, cursorPos) + text + currentText.slice(cursorPos);
//
this.$refs.editor.innerText = newText;
//
const textNode = this.$refs.editor.firstChild || this.$refs.editor;
const newRange = document.createRange();
newRange.setStart(textNode, cursorPos + 1); //
newRange.setEnd(textNode, cursorPos + 1);
selection.removeAllRanges();
selection.addRange(newRange);
//
this.content = this.$refs.editor.innerText;
this.$emit("update:modelValue", this.content);
this.highlightSyntax();
},
handleSelect() {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
this.selectedPattern = range.toString();
}
},
clearInput() {
this.content = "";
this.$refs.editor.innerText = "";
this.error = "";
this.$emit("update:modelValue", "");
this.highlightSyntax();
},
highlightSyntax() {
const text = this.content;
let html = text
//
.replace(
/[<>&]/g,
(c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" }[c])
)
//
.replace(/\\(?:.|$)/g, '<span class="escape">$&</span>')
//
.replace(/\[([^\]]*)\]/g, '<span class="charset">[$1]</span>')
//
.replace(/[?+*]|\{[^\}]+\}/g, '<span class="quantifier">$&</span>')
//
.replace(/(\((?:\?[=!:])?[^)]*\))/g, '<span class="group">$1</span>')
//
.replace(/[|^$]/g, '<span class="special">$&</span>')
//
.replace(/\\[bBAZz]/g, '<span class="anchor">$&</span>');
this.$refs.highlight.innerHTML = html;
},
syncScroll(e) {
const { scrollTop, scrollLeft } = e.target;
this.$refs.highlight.scrollTop = scrollTop;
this.$refs.highlight.scrollLeft = scrollLeft;
},
handleBlur() {
this.isFocused = false;
this.selectedPattern = ""; //
try {
new RegExp(this.content);
this.error = "";
this.$emit("update:modelValue", this.content);
this.highlightSyntax();
} catch (e) {
//
let errorMsg = e.message;
if (errorMsg.includes("Invalid regular expression")) {
if (errorMsg.includes("Unterminated group")) {
errorMsg = "括号未闭合";
} else if (errorMsg.includes("Unterminated character class")) {
errorMsg = "字符集未闭合";
} else if (errorMsg.includes("Invalid escape")) {
errorMsg = "无效的转义字符";
} else if (errorMsg.includes("Nothing to repeat")) {
errorMsg = "量词前缺少有效模式";
} else {
errorMsg = "无效的正则表达式";
}
}
this.error = errorMsg;
//
this.$refs.editor.classList.add("has-error");
setTimeout(() => {
this.$refs.editor.classList.remove("has-error");
}, 1000);
}
},
handleFocus() {
this.isFocused = true;
},
handleClick() {
this.selectedPattern = "";
this.syntaxHelp = {};
},
},
watch: {
modelValue: {
immediate: true,
handler(val) {
this.content = val;
this.$nextTick(() => {
if (this.$refs.editor) {
if (this.$refs.editor.innerText !== val) {
this.$refs.editor.innerText = val;
}
this.highlightSyntax();
}
});
},
},
},
mounted() {
if (this.modelValue) {
this.$refs.editor.innerText = this.modelValue;
this.highlightSyntax();
}
this.$refs.editor.addEventListener("scroll", this.syncScroll);
this.$refs.editor.addEventListener("focus", this.handleFocus);
},
beforeUnmount() {
this.$refs.editor.removeEventListener("scroll", this.syncScroll);
this.$refs.editor.removeEventListener("focus", this.handleFocus);
},
});
</script>
<style scoped>
.regex-input {
width: 100%;
}
.input-container {
flex: 1;
position: relative;
min-height: 36px;
height: 36px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.03);
padding: 0 0 0 32px; /* 只保留左侧图标的空间 */
}
.regex-input .input-icon {
position: absolute;
left: 9px;
top: 50%;
transform: translateY(-50%);
z-index: 3;
display: flex;
align-items: center;
}
.regex-input .input-icon :deep(.q-icon) {
color: rgba(0, 0, 0, 0.6);
}
/* 暗色模式下的图标颜色 */
.body--dark .regex-input .input-icon :deep(.q-icon) {
color: rgba(255, 255, 255, 0.7) !important;
}
.input-toolbar {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
z-index: 3;
background: linear-gradient(
to right,
transparent,
rgba(244, 244, 244, 0.95) 15%,
rgba(244, 244, 244, 1)
);
padding-left: 15px;
padding-right: 5px;
}
.input-toolbar :deep(.q-btn) {
opacity: 0.7;
transition: all 0.3s ease;
}
.input-toolbar :deep(.q-btn:hover) {
opacity: 1;
}
.input-toolbar :deep(.q-btn.text-primary) {
opacity: 1;
}
.editor,
.highlight {
padding: 8px 8px 8px 32px;
margin: 0;
border: none;
width: 100%;
height: 36px;
position: absolute;
top: 0;
left: 0;
font-family: monospace;
font-size: 14px;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
border-radius: 4px;
font-family: Consolas, Monaco, "Courier New";
padding-right: 115px;
transition: all 0.3s ease;
}
.editor {
color: transparent;
caret-color: black;
z-index: 2;
background: transparent;
outline: none;
resize: none;
}
.highlight {
z-index: 1;
pointer-events: none;
background: transparent;
}
.highlight * {
pointer-events: none;
}
.highlight :deep(.escape) {
color: #e67e22; /* 橙色 */
}
.highlight :deep(.charset) {
color: #2ecc71; /* 绿色 */
}
.highlight :deep(.quantifier) {
color: #9b59b6; /* 紫色 */
}
.highlight :deep(.group) {
color: #3498db; /* 蓝色 */
}
.highlight :deep(.special) {
color: #e74c3c; /* 红色 */
}
.highlight :deep(.anchor) {
color: #f1c40f; /* 黄色 */
}
.error-message {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
font-size: 12px;
color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
border-radius: 4px;
padding: 4px 28px 4px 28px;
margin-left: 32px;
backdrop-filter: blur(4px);
display: flex;
align-items: center;
gap: 4px;
animation: fadeIn 0.3s ease;
z-index: 4;
white-space: nowrap;
}
.close-error {
position: absolute;
right: 2px;
top: 50%;
transform: translateY(-50%);
opacity: 0.6;
transition: all 0.3s ease;
}
.close-error:hover {
opacity: 1;
}
.error-message::before {
content: "error";
font-family: "Material Icons";
position: absolute;
left: 8px;
font-size: 14px;
opacity: 0.8;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-50%) translateX(-10px);
}
to {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
}
/* 暗色模式适配 */
.body--dark .error-message {
background: rgba(231, 76, 60, 0.15);
color: #ff6b5b;
}
/* 输入框样式调整,为错误消息留出空间 */
.editor,
.highlight {
padding: 8px 8px 8px 32px;
margin: 0;
border: none;
width: 100%;
height: 36px;
position: absolute;
top: 0;
left: 0;
font-family: monospace;
font-size: 14px;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
border-radius: 4px;
font-family: Consolas, Monaco, "Courier New";
padding-right: 115px;
transition: all 0.3s ease;
}
/* 当有错误时输入框的样式 */
.input-container:has(.error-message) .editor {
border: 1px solid rgba(231, 76, 60, 0.3);
background: rgba(231, 76, 60, 0.02);
}
.body--dark .input-container:has(.error-message) .editor {
border: 1px solid rgba(231, 76, 60, 0.2);
background: rgba(231, 76, 60, 0.05);
}
/* 暗色模式适配 */
.body--dark .input-container {
background: rgba(255, 255, 255, 0.05);
}
.body--dark .editor {
caret-color: white;
}
.body--dark .highlight :deep(.escape) {
color: #f39c12;
}
.body--dark .highlight :deep(.charset) {
color: #27ae60;
}
.body--dark .highlight :deep(.quantifier) {
color: #8e44ad;
}
.body--dark .highlight :deep(.group) {
color: #2980b9;
}
.body--dark .highlight :deep(.special) {
color: #c0392b;
}
.body--dark .highlight :deep(.anchor) {
color: #f39c12;
}
/* 添加占位符样式 */
.editor:empty:before {
content: attr(data-placeholder);
color: rgba(0, 0, 0, 0.6);
font-size: 11px;
font-family: Roboto;
position: absolute;
top: 50%;
transform: translateY(-50%);
font-weight: 500;
}
/* 暗色模式下的占位符 */
.body--dark .editor:empty:before {
color: rgb(192, 193, 181);
}
/* 暗色模式下的工具栏背景 */
.body--dark .input-toolbar {
background: linear-gradient(
to right,
transparent,
rgba(48, 49, 50, 0.95) 15%,
rgba(48, 49, 50, 1)
);
}
.regex-input ::-webkit-scrollbar {
height: 1px;
}
.syntax-tooltip {
position: absolute;
top: -80px;
left: 32px;
background: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 8px 12px;
z-index: 5;
max-width: 300px;
pointer-events: none;
opacity: 0.95;
transition: all 0.3s ease;
}
.syntax-tooltip::after {
content: "";
position: absolute;
bottom: -6px;
left: 20px;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid white;
}
/* 暗色模式适配 */
.body--dark .syntax-tooltip {
background: #424242;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.body--dark .syntax-tooltip::after {
border-top-color: #424242;
}
/* 错误动画效果 */
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-2px);
}
75% {
transform: translateX(2px);
}
}
.editor.has-error {
animation: shake 0.2s ease-in-out 3;
}
</style>

View File

@ -0,0 +1,218 @@
<template>
<transition
name="tester"
@enter="el => el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })"
>
<div class="regex-tester" v-if="shouldShowPreview">
<div class="matches">
<div class="section-title">
<q-icon name="format_list_bulleted" size="14px" class="q-mr-xs" />
匹配结果
</div>
<div v-if="!matches.length" class="no-matches">
<q-icon name="sentiment_dissatisfied" size="14px" class="q-mr-xs" />
未匹配到任何结果
</div>
<div v-else class="match-list">
<q-chip
v-for="(match, index) in matches"
:key="index"
dense
square
color="primary"
text-color="white"
class="match-chip"
size="sm"
>
{{ match }}
</q-chip>
</div>
</div>
<div v-if="replace" class="preview">
<div class="section-title">
<q-icon name="find_replace" size="14px" class="q-mr-xs" />
替换预览
</div>
<div class="preview-text">
{{ replacedText }}
</div>
</div>
</div>
</transition>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "RegexTester",
props: {
text: {
type: String,
default: "",
},
regex: {
type: String,
default: "",
},
flags: {
type: Object,
required: true,
},
replace: {
type: String,
default: "",
},
},
computed: {
processedText() {
if (this.text && this.text.startsWith('"') && this.text.endsWith('"')) {
return this.text.slice(1, -1);
}
return this.text;
},
shouldShowPreview() {
return this.text && this.text.startsWith('"') && this.text.endsWith('"');
},
matches() {
if (!this.shouldShowPreview || !this.regex) return [];
try {
const flagStr = Object.entries(this.flags)
.filter(([_, value]) => value)
.map(([key]) => key.charAt(0))
.join("");
const re = new RegExp(this.regex, flagStr);
return this.processedText.match(re) || [];
} catch {
return [];
}
},
highlightedText() {
if (!this.shouldShowPreview || !this.regex) return this.processedText;
try {
const flagStr = Object.entries(this.flags)
.filter(([_, value]) => value)
.map(([key]) => key.charAt(0))
.join("");
const re = new RegExp(this.regex, flagStr);
return this.processedText.replace(
re,
'<span class="highlight">$&</span>'
);
} catch {
return this.processedText;
}
},
processedReplace() {
if (
this.replace &&
this.replace.startsWith('"') &&
this.replace.endsWith('"')
) {
return this.replace.slice(1, -1);
}
return this.replace;
},
replacedText() {
if (!this.shouldShowPreview || !this.regex) return this.processedText;
try {
const flagStr = Object.entries(this.flags)
.filter(([_, value]) => value)
.map(([key]) => key.charAt(0))
.join("");
const re = new RegExp(this.regex, flagStr);
return this.processedText.replace(re, this.processedReplace);
} catch {
return this.processedText;
}
},
},
});
</script>
<style scoped>
.regex-tester {
padding: 8px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.03);
overflow: hidden;
transform-origin: top;
}
.section-title {
font-size: 12px;
font-weight: 500;
color: var(--q-primary);
opacity: 0.8;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.match-list,
.no-matches {
min-height: 23px;
padding: 0px 8px;
margin-bottom: 8px;
}
.no-matches {
font-size: 12px;
opacity: 0.6;
}
.match-list {
display: flex;
flex-wrap: wrap;
gap: 2px;
}
.match-chip {
min-width: 30px;
transition: all 0.3s ease;
}
.match-chip :deep(.q-chip__content) {
flex: 1;
justify-content: center;
}
.preview-text {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
font-size: 13px;
line-height: 1.5;
padding: 8px;
background: rgba(255, 255, 255, 0.5);
border-radius: 4px;
margin: 0 8px;
}
.preview-text :deep(.highlight) {
background-color: rgba(var(--q-primary-rgb), 0.2);
border-radius: 2px;
padding: 0 2px;
transition: all 0.3s ease;
}
/* 暗色模式适配 */
.body--dark .regex-tester {
background: rgba(255, 255, 255, 0.05);
}
.body--dark .preview-text {
background: rgba(0, 0, 0, 0.2);
}
.tester-enter-active,
.tester-leave-active {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.tester-enter-from,
.tester-leave-to {
opacity: 0;
transform: scaleY(0);
}
</style>

View File

@ -35,7 +35,7 @@
type="textarea"
dense
borderless
style="font-family: monospace, monoca, consola"
style="font-family: Consolas, Monaco, 'Courier New'"
autogrow
@update:model-value="updateFunction"
>

View File

@ -121,6 +121,11 @@ export default defineComponent({
],
};
},
props: {
command: {
type: Object,
},
},
computed: {
mainKeyDisplay() {
if (!this.mainKey) return "";
@ -272,7 +277,7 @@ export default defineComponent({
const args = [this.mainKey, ...activeModifiers];
//
this.$emit("update:modelValue", `keyTap("${args.join('","')}")`);
this.$emit("update:modelValue", `${this.command.value}("${args.join('","')}")`);
},
parseKeyString(val) {
try {

View File

@ -54,7 +54,7 @@ export default defineComponent({
.filter((val) => val !== undefined && val !== "")
.join(",");
this.$emit("update:modelValue", argv);
this.$emit("update:modelValue", `${this.command.value}(${argv})`);
},
},
});

View File

@ -144,24 +144,12 @@ export const textProcessingCommands = {
],
},
{
value: "quickcomposer.textProcessing.regexExtract",
label: "正则提取",
config: [
{
key: "text",
label: "原始文本",
type: "input",
defaultValue: "",
icon: "text_fields",
},
{
key: "regex",
label: "正则表达式",
type: "input",
defaultValue: "",
icon: "regex",
},
],
value: "quickcomposer.textProcessing.regexTransform",
label: "正则提取/替换",
component: "RegexEditor",
componentProps: {
inputLabel: "要处理的文本",
},
},
],
};

View File

@ -13,7 +13,7 @@ export function generateCode(commandFlow) {
}
let awaitCmd = cmd.isAsync ? "await " : "";
line += `${awaitCmd}${cmd.value}(${cmd.argv})`;
line += `${awaitCmd} ${cmd.argv}`;
code.push(line);
});