feat. 终端主题设置.

This commit is contained in:
lijiahang
2023-12-08 19:05:00 +08:00
parent 73dd7cd3d5
commit fc81c78849
20 changed files with 267 additions and 142 deletions

View File

@@ -20,6 +20,11 @@ public enum PreferenceTypeEnum {
*/
SYSTEM("systemPreferenceStrategy"),
/**
* 终端偏好
*/
TERMINAL("terminalPreferenceStrategy"),
;
PreferenceTypeEnum(String beanName) {

View File

@@ -17,7 +17,7 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppPreferenceModel implements PreferenceModel {
public class SystemPreferenceModel implements PreferenceModel {
@Schema(description = "是否使用侧边菜单")
private Boolean menu;

View File

@@ -0,0 +1,32 @@
package com.orion.ops.module.infra.handler.preference.model;
import com.alibaba.fastjson.JSONObject;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 终端偏好模型
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/8 14:46
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "暗色主题")
private String darkTheme;
@Schema(description = "终端主题")
private JSONObject terminalTheme;
@Schema(description = "显示设置")
private JSONObject viewSetting;
}

View File

@@ -1,6 +1,6 @@
package com.orion.ops.module.infra.handler.preference.strategy;
import com.orion.ops.module.infra.handler.preference.model.AppPreferenceModel;
import com.orion.ops.module.infra.handler.preference.model.SystemPreferenceModel;
import org.springframework.stereotype.Component;
/**
@@ -11,11 +11,11 @@ import org.springframework.stereotype.Component;
* @since 2023/10/8 13:48
*/
@Component
public class SystemPreferenceStrategy implements IPreferenceStrategy<AppPreferenceModel> {
public class SystemPreferenceStrategy implements IPreferenceStrategy<SystemPreferenceModel> {
@Override
public AppPreferenceModel getDefault() {
return AppPreferenceModel.builder()
public SystemPreferenceModel getDefault() {
return SystemPreferenceModel.builder()
.menu(true)
.topMenu(false)
.navbar(true)

View File

@@ -0,0 +1,26 @@
package com.orion.ops.module.infra.handler.preference.strategy;
import com.alibaba.fastjson.JSONObject;
import com.orion.ops.module.infra.handler.preference.model.TerminalPreferenceModel;
import org.springframework.stereotype.Component;
/**
* 终端偏好处理策略
*
* @author Jiahang Li
* @version 1.0.0
* @since 2023/12/8 14:46
*/
@Component
public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalPreferenceModel> {
@Override
public TerminalPreferenceModel getDefault() {
return TerminalPreferenceModel.builder()
.darkTheme("dark")
.terminalTheme(new JSONObject())
.viewSetting(new JSONObject())
.build();
}
}

View File

@@ -6,6 +6,7 @@ import useTabBarStore from './modules/tab-bar';
import useCacheStore from './modules/cache';
import useTipsStore from './modules/tips';
import useDictStore from './modules/dict';
import useTerminalStore from './modules/terminal';
const pinia = createPinia();
@@ -17,6 +18,7 @@ export {
useCacheStore,
useTipsStore,
useDictStore,
useTerminalStore,
};
export default pinia;

View File

@@ -19,8 +19,6 @@ export type CacheType = 'users' | 'menus' | 'roles'
export default defineStore('cache', {
state: (): CacheState => ({}),
getters: {},
actions: {
// 设置
set(name: CacheType, value: any) {

View File

@@ -0,0 +1,68 @@
import type { TerminalPreference, TerminalState, TerminalTheme } from './types';
import { DarkTheme } from './types';
import { defineStore } from 'pinia';
import { getPreference, updatePreferencePartial } from '@/api/user/preference';
import { Message } from '@arco-design/web-vue';
import { useDark } from '@vueuse/core';
import { DEFAULT_SCHEMA } from '@/views/host-ops/terminal/types/terminal.theme';
export default defineStore('terminal', {
state: (): TerminalState => ({
isDarkTheme: useDark({
selector: 'body',
attribute: 'terminal-theme',
valueDark: DarkTheme.DARK,
valueLight: DarkTheme.LIGHT,
initialValue: DarkTheme.DARK as any,
storageKey: null
}),
preference: {
darkTheme: 'auto',
terminalTheme: {} as TerminalTheme,
}
}),
actions: {
// 修改暗色主题
changeDarkTheme(dark: boolean) {
this.isDarkTheme = dark;
},
// 加载终端偏好
async fetchPreference() {
try {
const { data } = await getPreference<TerminalPreference>('TERMINAL');
// 设置默认终端主题
if (!data.config.terminalTheme?.name) {
data.config.terminalTheme = DEFAULT_SCHEMA;
}
this.preference = data.config;
// 设置暗色主题
const userDarkTheme = data.config.darkTheme;
if (userDarkTheme === DarkTheme.AUTO) {
this.isDarkTheme = data.config.terminalTheme?.dark === true;
} else {
this.isDarkTheme = userDarkTheme === DarkTheme.DARK;
}
} catch (e) {
Message.error('配置加载失败');
}
},
// 更新终端偏好
async updatePreference(preference: TerminalPreference) {
try {
// 修改配置
await updatePreferencePartial({
type: 'TERMINAL',
config: preference
});
this.preference = preference;
Message.success('同步成功');
} catch (e) {
Message.error('同步失败');
}
},
},
});

View File

@@ -0,0 +1,50 @@
import type { Ref } from 'vue';
export interface TerminalState {
isDarkTheme: Ref<boolean>;
preference: TerminalPreference;
}
// 终端配置
export interface TerminalPreference {
darkTheme: string,
terminalTheme: TerminalTheme,
}
// 暗色主题
export const DarkTheme = {
DARK: 'dark',
LIGHT: 'light',
AUTO: 'auto'
};
// 终端主题
export interface TerminalTheme {
name: string;
dark: boolean;
background: string;
foreground: string;
cursor: string;
cursorAccent?: string;
selectionInactiveBackground?: string;
selectionBackground?: string;
selectionForeground?: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
[key: string]: unknown;
}

View File

@@ -27,7 +27,7 @@
</script>
<script lang="ts" setup>
import type { SidebarAction } from '../../types/terminal.type';
import type { SidebarAction } from '../../types/terminal.const';
import type { PropType } from 'vue';
defineProps({

View File

@@ -8,9 +8,7 @@
<!-- 设置 -->
<template v-if="tab.type === TabType.SETTING">
<!-- 主题设置 -->
<terminal-theme-setting v-if="tab.key === InnerTabs.THEME_SETTING.key"
:preference="preference"
@emitter="dispatchEmitter" />
<terminal-theme-setting v-if="tab.key === InnerTabs.THEME_SETTING.key" />
<span v-else>
{{ tab.title }}
</span>
@@ -35,11 +33,11 @@
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { TabItem, TerminalPreference } from '../../types/terminal.type';
import type { TabItem } from '../../types/terminal.const';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import { computed } from 'vue';
import { TabType, InnerTabs } from '../../types/terminal.type';
import useEmitter from '@/hooks/emitter';
import TerminalThemeSetting from '../terminal-theme-setting.vue';
import { TabType, InnerTabs } from '../../types/terminal.const';
import TerminalThemeSetting from '../theme-setting/terminal-theme-setting.vue';
const props = defineProps({
modelValue: {
@@ -50,16 +48,8 @@
type: Array as PropType<Array<TabItem>>,
required: true
},
preference: {
type: Object as PropType<TerminalPreference>,
required: true
},
});
const emits = defineEmits(['changeDarkTheme']);
const { dispatchEmitter } = useEmitter(emits);
const activeKey = computed<String>({
get() {
return props.modelValue;

View File

@@ -46,7 +46,7 @@
</script>
<script lang="ts" setup>
import type { SidebarAction, TabItem } from '../../types/terminal.type';
import type { SidebarAction, TabItem } from '../../types/terminal.const';
import type { PropType } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { computed } from 'vue';

View File

@@ -18,8 +18,8 @@
</script>
<script lang="ts" setup>
import type { SidebarAction, } from '../../types/terminal.type';
import { InnerTabs } from '../../types/terminal.type';
import type { SidebarAction } from '../../types/terminal.const';
import { InnerTabs } from '../../types/terminal.const';
import IconActions from './icon-actions.vue';
const emits = defineEmits(['switchTab', 'copyAddress']);
@@ -47,14 +47,9 @@
},
{
icon: 'icon-palette',
content: '主题设置',
content: '外观设置',
click: () => emits('switchTab', InnerTabs.THEME_SETTING)
},
{
icon: 'icon-tool',
content: '显示设置',
click: () => emits('switchTab', InnerTabs.VIEW_SETTING)
},
];
</script>

View File

@@ -18,7 +18,7 @@
</script>
<script lang="ts" setup>
import type { SidebarAction } from '../../types/terminal.type';
import type { SidebarAction } from '../../types/terminal.const';
import IconActions from './icon-actions.vue';
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'openHistory', 'screenshot']);

View File

@@ -1,5 +1,5 @@
<template>
<div id="" class="terminal-example" ref="terminal"></div>
<div class="terminal-example" ref="terminal"></div>
</template>
<script lang="ts">
@@ -9,9 +9,9 @@
</script>
<script lang="ts" setup>
import type { TerminalTheme } from '@/store/modules/terminal/types';
import { Terminal } from '@xterm/xterm';
import { onMounted, onUnmounted, ref } from 'vue';
import { TerminalTheme } from '../types/terminal.theme';
const props = defineProps<{
theme: TerminalTheme
@@ -22,13 +22,13 @@
onMounted(() => {
term.value = new Terminal({
theme: props.theme,
theme: { ...props.theme, cursor: props.theme.background },
cols: 47,
rows: 6,
fontSize: 15,
convertEol: true,
cursorBlink: false,
cursorInactiveStyle: 'none'
cursorInactiveStyle: 'none',
});
term.value.open(terminal.value);
@@ -42,6 +42,8 @@
);
});
defineExpose({ term });
onUnmounted(() => {
term.value?.dispose();
});

View File

@@ -1,16 +1,25 @@
<template>
<div class="terminal-setting-container theme-setting-container">
<div class="theme-setting-wrapper">
<!-- 标题 -->
<!-- 标题 -->
<h2 class="terminal-setting-title">
主题设置
外观设置
</h2>
<!-- 切换主题 -->
<!-- 基础设置 -->
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="theme-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
主题选择
字体设置
</h3>
</div>
</div>
<!-- 主题设置 -->
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="theme-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
主题设置
</h3>
<a-radio-group :default-value="preference.darkTheme"
size="mini"
@@ -59,38 +68,38 @@
</script>
<script lang="ts" setup>
import type { TerminalTheme } from '../types/terminal.theme';
import type { TerminalPreference } from '../types/terminal.type';
import { DarkTheme, darkThemeKey } from '../types/terminal.type';
import ThemeSchema from '../types/terminal.theme';
import useEmitter from '@/hooks/emitter';
import type { TerminalPreference, TerminalTheme } from '@/store/modules/terminal/types';
import { DarkTheme, DarkThemeChangeSymbol, darkThemeKey } from '../../types/terminal.const';
import ThemeSchema from '../../types/terminal.theme';
import { useDebounceFn } from '@vueuse/core';
import { useDictStore } from '@/store';
import { Message } from '@arco-design/web-vue';
import TerminalExample from './terminal-example.vue';
import { updatePreferencePartial } from '@/api/user/preference';
import { inject, ref } from 'vue';
const props = defineProps<{
preference: TerminalPreference
}>();
const preference = ref<TerminalPreference>({
darkTheme: 'auto',
terminalTheme: {} as TerminalTheme,
});
const emits = defineEmits(['emitter']);
const changeLayoutTheme = inject(DarkThemeChangeSymbol) as (s: boolean) => void;
const { bubblesEmitter } = useEmitter(emits);
const { toOptions } = useDictStore();
//
const changeDarkTheme = (value: string) => {
props.preference.darkTheme = value;
preference.value.darkTheme = value;
if (value === DarkTheme.DARK) {
//
bubblesEmitter('changeDarkTheme', true);
changeLayoutTheme(true);
} else if (value === DarkTheme.LIGHT) {
//
bubblesEmitter('changeDarkTheme', false);
changeLayoutTheme(false);
} else if (value === DarkTheme.AUTO) {
//
bubblesEmitter('changeDarkTheme', props.preference.terminalTheme.dark);
changeLayoutTheme(preference.value.terminalTheme.dark);
}
//
sync();
@@ -98,10 +107,10 @@
//
const checkTheme = (theme: TerminalTheme) => {
props.preference.terminalTheme = theme;
preference.value.terminalTheme = theme;
//
if (props.preference.darkTheme === DarkTheme.AUTO) {
bubblesEmitter('changeDarkTheme', theme.dark);
if (preference.value.darkTheme === DarkTheme.AUTO) {
changeLayoutTheme(theme.dark);
}
//
sync();
@@ -112,7 +121,7 @@
try {
await updatePreferencePartial({
type: 'TERMINAL',
config: props.preference
config: preference
});
Message.success('同步成功');
} catch (e) {

View File

@@ -16,9 +16,7 @@
<!-- 内容区域 -->
<div class="host-layout-content">
<terminal-content v-model="activeKey"
:tabs="tabs"
:preference="preference as TerminalPreference"
@change-dark-theme="changeLayoutTheme" />
:tabs="tabs" />
</div>
<!-- 右侧操作栏 -->
<div class="host-layout-right">
@@ -35,12 +33,13 @@
</script>
<script lang="ts" setup>
import type { TabItem, TerminalPreference } from './types/terminal.type';
import { ref, onBeforeMount } from 'vue';
import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { TabItem } from './types/terminal.const';
import { ref, onBeforeMount, provide } from 'vue';
import { useDark } from '@vueuse/core';
import { TabType, InnerTabs, DarkTheme, dictKeys } from './types/terminal.type';
import { TabType, InnerTabs, DarkTheme, dictKeys, DarkThemeChangeSymbol } from './types/terminal.const';
import { DEFAULT_SCHEMA } from './types/terminal.theme';
import { useDictStore } from '@/store';
import { useDictStore, useTerminalStore } from '@/store';
import { getPreference } from '@/api/user/preference';
import { Message } from '@arco-design/web-vue';
import TerminalHeader from './components/layout/terminal-header.vue';
@@ -51,14 +50,15 @@
import '@xterm/xterm/css/xterm.css';
// 系统主题
const darkTheme = useDark({
selector: 'body',
attribute: 'terminal-theme',
valueDark: DarkTheme.DARK,
valueLight: DarkTheme.LIGHT,
initialValue: DarkTheme.DARK as any,
storageKey: null
});
// const darkTheme = useDark({
// selector: 'body',
// attribute: 'terminal-theme',
// valueDark: DarkTheme.DARK,
// valueLight: DarkTheme.LIGHT,
// initialValue: DarkTheme.DARK as any,
// storageKey: null
// });
const terminalStore = useTerminalStore();
const dictStore = useDictStore();
const render = ref(false);
@@ -75,9 +75,11 @@
// 切换系统主题
const changeLayoutTheme = (dark: boolean) => {
darkTheme.value = dark;
terminalStore.changeDarkTheme(dark);
};
provide(DarkThemeChangeSymbol, changeLayoutTheme);
// 点击 tab
const clickTab = (key: string) => {
activeKey.value = key;
@@ -104,24 +106,8 @@
// 加载用户终端偏好
onBeforeMount(async () => {
try {
const { data } = await getPreference<TerminalPreference>('TERMINAL');
// 设置默认终端主题
if (!data.config.terminalTheme?.name) {
data.config.terminalTheme = DEFAULT_SCHEMA;
}
preference.value = data.config;
// 设置暗色主题
const userDarkTheme = data.config.darkTheme;
if (userDarkTheme === DarkTheme.AUTO) {
changeLayoutTheme(data.config.terminalTheme?.dark === true);
} else {
changeLayoutTheme(userDarkTheme === DarkTheme.DARK);
}
render.value = true;
} catch (e) {
Message.error('配置加载失败');
}
await terminalStore.fetchPreference();
render.value = true;
});
// 加载字典值
@@ -136,6 +122,7 @@
width: 100%;
height: 100vh;
position: relative;
color: var(--color-content-text-2);
&-header {
width: 100%;

View File

@@ -1,5 +1,4 @@
import type { CSSProperties } from 'vue';
import type { TerminalTheme } from './terminal.theme';
// 暗色主题
export const DarkTheme = {
@@ -8,12 +7,6 @@ export const DarkTheme = {
AUTO: 'auto'
};
// 用户终端偏好
export interface TerminalPreference {
darkTheme: string,
terminalTheme: TerminalTheme
}
// sidebar 操作类型
export interface SidebarAction {
icon: string;
@@ -46,11 +39,6 @@ export const InnerTabs = {
title: '主题设置',
type: TabType.SETTING
},
VIEW_SETTING: {
key: 'viewSetting',
title: '显示设置',
type: TabType.SETTING
},
};
// tab 元素
@@ -62,6 +50,9 @@ export interface TabItem {
[key: string]: unknown;
}
// 暗色主题切换标识
export const DarkThemeChangeSymbol = Symbol('DARK_THEME_CHANGE');
// 终端暗色模式 字典项
export const darkThemeKey = 'terminalDarkTheme';

View File

@@ -1,37 +1,8 @@
// 主题
export interface TerminalTheme {
name: string;
dark: boolean;
background: string;
foreground: string;
cursor: string;
cursorAccent?: string;
selectionInactiveBackground?: string;
selectionBackground?: string;
selectionForeground?: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
[key: string]: unknown;
}
import type { TerminalTheme } from '@/store/modules/terminal/types';
// 默认配色
export const DEFAULT_SCHEMA = {
name: 'frappe',
name: 'Frappe',
dark: true,
background: '#303446',
foreground: '#C6D0F5',
@@ -61,7 +32,7 @@ export const DEFAULT_SCHEMA = {
export default [
DEFAULT_SCHEMA,
{
name: 'latte',
name: 'Latte',
dark: false,
background: '#EFF1F5',
foreground: '#4C4F69',
@@ -88,7 +59,7 @@ export default [
brightWhite: '#BCC0CC'
},
{
name: 'macchiato',
name: 'Macchiato',
dark: true,
background: '#24273A',
foreground: '#CAD3F5',
@@ -115,7 +86,7 @@ export default [
brightWhite: '#A5ADCB'
},
{
name: 'mocha',
name: 'Mocha',
dark: true,
background: '#1E1E2E',
foreground: '#CDD6F4',
@@ -142,7 +113,7 @@ export default [
brightWhite: '#A6ADC8'
},
{
name: 'AtomOneLight',
name: 'Atom One Light',
dark: false,
background: '#F9F9F9',
foreground: '#2A2C33',
@@ -164,7 +135,7 @@ export default [
brightWhite: '#FFFFFF'
},
{
name: 'OneHalfDark',
name: 'One Half Dark',
dark: true,
background: '#282C34',
foreground: '#DCDFE4',
@@ -186,7 +157,7 @@ export default [
brightWhite: '#DCDFE4'
},
{
name: 'dracula',
name: 'Dracula',
dark: true,
background: '#282A36',
foreground: '#F8F8F2',

View File

@@ -60,5 +60,4 @@
</script>
<style lang="less" scoped>
</style>