feat. 终端主题设置.

This commit is contained in:
lijiahang
2023-12-08 16:40:14 +08:00
parent f68745b80d
commit 73dd7cd3d5
24 changed files with 128 additions and 94 deletions

View File

@@ -151,7 +151,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -155,7 +155,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -13,8 +13,8 @@ export interface PreferenceUpdateRequest {
/** /**
* 用户偏好查询响应 * 用户偏好查询响应
*/ */
export interface PreferenceQueryResponse { export interface PreferenceQueryResponse<T> {
config: object; config: T;
} }
/** /**
@@ -34,7 +34,7 @@ export function updatePreferencePartial(request: PreferenceUpdateRequest) {
/** /**
* 查询用户偏好 * 查询用户偏好
*/ */
export function getPreference(type: Preference) { export function getPreference<T>(type: Preference) {
return axios.get<PreferenceQueryResponse>('/infra/preference/get', { params: { type } }); return axios.get<PreferenceQueryResponse<T>>('/infra/preference/get', { params: { type } });
} }

View File

@@ -125,7 +125,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -86,8 +86,6 @@
onMounted(async () => { onMounted(async () => {
setLoading(true); setLoading(true);
try { try {
// 加载主机秘钥
hostKeys.value = await cacheStore.loadHostKeys();
// 加载主机身份 // 加载主机身份
hostIdentities.value = await cacheStore.loadHostIdentities(); hostIdentities.value = await cacheStore.loadHostIdentities();
} catch (e) { } catch (e) {
@@ -96,6 +94,12 @@
} }
}); });
// 初始化数据
onMounted(async () => {
// 加载主机秘钥
hostKeys.value = await cacheStore.loadHostKeys();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -176,7 +176,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -231,7 +231,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -28,4 +28,4 @@ export const AuthType = {
export const authTypeKey = 'hostAuthTypeType'; export const authTypeKey = 'hostAuthTypeType';
// 加载的字典值 // 加载的字典值
export const dictKeys = ['hostAuthTypeType']; export const dictKeys = [authTypeKey];

View File

@@ -183,7 +183,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

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

View File

@@ -10,7 +10,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { onMounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { TerminalTheme } from '../types/terminal.theme'; import { TerminalTheme } from '../types/terminal.theme';
const props = defineProps<{ const props = defineProps<{
@@ -18,9 +18,10 @@
}>(); }>();
const terminal = ref(); const terminal = ref();
const term = ref();
onMounted(() => { onMounted(() => {
const term = new Terminal({ term.value = new Terminal({
theme: props.theme, theme: props.theme,
cols: 47, cols: 47,
rows: 6, rows: 6,
@@ -29,9 +30,9 @@
cursorBlink: false, cursorBlink: false,
cursorInactiveStyle: 'none' cursorInactiveStyle: 'none'
}); });
term.open(terminal.value); term.value.open(terminal.value);
term.write( term.value.write(
'[root@OrionServer usr]#\n' + '[root@OrionServer usr]#\n' +
'dr-xr-xr-x. 2 root root bin\n' + 'dr-xr-xr-x. 2 root root bin\n' +
'dr-xr-xr-x. 2 root root sbin\n' + 'dr-xr-xr-x. 2 root root sbin\n' +
@@ -41,6 +42,10 @@
); );
}); });
onUnmounted(() => {
term.value?.dispose();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -12,13 +12,11 @@
<h3 class="terminal-setting-subtitle"> <h3 class="terminal-setting-subtitle">
主题选择 主题选择
</h3> </h3>
<a-radio-group v-model="userDarkTheme" <a-radio-group :default-value="preference.darkTheme"
size="mini" size="mini"
type="button" type="button"
@change="changeDarkTheme"> @change="changeDarkTheme"
<a-radio v-for="theme in DarkTheme" :key="theme.value" :value="theme.value"> :options="toOptions(darkThemeKey)">
{{ theme.label }}
</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<!-- 内容区域 --> <!-- 内容区域 -->
@@ -30,7 +28,7 @@
:key="theme.name" :key="theme.name"
class="terminal-theme-card simple-card" class="terminal-theme-card simple-card"
:class="{ :class="{
'terminal-theme-card-check': theme.name === userTerminalTheme.name 'terminal-theme-card-check': theme.name === preference.terminalTheme.name
}" }"
:title="theme.name" :title="theme.name"
:style="{ :style="{
@@ -44,7 +42,7 @@
<!-- 样例 --> <!-- 样例 -->
<terminal-example :theme="theme" /> <terminal-example :theme="theme" />
<icon-check class="theme-check-icon" :style="{ <icon-check class="theme-check-icon" :style="{
display: theme.name === userTerminalTheme.name ? 'flex': 'none' display: theme.name === preference.terminalTheme.name ? 'flex': 'none'
}" /> }" />
</a-card> </a-card>
</div> </div>
@@ -62,59 +60,60 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TerminalTheme } from '../types/terminal.theme'; import type { TerminalTheme } from '../types/terminal.theme';
import { DarkTheme } from '../types/terminal.type'; import type { TerminalPreference } from '../types/terminal.type';
import ThemeSchema, { FRAPPE } from '../types/terminal.theme'; import { DarkTheme, darkThemeKey } from '../types/terminal.type';
import ThemeSchema from '../types/terminal.theme';
import useEmitter from '@/hooks/emitter'; import useEmitter from '@/hooks/emitter';
import { onBeforeMount, ref } from 'vue';
import TerminalExample from './terminal-example.vue';
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
import { useDictStore } from '@/store';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import TerminalExample from './terminal-example.vue';
import { updatePreferencePartial } from '@/api/user/preference';
defineProps(); const props = defineProps<{
preference: TerminalPreference
}>();
const emits = defineEmits(['emitter']); const emits = defineEmits(['emitter']);
const { bubblesEmitter } = useEmitter(emits); const { bubblesEmitter } = useEmitter(emits);
const { toOptions } = useDictStore();
interface TerminalPreference {
darkTheme: string,
terminalTheme: TerminalTheme
}
const userDarkTheme = ref(DarkTheme.DARK.value);
const userTerminalTheme = ref<TerminalTheme>(FRAPPE);
// 修改暗色主题 // 修改暗色主题
const changeDarkTheme = (value: string) => { const changeDarkTheme = (value: string) => {
if (value === DarkTheme.DARK.value) { props.preference.darkTheme = value;
if (value === DarkTheme.DARK) {
// 暗色 // 暗色
bubblesEmitter('changeDarkTheme', true); bubblesEmitter('changeDarkTheme', true);
} else if (value === DarkTheme.LIGHT.value) { } else if (value === DarkTheme.LIGHT) {
// 亮色 // 亮色
bubblesEmitter('changeDarkTheme', false); bubblesEmitter('changeDarkTheme', false);
} else if (value === DarkTheme.AUTO.value) { } else if (value === DarkTheme.AUTO) {
// 自动配色 // 自动配色
bubblesEmitter('changeDarkTheme', userTerminalTheme.value.dark ? DarkTheme.DARK.value : DarkTheme.LIGHT.value); bubblesEmitter('changeDarkTheme', props.preference.terminalTheme.dark);
} }
// 同步用户偏好
sync(); sync();
}; };
// 选择终端主题 // 选择终端主题
const checkTheme = (theme: TerminalTheme) => { const checkTheme = (theme: TerminalTheme) => {
userTerminalTheme.value = theme; props.preference.terminalTheme = theme;
// 切换主题配色 // 切换主题配色
if (userDarkTheme.value === DarkTheme.AUTO.value) { if (props.preference.darkTheme === DarkTheme.AUTO) {
changeDarkTheme(theme.dark ? DarkTheme.DARK.value : DarkTheme.LIGHT.value); bubblesEmitter('changeDarkTheme', theme.dark);
} else {
sync();
} }
// 同步用户偏好
sync();
}; };
// 同步用户偏好 // 同步用户偏好
const syncUserPreference = async () => { const syncUserPreference = async () => {
try { try {
// FIXME 同步用户配置 await updatePreferencePartial({
type: 'TERMINAL',
config: props.preference
});
Message.success('同步成功'); Message.success('同步成功');
} catch (e) { } catch (e) {
Message.error('同步失败'); Message.error('同步失败');
@@ -123,10 +122,6 @@
// 同步用户偏好防抖 // 同步用户偏好防抖
const sync = useDebounceFn(syncUserPreference, 1500); const sync = useDebounceFn(syncUserPreference, 1500);
onBeforeMount(() => {
// FIXME 加载用户配置
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="host-layout"> <div class="host-layout" v-if="render">
<!-- 头部区域 --> <!-- 头部区域 -->
<header class="host-layout-header"> <header class="host-layout-header">
<terminal-header v-model="activeKey" <terminal-header v-model="activeKey"
@@ -17,6 +17,7 @@
<div class="host-layout-content"> <div class="host-layout-content">
<terminal-content v-model="activeKey" <terminal-content v-model="activeKey"
:tabs="tabs" :tabs="tabs"
:preference="preference as TerminalPreference"
@change-dark-theme="changeLayoutTheme" /> @change-dark-theme="changeLayoutTheme" />
</div> </div>
<!-- 右侧操作栏 --> <!-- 右侧操作栏 -->
@@ -34,30 +35,36 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import type { TabItem } from './types/terminal.type'; import type { TabItem, TerminalPreference } from './types/terminal.type';
import { ref } from 'vue'; import { ref, onBeforeMount } from 'vue';
import { useDark } from '@vueuse/core'; import { useDark } from '@vueuse/core';
import { TabType, InnerTabs, DarkTheme } from './types/terminal.type'; import { TabType, InnerTabs, DarkTheme, dictKeys } from './types/terminal.type';
import { DEFAULT_SCHEMA } from './types/terminal.theme';
import { useDictStore } from '@/store';
import { getPreference } from '@/api/user/preference';
import { Message } from '@arco-design/web-vue';
import TerminalHeader from './components/layout/terminal-header.vue'; import TerminalHeader from './components/layout/terminal-header.vue';
import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue'; import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
import TerminalRightSidebar from './components/layout/terminal-right-sidebar.vue'; import TerminalRightSidebar from './components/layout/terminal-right-sidebar.vue';
import TerminalContent from './components/layout/terminal-content.vue'; import TerminalContent from './components/layout/terminal-content.vue';
import './assets/styles/layout.less'; import './assets/styles/layout.less';
import '@xterm/xterm/css/xterm.css'; import '@xterm/xterm/css/xterm.css';
import { onBeforeMount } from 'vue/dist/vue';
// 系统主题 // 系统主题
const darkTheme = useDark({ const darkTheme = useDark({
selector: 'body', selector: 'body',
attribute: 'terminal-theme', attribute: 'terminal-theme',
valueDark: DarkTheme.DARK.value, valueDark: DarkTheme.DARK,
valueLight: DarkTheme.LIGHT.value, valueLight: DarkTheme.LIGHT,
initialValue: DarkTheme.DARK.value as any, initialValue: DarkTheme.DARK as any,
storageKey: null storageKey: null
}); });
const dictStore = useDictStore();
const render = ref(false);
const activeKey = ref(InnerTabs.THEME_SETTING.key); const activeKey = ref(InnerTabs.THEME_SETTING.key);
const tabs = ref<Array<TabItem>>([InnerTabs.THEME_SETTING]); const tabs = ref<Array<TabItem>>([InnerTabs.THEME_SETTING]);
const preference = ref<TerminalPreference>();
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
tabs.value.push({ tabs.value.push({
key: `host${i}`, key: `host${i}`,
@@ -70,7 +77,6 @@
const changeLayoutTheme = (dark: boolean) => { const changeLayoutTheme = (dark: boolean) => {
darkTheme.value = dark; darkTheme.value = dark;
}; };
changeLayoutTheme(false);
// 点击 tab // 点击 tab
const clickTab = (key: string) => { const clickTab = (key: string) => {
@@ -96,8 +102,31 @@
activeKey.value = tab.key; activeKey.value = tab.key;
}; };
onBeforeMount(() => { // 加载用户终端偏好
// FIXME 加载用户配置 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('配置加载失败');
}
});
// 加载字典值
onBeforeMount(async () => {
await dictStore.loadKeys([...dictKeys]);
}); });
</script> </script>

View File

@@ -30,7 +30,7 @@ export interface TerminalTheme {
} }
// 默认配色 // 默认配色
export const FRAPPE = { export const DEFAULT_SCHEMA = {
name: 'frappe', name: 'frappe',
dark: true, dark: true,
background: '#303446', background: '#303446',
@@ -59,7 +59,7 @@ export const FRAPPE = {
}; };
export default [ export default [
FRAPPE, DEFAULT_SCHEMA,
{ {
name: 'latte', name: 'latte',
dark: false, dark: false,

View File

@@ -1,21 +1,19 @@
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import type { TerminalTheme } from './terminal.theme';
// 暗色主题 // 暗色主题
export const DarkTheme = { export const DarkTheme = {
DARK: { DARK: 'dark',
value: 'dark', LIGHT: 'light',
label: '暗色' AUTO: 'auto'
},
LIGHT: {
value: 'light',
label: '亮色'
},
AUTO: {
value: 'auto',
label: '自动'
}
}; };
// 用户终端偏好
export interface TerminalPreference {
darkTheme: string,
terminalTheme: TerminalTheme
}
// sidebar 操作类型 // sidebar 操作类型
export interface SidebarAction { export interface SidebarAction {
icon: string; icon: string;
@@ -63,3 +61,9 @@ export interface TabItem {
[key: string]: unknown; [key: string]: unknown;
} }
// 终端暗色模式 字典项
export const darkThemeKey = 'terminalDarkTheme';
// 加载的字典值
export const dictKeys = [darkThemeKey];

View File

@@ -245,7 +245,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -144,7 +144,7 @@
import { usePagination } from '@/types/table'; import { usePagination } from '@/types/table';
import { dictValueTypeKey } from '../types/const'; import { dictValueTypeKey } from '../types/const';
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
import { useDictStore } from '@/store'; import { useCacheStore, useDictStore } from '@/store';
import { getDictValueList } from '@/api/system/dict-value'; import { getDictValueList } from '@/api/system/dict-value';
const tableRenderData = ref<DictKeyQueryResponse[]>([]); const tableRenderData = ref<DictKeyQueryResponse[]>([]);
@@ -154,6 +154,7 @@
const { copy } = useCopy(); const { copy } = useCopy();
const { loading, setLoading } = useLoading(); const { loading, setLoading } = useLoading();
const { toOptions, getDictValue } = useDictStore(); const { toOptions, getDictValue } = useDictStore();
const cacheStore = useCacheStore();
const formModel = reactive<DictKeyQueryRequest>({ const formModel = reactive<DictKeyQueryRequest>({
id: undefined, id: undefined,
@@ -170,6 +171,7 @@
// 调用删除接口 // 调用删除接口
await deleteDictKey(id); await deleteDictKey(id);
Message.success('删除成功'); Message.success('删除成功');
cacheStore.reset('dictKeys');
// 重新加载数据 // 重新加载数据
fetchTableData(); fetchTableData();
} catch (e) { } catch (e) {
@@ -181,11 +183,13 @@
// 添加后回调 // 添加后回调
const addedCallback = () => { const addedCallback = () => {
fetchTableData(); fetchTableData();
cacheStore.reset('dictKeys');
}; };
// 更新后回调 // 更新后回调
const updatedCallback = () => { const updatedCallback = () => {
fetchTableData(); fetchTableData();
cacheStore.reset('dictKeys');
}; };
defineExpose({ defineExpose({

View File

@@ -21,7 +21,9 @@
:wrapper-col-props="{ span: 18 }" :wrapper-col-props="{ span: 18 }"
:rules="formRules"> :rules="formRules">
<!-- 配置项 --> <!-- 配置项 -->
<a-form-item field="keyId" label="配置项"> <a-form-item v-if="visible"
field="keyId"
label="配置项">
<dict-key-selector v-model="formModel.keyId" @change="changeKey" /> <dict-key-selector v-model="formModel.keyId" @change="changeKey" />
</a-form-item> </a-form-item>
<!-- 配置值 --> <!-- 配置值 -->
@@ -123,18 +125,18 @@
const emits = defineEmits(['added', 'updated']); const emits = defineEmits(['added', 'updated']);
// 打开新增 // 打开新增
const openAdd = () => { const openAdd = async () => {
title.value = '添加字典配置值'; title.value = '添加字典配置值';
isAddHandle.value = true; isAddHandle.value = true;
renderForm({ ...defaultForm(), keyId: formModel.value.keyId, sort: (formModel.value.sort || 0) + sortStep }); await renderForm({ ...defaultForm(), keyId: formModel.value.keyId, sort: (formModel.value.sort || 0) + sortStep });
setVisible(true); setVisible(true);
}; };
// 打开修改 // 打开修改
const openUpdate = (record: any) => { const openUpdate = async (record: any) => {
title.value = '修改字典配置值'; title.value = '修改字典配置值';
isAddHandle.value = false; isAddHandle.value = false;
renderForm({ ...defaultForm(), ...record }); await renderForm({ ...defaultForm(), ...record });
setVisible(true); setVisible(true);
}; };
@@ -217,7 +219,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -255,7 +255,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
// 触发 watch 防止第二次加载变成根目录 // 触发 watch 防止第二次加载变成根目录
renderForm(defaultForm()); renderForm(defaultForm());
}; };

View File

@@ -127,7 +127,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -115,7 +115,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -145,7 +145,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -112,7 +112,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>

View File

@@ -107,7 +107,6 @@
// 清空 // 清空
const handlerClear = () => { const handlerClear = () => {
setLoading(false); setLoading(false);
setVisible(false);
}; };
</script> </script>