[Editor] Support custom configure (#452)

This commit is contained in:
qianmoQ 2023-10-21 19:23:16 -05:00 committed by GitHub
commit 73ac4bd117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 389 additions and 22 deletions

View File

@ -10,6 +10,7 @@ import io.edurt.datacap.service.entity.PageEntity;
import io.edurt.datacap.service.entity.RoleEntity;
import io.edurt.datacap.service.entity.UserEntity;
import io.edurt.datacap.service.entity.UserLogEntity;
import io.edurt.datacap.service.entity.itransient.user.UserEditorEntity;
import io.edurt.datacap.service.model.AiModel;
import io.edurt.datacap.service.record.TreeRecord;
import io.edurt.datacap.service.repository.RoleRepository;
@ -108,4 +109,10 @@ public class UserController
user.setRoles(roles);
return this.userService.saveOrUpdate(user);
}
@PutMapping(value = "changeEditorConfigure")
public CommonResponse<Long> changeEditorConfigure(@Validated @RequestBody UserEditorEntity configure)
{
return this.userService.changeEditorConfigure(configure);
}
}

View File

@ -0,0 +1,2 @@
ALTER TABLE `datacap_user`
ADD COLUMN `editor_configure` TEXT NULL;

View File

@ -0,0 +1,28 @@
package io.edurt.datacap.service.converter;
import io.edurt.datacap.common.utils.JsonUtils;
import io.edurt.datacap.service.entity.itransient.user.UserEditorEntity;
import org.apache.commons.lang3.StringUtils;
import javax.persistence.AttributeConverter;
public class UserEditorConverter
implements AttributeConverter<UserEditorEntity, String>
{
@Override
public String convertToDatabaseColumn(UserEditorEntity userEditorEntity)
{
return JsonUtils.toJSON(userEditorEntity);
}
@Override
public UserEditorEntity convertToEntityAttribute(String s)
{
if (StringUtils.isEmpty(s)) {
return null;
}
else {
return JsonUtils.toObject(s, UserEditorEntity.class);
}
}
}

View File

@ -3,6 +3,8 @@ package io.edurt.datacap.service.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.edurt.datacap.service.converter.UserEditorConverter;
import io.edurt.datacap.service.entity.itransient.user.UserEditorEntity;
import io.edurt.datacap.service.validation.ValidationGroup;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -13,6 +15,7 @@ import lombok.Setter;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
@ -78,6 +81,10 @@ public class UserEntity
@Column(name = "is_system")
private boolean system;
@Column(name = "editor_configure")
@Convert(converter = UserEditorConverter.class)
private UserEditorEntity editorConfigure;
@Column(name = "create_time", columnDefinition = "datetime(5) default CURRENT_TIMESTAMP()")
private Timestamp createTime;

View File

@ -0,0 +1,16 @@
package io.edurt.datacap.service.entity.itransient.user;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserEditorEntity
{
private Integer fontSize = 12;
private String theme = "chrome";
}

View File

@ -6,6 +6,7 @@ import io.edurt.datacap.service.body.UserNameBody;
import io.edurt.datacap.service.body.UserPasswordBody;
import io.edurt.datacap.service.entity.PageEntity;
import io.edurt.datacap.service.entity.UserEntity;
import io.edurt.datacap.service.entity.itransient.user.UserEditorEntity;
import io.edurt.datacap.service.model.AiModel;
import io.edurt.datacap.service.record.TreeRecord;
@ -30,4 +31,6 @@ public interface UserService
CommonResponse<List<TreeRecord>> getMenus();
CommonResponse<PageEntity<UserEntity>> getAll(FilterBody filter);
CommonResponse<Long> changeEditorConfigure(UserEditorEntity configure);
}

View File

