feat. 终端主题设置.
This commit is contained in:
@@ -151,7 +151,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -155,7 +155,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -13,8 +13,8 @@ export interface PreferenceUpdateRequest {
|
||||
/**
|
||||
* 用户偏好查询响应
|
||||
*/
|
||||
export interface PreferenceQueryResponse {
|
||||
config: object;
|
||||
export interface PreferenceQueryResponse<T> {
|
||||
config: T;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ export function updatePreferencePartial(request: PreferenceUpdateRequest) {
|
||||
/**
|
||||
* 查询用户偏好
|
||||
*/
|
||||
export function getPreference(type: Preference) {
|
||||
return axios.get<PreferenceQueryResponse>('/infra/preference/get', { params: { type } });
|
||||
export function getPreference<T>(type: Preference) {
|
||||
return axios.get<PreferenceQueryResponse<T>>('/infra/preference/get', { params: { type } });
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -86,8 +86,6 @@
|
||||
onMounted(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 加载主机秘钥
|
||||
hostKeys.value = await cacheStore.loadHostKeys();
|
||||
// 加载主机身份
|
||||
hostIdentities.value = await cacheStore.loadHostIdentities();
|
||||
} catch (e) {
|
||||
@@ -96,6 +94,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化数据
|
||||
onMounted(async () => {
|
||||
// 加载主机秘钥
|
||||
hostKeys.value = await cacheStore.loadHostKeys();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -176,7 +176,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -231,7 +231,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -28,4 +28,4 @@ export const AuthType = {
|
||||
export const authTypeKey = 'hostAuthTypeType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = ['hostAuthTypeType'];
|
||||
export const dictKeys = [authTypeKey];
|
||||
|
||||
@@ -183,7 +183,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<template v-if="tab.type === TabType.SETTING">
|
||||
<!-- 主题设置 -->
|
||||
<terminal-theme-setting v-if="tab.key === InnerTabs.THEME_SETTING.key"
|
||||
:preference="preference"
|
||||
@emitter="dispatchEmitter" />
|
||||
<span v-else>
|
||||
{{ tab.title }}
|
||||
@@ -34,11 +35,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 { TabType, InnerTabs } from '../../types/terminal.type';
|
||||
import TerminalThemeSetting from '../terminal-theme-setting.vue';
|
||||
import useEmitter from '@/hooks/emitter';
|
||||
import TerminalThemeSetting from '../terminal-theme-setting.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -48,7 +49,11 @@
|
||||
tabs: {
|
||||
type: Array as PropType<Array<TabItem>>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
preference: {
|
||||
type: Object as PropType<TerminalPreference>,
|
||||
required: true
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['changeDarkTheme']);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { TerminalTheme } from '../types/terminal.theme';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -18,9 +18,10 @@
|
||||
}>();
|
||||
|
||||
const terminal = ref();
|
||||
const term = ref();
|
||||
|
||||
onMounted(() => {
|
||||
const term = new Terminal({
|
||||
term.value = new Terminal({
|
||||
theme: props.theme,
|
||||
cols: 47,
|
||||
rows: 6,
|
||||
@@ -29,9 +30,9 @@
|
||||
cursorBlink: false,
|
||||
cursorInactiveStyle: 'none'
|
||||
});
|
||||
term.open(terminal.value);
|
||||
term.value.open(terminal.value);
|
||||
|
||||
term.write(
|
||||
term.value.write(
|
||||
'[94m[root[0m@[96mOrionServer usr]#[0m\n' +
|
||||
'[92mdr-xr-xr-x.[0m 2 root root [96mbin[0m\n' +
|
||||
'[92mdr-xr-xr-x.[0m 2 root root [96msbin[0m\n' +
|
||||
@@ -41,6 +42,10 @@
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
term.value?.dispose();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -12,13 +12,11 @@
|
||||
<h3 class="terminal-setting-subtitle">
|
||||
主题选择
|
||||
</h3>
|
||||
<a-radio-group v-model="userDarkTheme"
|
||||
<a-radio-group :default-value="preference.darkTheme"
|
||||
size="mini"
|
||||
type="button"
|
||||
@change="changeDarkTheme">
|
||||
<a-radio v-for="theme in DarkTheme" :key="theme.value" :value="theme.value">
|
||||
{{ theme.label }}
|
||||
</a-radio>
|
||||
@change="changeDarkTheme"
|
||||
:options="toOptions(darkThemeKey)">
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
@@ -30,7 +28,7 @@
|
||||
:key="theme.name"
|
||||
class="terminal-theme-card simple-card"
|
||||
:class="{
|
||||
'terminal-theme-card-check': theme.name === userTerminalTheme.name
|
||||
'terminal-theme-card-check': theme.name === preference.terminalTheme.name
|
||||
}"
|
||||
:title="theme.name"
|
||||
:style="{
|
||||
@@ -44,7 +42,7 @@
|
||||
<!-- 样例 -->
|
||||
<terminal-example :theme="theme" />
|
||||
<icon-check class="theme-check-icon" :style="{
|
||||
display: theme.name === userTerminalTheme.name ? 'flex': 'none'
|
||||
display: theme.name === preference.terminalTheme.name ? 'flex': 'none'
|
||||
}" />
|
||||
</a-card>
|
||||
</div>
|
||||
@@ -62,59 +60,60 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalTheme } from '../types/terminal.theme';
|
||||
import { DarkTheme } from '../types/terminal.type';
|
||||
import ThemeSchema, { FRAPPE } 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 { onBeforeMount, ref } from 'vue';
|
||||
import TerminalExample from './terminal-example.vue';
|
||||
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';
|
||||
|
||||
defineProps();
|
||||
const props = defineProps<{
|
||||
preference: TerminalPreference
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['emitter']);
|
||||
|
||||
const { bubblesEmitter } = useEmitter(emits);
|
||||
|
||||
interface TerminalPreference {
|
||||
darkTheme: string,
|
||||
terminalTheme: TerminalTheme
|
||||
}
|
||||
|
||||
const userDarkTheme = ref(DarkTheme.DARK.value);
|
||||
const userTerminalTheme = ref<TerminalTheme>(FRAPPE);
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
// 修改暗色主题
|
||||
const changeDarkTheme = (value: string) => {
|
||||
if (value === DarkTheme.DARK.value) {
|
||||
props.preference.darkTheme = value;
|
||||
if (value === DarkTheme.DARK) {
|
||||
// 暗色
|
||||
bubblesEmitter('changeDarkTheme', true);
|
||||
} else if (value === DarkTheme.LIGHT.value) {
|
||||
} else if (value === DarkTheme.LIGHT) {
|
||||
// 亮色
|
||||
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();
|
||||
};
|
||||
|
||||
// 选择终端主题
|
||||
const checkTheme = (theme: TerminalTheme) => {
|
||||
userTerminalTheme.value = theme;
|
||||
props.preference.terminalTheme = theme;
|
||||
// 切换主题配色
|
||||
if (userDarkTheme.value === DarkTheme.AUTO.value) {
|
||||
changeDarkTheme(theme.dark ? DarkTheme.DARK.value : DarkTheme.LIGHT.value);
|
||||
} else {
|
||||
sync();
|
||||
if (props.preference.darkTheme === DarkTheme.AUTO) {
|
||||
bubblesEmitter('changeDarkTheme', theme.dark);
|
||||
}
|
||||
// 同步用户偏好
|
||||
sync();
|
||||
};
|
||||
|
||||
// 同步用户偏好
|
||||
const syncUserPreference = async () => {
|
||||
try {
|
||||
// FIXME 同步用户配置
|
||||
|
||||
await updatePreferencePartial({
|
||||
type: 'TERMINAL',
|
||||
config: props.preference
|
||||
});
|
||||
Message.success('同步成功');
|
||||
} catch (e) {
|
||||
Message.error('同步失败');
|
||||
@@ -123,10 +122,6 @@
|
||||
// 同步用户偏好防抖
|
||||
const sync = useDebounceFn(syncUserPreference, 1500);
|
||||
|
||||
onBeforeMount(() => {
|
||||
// FIXME 加载用户配置
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="host-layout">
|
||||
<div class="host-layout" v-if="render">
|
||||
<!-- 头部区域 -->
|
||||
<header class="host-layout-header">
|
||||
<terminal-header v-model="activeKey"
|
||||
@@ -17,6 +17,7 @@
|
||||
<div class="host-layout-content">
|
||||
<terminal-content v-model="activeKey"
|
||||
:tabs="tabs"
|
||||
:preference="preference as TerminalPreference"
|
||||
@change-dark-theme="changeLayoutTheme" />
|
||||
</div>
|
||||
<!-- 右侧操作栏 -->
|
||||
@@ -34,30 +35,36 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TabItem } from './types/terminal.type';
|
||||
import { ref } from 'vue';
|
||||
import type { TabItem, TerminalPreference } from './types/terminal.type';
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
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 TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
|
||||
import TerminalRightSidebar from './components/layout/terminal-right-sidebar.vue';
|
||||
import TerminalContent from './components/layout/terminal-content.vue';
|
||||
import './assets/styles/layout.less';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { onBeforeMount } from 'vue/dist/vue';
|
||||
|
||||
// 系统主题
|
||||
const darkTheme = useDark({
|
||||
selector: 'body',
|
||||
attribute: 'terminal-theme',
|
||||
valueDark: DarkTheme.DARK.value,
|
||||
valueLight: DarkTheme.LIGHT.value,
|
||||
initialValue: DarkTheme.DARK.value as any,
|
||||
valueDark: DarkTheme.DARK,
|
||||
valueLight: DarkTheme.LIGHT,
|
||||
initialValue: DarkTheme.DARK as any,
|
||||
storageKey: null
|
||||
});
|
||||
const dictStore = useDictStore();
|
||||
|
||||
const render = ref(false);
|
||||
const activeKey = ref(InnerTabs.THEME_SETTING.key);
|
||||
const tabs = ref<Array<TabItem>>([InnerTabs.THEME_SETTING]);
|
||||
const preference = ref<TerminalPreference>();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
tabs.value.push({
|
||||
key: `host${i}`,
|
||||
@@ -70,7 +77,6 @@
|
||||
const changeLayoutTheme = (dark: boolean) => {
|
||||
darkTheme.value = dark;
|
||||
};
|
||||
changeLayoutTheme(false);
|
||||
|
||||
// 点击 tab
|
||||
const clickTab = (key: string) => {
|
||||
@@ -96,8 +102,31 @@
|
||||
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>
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface TerminalTheme {
|
||||
}
|
||||
|
||||
// 默认配色
|
||||
export const FRAPPE = {
|
||||
export const DEFAULT_SCHEMA = {
|
||||
name: 'frappe',
|
||||
dark: true,
|
||||
background: '#303446',
|
||||
@@ -59,7 +59,7 @@ export const FRAPPE = {
|
||||
};
|
||||
|
||||
export default [
|
||||
FRAPPE,
|
||||
DEFAULT_SCHEMA,
|
||||
{
|
||||
name: 'latte',
|
||||
dark: false,
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
import type { TerminalTheme } from './terminal.theme';
|
||||
|
||||
// 暗色主题
|
||||
export const DarkTheme = {
|
||||
DARK: {
|
||||
value: 'dark',
|
||||
label: '暗色'
|
||||
},
|
||||
LIGHT: {
|
||||
value: 'light',
|
||||
label: '亮色'
|
||||
},
|
||||
AUTO: {
|
||||
value: 'auto',
|
||||
label: '自动'
|
||||
}
|
||||
DARK: 'dark',
|
||||
LIGHT: 'light',
|
||||
AUTO: 'auto'
|
||||
};
|
||||
|
||||
// 用户终端偏好
|
||||
export interface TerminalPreference {
|
||||
darkTheme: string,
|
||||
terminalTheme: TerminalTheme
|
||||
}
|
||||
|
||||
// sidebar 操作类型
|
||||
export interface SidebarAction {
|
||||
icon: string;
|
||||
@@ -63,3 +61,9 @@ export interface TabItem {
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端暗色模式 字典项
|
||||
export const darkThemeKey = 'terminalDarkTheme';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [darkThemeKey];
|
||||
|
||||
@@ -245,7 +245,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
import { usePagination } from '@/types/table';
|
||||
import { dictValueTypeKey } from '../types/const';
|
||||
import useCopy from '@/hooks/copy';
|
||||
import { useDictStore } from '@/store';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { getDictValueList } from '@/api/system/dict-value';
|
||||
|
||||
const tableRenderData = ref<DictKeyQueryResponse[]>([]);
|
||||
@@ -154,6 +154,7 @@
|
||||
const { copy } = useCopy();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const formModel = reactive<DictKeyQueryRequest>({
|
||||
id: undefined,
|
||||
@@ -170,6 +171,7 @@
|
||||
// 调用删除接口
|
||||
await deleteDictKey(id);
|
||||
Message.success('删除成功');
|
||||
cacheStore.reset('dictKeys');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
@@ -181,11 +183,13 @@
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchTableData();
|
||||
cacheStore.reset('dictKeys');
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchTableData();
|
||||
cacheStore.reset('dictKeys');
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
: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" />
|
||||
</a-form-item>
|
||||
<!-- 配置值 -->
|
||||
@@ -123,18 +125,18 @@
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
const openAdd = async () => {
|
||||
title.value = '添加字典配置值';
|
||||
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);
|
||||
};
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = (record: any) => {
|
||||
const openUpdate = async (record: any) => {
|
||||
title.value = '修改字典配置值';
|
||||
isAddHandle.value = false;
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
await renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
@@ -217,7 +219,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -255,7 +255,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
// 触发 watch 防止第二次加载变成根目录
|
||||
renderForm(defaultForm());
|
||||
};
|
||||
|
||||
@@ -127,7 +127,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -115,7 +115,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -145,7 +145,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -107,7 +107,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user