diff --git a/README.md b/README.md index 33595c97..9a268c86 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ### 介绍 -web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台** +web 版 **linux(终端[终端回放、命令过滤] 文件 脚本 进程 计划任务)、数据库(mysql postgres oracle sqlserver 达梦 高斯 sqlite)数据操作 数据同步 数据迁移、redis(单机 哨兵 集群)、mongo 等集工单流程审批于一体的统一管理操作平台** ### 开发语言与主要框架 diff --git a/mayfly_go_web/package.json b/mayfly_go_web/package.json index 6e12e1be..dfd2235a 100644 --- a/mayfly_go_web/package.json +++ b/mayfly_go_web/package.json @@ -17,7 +17,7 @@ "cropperjs": "^1.6.1", "dayjs": "^1.11.11", "echarts": "^5.5.0", - "element-plus": "^2.7.3", + "element-plus": "^2.7.4", "js-base64": "^3.7.7", "jsencrypt": "^3.3.2", "lodash": "^4.17.21", @@ -57,7 +57,7 @@ "prettier": "^3.2.5", "sass": "^1.77.1", "typescript": "^5.4.5", - "vite": "^5.2.11", + "vite": "^5.2.12", "vue-eslint-parser": "^9.4.2" }, "browserslist": [ diff --git a/mayfly_go_web/src/common/utils/string.ts b/mayfly_go_web/src/common/utils/string.ts index 1f593b40..923df11d 100644 --- a/mayfly_go_web/src/common/utils/string.ts +++ b/mayfly_go_web/src/common/utils/string.ts @@ -97,43 +97,6 @@ export function getTextWidth(str: string) { return width; } -/** - * 获取内容所需要占用的宽度 - */ -export function getContentWidth(content: any): number { - if (!content) { - return 50; - } - // 以下分配的单位长度可根据实际需求进行调整 - let flexWidth = 0; - for (const char of content) { - if (flexWidth > 500) { - break; - } - if ((char >= '0' && char <= '9') || (char >= 'a' && char <= 'z')) { - // 小写字母、数字字符 - flexWidth += 9.3; - continue; - } - if (char >= 'A' && char <= 'Z') { - flexWidth += 9; - continue; - } - if (char >= '\u4e00' && char <= '\u9fa5') { - // 如果是中文字符,为字符分配16个单位宽度 - flexWidth += 20; - } else { - // 其他种类字符 - flexWidth += 8; - } - } - // if (flexWidth > 450) { - // // 设置最大宽度 - // flexWidth = 450; - // } - return flexWidth; -} - /** * * @returns uuid @@ -179,3 +142,38 @@ export async function copyToClipboard(txt: string, selector: string = '#copyValu clipboard.destroy(); }); } + +export function fuzzyMatchField(keyword: string, fields: any[], ...valueExtractFuncs: Function[]) { + keyword = keyword?.toLowerCase(); + return fields.filter((field) => { + for (let valueExtractFunc of valueExtractFuncs) { + const value = valueExtractFunc(field)?.toLowerCase(); + if (isPrefixSubsequence(keyword, value)) { + return true; + } + } + return false; + }); +} + +/** + * 匹配是否为前缀子序列 targetTemplate=username prefix=uname -> true,prefix=uname2 -> false + * @param prefix 字符串前缀(不连续也可以,但不改变字符的相对顺序) + * @param targetTemplate 目标模板 + * @returns 是否匹配 + */ +export function isPrefixSubsequence(prefix: string, targetTemplate: string) { + let i = 0; // 指向prefix的索引 + let j = 0; // 指向targetTemplate的索引 + + while (i < prefix.length && j < targetTemplate.length) { + if (prefix[i] === targetTemplate[j]) { + // 字符匹配,两个指针都向前移动 + i++; + } + j++; // 目标字符串指针始终向前移动 + } + + // 如果prefix的所有字符都被找到,返回true + return i === prefix.length; +} diff --git a/mayfly_go_web/src/views/ops/component/TagTree.vue b/mayfly_go_web/src/views/ops/component/TagTree.vue index 6feee81a..7c686200 100644 --- a/mayfly_go_web/src/views/ops/component/TagTree.vue +++ b/mayfly_go_web/src/views/ops/component/TagTree.vue @@ -53,6 +53,7 @@ import { NodeType, TagTreeNode } from './tag'; import TagInfo from './TagInfo.vue'; import { Contextmenu } from '@/components/contextmenu'; import { tagApi } from '../tag/api'; +import { isPrefixSubsequence } from '@/common/utils/string'; const props = defineProps({ resourceType: { @@ -105,8 +106,7 @@ watch(filterText, (val) => { }); const filterNode = (value: string, data: any) => { - if (!value) return true; - return data.label.includes(value); + return !value || isPrefixSubsequence(value, data.label); }; /** diff --git a/mayfly_go_web/src/views/ops/component/TagTreeCheck.vue b/mayfly_go_web/src/views/ops/component/TagTreeCheck.vue index 9575d817..3adb5735 100755 --- a/mayfly_go_web/src/views/ops/component/TagTreeCheck.vue +++ b/mayfly_go_web/src/views/ops/component/TagTreeCheck.vue @@ -50,6 +50,7 @@ import { ref, reactive, onMounted } from 'vue'; import { tagApi } from '../tag/api'; import { TagResourceTypeEnum } from '@/common/commonEnum'; import EnumValue from '@/common/Enum'; +import { isPrefixSubsequence } from '@/common/utils/string'; const props = defineProps({ height: { @@ -102,10 +103,7 @@ const search = async () => { }; const filterNode = (value: string, data: any) => { - if (!value) { - return true; - } - return data.codePath.toLowerCase().includes(value) || data.name.includes(value); + return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name); }; const onFilterValChanged = (val: string) => { diff --git a/mayfly_go_web/src/views/ops/db/component/table/DbTableDataOp.vue b/mayfly_go_web/src/views/ops/db/component/table/DbTableDataOp.vue index 34882f73..c6b86983 100644 --- a/mayfly_go_web/src/views/ops/db/component/table/DbTableDataOp.vue +++ b/mayfly_go_web/src/views/ops/db/component/table/DbTableDataOp.vue @@ -259,7 +259,7 @@ import DbTableData from './DbTableData.vue'; import { DbDialect } from '@/views/ops/db/dialect'; import SvgIcon from '@/components/svgIcon/index.vue'; import { useEventListener, useStorage } from '@vueuse/core'; -import { copyToClipboard } from '@/common/utils/string'; +import { copyToClipboard, fuzzyMatchField } from '@/common/utils/string'; import DbTableDataForm from './DbTableDataForm.vue'; const props = defineProps({ @@ -476,10 +476,7 @@ const getColumnTips = (queryString: string, callback: any) => { let res = []; if (columnNameSearch) { - columnNameSearch = columnNameSearch.toLowerCase(); - res = columns.filter((data: any) => { - return data.columnName.toLowerCase().includes(columnNameSearch); - }); + res = fuzzyMatchField(columnNameSearch, columns, (x: any) => x.columnName); } completeCond = condition.value; @@ -534,10 +531,12 @@ const filterColumns = (searchKey: string) => { if (!searchKey) { return columns; } - searchKey = searchKey.toLowerCase(); - return columns.filter((data: any) => { - return data.columnName.toLowerCase().includes(searchKey) || data.columnComment.toLowerCase().includes(searchKey); - }); + return fuzzyMatchField( + searchKey, + columns, + (x: any) => x.columnName, + (x: any) => x.columnComment + ); }; /** diff --git a/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue b/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue index b3f22b47..b3e60c19 100644 --- a/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue +++ b/mayfly_go_web/src/views/ops/db/component/table/DbTablesOp.vue @@ -131,6 +131,7 @@ import { compatibleMysql, editDbTypes, getDbDialect } from '../../dialect/index' import { DbInst } from '../../db'; import MonacoEditor from '@/components/monaco/MonacoEditor.vue'; import { format as sqlFormatter } from 'sql-formatter'; +import { fuzzyMatchField } from '@/common/utils/string'; const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue')); @@ -219,17 +220,11 @@ const filterTableInfos = computed(() => { if (!tableNameSearch && !tableCommentSearch) { return tables; } - return tables.filter((data: any) => { - let tnMatch = true; - let tcMatch = true; - if (tableNameSearch) { - tnMatch = data.tableName.toLowerCase().includes(tableNameSearch.toLowerCase()); - } - if (tableCommentSearch) { - tcMatch = data.tableComment.includes(tableCommentSearch); - } - return tnMatch && tcMatch; - }); + + if (tableNameSearch) { + return fuzzyMatchField(tableNameSearch, tables, (table: any) => table.tableName); + } + return fuzzyMatchField(tableCommentSearch, tables, (table: any) => table.tableComment); }); const getTables = async () => { diff --git a/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue b/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue index 7ab9cd5c..88b79da2 100755 --- a/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue +++ b/mayfly_go_web/src/views/ops/machine/file/MachineFile.vue @@ -293,6 +293,7 @@ import { getToken } from '@/common/utils/storage'; import { convertToBytes, formatByteSize } from '@/common/utils/format'; import { getMachineConfig } from '@/common/sysconfig'; import { MachineProtocolEnum } from '../enums'; +import { fuzzyMatchField } from '@/common/utils/string'; const props = defineProps({ machineId: { type: Number }, @@ -371,33 +372,7 @@ onMounted(async () => { state.machineConfig = await getMachineConfig(); }); -// watch( -// () => props.machineId, -// () => { -// if (props.protocol != MachineProtocolEnum.Ssh.value) { -// userMap.clear(); -// groupMap.clear(); -// return; -// } - -// const machineId = props.machineId; -// machineApi.users.request({ machineId }).then((res: any) => { -// for (let user of res) { -// userMap.set(user.uid, user); -// } -// }); - -// machineApi.groups.request({ machineId }).then((res: any) => { -// for (let group of res) { -// groupMap.set(group.gid, group); -// } -// }); -// } -// ); - -const filterFiles = computed(() => - state.files.filter((data: any) => !state.fileNameFilter || data.name.toLowerCase().includes(state.fileNameFilter.toLowerCase())) -); +const filterFiles = computed(() => fuzzyMatchField(state.fileNameFilter, state.files, (file: any) => file.name)); const filePathNav = computed(() => { let basePath = state.basePath; diff --git a/mayfly_go_web/src/views/ops/tag/TagTreeList.vue b/mayfly_go_web/src/views/ops/tag/TagTreeList.vue index e015c316..73578b86 100644 --- a/mayfly_go_web/src/views/ops/tag/TagTreeList.vue +++ b/mayfly_go_web/src/views/ops/tag/TagTreeList.vue @@ -156,6 +156,7 @@ import { TagResourceTypeEnum } from '@/common/commonEnum'; import EnumTag from '@/components/enumtag/EnumTag.vue'; import EnumValue from '@/common/Enum'; import TagCodePath from '../component/TagCodePath.vue'; +import { isPrefixSubsequence } from '@/common/utils/string'; const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue')); const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue')); @@ -371,8 +372,7 @@ const setNowTabData = () => { }; const filterNode = (value: string, data: Tree) => { - if (!value) return true; - return data.codePath.toLowerCase().includes(value) || data.name.includes(value); + return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name); }; const search = async () => { diff --git a/mayfly_go_web/src/views/system/resource/ResourceList.vue b/mayfly_go_web/src/views/system/resource/ResourceList.vue index cedad989..66ddc10b 100644 --- a/mayfly_go_web/src/views/system/resource/ResourceList.vue +++ b/mayfly_go_web/src/views/system/resource/ResourceList.vue @@ -123,6 +123,7 @@ import { formatDate } from '@/common/utils/format'; import EnumTag from '@/components/enumtag/EnumTag.vue'; import { Contextmenu, ContextmenuItem } from '@/components/contextmenu'; import { Splitpanes, Pane } from 'splitpanes'; +import { isPrefixSubsequence } from '@/common/utils/string'; const menuTypeValue = ResourceTypeEnum.Menu.value; const permissionTypeValue = ResourceTypeEnum.Permission.value; @@ -209,10 +210,7 @@ watch(filterResource, (val) => { }); const filterNode = (value: string, data: any) => { - if (!value) { - return true; - } - return data.name.includes(value); + return !value || isPrefixSubsequence(value, data.name); }; const search = async () => { diff --git a/server/go.mod b/server/go.mod index a491e4d7..6bd26f60 100644 --- a/server/go.mod +++ b/server/go.mod @@ -32,8 +32,8 @@ require ( github.com/stretchr/testify v1.9.0 github.com/veops/go-ansiterm v0.0.5 go.mongodb.org/mongo-driver v1.15.0 // mongo - golang.org/x/crypto v0.23.0 // ssh - golang.org/x/oauth2 v0.20.0 + golang.org/x/crypto v0.24.0 // ssh + golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.7.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -94,8 +94,8 @@ require ( golang.org/x/exp v0.0.0-20230519143937-03e91628a987 // indirect golang.org/x/image v0.13.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect google.golang.org/genproto v0.0.0-20230131230820-1c016267d619 // indirect google.golang.org/grpc v1.52.3 // indirect google.golang.org/protobuf v1.34.1 // indirect diff --git a/server/internal/db/application/db_instance.go b/server/internal/db/application/db_instance.go index 96d5e196..dd93d0c0 100644 --- a/server/internal/db/application/db_instance.go +++ b/server/internal/db/application/db_instance.go @@ -2,7 +2,6 @@ package application import ( "context" - "errors" "mayfly-go/internal/common/consts" "mayfly-go/internal/db/application/dto" "mayfly-go/internal/db/dbm" @@ -13,14 +12,11 @@ import ( tagdto "mayfly-go/internal/tag/application/dto" tagentity "mayfly-go/internal/tag/domain/entity" "mayfly-go/pkg/base" - "mayfly-go/pkg/biz" "mayfly-go/pkg/errorx" "mayfly-go/pkg/logx" "mayfly-go/pkg/model" "mayfly-go/pkg/utils/collx" "mayfly-go/pkg/utils/structx" - - "gorm.io/gorm" ) type Instance interface { @@ -171,7 +167,7 @@ func (app *instanceAppImpl) SaveDbInstance(ctx context.Context, instance *dto.Sa } func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error { - instance, err := app.GetById(instanceId, "name") + instance, err := app.GetById(instanceId) if err != nil { return errorx.NewBiz("获取数据库实例错误,数据库实例ID为: %d", instance.Id) } @@ -180,26 +176,16 @@ func (app *instanceAppImpl) Delete(ctx context.Context, instanceId uint64) error DbInstanceId: instanceId, } err = app.restoreApp.restoreRepo.GetByCond(restore) - switch { - case err == nil: - biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name) - case errors.Is(err, gorm.ErrRecordNotFound): - break - default: - biz.ErrIsNil(err, "删除数据库实例失败: %v", err) + if err != nil { + return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库恢复任务。", instance.Name) } backup := &entity.DbBackup{ DbInstanceId: instanceId, } err = app.backupApp.backupRepo.GetByCond(backup) - switch { - case err == nil: - biz.ErrNotNil(err, "不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name) - case errors.Is(err, gorm.ErrRecordNotFound): - break - default: - biz.ErrIsNil(err, "删除数据库实例失败: %v", err) + if err != nil { + return errorx.NewBiz("不能删除数据库实例【%s】,请先删除关联的数据库备份任务。", instance.Name) } dbs, _ := app.dbApp.ListByCond(&entity.Db{ diff --git a/server/internal/machine/mcm/stats.go b/server/internal/machine/mcm/stats.go index f96c5b09..947786d2 100644 --- a/server/internal/machine/mcm/stats.go +++ b/server/internal/machine/mcm/stats.go @@ -264,6 +264,9 @@ func getInterfaceInfo(iInfo string, stats *Stats) (err error) { } func getCPU(cpuInfo string, stats *Stats) (err error) { + if !strings.Contains(cpuInfo, ":") { + return + } // %Cpu(s): 6.1 us, 3.0 sy, 0.0 ni, 90.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st value := strings.Split(cpuInfo, ":")[1] values := strings.Split(value, ",") diff --git a/server/internal/tag/application/tag_tree.go b/server/internal/tag/application/tag_tree.go index 98bc31b2..65a816cd 100644 --- a/server/internal/tag/application/tag_tree.go +++ b/server/internal/tag/application/tag_tree.go @@ -312,6 +312,7 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes } delTagType := param.ChildType + var childrenTagIds []uint64 for _, resourceTag := range resourceTags { // 获取所有关联的子标签 childrenTag, _ := p.ListByCond(model.NewCond().RLike("code_path", resourceTag.CodePath).Eq("type", delTagType)) @@ -319,14 +320,16 @@ func (p *tagTreeAppImpl) DeleteTagByParam(ctx context.Context, param *dto.DelRes continue } - childrenTagIds := collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 { + childrenTagIds = append(childrenTagIds, collx.ArrayMap(childrenTag, func(item *entity.TagTree) uint64 { return item.Id - }) - // 删除code_path下的所有子标签 - return p.deleteByIds(ctx, childrenTagIds) + })...) } - return nil + if len(childrenTagIds) == 0 { + return nil + } + // 删除code_path下的所有子标签 + return p.deleteByIds(ctx, collx.ArrayDeduplicate(childrenTagIds)) } func (p *tagTreeAppImpl) ListByQuery(condition *entity.TagTreeQuery, toEntity any) {