@ -16,6 +16,7 @@ import io.edurt.datacap.service.entity.PageEntity;
import io.edurt.datacap.service.entity.RoleEntity;
import io.edurt.datacap.service.entity.SourceEntity;
import io.edurt.datacap.service.entity.UserEntity;
import io.edurt.datacap.service.entity.itransient.user.UserEditorEntity;
import io.edurt.datacap.service.model.AiModel;
import io.edurt.datacap.service.record.TreeRecord;
import io.edurt.datacap.service.repository.RoleRepository;
@ -243,4 +244,17 @@ public class UserServiceImpl
Pageable pageable = PageRequestAdapter.of(filter);
return CommonResponse.success(PageEntity.build(this.userRepository.findAll(pageable)));
}
@Override
public CommonResponse<Long> changeEditorConfigure(UserEditorEntity configure)
{
Optional<UserEntity> userOptional = this.userRepository.findById(UserDetailsService.getUser().getId());
if (!userOptional.isPresent()) {
return CommonResponse.failure(ServiceState.USER_NOT_FOUND);
}
UserEntity user = userOptional.get();
user.setEditorConfigure(configure);
this.userRepository.save(user);
return CommonResponse.success(user.getId());
}
}

View File

@ -0,0 +1,45 @@
import 'ace-builds/src-noconflict/mode-mysql';
import 'ace-builds/src-noconflict/theme-ambiance';
import 'ace-builds/src-noconflict/theme-chaos';
import 'ace-builds/src-noconflict/theme-chrome';
import 'ace-builds/src-noconflict/theme-clouds';
import 'ace-builds/src-noconflict/theme-cloud9_day';
import 'ace-builds/src-noconflict/theme-cloud9_night';
import 'ace-builds/src-noconflict/theme-cloud9_night_low_color';
import 'ace-builds/src-noconflict/theme-clouds_midnight';
import 'ace-builds/src-noconflict/theme-cobalt';
import 'ace-builds/src-noconflict/theme-crimson_editor';
import 'ace-builds/src-noconflict/theme-dawn';
import 'ace-builds/src-noconflict/theme-dracula';
import 'ace-builds/src-noconflict/theme-dreamweaver';
import 'ace-builds/src-noconflict/theme-eclipse';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-gob';
import 'ace-builds/src-noconflict/theme-github_dark';
import 'ace-builds/src-noconflict/theme-gruvbox';
import 'ace-builds/src-noconflict/theme-gruvbox_dark_hard';
import 'ace-builds/src-noconflict/theme-gruvbox_light_hard';
import 'ace-builds/src-noconflict/theme-iplastic';
import 'ace-builds/src-noconflict/theme-idle_fingers';
import 'ace-builds/src-noconflict/theme-kr_theme';
import 'ace-builds/src-noconflict/theme-katzenmilch';
import 'ace-builds/src-noconflict/theme-kuroir';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-merbivore';
import 'ace-builds/src-noconflict/theme-mono_industrial';
import 'ace-builds/src-noconflict/theme-nord_dark';
import 'ace-builds/src-noconflict/theme-one_dark';
import 'ace-builds/src-noconflict/theme-pastel_on_dark';
import 'ace-builds/src-noconflict/theme-sqlserver';
import 'ace-builds/src-noconflict/theme-solarized_dark';
import 'ace-builds/src-noconflict/theme-solarized_light';
import 'ace-builds/src-noconflict/theme-terminal';
import 'ace-builds/src-noconflict/theme-textmate';
import 'ace-builds/src-noconflict/theme-tomorrow_night';
import 'ace-builds/src-noconflict/theme-tomorrow_night_blue';
import 'ace-builds/src-noconflict/theme-tomorrow_night_bright';
import 'ace-builds/src-noconflict/theme-tomorrow_night_eighties';
import 'ace-builds/src-noconflict/theme-tomorrow';
import 'ace-builds/src-noconflict/theme-twilight';
import 'ace-builds/src-noconflict/theme-vibrant_ink';
import 'ace-builds/src-noconflict/theme-xcode';

View File

