mirror of
https://github.com/certd/certd.git
synced 2026-04-24 12:27:25 +08:00
🔱: [client] sync upgrade with 21 commits [trident-sync]
Update README.md
This commit is contained in:
+56
@@ -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 © 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>
|
||||
Reference in New Issue
Block a user