feat(admin): add msh_single_admin project and harden ignore rules

Introduce the new Vue admin project into version control while tightening gitignore patterns to keep env files, logs, build artifacts, and test outputs out of commits.

Made-with: Cursor
This commit is contained in:
msh-agent
2026-04-15 19:31:32 +08:00
parent a840045fc1
commit ceac1c0340
713 changed files with 119926 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
<template>
<div :class="{ show: show }" class="header-search">
<i class="iconfont iconios-search" style="font-size: 20px" @click.stop="click"></i>
<!--<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />-->
<el-select
ref="headerSearchSelect"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="搜索菜单"
class="header-search-select"
@change="change"
>
<el-option v-for="item in options" :key="item.url" :value="item" :label="item.name.join(' > ')" />
</el-select>
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js';
import path from 'path';
import { mapGetters } from 'vuex';
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined,
};
},
computed: {
...mapGetters(['permission_routes']),
// routes() {
// return this.$store.getters.permission_routes
// }
},
watch: {
routes(n) {
this.searchPool = this.generateRoutes(this.permission_routes);
},
searchPool(list) {
this.initFuse(list);
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close);
} else {
document.body.removeEventListener('click', this.close);
}
},
},
mounted() {
this.searchPool = this.generateRoutes(this.permission_routes);
},
methods: {
click() {
this.show = !this.show;
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus();
}
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur();
this.options = [];
this.show = false;
},
change(val) {
this.$router.push(val.path);
this.search = '';
this.options = [];
this.$nextTick(() => {
this.show = false;
});
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{
name: 'name',
weight: 0.7,
},
{
name: 'url',
weight: 0.3,
},
],
});
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = [];
for (const router of routes) {
// skip hidden router
if (router.hidden) {
continue;
}
const data = {
path: path.resolve(basePath, router.url),
name: [...prefixTitle],
children: router.child || [],
};
if (router.name) {
data.name = [...data.name, router.name];
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data);
}
}
// recursive child routes
if (router.child) {
const tempRoutes = this.generateRoutes(router.child, data.url, data.name);
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes];
}
}
}
return res;
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query);
} else {
this.options = [];
}
},
},
};
</script>
<style lang="scss" scoped>
.header-search {
font-size: 0 !important;
display: inline-flex !important;
cursor: pointer;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
/*vertical-align: middle;*/
line-height: 50px;
::v-deep .el-input__inner {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
/*border-bottom: 1px solid #d9d9d9;*/
vertical-align: middle;
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
}
}
</style>