🔱: [client] sync upgrade with 21 commits [trident-sync]

Update README.md
This commit is contained in:
xiaojunnuo
2023-01-29 15:26:45 +08:00
parent 62e3945d30
commit d10e80bf83
567 changed files with 36438 additions and 2 deletions
@@ -0,0 +1,56 @@
<template>
<div class="fs-contentmenu-list" @click="rowClick">
<div
v-for="item in menulist"
:key="item.value"
:data-value="item.value"
class="fs-contentmenu-item"
flex="cross:center main:center"
>
<d2-icon v-if="item.icon" :name="item.icon" />
<div class="fs-contentmenu-item-title" flex-box="1">
{{ item.title }}
</div>
</div>
</div>
</template>
<script>
export default {
name: "FsContextmenuList",
props: {
menulist: {
type: Array,
default: () => []
}
},
methods: {
rowClick(event) {
let target = event.target;
while (!target.dataset.value) {
target = target.parentNode;
}
this.$emit("rowClick", target.dataset.value);
}
}
};
</script>
<style lang="less">
.fs-contentmenu-list {
.fs-contentmenu-item {
padding: 8px 20px 8px 15px;
margin: 0;
font-size: 14px;
color: #606266;
cursor: pointer;
&:hover {
background: #ecf5ff;
color: #66b1ff;
}
.fs-contentmenu-item-title {
margin-left: 10px;
}
}
}
</style>
@@ -0,0 +1,68 @@
<template>
<div v-show="flag" class="fs-contextmenu" :style="style">
<slot />
</div>
</template>
<script>
export default {
name: "FsContextmenu",
props: {
visible: {
type: Boolean,
default: false
},
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
}
},
computed: {
flag: {
get() {
if (this.visible) {
// 注册全局监听事件 [ 目前只考虑鼠标解除触发 ]
window.addEventListener("mousedown", this.watchContextmenu);
}
return this.visible;
},
set(newVal) {
this.$emit("update:visible", newVal);
}
},
style() {
return {
left: this.x + "px",
top: this.y + "px",
display: this.visible ? "block" : "none "
};
}
},
mounted() {
// 将菜单放置到body下
document.querySelector("body").appendChild(this.$el);
},
methods: {
watchContextmenu(event) {
if (!this.$el.contains(event.target) || event.button !== 0) this.flag = false;
window.removeEventListener("mousedown", this.watchContextmenu);
}
}
};
</script>
<style>
.fs-contextmenu {
position: absolute;
padding: 5px 0;
z-index: 2018;
background: #fff;
border: 1px solid #cfd7e5;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
</style>
@@ -0,0 +1,76 @@
<template>
<a-dropdown class="fs-locale-picker">
<fs-iconify icon="ion-globe-outline" @click.prevent></fs-iconify>
<template #overlay>
<a-menu @click="changeLocale">
<a-menu-item v-for="item in languages" :key="item.key" :command="item.key">
<div class="language-item">
<span v-if="item.key === current" class="icon-radio">
<span class="iconify" data-icon="ion:radio-button-on" data-inline="false"></span>
</span>
<span v-else class="icon-radio">
<span class="iconify" data-icon="ion:radio-button-off" data-inline="false"></span>
</span>
{{ item.label }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script>
import i18n from "../../../i18n";
import { computed, inject } from "vue";
import _ from "lodash-es";
export default {
name: "FsLocale",
setup() {
const languages = computed(() => {
const map = i18n.global.messages?.value || {};
const list = [];
_.forEach(map, (item, key) => {
list.push({
key,
label: item.label
});
});
return list;
});
const current = computed(() => {
return i18n.global.locale.value;
});
const routerReload = inject("fn:router.reload");
const localeChanged = inject("fn:locale.changed");
const changeLocale = (change) => {
i18n.global.locale.value = change.key;
routerReload();
localeChanged(change.key)
};
return {
languages,
current,
changeLocale
};
}
};
</script>
<style lang="less">
.locale-picker {
display: flex;
align-items: center;
}
.language-item {
display: flex;
align-items: center;
.icon-radio {
display: flex;
align-items: center;
}
.iconify {
margin-right: 5px;
}
}
</style>
@@ -0,0 +1,224 @@
import { useRoute, useRouter } from "vue-router";
import { ref, watch, onMounted, onUnmounted, resolveComponent, nextTick, defineComponent } from "vue";
import getEachDeep from "deepdash-es/getEachDeep";
import _ from "lodash-es";
import BScroll from "better-scroll";
import "./index.less";
const eachDeep = getEachDeep(_);
function useBetterScroll(enabled = true) {
let bsRef = ref(null);
let asideMenuRef = ref();
let onOpenChange = () => {};
if (enabled) {
function bsInit() {
if (asideMenuRef.value == null) {
return;
}
bsRef.value = new BScroll(asideMenuRef.value, {
mouseWheel: true,
click: true,
momentum: false,
// 如果你愿意可以打开显示滚动条
scrollbar: {
fade: true,
interactive: false
},
bounce: false
});
}
function bsDestroy() {
if (bsRef.value != null && bsRef.value.destroy) {
try {
bsRef.value.destroy();
} catch (e) {
// console.error(e);
} finally {
bsRef.value = null;
}
}
}
onMounted(() => {
bsInit();
});
onUnmounted(() => {
bsDestroy();
});
onOpenChange = async () => {
console.log("onOpenChange");
setTimeout(() => {
bsRef.value?.refresh();
}, 300);
};
}
return {
onOpenChange,
asideMenuRef
};
}
export default defineComponent({
name: "FsMenu",
inheritAttrs: true,
props: {
menus: {},
expandSelected: {
default: false
},
scroll: {}
},
setup(props, ctx) {
async function open(path) {
if (path == null) {
return;
}
if (path.startsWith("http://") || path.startsWith("https://")) {
window.open(path);
return;
}
try {
const navigationResult = await router.push(path);
if (navigationResult) {
// 导航被阻止
} else {
// 导航成功 (包括重新导航的情况)
}
} catch (e) {
console.error("导航失败", e);
}
}
function onSelect(item) {
open(item.key);
}
const FsIcon = resolveComponent("FsIcon");
const buildMenus = (children) => {
const slots = [];
if (children == null) {
return slots;
}
for (let sub of children) {
const title = () => {
if (sub?.meta?.icon) {
return (
<div class={"menu-item-title"}>
<FsIcon class={"anticon"} icon={sub.meta.icon} />
<span>{sub.title}</span>
</div>
);
}
return sub.title;
};
if (sub.children && sub.children.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const subSlots = {
default: () => {
return buildMenus(sub.children);
},
title
};
function onTitleClick() {
if (sub.path && ctx.attrs.mode === "horizontal") {
open(sub.path);
}
}
slots.push(<a-sub-menu key={sub.index} v-slots={subSlots} onTitleClick={onTitleClick} />);
} else {
slots.push(
<a-menu-item key={sub.path} title={sub.title}>
{title}
</a-menu-item>
);
}
}
return slots;
};
const slots = {
default() {
return buildMenus(props.menus);
}
};
const selectedKeys = ref([]);
const openKeys = ref([]);
const route = useRoute();
const router = useRouter();
function openSelectedParents(fullPath) {
if (!props.expandSelected) {
return;
}
if (props.menus == null) {
return;
}
const keys = [];
let changed = false;
eachDeep(props.menus, (value, key, parent, context) => {
if (value == null) {
return;
}
if (value.path === fullPath) {
_.forEach(context.parents, (item) => {
if (item.value instanceof Array) {
return;
}
keys.push(item.value.index);
});
}
});
if (keys.length > 0) {
for (let key of keys) {
if (openKeys.value.indexOf(key) === -1) {
openKeys.value.push(key);
changed = true;
}
}
}
return changed;
}
const { asideMenuRef, onOpenChange } = useBetterScroll(props.scroll);
watch(
() => {
return route.fullPath;
},
(path) => {
// path = route.fullPath;
selectedKeys.value = [path];
const changed = openSelectedParents(path);
if (changed) {
onOpenChange();
}
},
{
immediate: true
}
);
return () => {
const menu = (
<a-menu
mode={"inline"}
theme={"light"}
v-slots={slots}
onClick={onSelect}
onOpenChange={onOpenChange}
v-models={[
[openKeys.value, "openKeys"],
[selectedKeys.value, "selectedKeys"]
]}
{...ctx.attrs}
/>
);
const classNames = { "fs-menu-wrapper": true, "fs-menu-better-scroll": props.scroll };
return (
<div ref={asideMenuRef} class={classNames}>
{menu}
</div>
);
};
}
});
@@ -0,0 +1,11 @@
.fs-menu-wrapper{
height: 100%;
overflow-y: auto;
&.fs-menu-better-scroll{
overflow-y: hidden;
}
.menu-item-title{
display: flex;
align-items: center;
}
}
@@ -0,0 +1,54 @@
<template>
<div v-if="showSourceLink" class="fs-source-link-group">
<div class="fs-source-link" @click="goSource('https://gitee.com')">本页源码Gitee</div>
<div class="fs-source-link" @click="goSource('https://github.com')">本页源码Github</div>
</div>
</template>
<script>
import { defineComponent, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
export default defineComponent({
name: "FsSourceLink",
setup() {
const router = useRouter();
const showSourceLink = ref(false);
watch(
() => {
return router.currentRoute.value.fullPath;
},
(value) => {
showSourceLink.value = value !== "/index";
},
{ immediate: true }
);
const middle = "/fast-crud/fs-admin-antdv/tree/main/src/views";
function goSource(prefix) {
const path = router.currentRoute.value.fullPath;
window.open(prefix + middle + path + "/index.vue");
}
return {
goSource,
showSourceLink
};
}
});
</script>
<style lang="less">
.fs-source-link-group {
position: fixed;
right: 3px;
bottom: 20px;
.fs-source-link {
text-align: left;
cursor: pointer;
font-size: 12px;
border-radius: 5px 0 0 5px;
padding: 5px;
background: #666;
color: #fff;
margin-bottom: 5px;
}
}
</style>
@@ -0,0 +1,295 @@
<template>
<div class="fs-multiple-page-control-group">
<div class="fs-multiple-page-control-content">
<div class="fs-multiple-page-control-content-inner">
<a-tabs
class="fs-multiple-page-control fs-multiple-page-sort"
:active-key="page.getCurrent"
type="editable-card"
hide-add
@tabClick="handleClick"
@edit="handleTabEdit"
@contextmenu="handleContextmenu"
>
<a-tab-pane
v-for="item in page.getOpened"
:key="item.fullPath"
:tab="item.meta?.title || '未命名'"
:name="item.fullPath"
:closable="isTabClosable(item)"
/>
</a-tabs>
<!-- <fs-contextmenu v-model:visible="contextmenuFlag" :x="contentmenuX" :y="contentmenuY">-->
<!-- <fs-contextmenu-list-->
<!-- :menulist="tagName === '/index' ? contextmenuListIndex : contextmenuList"-->
<!-- @rowClick="contextmenuClick"-->
<!-- />-->
<!-- </fs-contextmenu>-->
</div>
</div>
<div class="fs-multiple-page-control-btn">
<a-dropdown-button class="control-btn-dropdown" split-button @click="closeAll">
<span class="iconify" data-icon="ion:close-circle" data-inline="false"></span>
<template #icon><DownOutlined /></template>
<template #overlay>
<a-menu @click="(command) => handleControlItemClick(command)">
<a-menu-item key="left">
<fs-icon name="arrow-left" class="fs-mr-10" />
关闭左侧
</a-menu-item>
<a-menu-item key="right">
<fs-icon name="arrow-right" class="fs-mr-10" />
关闭右侧
</a-menu-item>
<a-menu-item key="other">
<fs-icon name="times" class="fs-mr-10" />
关闭其它
</a-menu-item>
<a-menu-item key="all">
<fs-icon name="times-circle" class="fs-mr-10" />
全部关闭
</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>
</div>
</div>
</template>
<script>
import Sortable from "sortablejs";
import { usePageStore } from "../../../store/modules/page";
import { computed } from "vue";
export default {
name: "FsTabs",
components: {
// FsContextmenu: () => import("../contextmenu/index.vue"),
// FsContextmenuList: () => import("../contextmenu/components/contentmenuList/index.vue")
},
setup() {
const pageStore = usePageStore();
const actions = {
close: pageStore.close,
closeLeft: pageStore.closeLeft,
closeRight: pageStore.closeRight,
closeOther: pageStore.closeOther,
closeAll: pageStore.closeAll,
openedSort: pageStore.openedSort
};
console.log("opened", pageStore.getOpened);
const computeOpened = computed(() => {
console.log("opened", pageStore.getOpened);
return pageStore.getOpened;
});
return {
page: pageStore,
...actions,
computeOpened
};
},
data() {
return {
contextmenuFlag: false,
contentmenuX: 0,
contentmenuY: 0,
contextmenuListIndex: [{ icon: "times-circle", title: "关闭全部", value: "all" }],
contextmenuList: [
{ icon: "arrow-left", title: "关闭左侧", value: "left" },
{ icon: "arrow-right", title: "关闭右侧", value: "right" },
{ icon: "times", title: "关闭其它", value: "other" },
{ icon: "times-circle", title: "关闭全部", value: "all" }
],
tagName: "/index"
};
},
mounted() {
const el = document.querySelectorAll(".fs-multiple-page-sort .el-tabs__nav")[0];
// Sortable.create(el, {
// onEnd: (evt) => {
// const { oldIndex, newIndex } = evt;
// this.openedSort({ oldIndex, newIndex });
// }
// });
},
methods: {
/**
* @description 计算某个标签页是否可关闭
* @param {Object} page 其中一个标签页
*/
isTabClosable(page) {
return page.name !== "index";
},
/**
* @description 右键菜单功能点击
* @param {Object} event 事件
*/
handleContextmenu(event) {
let target = event.target;
// fix https://github.com/fs-projects/fs-admin/issues/54
let flag = false;
if (target.className.indexOf("el-tabs__item") > -1) flag = true;
else if (target.parentNode.className.indexOf("el-tabs__item") > -1) {
target = target.parentNode;
flag = true;
}
if (flag) {
event.preventDefault();
event.stopPropagation();
this.contentmenuX = event.clientX;
this.contentmenuY = event.clientY;
this.tagName = target.getAttribute("aria-controls").slice(5);
this.contextmenuFlag = true;
}
},
/**
* @description 右键菜单的 row-click 事件
* @param {String} command 事件类型
*/
contextmenuClick(command) {
this.handleControlItemClick(command, this.tagName);
},
/**
* @description 接收点击关闭控制上选项的事件
* @param {String} command 事件类型
* @param {String} tagName tab 名称
*/
handleControlItemClick(command, tagName = null) {
//if (tagName) this.contextmenuFlag = false;
const params = { pageSelect: tagName };
switch (command.key) {
case "left":
this.closeLeft(params);
break;
case "right":
this.closeRight(params);
break;
case "other":
this.closeOther(params);
break;
case "all":
this.closeAll();
break;
default:
this.$message.error("无效的操作");
break;
}
},
/**
* @description 接收点击 tab 标签的事件
* @param {object} tab 标签
* @param {object} event 事件
*/
handleClick(tab) {
// 找到点击的页面在 tag 列表里是哪个
const page = this.page.getOpened.find((page) => page.fullPath === tab);
if (page) {
const { name, params, query } = page;
this.$router.push({ name, params, query });
}
},
/**
* @description 点击 tab 上的删除按钮触发这里
* @param {String} tagName tab 名称
*/
handleTabEdit(tagName, action) {
if (action === "remove") {
this.close({ tagName });
}
}
}
};
</script>
<style lang="less">
//common
.fs-multiple-page-control-group {
width: 100%;
display: flex;
.fs-multiple-page-control-content {
flex: 1;
overflow-x: auto;
}
.fs-multiple-page-control-btn {
flex: 0;
}
}
//antdv
.fs-multiple-page-control-group {
.ant-tabs-bar {
margin: 0;
border-bottom: 1px solid #f0f0f0;
}
.ant-tabs-top > .ant-tabs-nav,
.ant-tabs-bottom > .ant-tabs-nav,
.ant-tabs-top > div > .ant-tabs-nav,
.ant-tabs-bottom > div > .ant-tabs-nav {
margin: 0;
}
.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav {
.ant-tabs-tab {
margin-right: 0;
border-right: 0;
&:first-of-type {
border-top-left-radius: 2px;
}
&:last-of-type {
border-top-right-radius: 2px;
border-right: 1px;
}
&:not(.ant-tabs-tab-active) {
color: #666;
}
}
.ant-tabs-tab-active {
border-bottom-color: #fff;
}
}
.ant-tabs-close-x {
display: none;
}
.ant-tabs-tab {
&:hover {
.ant-tabs-close-x {
display: initial;
}
}
}
.ant-tabs-tab-active {
.ant-tabs-close-x {
display: initial;
}
}
.fs-multiple-page-control-btn {
display: flex;
.ant-btn {
display: flex;
align-items: center;
justify-items: center;
height: 100%;
color: #666;
border-bottom: 1px solid #f0f0f0;
}
.control-btn-dropdown {
text-align: center;
}
}
.ant-tabs-tab-arrow-show {
border: 1px solid #e5e7eb;
}
.ant-tabs-tab-prev {
border-right: 0;
border-bottom: 0;
}
.ant-tabs-tab-next {
border-left: 0;
border-bottom: 0;
}
}
//element
</style>
@@ -0,0 +1,101 @@
<template>
<div class="fs-theme-color-picker">
<h4>主题色</h4>
<div class="fs-theme-colors">
<a-tooltip v-for="(item, index) in colorList" :key="index" class="fs-theme-color-item">
<template #title>
{{ item.key }}
</template>
<a-tag :color="item.color" @click="changeColor(item.color)">
<CheckOutlined v-if="item.color === primaryColor" />
</a-tag>
</a-tooltip>
</div>
</div>
</template>
<script>
import { defineComponent, ref } from "vue";
const colorListDefine = [
{
key: "薄暮",
color: "#f5222d"
},
{
key: "火山",
color: "#fa541c"
},
{
key: "日暮",
color: "#faad14"
},
{
key: "明青",
color: "#13c2c2"
},
{
key: "极光绿",
color: "#52c41a"
},
{
key: "拂晓蓝(默认)",
color: "#1890ff"
},
{
key: "极客蓝",
color: "#2f54eb"
},
{
key: "酱紫",
color: "#722ed1"
}
];
export default defineComponent({
name: "FsThemeColorPicker",
props: {
primaryColor: {
default: "#1890ff"
}
},
emits: ["change"],
setup(props, ctx) {
const colorList = ref(colorListDefine);
function changeColor(color) {
ctx.emit("change", color);
}
return {
colorList,
changeColor
};
}
});
</script>
<style lang="less">
.fs-theme-color-picker {
.fs-theme-colors {
margin-top: 10px;
display: flex;
justify-content: left;
justify-items: center;
align-items: center;
.fs-theme-color-item {
width: 20px;
height: 20px;
border-radius: 2px;
cursor: pointer;
margin-right: 8px;
padding-left: 0px;
padding-right: 0px;
text-align: center;
color: #fff;
font-weight: 700;
display: flex;
justify-content: center;
align-items: center;
i {
font-size: 14px;
}
}
}
}
</style>
@@ -0,0 +1,51 @@
<template>
<div class="fs-theme" @click="show()">
<fs-iconify icon="ion:sparkles-outline" />
<a-drawer
v-model:visible="visible"
title="主题设置"
placement="right"
width="350px"
:closable="false"
@after-visible-change="afterVisibleChange"
>
<fs-theme-color-picker
:primary-color="setting.getTheme.primaryColor"
@change="setting.setPrimaryColor"
></fs-theme-color-picker>
</a-drawer>
</div>
</template>
<script>
import { ref, defineComponent } from "vue";
import FsThemeColorPicker from "./color-picker.vue";
import { useSettingStore } from "/@/store/modules/settings";
export default defineComponent({
name: "FsTheme",
components: { FsThemeColorPicker },
setup() {
const visible = ref(false);
function afterVisibleChange() {}
function show() {
visible.value = true;
}
const setting = useSettingStore();
return {
visible,
show,
afterVisibleChange,
setting
};
}
});
</script>
<style lang="less">
.fs-theme {
}
.fs-theme-drawer {
}
</style>
@@ -0,0 +1,40 @@
<template>
<a-dropdown>
<div class="fs-user-info">您好{{ userStore.getUserInfo?.nickName }}</div>
<template #overlay>
<a-menu>
<a-menu-item>
<div @click="doLogout">注销登录</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script>
import { defineComponent } from "vue";
import { useUserStore } from "/src/store/modules/user";
import { Modal } from "ant-design-vue";
import { useI18n } from "vue-i18n";
export default defineComponent({
name: "FsUserInfo",
setup() {
const userStore = useUserStore();
console.log("user", userStore);
const { t } = useI18n();
function doLogout() {
Modal.confirm({
iconType: "warning",
title: t("app.login.logoutTip"),
content: t("app.login.logoutMessage"),
onOk: async () => {
await userStore.logout(true);
}
});
}
return {
userStore,
doLogout
};
}
});
</script>
@@ -0,0 +1,230 @@
<template xmlns:w="http://www.w3.org/1999/xhtml">
<a-layout class="fs-framework">
<a-layout-sider v-model:collapsed="asideCollapsed" :trigger="null" collapsible>
<div class="header-logo">
<img src="/images/logo/rect-black.svg" />
<span v-if="!asideCollapsed" class="title">FsAdmin</span>
</div>
<div class="aside-menu">
<fs-menu :scroll="true" :menus="asideMenus" :expand-selected="!asideCollapsed" />
</div>
</a-layout-sider>
<a-layout class="layout-body">
<a-layout-header class="header">
<div class="header-buttons">
<div class="menu-fold" @click="asideCollapsedToggle">
<MenuUnfoldOutlined v-if="asideCollapsed" />
<MenuFoldOutlined v-else />
</div>
</div>
<fs-menu
class="header-menu"
mode="horizontal"
:expand-selected="false"
:selectable="false"
:menus="frameworkMenus"
/>
<div class="header-right header-buttons">
<!-- <button-->
<!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"-->
<!-- w:text="sm white"-->
<!-- w:font="mono light"-->
<!-- w:p="y-2 x-4"-->
<!-- w:border="2 rounded blue-200"-->
<!-- >-->
<!-- Button-->
<!-- </button>-->
<fs-menu
class="header-menu"
mode="horizontal"
:expand-selected="false"
:selectable="false"
:menus="headerMenus"
/>
<fs-locale class="btn" />
<!-- <fs-theme-set class="btn" />-->
<fs-user-info class="btn" />
</div>
</a-layout-header>
<fs-tabs></fs-tabs>
<a-layout-content class="fs-framework-content">
<router-view>
<template #default="{ Component, route }">
<transition name="fade-transverse">
<keep-alive :include="keepAlive">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</template>
</router-view>
</a-layout-content>
<a-layout-footer class="fs-framework-footer"
>by fast-crud
<fs-source-link />
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script>
import { computed, onErrorCaptured, ref } from "vue";
import FsMenu from "./components/menu/index.jsx";
import FsLocale from "./components/locale/index.vue";
import FsSourceLink from "./components/source-link/index.vue";
import FsUserInfo from "./components/user-info/index.vue";
import FsTabs from "./components/tabs/index.vue";
import { useResourceStore } from "../store/modules/resource";
import { usePageStore } from "/@/store/modules/page";
import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
import FsThemeSet from "/@/layout/components/theme/index.vue";
import { notification } from "ant-design-vue";
export default {
name: "LayoutFramework",
// eslint-disable-next-line vue/no-unused-components
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs },
setup() {
const resourceStore = useResourceStore();
const frameworkMenus = computed(() => {
return resourceStore.getFrameworkMenus;
});
const headerMenus = computed(() => {
return resourceStore.getHeaderMenus;
});
const asideMenus = computed(() => {
return resourceStore.getAsideMenus;
});
const pageStore = usePageStore();
const keepAlive = pageStore.keepAlive;
const asideCollapsed = ref(false);
function asideCollapsedToggle() {
asideCollapsed.value = !asideCollapsed.value;
}
onErrorCaptured((e) => {
console.error("ErrorCaptured:", e);
notification.error({ message: e.message });
//阻止错误向上传递
return false;
});
return {
frameworkMenus,
headerMenus,
asideMenus,
keepAlive,
asideCollapsed,
asideCollapsedToggle
};
}
};
</script>
<style lang="less">
@import "../style/theme/index.less";
.fs-framework {
height: 100%;
overflow-x: hidden;
.header-logo {
width: 100%;
height: 50px;
display: flex;
justify-items: center;
align-items: center;
justify-content: center;
// margin: 16px 24px 16px 0;
//background: rgba(255, 255, 255, 0.3);
img {
height: 80%;
}
.title {
margin-left: 5px;
font-weight: bold;
}
}
.fs-framework-content {
flex: 1;
border-left: 1px solid #f0f0f0;
}
.fs-framework-footer {
border-left: 1px solid #f0f0f0;
padding: 10px 20px;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
background: #f6f6f6;
}
.header-buttons {
display: flex;
align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
& > .btn {
&:hover {
background-color: #fff;
color: @primary-color;
}
}
}
.header-right {
justify-content: flex-end;
align-items: center;
display: flex;
}
.header-menu {
flex: 1;
}
.aside-menu {
flex: 1;
ui {
height: 100%;
}
overflow: hidden;
// overflow-y: auto;
}
.layout-body {
flex: 1;
}
}
//antdv
.fs-framework {
&.ant-layout {
flex-direction: row;
}
.ant-layout-sider-children {
display: flex;
flex-direction: column;
}
.ant-layout-sider {
// border-right: 1px solid #eee;
}
.ant-layout-header {
height: 50px;
padding: 0 10px;
line-height: 50px;
display: flex;
justify-content: flex-start;
align-items: center;
}
.ant-layout-content {
background: #fff;
height: 100%;
position: relative;
}
}
//element
.fs-framework {
.el-aside {
.el-menu {
height: 100%;
}
}
}
</style>
@@ -0,0 +1,154 @@
<template>
<div id="userLayout" :class="['user-layout-wrapper']">
<div class="login-container flex-center">
<div class="user-layout-lang"></div>
<div class="user-layout-content">
<div class="top flex flex-col items-center justify-center">
<div class="header flex flex-row items-center">
<img src="/images/logo/rect-black.svg" class="logo" alt="logo" />
<span class="title">FsAdmin</span>
</div>
<div class="desc">fast-crud开发crud快如闪电</div>
</div>
<router-view />
<div class="footer">
<div class="links">
<a href="_self">帮助</a>
<a href="_self">隐私</a>
<a href="_self">条款</a>
</div>
<div class="copyright">Copyright &copy; 2021 Greper</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LayoutOutside"
};
</script>
<style lang="less" scoped>
#userLayout.user-layout-wrapper {
height: 100%;
&.mobile {
.container {
.main {
max-width: 368px;
width: 98%;
}
}
}
.login-container {
width: 100%;
min-height: 100%;
background: #f0f2f5 url(/src/assets/background.svg) no-repeat 50%;
background-size: 100%;
//padding: 50px 0 84px;
position: relative;
.user-layout-lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
.select-lang-trigger {
cursor: pointer;
padding: 12px;
margin-right: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 18px;
vertical-align: middle;
}
}
.user-layout-content {
padding: 32px 0 24px;
.top {
text-align: center;
.header {
height: 44px;
line-height: 44px;
.badge {
position: absolute;
display: inline-block;
line-height: 1;
vertical-align: middle;
margin-left: -12px;
margin-top: -10px;
opacity: 0.8;
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
border-style: none;
}
.title {
font-size: 33px;
color: rgba(0, 0, 0, 0.85);
font-family: Avenir, "Helvetica Neue", Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
}
.desc {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin-top: 12px;
margin-bottom: 40px;
}
}
.main {
min-width: 260px;
width: 368px;
margin: 0 auto;
}
.footer {
// position: absolute;
width: 100%;
bottom: 0;
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;
.links {
margin-bottom: 8px;
font-size: 14px;
a {
color: rgba(0, 0, 0, 0.45);
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
}
}
}
.copyright {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
}
a {
text-decoration: none;
}
}
}
</style>
@@ -0,0 +1,3 @@
<template>
<router-view />
</template>