设置用户偏好.

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,9 @@
<div v-if="navbar" class="layout-navbar"> <div v-if="navbar" class="layout-navbar">
<NavBar /> <NavBar />
</div> </div>
<a-layout>
<a-layout> <a-layout>
<!-- 左侧菜单栏 --> <!-- 左侧菜单栏 -->
<a-layout-sider <a-layout-sider v-if="renderMenu"
v-if="renderMenu"
v-show="!hideMenu" v-show="!hideMenu"
class="layout-sider" class="layout-sider"
breakpoint="xl" breakpoint="xl"
@@ -23,8 +21,7 @@
</div> </div>
</a-layout-sider> </a-layout-sider>
<!-- 顶部菜单栏 --> <!-- 顶部菜单栏 -->
<a-drawer <a-drawer v-if="hideMenu"
v-if="hideMenu"
:visible="drawerVisible" :visible="drawerVisible"
placement="left" placement="left"
:header="false" :header="false"
@@ -44,7 +41,6 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
</a-layout> </a-layout>
</a-layout>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -57,6 +53,7 @@
import TabBar from '@/components/tab-bar/index.vue'; import TabBar from '@/components/tab-bar/index.vue';
import useResponsive from '@/hooks/responsive'; import useResponsive from '@/hooks/responsive';
import PageLayout from './page-layout.vue'; import PageLayout from './page-layout.vue';
import { toggleDrawerMenuKey } from '@/types/symbol';
const isInit = ref(false); const isInit = ref(false);
const appStore = useAppStore(); const appStore = useAppStore();
@@ -98,7 +95,7 @@
}; };
// 对外暴露触发收缩菜单 // 对外暴露触发收缩菜单
provide('toggleDrawerMenu', () => { provide(toggleDrawerMenuKey, () => {
drawerVisible.value = !drawerVisible.value; drawerVisible.value = !drawerVisible.value;
}); });

View File

@@ -1,10 +1,29 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import defaultSettings from '@/config/settings.json';
import { AppState } from './types'; 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', { export default defineStore('app', {
state: (): AppState => ({ state: (): AppState => ({
...defaultSettings, ...defaultConfig
}), }),
getters: { getters: {
@@ -17,31 +36,17 @@ export default defineStore('app', {
}, },
actions: { actions: {
// 修改颜色主题
toggleTheme(dark: boolean) {
this.updateSettings({
theme: dark ? 'dark' : 'light'
});
document.body.setAttribute('arco-theme', dark ? 'dark' : 'light');
},
// 更新配置 // 更新配置
updateSettings(partial: Partial<AppState>) { updateSettings(partial: Partial<AppState>) {
this.$patch(partial as object); 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 { export type Theme = 'light' | 'dark'
theme: string; export type Device = 'desktop' | 'mobile'
colorWeak: boolean; export type ViewType = 'table' | 'card'
navbar: boolean;
menu: boolean;
topMenu: boolean;
hideMenu: boolean;
menuCollapse: boolean;
footer: boolean;
menuWidth: number;
globalSettings: boolean;
device: string;
tabBar: boolean;
/**
* 应用状态
*/
export interface AppState extends AppSetting, UserPreferenceLayout, UserPreferenceViews {
[key: string]: unknown; [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() { async info() {
// TODO 查询偏好
const { data } = await getUserPermission(); const { data } = await getUserPermission();
// 设置用户信息
this.setInfo({ this.setInfo({
id: data.user.id, id: data.user.id,
username: data.user.username, username: data.user.username,
@@ -39,6 +41,8 @@ export default defineStore('user', {
roles: data.roles, roles: data.roles,
permission: data.permissions, permission: data.permissions,
}); });
// TODO 设置用户偏好
}, },
// 登录 // 登录

View File

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