@ -1,5 +1,6 @@
const token = 'DataCapAuthToken';
const menu = 'DataCapAvailableMenus';
const userEditorConfigure = 'DataCapUserEditorConfigure';
const getCurrentUserId = () => {
return JSON.parse(localStorage.getItem(token) || '{}').id;
}
@ -7,5 +8,6 @@ const getCurrentUserId = () => {
export default {
token: token,
menu: menu,
getCurrentUserId: getCurrentUserId
getCurrentUserId: getCurrentUserId,
userEditorConfigure: userEditorConfigure
}

View File

@ -158,5 +158,9 @@ export default {
notSpecifiedFormat: 'Not Specified Format',
notSpecifiedIndex: 'Not Specified Index',
notUpdated: 'Not Update',
notSpecified: 'Not Specified'
notSpecified: 'Not Specified',
editor: 'Editor',
fontSize: 'Font Size',
theme: 'Theme',
preview: 'Preview',
}

View File

@ -11,4 +11,5 @@ export default {
log: 'Login Log',
changeChatGpt: 'Change the chatgpt',
contentCount: 'Content Count',
changeEditor: 'Change the editor',
}

View File

@ -158,5 +158,9 @@ export default {
notSpecifiedFormat: '未指定格式',
notSpecifiedIndex: '未指定索引',
notUpdated: '未更新',
notSpecified: '未指定'
notSpecified: '未指定',
editor: '编辑器',
fontSize: '字体大小',
theme: '主题',
preview: '预览'
}

View File

@ -10,5 +10,6 @@ export default {
newUsername: '新用户名',
log: '登录日志',
changeChatGpt: '修改 ChatGPT',
contentCount: '上下文关联数'
contentCount: '上下文关联数',
changeEditor: '修改编辑器',
}

View File

@ -42,3 +42,9 @@ export class ThirdConfigure
timeout = 30;
contentCount = 5
}
export class EditorConfigure
{
fontSize = 12;
theme = "chrome";
}

View File

@ -3,6 +3,7 @@ import {ResponseModel} from "@/model/ResponseModel";
import {UserPassword} from "@/model/UserPassword";
import {UserName} from "@/model/UserName";
import {Filter} from "@/model/Filter";
import {EditorConfigure} from "@/model/User";
const baseUrl = "/api/v1/user";
@ -42,6 +43,11 @@ class UserService
{
return new HttpCommon().get(baseUrl + '/menus');
}
changeEditorConfigure(configure: EditorConfigure): Promise<ResponseModel>
{
return new HttpCommon().put(`${baseUrl}/changeEditorConfigure`, configure);
}
}
export default new UserService();

View File

