设置用户偏好.

This commit is contained in:
lijiahangmax
2023-09-26 01:11:57 +08:00
parent 99d6d16a04
commit b46541ad36
12 changed files with 243 additions and 205 deletions

View File

@@ -1,15 +1,16 @@
<template>
<a-config-provider :locale="locale">
<router-view />
<global-setting />
<global-setting ref="globalSettingRef" />
</a-config-provider>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, provide, ref } from 'vue';
import zhCN from '@arco-design/web-vue/es/locale/lang/zh-cn';
import GlobalSetting from '@/components/global-setting/index.vue';
import useLocale from '@/hooks/locale';
import { openGlobalSettingKey } from '@/types/symbol';
const { currentLocale } = useLocale();
const locale = computed(() => {
@@ -20,4 +21,11 @@
return zhCN;
}
});
// 对外暴露打开配置方法
const globalSettingRef = ref();
provide(openGlobalSettingKey, () => {
globalSettingRef.value.open();
});
</script>

View File

@@ -1,14 +1,15 @@
<template>
<div class="block">
<h5 class="title">{{ title }}</h5>
<div v-for="option in options" :key="option.name" class="switch-wrapper">
<div v-for="option in options" :key="option.name" class="option-wrapper">
<!-- 偏好项 -->
<span>{{ option.name }}</span>
<form-wrapper
:type="option.type || 'switch'"
:name="option.key"
:default-value="option.defaultVal"
@input-change="handleChange"
/>
<!-- input -->
<form-wrapper :name="option.key"
:type="option.type"
:default-value="option.defaultVal"
:options="option.options"
@input-change="handleChange" />
</div>
</div>
</template>
@@ -17,19 +18,18 @@
import { PropType } from 'vue';
import { useAppStore } from '@/store';
import FormWrapper from './form-wrapper.vue';
import { RadioOption } from '@arco-design/web-vue/es/radio/interface';
interface OptionsProps {
name: string;
key: string;
type?: string;
defaultVal?: boolean | string | number;
options?: Array<RadioOption>;
}
defineProps({
title: {
type: String,
default: '',
},
title: String,
options: {
type: Array as PropType<OptionsProps[]>,
default() {
@@ -56,13 +56,17 @@
menuCollapse: false,
});
}
// 修改配置
appStore.updateSettings({ [key]: value });
// TODO 同步偏好
};
</script>
<style scoped lang="less">
.block {
margin-bottom: 24px;
user-select: none;
}
.title {
@@ -71,7 +75,7 @@
font-size: 14px;
}
.switch-wrapper {
.option-wrapper {
display: flex;
align-items: center;
justify-content: space-between;

View File

@@ -1,22 +1,34 @@
<template>
<!-- 数字框 -->
<a-input-number v-if="type === 'number'"
:style="{ width: '80px' }"
size="small"
:default-value="defaultValue as number"
@change="handleChange"
hide-button
/>
<a-switch v-else
hide-button />
<!-- 开关 -->
<a-switch v-else-if="type === 'switch'"
type="round"
:default-checked="defaultValue"
size="small"
@change="handleChange" />
<!-- 单选按钮 -->
<a-radio-group v-else-if="type === 'radio-group'"
type="button"
size="mini"
:default-value="defaultValue"
:options="options"
@change="handleChange" />
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
import { RadioOption } from '@arco-design/web-vue/es/radio/interface';
const props = defineProps({
type: {
type: String,
default: '',
default: 'switch',
},
name: {
type: String,
@@ -26,6 +38,10 @@
type: [String, Boolean, Number],
default: '',
},
options: {
type: Array as PropType<Array<RadioOption>>,
default: []
}
});
const emit = defineEmits(['inputChange']);
const handleChange = (value: unknown) => {

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="!appStore.navbar" class="fixed-settings" @click="setVisible">
<div v-if="!appStore.navbar" class="fixed-settings" @click="open">
<a-button type="primary">
<template #icon>
<icon-settings />
@@ -8,14 +8,12 @@
</div>
<a-drawer title="偏好设置"
:width="300"
unmount-on-close
:visible="visible"
ok-text="保存"
cancel-text="关闭"
@ok="saveConfig"
@cancel="cancel">
<Block :options="contentOpts" title="内容区域" />
<Block :options="othersOpts" title="其他设置" />
:footer="false"
:unmount-on-close="true"
@cancel="() => setVisible(false)">
<Block :options="layoutOpts" title="布局设置" />
<Block :options="viewsOpts" title="页面视图" />
</a-drawer>
</template>
@@ -23,16 +21,19 @@
import { computed } from 'vue';
import { useAppStore } from '@/store';
import Block from './block.vue';
const emit = defineEmits(['cancel']);
import useVisible from '@/hooks/visible';
const appStore = useAppStore();
const visible = computed(() => appStore.globalSettings);
const { visible, setVisible } = useVisible();
/**
* 内容配置
*/
const contentOpts = computed(() => [
// 打开
const open = () => {
setVisible(true);
};
defineExpose({ open });
// 布局设置
const layoutOpts = computed(() => [
{
name: '导航栏',
key: 'navbar',
@@ -58,45 +59,44 @@
key: 'tabBar',
defaultVal: appStore.tabBar
},
{
name: '菜单宽度 (px)',
key: 'menuWidth',
defaultVal: appStore.menuWidth,
type: 'number',
},
]);
/**
* 其他配置
*/
const othersOpts = computed(() => [
{
name: '色弱模式',
key: 'colorWeak',
defaultVal: appStore.colorWeak,
},
{
name: '菜单宽度 (px)',
key: 'menuWidth',
type: 'number',
defaultVal: appStore.menuWidth,
},
]);
/**
* 取消配置
*/
const cancel = () => {
appStore.updateSettings({ globalSettings: false });
emit('cancel');
};
// 页面视图配置
const viewsOpts = computed(() => [
{
name: '主机列表',
key: 'host',
type: 'radio-group',
defaultVal: appStore.host,
options: [{ value: 'table', label: '表格' }, { value: 'card', label: '卡片' }]
},
{
name: '主机秘钥',
key: 'hostKeys',
type: 'radio-group',
defaultVal: appStore.hostKeys,
options: [{ value: 'table', label: '表格' }, { value: 'card', label: '卡片' }]
},
{
name: '主机身份',
key: 'hostIdentity',
type: 'radio-group',
defaultVal: appStore.hostIdentity,
options: [{ value: 'table', label: '表格' }, { value: 'card', label: '卡片' }]
},
]);
/**
* 复制配置
*/
const saveConfig = async () => {
};
/**
* 显示菜单
*/
const setVisible = () => {
appStore.updateSettings({ globalSettings: true });
};
</script>
<style scoped lang="less">

View File

@@ -5,22 +5,16 @@
<a-space>
<!-- FIXME -->
<!-- LOGO -->
<img
alt="logo"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image"
/>
<img alt="logo"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image" />
<!-- 标头 -->
<a-typography-title
:style="{ margin: 0, fontSize: '18px' }"
:heading="5">
<a-typography-title :heading="5" :style="{ margin: 0, fontSize: '18px' }">
Orion Ops Pro
</a-typography-title>
<!-- 收缩菜单 -->
<icon-menu-fold
v-if="!topMenu && appStore.device === 'mobile'"
style="font-size: 22px; cursor: pointer"
@click="toggleDrawerMenu"
/>
<icon-menu-fold v-if="!topMenu && appStore.device === 'mobile'"
style="font-size: 22px; cursor: pointer"
@click="toggleDrawerMenu" />
</a-space>
</div>
<!-- 顶部菜单 -->
@@ -42,18 +36,17 @@
<!-- 切换语言 -->
<li v-if="false">
<a-tooltip content="语言">
<a-button
class="nav-btn"
type="outline"
:shape="'circle'"
@click="setUserInfoVisible">
<a-button class="nav-btn"
type="outline"
:shape="'circle'"
@click="setUserInfoVisible">
<template #icon>
<icon-language />
</template>
</a-button>
</a-tooltip>
<a-dropdown trigger="click" @select="changeLocale">
<div ref="refUserInfoTrigger" class="trigger-btn"></div>
<div ref="refUserInfoTrigger" class="trigger-btn" />
<template #content>
<a-doption v-for="item in locales"
:key="item.value"
@@ -71,11 +64,10 @@
<a-tooltip :content="theme === 'light'
? '点击切换为暗黑模式'
: '点击切换为亮色模式'">
<a-button
class="nav-btn"
type="outline"
:shape="'circle'"
@click="handleToggleTheme">
<a-button class="nav-btn"
type="outline"
:shape="'circle'"
@click="handleToggleTheme">
<template #icon>
<icon-moon-fill v-if="theme === 'dark'" />
<icon-sun-fill v-else />
@@ -88,22 +80,20 @@
<a-tooltip content="消息通知">
<div class="message-box-trigger">
<a-badge :count="9" dot>
<a-button
class="nav-btn"
type="outline"
:shape="'circle'"
@click="setMessageBoxVisible">
<a-button class="nav-btn"
type="outline"
:shape="'circle'"
@click="setMessageBoxVisible">
<icon-notification />
</a-button>
</a-badge>
</div>
</a-tooltip>
<a-popover
trigger="click"
:arrow-style="{ display: 'none' }"
:content-style="{ padding: 0, minWidth: '400px' }"
content-class="message-popover">
<div ref="refMessageBoxTrigger" class="ref-btn"></div>
<a-popover trigger="click"
:arrow-style="{ display: 'none' }"
:content-style="{ padding: 0, minWidth: '400px' }"
content-class="message-popover">
<div ref="refMessageBoxTrigger" class="ref-btn" />
<template #content>
<message-box />
</template>
@@ -114,11 +104,10 @@
<a-tooltip :content="isFullscreen
? '点击退出全屏模式'
: '点击切换全屏模式'">
<a-button
class="nav-btn"
type="outline"
shape="circle"
@click="toggleFullScreen">
<a-button class="nav-btn"
type="outline"
shape="circle"
@click="toggleFullScreen">
<template #icon>
<icon-fullscreen-exit v-if="isFullscreen" />
<icon-fullscreen v-else />
@@ -129,11 +118,10 @@
<!-- 偏好设置 -->
<li>
<a-tooltip content="偏好设置">
<a-button
class="nav-btn"
type="outline"
shape="circle"
@click="setSettingVisible">
<a-button class="nav-btn"
type="outline"
shape="circle"
@click="openGlobalSetting">
<template #icon>
<icon-settings />
</template>
@@ -144,9 +132,8 @@
<li>
<a-dropdown trigger="click">
<!-- 头像 -->
<a-avatar
:size="32"
:style="{ cursor: 'pointer', backgroundColor: '#3370ff' }">
<a-avatar :size="32"
:style="{ cursor: 'pointer', backgroundColor: '#3370ff' }">
{{ nickname }}
</a-avatar>
<template #content>
@@ -188,6 +175,7 @@
import { triggerMouseEvent } from '@/utils';
import Menu from '@/components/menu/tree/index.vue';
import MessageBox from '../message-box/index.vue';
import { openGlobalSettingKey, toggleDrawerMenuKey } from '@/types/symbol';
const appStore = useAppStore();
const userStore = useUserStore();
@@ -223,9 +211,7 @@
};
// 打开系统设置
const setSettingVisible = () => {
appStore.updateSettings({ globalSettings: true });
};
const openGlobalSetting = inject(openGlobalSettingKey) as () => void;
// 消息触发器 ref
const refMessageBoxTrigger = ref();
@@ -245,7 +231,7 @@
};
// 注入收缩菜单
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
const toggleDrawerMenu = inject(toggleDrawerMenuKey) as () => void;
</script>
<style scoped lang="less">

View File

@@ -1,14 +0,0 @@
{
"theme": "light",
"colorWeak": false,
"navbar": true,
"menu": true,
"topMenu": false,
"hideMenu": false,
"menuCollapse": false,
"footer": true,
"menuWidth": 220,
"globalSettings": false,
"device": "desktop",
"tabBar": true
}

View File

@@ -16,8 +16,10 @@ export default function useResponsive(immediate?: boolean) {
function resizeHandler() {
if (!document.hidden) {
const isMobile = queryDevice();
appStore.toggleDevice(isMobile ? 'mobile' : 'desktop');
appStore.toggleMenu(isMobile);
appStore.updateSettings({
device: isMobile ? 'mobile' : 'desktop',
hideMenu: isMobile
});
}
}

View File

@@ -5,43 +5,39 @@
<NavBar />
</div>
<a-layout>
<a-layout>
<!-- 左侧菜单栏 -->
<a-layout-sider
v-if="renderMenu"
v-show="!hideMenu"
class="layout-sider"
breakpoint="xl"
:collapsed="collapsed"
:collapsible="true"
:width="menuWidth"
:style="{ paddingTop: navbar ? '60px' : '' }"
:hide-trigger="true"
@collapse="setCollapsed">
<div class="menu-wrapper">
<Menu />
</div>
</a-layout-sider>
<!-- 顶部菜单栏 -->
<a-drawer
v-if="hideMenu"
:visible="drawerVisible"
placement="left"
:header="false"
:footer="false"
mask-closable
:closable="false"
@cancel="drawerCancel">
<!-- 左侧菜单栏 -->
<a-layout-sider v-if="renderMenu"
v-show="!hideMenu"
class="layout-sider"
breakpoint="xl"
:collapsed="collapsed"
:collapsible="true"
:width="menuWidth"
:style="{ paddingTop: navbar ? '60px' : '' }"
:hide-trigger="true"
@collapse="setCollapsed">
<div class="menu-wrapper">
<Menu />
</a-drawer>
<!-- body -->
<a-layout class="layout-content" :style="paddingStyle">
<TabBar v-if="appStore.tabBar" />
<a-layout-content>
<PageLayout />
</a-layout-content>
<Footer v-if="footer" />
</a-layout>
</div>
</a-layout-sider>
<!-- 顶部菜单栏 -->
<a-drawer v-if="hideMenu"
:visible="drawerVisible"
placement="left"
:header="false"
:footer="false"
mask-closable
:closable="false"
@cancel="drawerCancel">
<Menu />
</a-drawer>
<!-- body -->
<a-layout class="layout-content" :style="paddingStyle">
<TabBar v-if="appStore.tabBar" />
<a-layout-content>
<PageLayout />
</a-layout-content>
<Footer v-if="footer" />
</a-layout>
</a-layout>
</a-layout>
@@ -57,6 +53,7 @@
import TabBar from '@/components/tab-bar/index.vue';
import useResponsive from '@/hooks/responsive';
import PageLayout from './page-layout.vue';
import { toggleDrawerMenuKey } from '@/types/symbol';
const isInit = ref(false);
const appStore = useAppStore();
@@ -98,7 +95,7 @@
};
// 对外暴露触发收缩菜单
provide('toggleDrawerMenu', () => {
provide(toggleDrawerMenuKey, () => {
drawerVisible.value = !drawerVisible.value;
});

View File

@@ -1,10 +1,29 @@
import { defineStore } from 'pinia';
import defaultSettings from '@/config/settings.json';
import { AppState } from './types';
const defaultConfig: AppState = {
// 应用设置
device: 'desktop',
menuCollapse: false,
hideMenu: false,
// 用户偏好-布局
theme: 'light',
menu: true,
topMenu: false,
navbar: true,
footer: true,
tabBar: true,
menuWidth: 220,
colorWeak: false,
// 用户偏好-页面视图
host: 'table',
hostKeys: 'table',
hostIdentity: 'table',
};
export default defineStore('app', {
state: (): AppState => ({
...defaultSettings,
...defaultConfig
}),
getters: {
@@ -17,31 +36,17 @@ export default defineStore('app', {
},
actions: {
// 修改颜色主题
toggleTheme(dark: boolean) {
this.updateSettings({
theme: dark ? 'dark' : 'light'
});
document.body.setAttribute('arco-theme', dark ? 'dark' : 'light');
},
// 更新配置
updateSettings(partial: Partial<AppState>) {
this.$patch(partial as object);
console.log(partial);
},
// 修改颜色主题
toggleTheme(dark: boolean) {
if (dark) {
this.theme = 'dark';
document.body.setAttribute('arco-theme', 'dark');
} else {
this.theme = 'light';
document.body.removeAttribute('arco-theme');
}
},
// 切换设备
toggleDevice(device: string) {
this.device = device;
},
// 切换菜单状态
toggleMenu(value: boolean) {
this.hideMenu = value;
},
},
});

View File

@@ -1,16 +1,42 @@
export interface AppState {
theme: string;
colorWeak: boolean;
navbar: boolean;
menu: boolean;
topMenu: boolean;
hideMenu: boolean;
menuCollapse: boolean;
footer: boolean;
menuWidth: number;
globalSettings: boolean;
device: string;
tabBar: boolean;
export type Theme = 'light' | 'dark'
export type Device = 'desktop' | 'mobile'
export type ViewType = 'table' | 'card'
/**
* 应用状态
*/
export interface AppState extends AppSetting, UserPreferenceLayout, UserPreferenceViews {
[key: string]: unknown;
}
/**
* 应用内配置
*/
export interface AppSetting {
device: Device;
menuCollapse: boolean;
hideMenu: boolean;
}
/**
* 用户偏好 - 布局
*/
export interface UserPreferenceLayout {
theme: Theme;
menu: boolean;
topMenu: boolean;
navbar: boolean;
footer: boolean;
tabBar: boolean;
menuWidth: number;
colorWeak: boolean;
}
/**
* 用户偏好 - 页面视图
*/
export interface UserPreferenceViews {
host: ViewType | string;
hostKeys: ViewType | string;
hostIdentity: ViewType | string;
}

View File

@@ -30,7 +30,9 @@ export default defineStore('user', {
// 获取用户信息
async info() {
// TODO 查询偏好
const { data } = await getUserPermission();
// 设置用户信息
this.setInfo({
id: data.user.id,
username: data.user.username,
@@ -39,6 +41,8 @@ export default defineStore('user', {
roles: data.roles,
permission: data.permissions,
});
// TODO 设置用户偏好
},
// 登录

View File

@@ -0,0 +1,4 @@
// 切换菜单状态
export const toggleDrawerMenuKey = Symbol();
// 打开偏好设置
export const openGlobalSettingKey = Symbol();