mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-06-28 20:02:44 +08:00
编排添加正则提取/替换
This commit is contained in:
parent
c163bdd9d6
commit
b532630ab9
@ -103,10 +103,14 @@ const textProcessing = {
|
|||||||
substring: function (text, start, end) {
|
substring: function (text, start, end) {
|
||||||
return text.substring(start, end);
|
return text.substring(start, end);
|
||||||
},
|
},
|
||||||
// 正则提取
|
// 正则处理
|
||||||
regexExtract: function (text, regex) {
|
regexTransform: function (text, regex, replace) {
|
||||||
const match = text.match(regex);
|
try {
|
||||||
return match ? match[0] : "";
|
if (replace === undefined) return text.match(regex);
|
||||||
|
return text.replace(regex, replace);
|
||||||
|
} catch (e) {
|
||||||
|
throw "正则表达式格式错误";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// 非对称加解密
|
// 非对称加解密
|
||||||
asymmetricCrypto: function (config) {
|
asymmetricCrypto: function (config) {
|
||||||
|
@ -188,13 +188,12 @@ export default defineComponent({
|
|||||||
/* 输入框标签字体大小,占位时的位置 */
|
/* 输入框标签字体大小,占位时的位置 */
|
||||||
.command-composer :deep(.q-field--filled .q-field__label) {
|
.command-composer :deep(.q-field--filled .q-field__label) {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
top: 50%;
|
top: 11px;
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框标签悬浮的位置 */
|
/* 输入框标签悬浮的位置 */
|
||||||
.command-composer :deep(.q-field--filled.q-field--float .q-field__label) {
|
.command-composer :deep(.q-field--filled .q-field--float .q_field__label) {
|
||||||
transform: translateY(-90%) scale(0.7);
|
transform: translateY(-35%) scale(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 去除filled输入框边框 */
|
/* 去除filled输入框边框 */
|
||||||
|
@ -122,6 +122,7 @@ import AxiosConfigEditor from "components/composer/http/AxiosConfigEditor.vue";
|
|||||||
import SymmetricCryptoEditor from "components/composer/crypto/SymmetricCryptoEditor.vue";
|
import SymmetricCryptoEditor from "components/composer/crypto/SymmetricCryptoEditor.vue";
|
||||||
import AsymmetricCryptoEditor from "components/composer/crypto/AsymmetricCryptoEditor.vue";
|
import AsymmetricCryptoEditor from "components/composer/crypto/AsymmetricCryptoEditor.vue";
|
||||||
import FunctionSelector from "components/composer/ui/FunctionSelector.vue";
|
import FunctionSelector from "components/composer/ui/FunctionSelector.vue";
|
||||||
|
import RegexEditor from "components/composer/regex/RegexEditor.vue";
|
||||||
import { validateVariableName } from "js/common/variableValidator";
|
import { validateVariableName } from "js/common/variableValidator";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -135,6 +136,7 @@ export default defineComponent({
|
|||||||
SymmetricCryptoEditor,
|
SymmetricCryptoEditor,
|
||||||
AsymmetricCryptoEditor,
|
AsymmetricCryptoEditor,
|
||||||
FunctionSelector,
|
FunctionSelector,
|
||||||
|
RegexEditor,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
command: {
|
command: {
|
||||||
@ -321,6 +323,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
.command-item:hover {
|
.command-item:hover {
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
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 {
|
.body--dark .command-item {
|
||||||
.command-item {
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
}
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.command-item:hover {
|
.body--dark .command-item:hover {
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 8px rgba(58, 58, 58, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.can-drop {
|
.body--dark .can-drop {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-section :deep(.q-field) {
|
.body--dark .output-section :deep(.q-field) {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-section :deep(.q-field--focused) {
|
.body--dark .output-section :deep(.q-field--focused) {
|
||||||
background: #1d1d1d;
|
background: #1d1d1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-btn {
|
.body--dark .output-btn {
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-btn:hover {
|
.body--dark .output-btn:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -198,9 +198,14 @@ export default defineComponent({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
command: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateConfig() {
|
updateConfig() {
|
||||||
const code = `quickcomposer.textProcessing.asymmetricCrypto(${formatJsonVariables(
|
const code = `${this.command.value}(${formatJsonVariables(
|
||||||
{
|
{
|
||||||
text: this.text,
|
text: this.text,
|
||||||
algorithm: this.algorithm,
|
algorithm: this.algorithm,
|
||||||
|
@ -164,6 +164,9 @@ export default defineComponent({
|
|||||||
type: [String, Object],
|
type: [String, Object],
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
command: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -243,7 +246,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateConfig() {
|
updateConfig() {
|
||||||
const code = `quickcomposer.textProcessing.symmetricCrypto(${formatJsonVariables(
|
const code = `${this.command.value}(${formatJsonVariables(
|
||||||
{
|
{
|
||||||
text: this.text,
|
text: this.text,
|
||||||
algorithm: this.algorithm,
|
algorithm: this.algorithm,
|
||||||
|
@ -240,6 +240,9 @@ export default defineComponent({
|
|||||||
type: [Object, String],
|
type: [Object, String],
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
command: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
data() {
|
data() {
|
||||||
@ -316,9 +319,9 @@ export default defineComponent({
|
|||||||
? `, ${formatJsonVariables(restConfig, null, excludeFields)}`
|
? `, ${formatJsonVariables(restConfig, null, excludeFields)}`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const code = `axios.${method.toLowerCase()}(${url}${
|
const code = `${this.command.value}(${url}${
|
||||||
this.hasRequestData ? `, ${formatJsonVariables(data)}` : ""
|
this.hasRequestData ? `, ${formatJsonVariables(data)}` : ""
|
||||||
}${configStr})?.data`;
|
}${configStr})`;
|
||||||
|
|
||||||
this.$emit("update:modelValue", code);
|
this.$emit("update:modelValue", code);
|
||||||
},
|
},
|
||||||
@ -344,7 +347,7 @@ export default defineComponent({
|
|||||||
deep: true,
|
deep: true,
|
||||||
handler(newValue) {
|
handler(newValue) {
|
||||||
if (typeof newValue === "string") {
|
if (typeof newValue === "string") {
|
||||||
// 如果是字符串,明是编辑现有的配置
|
// 如果是字符串,说明是编辑现有的配置
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(newValue);
|
const config = JSON.parse(newValue);
|
||||||
this.localConfig = {
|
this.localConfig = {
|
||||||
|
520
src/components/composer/regex/RegexBuilder.vue
Normal file
520
src/components/composer/regex/RegexBuilder.vue
Normal 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>
|
273
src/components/composer/regex/RegexEditor.vue
Normal file
273
src/components/composer/regex/RegexEditor.vue
Normal 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>
|
786
src/components/composer/regex/RegexInput.vue
Normal file
786
src/components/composer/regex/RegexInput.vue
Normal 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) => ({ "<": "<", ">": ">", "&": "&" }[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>
|
218
src/components/composer/regex/RegexTester.vue
Normal file
218
src/components/composer/regex/RegexTester.vue
Normal 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>
|
@ -35,7 +35,7 @@
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
dense
|
dense
|
||||||
borderless
|
borderless
|
||||||
style="font-family: monospace, monoca, consola"
|
style="font-family: Consolas, Monaco, 'Courier New'"
|
||||||
autogrow
|
autogrow
|
||||||
@update:model-value="updateFunction"
|
@update:model-value="updateFunction"
|
||||||
>
|
>
|
||||||
|
@ -121,6 +121,11 @@ export default defineComponent({
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
command: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mainKeyDisplay() {
|
mainKeyDisplay() {
|
||||||
if (!this.mainKey) return "";
|
if (!this.mainKey) return "";
|
||||||
@ -272,7 +277,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const args = [this.mainKey, ...activeModifiers];
|
const args = [this.mainKey, ...activeModifiers];
|
||||||
// 为每个参数添加引号
|
// 为每个参数添加引号
|
||||||
this.$emit("update:modelValue", `keyTap("${args.join('","')}")`);
|
this.$emit("update:modelValue", `${this.command.value}("${args.join('","')}")`);
|
||||||
},
|
},
|
||||||
parseKeyString(val) {
|
parseKeyString(val) {
|
||||||
try {
|
try {
|
||||||
|
@ -54,7 +54,7 @@ export default defineComponent({
|
|||||||
.filter((val) => val !== undefined && val !== "")
|
.filter((val) => val !== undefined && val !== "")
|
||||||
.join(",");
|
.join(",");
|
||||||
|
|
||||||
this.$emit("update:modelValue", argv);
|
this.$emit("update:modelValue", `${this.command.value}(${argv})`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -144,24 +144,12 @@ export const textProcessingCommands = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "quickcomposer.textProcessing.regexExtract",
|
value: "quickcomposer.textProcessing.regexTransform",
|
||||||
label: "正则提取",
|
label: "正则提取/替换",
|
||||||
config: [
|
component: "RegexEditor",
|
||||||
{
|
componentProps: {
|
||||||
key: "text",
|
inputLabel: "要处理的文本",
|
||||||
label: "原始文本",
|
},
|
||||||
type: "input",
|
|
||||||
defaultValue: "",
|
|
||||||
icon: "text_fields",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "regex",
|
|
||||||
label: "正则表达式",
|
|
||||||
type: "input",
|
|
||||||
defaultValue: "",
|
|
||||||
icon: "regex",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ export function generateCode(commandFlow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let awaitCmd = cmd.isAsync ? "await " : "";
|
let awaitCmd = cmd.isAsync ? "await " : "";
|
||||||
line += `${awaitCmd}${cmd.value}(${cmd.argv})`;
|
line += `${awaitCmd} ${cmd.argv}`;
|
||||||
|
|
||||||
code.push(line);
|
code.push(line);
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user