v0.3.0: 单 sources 入口 + 路径后缀覆盖约定
破坏性重构。框架对外只暴露一个 `sources` glob 入口,消费侧任何 .vue 都按文件路径后缀匹配同后缀覆盖: - shrimp/components/<name>.vue 同后缀覆盖框架内置(login/layout/topbar/ShrimpTable/...) - shrimp/components/fields/Field<X>.vue 注册类型 <x>(首字母小写) - shrimp/module/<key>.vue 业务模块 变更: - 删除 customFields / moduleSources / overrideField / registerField 旧入口 - 删除 fieldOverrides Map,字段表纯靠 glob 扫描注册 - 框架内 .vue 对可覆盖 peer 改用 defineAsyncComponent(resolveVue(...)) (login → ShrimpLangSwitch / layout → sideM/navMenu/topbar/userbar/logo / ShrimpTable → ShrimpEditDrawer/ShrimpFilterDrawer/...) - views/ 全部并入 components/,App.vue 上提到 src/ 根 - LangSwitch → ShrimpLangSwitch,PowerTree → ShrimpPowerTree - module/FieldRelativeTime.vue → components/fields/FieldRelativeTime.vue - module/user.vue 改用 #column-updatedAt slot 替代旧的 overrideField 字段类型约定从 'field' + Name 改为 lowercase-first(FieldString → 'string'), 内置和消费侧统一。
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>404</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -78,21 +78,22 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, useSlots, onMounted, type Component } from 'vue'
|
||||
import { defineAsyncComponent, ref, computed, useSlots, onMounted, type Component } from 'vue'
|
||||
import { ITableCrudProto, IFieldConfig, IModuleInfo, IPageQuery } from '../IShrimpType'
|
||||
import { vueAdmin } from '../VueAdmin'
|
||||
import { vueAdmin, resolveVue } from '../VueAdmin'
|
||||
import { Plus, Delete as DeleteIcon, Download, Upload, Edit, VideoPlay, VideoPause, Operation } from '@element-plus/icons-vue'
|
||||
import * as ElIcons from '@element-plus/icons-vue'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useTableUrlSync } from '../composables/useTableUrlSync'
|
||||
import ShrimpEditDrawer from './ShrimpEditDrawer.vue'
|
||||
import ShrimpFilterDrawer from './ShrimpFilterDrawer.vue'
|
||||
import ShrimpSearchInput from './ShrimpSearchInput.vue'
|
||||
import ShrimpFilterTrigger from './ShrimpFilterTrigger.vue'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
const ShrimpEditDrawer = defineAsyncComponent(resolveVue('components/ShrimpEditDrawer.vue'))
|
||||
const ShrimpFilterDrawer = defineAsyncComponent(resolveVue('components/ShrimpFilterDrawer.vue'))
|
||||
const ShrimpSearchInput = defineAsyncComponent(resolveVue('components/ShrimpSearchInput.vue'))
|
||||
const ShrimpFilterTrigger = defineAsyncComponent(resolveVue('components/ShrimpFilterTrigger.vue'))
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<template v-if="mode === 'display'">
|
||||
<el-tooltip :content="fullTime" placement="top">
|
||||
<span>{{ relativeTime }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-date-picker v-else :model-value="modelValue" @update:model-value="$emit('update:modelValue', $event)" :disabled="disabled" type="datetime" style="width: 100%" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { vueAdmin } from '../VueAdmin'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
field: any
|
||||
modelValue: any
|
||||
mode: 'display' | 'edit'
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: any]
|
||||
}>()
|
||||
|
||||
const fullTime = computed(() => {
|
||||
if (!props.modelValue) return '-'
|
||||
return new Date(props.modelValue).toLocaleString(vueAdmin.locale === 'en' ? 'en-US' : 'zh-CN')
|
||||
})
|
||||
|
||||
const relativeTime = computed(() => {
|
||||
if (!props.modelValue) return '-'
|
||||
const diff = Date.now() - props.modelValue
|
||||
if (diff < 60000) return t('table.justNow')
|
||||
if (diff < 3600000) return t('table.minutesAgo', { n: Math.floor(diff / 60000) })
|
||||
if (diff < 86400000) return t('table.hoursAgo', { n: Math.floor(diff / 3600000) })
|
||||
if (diff < 2592000000) return t('table.daysAgo', { n: Math.floor(diff / 86400000) })
|
||||
return fullTime.value
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<section class="aminui-wrapper">
|
||||
<div v-if="!vueAdmin.state.isMobile" class="aminui-side-split">
|
||||
<div class="aminui-side-split-top">
|
||||
<logo />
|
||||
</div>
|
||||
<div class="adminui-side-split-scroll">
|
||||
<div class="sidebar-menu-wrap">
|
||||
<el-scrollbar>
|
||||
<ul>
|
||||
<li v-for="item in vueAdmin.menuTree"
|
||||
:class="pmenu?.name == item.name ? 'active' : ''"
|
||||
@click="showMenu(item)">
|
||||
<el-icon>
|
||||
<component :is="item.icon || 'el-icon-menu'" />
|
||||
</el-icon>
|
||||
<p>{{ vueAdmin.t(item.label) }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!vueAdmin.state.isMobile && nextMenu.length > 0" class="aminui-side">
|
||||
<div class="adminui-side-scroll">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="$route.fullPath" router>
|
||||
<navMenu :navMenus="nextMenu"></navMenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<SideM v-if="vueAdmin.state.isMobile"></SideM>
|
||||
<div class="aminui-body el-container">
|
||||
<Topbar>
|
||||
<userbar></userbar>
|
||||
</Topbar>
|
||||
<div class="adminui-main" id="adminui-main">
|
||||
<router-view :key="$route.path" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, watch } from "vue";
|
||||
import { vueAdmin, resolveVue } from "../VueAdmin";
|
||||
import { type IMenuItem } from "../IShrimpType";
|
||||
|
||||
const SideM = defineAsyncComponent(resolveVue('components/sideM.vue'));
|
||||
const navMenu = defineAsyncComponent(resolveVue('components/navMenu.vue'));
|
||||
const Topbar = defineAsyncComponent(resolveVue('components/topbar.vue'));
|
||||
const Userbar = defineAsyncComponent(resolveVue('components/userbar.vue'));
|
||||
const Logo = defineAsyncComponent(resolveVue('components/logo.vue'));
|
||||
|
||||
// 初始化选中菜单
|
||||
let currentPath = vueAdmin.router.currentRoute.value.path;
|
||||
let pick = vueAdmin.getMenuIndex(currentPath);
|
||||
let menu = pick >= 0 ? vueAdmin.menuTree[pick] : vueAdmin.menuTree[0];
|
||||
let pmenu = ref(menu);
|
||||
let nextMenu = ref(getMenu(vueAdmin.isGroupMenu(menu) ? menu.children : []));
|
||||
|
||||
function getMenu(children?: IMenuItem[]) {
|
||||
if (children == null) return [];
|
||||
return children.slice()
|
||||
}
|
||||
|
||||
function getFirstLeafPath(item: IMenuItem): string {
|
||||
if (item.path) return item.path
|
||||
if (item.moduleKey) return '/' + item.moduleKey
|
||||
if (item.children && item.children.length > 0) {
|
||||
return getFirstLeafPath(item.children[0])
|
||||
}
|
||||
return '/' + item.name
|
||||
}
|
||||
|
||||
function showMenu(route: any) {
|
||||
pmenu.value = route;
|
||||
nextMenu.value = getMenu(route.children);
|
||||
if (route.children == null || route.children.length == 0) {
|
||||
const path = route.path || '/' + route.name
|
||||
vueAdmin.router.push(path)
|
||||
} else {
|
||||
// 展开菜单的同时,跳到第一个可跳转的叶子节点
|
||||
const path = getFirstLeafPath(route.children[0])
|
||||
vueAdmin.router.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
// 路由变化时同步更新 pmenu 和 nextMenu
|
||||
watch(() => vueAdmin.router.currentRoute.value.path, (path) => {
|
||||
const idx = vueAdmin.getMenuIndex(path)
|
||||
if (idx >= 0) {
|
||||
const m = vueAdmin.menuTree[idx]
|
||||
if (pmenu.value?.name !== m.name) {
|
||||
pmenu.value = m
|
||||
nextMenu.value = getMenu(vueAdmin.isGroupMenu(m) ? m.children : [])
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.sidebar-menu-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-menu-wrap .el-scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="login_bg">
|
||||
<LangSwitch class="login-lang" />
|
||||
<div class="login_main">
|
||||
<div class="login-form">
|
||||
<div class="login-header">
|
||||
<div class="logo">
|
||||
<label>{{ vueAdmin.adminConfig.title || 'Admin' }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="loginForm" :model="form" :rules="rules" label-width="0" size="large" @keyup.enter="login">
|
||||
<el-form-item prop="user">
|
||||
<el-input v-model="form.user" prefix-icon="el-icon-user" clearable
|
||||
:placeholder="$t('login.userPlaceholder')">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" prefix-icon="el-icon-lock" clearable show-password
|
||||
:placeholder="$t('login.PWPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" style="width: 100%" :loading="info.islogin" round @click="login">{{
|
||||
$t("login.signIn") }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { vueAdmin, resolveVue } from "../VueAdmin";
|
||||
import { defineAsyncComponent, reactive, computed, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const LangSwitch = defineAsyncComponent(resolveVue('components/ShrimpLangSwitch.vue'));
|
||||
|
||||
vueAdmin.token = null;
|
||||
|
||||
let { t } = useI18n();
|
||||
|
||||
let info = reactive({ islogin: false });
|
||||
let form = reactive({ user: import.meta.env.DEV ? "admin" : "", password: import.meta.env.DEV ? "admin" : "" });
|
||||
let rules = computed(() => ({
|
||||
user: [{ required: true, message: t("login.userError"), trigger: "blur" }],
|
||||
password: [{ required: true, message: t("login.PWError"), trigger: "blur" }],
|
||||
}));
|
||||
|
||||
// 页面加载时获取后台配置
|
||||
onMounted(() => {
|
||||
vueAdmin.fetchConfig().then(data => {
|
||||
Object.assign(vueAdmin.adminConfig, data)
|
||||
}).catch(() => {})
|
||||
})
|
||||
|
||||
function login() {
|
||||
info.islogin = true;
|
||||
vueAdmin.fetchLogin({ username: form.user, password: form.password })
|
||||
.then(async (data) => {
|
||||
vueAdmin.token = data.token;
|
||||
await vueAdmin.loadUserInfo();
|
||||
info.islogin = false;
|
||||
vueAdmin.router.push(vueAdmin.getFirstMenuPath());
|
||||
})
|
||||
.catch((e: any) => {
|
||||
console.warn('[Login]', e.message)
|
||||
info.islogin = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login_bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login_main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-header .logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-header .logo label {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.login-lang {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.login-form {
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.login_main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<router-link :to="vueAdmin.getFirstMenuPath()">
|
||||
<el-icon class="logo" size="20">
|
||||
<el-icon-home-filled />
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { vueAdmin } from "../VueAdmin";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div v-if="navMenus.length <= 0" style="padding: 20px">
|
||||
<el-alert :title="$t('base.noNavMenu')" center type="info" :closable="false"></el-alert>
|
||||
</div>
|
||||
<template v-for="navMenu in navMenus" v-bind:key="navMenu">
|
||||
<el-menu-item v-if="!hasChildren(navMenu)" :index="'/' + navMenu.name">
|
||||
<el-icon v-if="navMenu.icon">
|
||||
<component :is="navMenu.icon || 'el-icon-menu'" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span>{{ vueAdmin.t(navMenu.label) }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="'/' + navMenu.name">
|
||||
<template #title>
|
||||
<el-icon v-if="navMenu.icon">
|
||||
<component :is="navMenu.icon || 'el-icon-menu'" />
|
||||
</el-icon>
|
||||
<span>{{ vueAdmin.t(navMenu.label) }}</span>
|
||||
</template>
|
||||
<NavMenu :navMenus="navMenu.children"></NavMenu>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { vueAdmin } from "../VueAdmin";
|
||||
defineProps({
|
||||
navMenus: {
|
||||
type: [Object],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
function hasChildren(item: any) {
|
||||
return item.children && item.children.length > 0;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="mobile-nav-button" @click="showMobileNav">
|
||||
<el-icon><el-icon-menu /></el-icon>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
:size="240"
|
||||
v-model="nav"
|
||||
direction="ltr"
|
||||
:with-header="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-container class="mobile-nav">
|
||||
<el-header>
|
||||
<logo/>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="$route.fullPath"
|
||||
@select="select"
|
||||
router
|
||||
background-color="#212d3d"
|
||||
text-color="#fff"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<navMenu :navMenus="vueAdmin.menuTree"></navMenu>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from "vue";
|
||||
import { vueAdmin, resolveVue } from '../VueAdmin';
|
||||
|
||||
const navMenu = defineAsyncComponent(resolveVue('components/navMenu.vue'));
|
||||
const Logo = defineAsyncComponent(resolveVue('components/logo.vue'));
|
||||
|
||||
let nav = ref(false);
|
||||
function showMobileNav() { nav.value = true; }
|
||||
function select() {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-nav-button {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
z-index: 10;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #409eff;
|
||||
box-shadow: 0 2px 12px 0 rgba(64, 158, 255, 1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.mobile-nav-button i {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
background: #212d3d;
|
||||
}
|
||||
.mobile-nav .el-header {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.mobile-nav .el-main {
|
||||
padding: 0;
|
||||
}
|
||||
.mobile-nav .logo-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="adminui-topbar">
|
||||
<div class="left-panel">
|
||||
<el-button v-if="canBack" :icon="ArrowLeft" link size="large" class="back-btn" @click="handleBack">{{ $t('base.back') }}</el-button>
|
||||
<el-breadcrumb separator-icon="el-icon-arrow-right">
|
||||
<template v-for="(item, index) in breadcrumb" :key="index">
|
||||
<el-breadcrumb-item v-if="item.to && index < breadcrumb.length - 1" :to="item.to">
|
||||
{{ vueAdmin.t(item.label) }}
|
||||
</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-else>{{ vueAdmin.t(item.label) }}</el-breadcrumb-item>
|
||||
</template>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="center-panel"></div>
|
||||
<div class="right-panel">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { vueAdmin } from "../VueAdmin";
|
||||
import { computed } from "vue";
|
||||
import { ArrowLeft } from "@element-plus/icons-vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
interface BreadcrumbItem {
|
||||
key: string;
|
||||
label: any;
|
||||
to?: RouteLocationRaw;
|
||||
}
|
||||
|
||||
// 把 ?id + ?pids 复原成 root→immediate-parent 的 id 数组
|
||||
function getAncestorIds(): string[] {
|
||||
const route = vueAdmin.router.currentRoute.value;
|
||||
const pids = ((route.query.pids as string) || '').split(',').filter(Boolean);
|
||||
const id = route.query.id as string | undefined;
|
||||
return id ? [...pids, id] : pids;
|
||||
}
|
||||
|
||||
const breadcrumb = computed<BreadcrumbItem[]>(() => {
|
||||
const route = vueAdmin.router.currentRoute.value;
|
||||
const raw = vueAdmin.getBreadcrumb(route.path);
|
||||
const ancestors = getAncestorIds();
|
||||
|
||||
return raw.map(item => {
|
||||
if (!item.path) return { key: item.key, label: item.label };
|
||||
// path 段数 - 1 = 模块链深度 (0=顶层, >=1=子模块)
|
||||
const depth = item.path.split('/').filter(Boolean).length - 1;
|
||||
if (depth <= 0) return { key: item.key, label: item.label, to: item.path };
|
||||
// 该 depth 的页面需要 ?id = ancestors[depth-1], pids = ancestors[0..depth-2]
|
||||
const id = ancestors[depth - 1];
|
||||
if (!id) return { key: item.key, label: item.label, to: item.path };
|
||||
const newPids = ancestors.slice(0, depth - 1);
|
||||
const query: Record<string, string> = { id };
|
||||
if (newPids.length > 0) query.pids = newPids.join(',');
|
||||
return { key: item.key, label: item.label, to: { path: item.path, query } };
|
||||
});
|
||||
});
|
||||
|
||||
// 倒数第二个可点的 item 即"上一级"
|
||||
const backTarget = computed<RouteLocationRaw | undefined>(() => {
|
||||
const list = breadcrumb.value;
|
||||
for (let i = list.length - 2; i >= 0; i--) {
|
||||
if (list[i].to) return list[i].to;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const canBack = computed(() => !!backTarget.value);
|
||||
|
||||
function handleBack() {
|
||||
// 优先用浏览器历史(保留来时的完整 query);无历史则按 breadcrumb 计算的目标 push
|
||||
if (window.history.length > 1) {
|
||||
vueAdmin.router.back();
|
||||
} else if (backTarget.value) {
|
||||
vueAdmin.router.push(backTarget.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-left: 10px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.el-breadcrumb {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 小屏: 只保留最后一段(当前页);返回按钮和菜单覆盖上层导航 */
|
||||
@media (max-width: 768px) {
|
||||
.el-breadcrumb :deep(.el-breadcrumb__item):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="user-bar">
|
||||
<LangSwitch class="panel-item" />
|
||||
<div class="panel-item" @click="screen">
|
||||
<el-icon><el-icon-full-screen /></el-icon>
|
||||
</div>
|
||||
<el-dropdown class="user" trigger="click" @command="handleUser">
|
||||
<div class="user-avatar dropdown-panel-item">
|
||||
<label>{{ vueAdmin.userName || 'Admin' }}</label>
|
||||
<el-icon class="el-icon--right"><el-icon-arrow-down /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="clearCache">{{ $t("base.clearCache") }}</el-dropdown-item>
|
||||
<el-dropdown-item divided command="outLogin">{{ $t("base.logout") }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { vueAdmin, resolveVue } from "../VueAdmin";
|
||||
import { defineAsyncComponent } from "vue";
|
||||
|
||||
const LangSwitch = defineAsyncComponent(resolveVue('components/ShrimpLangSwitch.vue'));
|
||||
|
||||
function screen() {
|
||||
vueAdmin.fullscreen();
|
||||
}
|
||||
function handleUser(command: string) {
|
||||
switch (command) {
|
||||
case "clearCache":
|
||||
vueAdmin.clearCache();
|
||||
vueAdmin.reset();
|
||||
break;
|
||||
case "outLogin":
|
||||
vueAdmin.reset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-bar .panel-item {
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bar .panel-item i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.user-bar .panel-item:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-panel-item {
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-panel-item:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-bar .user-avatar {
|
||||
height: 49px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-bar .user-avatar label {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user