@ -44,6 +44,7 @@ import UserService from "@/services/UserService";
import {createDefaultRouter, createRemoteRouter} from "@/router/default";
import {useI18n} from "vue-i18n";
import CaptchaService from '@/services/CaptchaService';
import {HttpCommon} from "@/common/HttpCommon";
export default defineComponent({
setup()
@ -110,18 +111,21 @@ export default defineComponent({
.then(response => {
if (response.status) {
localStorage.setItem(Common.token, JSON.stringify(response.data));
UserService.getMenus()
.then(menuResponse => {
if (menuResponse.status) {
localStorage.setItem(Common.menu, JSON.stringify(menuResponse.data))
const client = new HttpCommon().getAxios();
client.all([UserService.getMenus(), UserService.getInfo()])
.then(client.spread((fetchMenu, fetchInfo) => {
if (fetchMenu.status && fetchInfo.status) {
localStorage.setItem(Common.menu, JSON.stringify(fetchMenu.data))
createDefaultRouter(router)
createRemoteRouter(menuResponse.data, router)
createRemoteRouter(fetchMenu.data, router)
localStorage.setItem(Common.userEditorConfigure, JSON.stringify(fetchInfo.data.editorConfigure))
router.push('/dashboard/index');
}
else {
this.$Message.error(menuResponse.message)
this.$Message.error(fetchMenu.message);
this.$Message.error(fetchInfo.message);
}
})
}));
}
else {
this.$Message.error(response.message);

View File

@ -98,10 +98,11 @@ export default defineComponent({
const handlerLogout = () => {
localStorage.removeItem(Common.token);
localStorage.removeItem(Common.menu)
createDefaultRouter(router)
createRemoteRouter([], router)
router.push('/auth/signin')
localStorage.removeItem(Common.menu);
localStorage.removeItem(Common.userEditorConfigure);
createDefaultRouter(router);
createRemoteRouter([], router);
router.push('/auth/signin');
}
const language = 'zh_cn';
const version = config.version;

View File

@ -111,10 +111,10 @@
:label="editor.title"
:closable="editor.closable">
<VAceEditor lang="mysql"
theme="chrome"
style="height: 300px"
:theme="editor.configure.theme"
:style="{ height: '300px', fontSize: editor.configure.fontSize + 'px' }"
:key="editor.key"
:options="{enableSnippets: true, enableLiveAutocompletion: true}"
:options="{ enableSnippets: true, enableLiveAutocompletion: true }"
@init="handlerEditorDidMount($event, 'mysql', editor.key)">
</VAceEditor>
</TabPane>
@ -173,6 +173,9 @@ import 'ace-builds/src-noconflict/theme-chrome';
import langTools from 'ace-builds/src-noconflict/ext-language_tools';
import {HttpCommon} from "@/common/HttpCommon";
import FunctionsService from "@/services/settings/functions/FunctionsService";
import Common from "@/common/Common";
import {EditorConfigure} from "@/model/User";
import '@/ace-editor-theme';
import Editor = Ace.Editor;
interface EditorInstance
@ -180,7 +183,8 @@ interface EditorInstance
title: string;
key: string;
closable?: boolean,
instance?: Editor
instance?: Editor,
configure?: EditorConfigure
}
export default defineComponent({
@ -213,6 +217,7 @@ export default defineComponent({
response: {} as ResponseModel,
snippetDetails: false,
editorMaps: new Map<string, EditorInstance>(),
editorConfigure: null,
activeKey: null,
content: null,
visibleAiHelp: false,
@ -230,10 +235,13 @@ export default defineComponent({
methods: {
handlerInitialize()
{
const localEditorConfigure = localStorage.getItem(Common.userEditorConfigure);
this.editorConfigure = localEditorConfigure ? JSON.parse(localEditorConfigure) : new EditorConfigure();
this.buttonRunText = this.i18n.t('common.run');
const defaultEditor: EditorInstance = {
title: 'New Query',
key: Date.now().toString()
key: Date.now().toString(),
configure: this.editorConfigure
};
this.activeKey = defaultEditor.key;
this.editorMaps.set(defaultEditor.key, defaultEditor);
@ -432,7 +440,8 @@ export default defineComponent({
const newEditor: EditorInstance = {
title: 'New Query',
key: Date.now().toString(),
closable: true
closable: true,
configure: this.editorConfigure
};
this.activeKey = newEditor.key;
this.editorMaps.set(newEditor.key, newEditor);

View File

@ -27,6 +27,13 @@
{{ $t('setting.changeChatGpt') }}
</Button>
</FormItem>
<FormItem :label="$t('common.editor')">
<Button type="text"
style="float: right;"
@click="handlerEditor(true)">
{{ $t('setting.changeEditor') }}
</Button>
</FormItem>
</Form>
</Content>
</Layout>
@ -58,6 +65,10 @@
:is-visible="changeChatGpt"
@close="handlerChatGPT(false)">
</ChangeChatGpt>
<ChangeEditor v-if="changeEditor"
:is-visible="changeEditor"
@close="handlerEditor(false)">
</ChangeEditor>
</div>
</template>
<script lang="ts">
@ -68,9 +79,10 @@ import Common from "@/common/Common";
import router from "@/router";
import ChangeUsername from "@/views/user/profile/components/ChangeUsername.vue";
import ChangeChatGpt from "@/views/user/profile/components/ChangeChatGpt.vue";
import ChangeEditor from "@/views/user/profile/components/ChangeEditor.vue";
export default defineComponent({
components: {ChangeChatGpt, ChangeUsername},
components: {ChangeEditor, ChangeChatGpt, ChangeUsername},
setup()
{
const formState = reactive<UserPassword>({
@ -88,6 +100,7 @@ export default defineComponent({
changePasswordVisible: false,
changeUsername: false,
changeChatGpt: false,
changeEditor: false,
loading: false
}
},
@ -127,6 +140,10 @@ export default defineComponent({
handlerChatGPT(opened: boolean)
{
this.changeChatGpt = opened
},
handlerEditor(opened: boolean)
{
this.changeEditor = opened
}
}
});

View File

@ -0,0 +1,52 @@
import '@/ace-editor-theme';
const themes = [
'ambiance',
'chaos',
'chrome',
'clouds',
'cloud9_day',
'cloud9_night',
'cloud9_night_low_color',
'clouds_midnight',
'cobalt',
'crimson_editor',
'dawn',
'dracula',
'dreamweaver',
'eclipse',
'github',
'gob',
'github_dark',
'gruvbox',
'gruvbox_dark_hard',
'gruvbox_light_hard',
'iplastic',
'idle_fingers',
'kr_theme',
'katzenmilch',
'kuroir',
'monokai',
'merbivore',
'mono_industrial',
'nord_dark',
'one_dark',
'pastel_on_dark',
'sqlserver',
'solarized_dark',
'solarized_light',
'terminal',
'textmate',
'tomorrow_night',
'tomorrow_night_blue',
'tomorrow_night_bright',
'tomorrow_night_eighties',
'tomorrow',
'twilight',
'vibrant_ink',
'xcode',
];
export default {
themes
}

View File

@ -0,0 +1,138 @@
<template>
<div>
<Modal v-model="visible"
:title="$t('common.editor')"
:closable="false"
width="70%"
:mask-closable="false"
@cancel="handlerCancel()">
<Divider orientation="left">{{ $t('common.preview') }}</Divider>
<VAceEditor lang="mysql"
:theme="configure.theme"
:style="{height: '96px', fontSize: configure.fontSize + 'px'}"
:value="sqlContent"
:options="{readOnly: true}">
</VAceEditor>
<Divider orientation="left">{{ $t('common.configure') }}</Divider>
<Form :model="configure"
:label-width="180">
<FormItem name="fontSize"
:label="$t('common.fontSize')">
<InputNumber v-model="configure.fontSize"/>
</FormItem>
<FormItem name="theme"
:label="$t('common.theme')">
<Select v-model="configure.theme"
style="width: 200px;">
<Option v-for="theme in themes"
:value="theme"
:key="theme">
{{ theme }}
</Option>
</Select>
</FormItem>
<CircularLoading v-if="loadingUserInfo"
:show="loadingUserInfo">
</CircularLoading>
</Form>
<template #footer>
<Button danger
@click="handlerCancel">
{{ $t('common.cancel') }}
</Button>
<Button type="primary"
:loading="loadingChange"
@click="handlerSave()">
{{ $t('common.save') }}
</Button>
</template>
</Modal>
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import UserService from "@/services/UserService";
import CircularLoading from "@/components/loading/CircularLoading.vue";
import {EditorConfigure} from "@/model/User";
import {VAceEditor} from "vue3-ace-editor";
import themes from './AceEditor';
import Common from "@/common/Common";
export default defineComponent({
name: 'ChangeEditor',
components: {VAceEditor, CircularLoading},
props: {
isVisible: {
type: Boolean,
default: () => false
}
},
data()
{
return {
sqlContent: 'SELECT *\nFROM `database`.`table`\nGROUP BY `name`\nORDER BY `id`\nLIMIT 100\nOFFSET 1000',
themes: [],
loadingUserInfo: false,
loadingChange: false,
configure: {} as EditorConfigure
}
},
created()
{
this.handlerInitialize()
},
methods: {
handlerInitialize()
{
this.themes = themes.themes;
this.loadingUserInfo = true
UserService.getInfo()
.then(response => {
if (response.data.editorConfigure) {
this.configure = response.data.editorConfigure;
}
else {
this.configure = new EditorConfigure()
}
})
.finally(() => {
this.loadingUserInfo = false
})
},
handlerSave()
{
this.loadingChange = true;
UserService.changeEditorConfigure(this.configure)
.then((response) => {
if (response.status) {
this.$Message.success('Success');
localStorage.setItem(Common.userEditorConfigure, JSON.stringify(this.configure))
this.handlerCancel()
}
else {
this.$Message.error(response.message);
}
})
.finally(() => {
this.loadingChange = false;
});
},
handlerCancel()
{
this.visible = false;
},
},
computed: {
visible: {
get(): boolean
{
return this.isVisible;
},
set(value: boolean)
{
this.$emit('close', value);
}
}
}
});
</script>