feat: Add MySQL character set and collation configuration during application installation (#11062)

This commit is contained in:
CityFun 2025-11-25 11:51:17 +08:00 committed by GitHub
parent d6b00967ca
commit 48e2e01a57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 76 additions and 27 deletions

View file

@ -12,6 +12,8 @@ type AppDatabase struct {
DbUser string `json:"PANEL_DB_USER"`
Password string `json:"PANEL_DB_USER_PASSWORD"`
DatabaseName string `json:"DATABASE_NAME"`
Format string `json:"format"`
Collation string `json:"collation"`
}
type AuthParam struct {

View file

@ -300,10 +300,11 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
createMysql.Name = dbConfig.DbName
createMysql.Username = dbConfig.DbUser
createMysql.Database = database.Name
createMysql.Format = "utf8mb4"
createMysql.Format = dbConfig.Format
createMysql.Permission = "%"
createMysql.Password = dbConfig.Password
createMysql.From = database.From
createMysql.Collation = dbConfig.Collation
mysqldb, err := NewIMysqlService().Create(ctx, createMysql)
if err != nil {
return err

View file

@ -730,6 +730,6 @@ var AddGPUMonitor = &gormigrate.Migration{
var UpdateDatabaseMysql = &gormigrate.Migration{
ID: "20251125-update-database-mysql",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Database{})
return tx.AutoMigrate(&model.DatabaseMysql{})
},
}

View file

@ -87,6 +87,7 @@
"vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^0.29.8"

View file

@ -201,6 +201,7 @@ const formRules = ref<FormRules>({
memoryLimit: [Rules.requiredInput, checkNumberRange(0, 9999999999)],
specifyIP: [Rules.ipv4orV6],
restartPolicy: [Rules.requiredSelect],
format: [Rules.requiredInput],
});
const initFormData = () => ({

View file

@ -49,6 +49,8 @@ const formData = ref({
taskID: '',
gpuConfig: false,
specifyIP: '',
format: 'utf8mb4',
collation: '',
});
const handleClose = () => {

View file

@ -118,6 +118,17 @@
</div>
<span class="input-help" v-if="p.description">{{ getDescription(p) }}</span>
</el-form-item>
<el-form-item v-if="form[p.envKey] == 'mysql'" :label="$t('database.format')" prop="format">
<el-select filterable v-model="form.format" @change="loadCollations()">
<el-option v-for="item of formatOptions" :key="item.format" :label="item.format" :value="item.format" />
</el-select>
</el-form-item>
<el-form-item v-if="form[p.envKey] == 'mysql'" :label="$t('database.collation')" prop="collation">
<el-select filterable v-model="form.collation">
<el-option v-for="item of collationOptions" :key="item" :label="item" :value="item" />
</el-select>
<span class="input-help">{{ $t('database.collationHelper', [form.format]) }}</span>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
@ -128,6 +139,7 @@ import { Rules } from '@/global/form-rules';
import { App } from '@/api/interface/app';
import { getDBName, getLabel, getDescription } from '@/utils/util';
import { getPathByType } from '@/api/modules/files';
import { loadFormatCollations } from '@/api/modules/database';
interface ParamObj extends App.FromField {
services: App.AppService[];
@ -163,7 +175,9 @@ const props = defineProps({
},
});
const form = reactive({});
const form = reactive({
format: '',
});
let rules = reactive({});
const params = computed({
get() {
@ -241,6 +255,10 @@ const handleParams = () => {
const getServices = async (childKey: string, key: string | undefined, pObj: ParamObj | undefined) => {
pObj.services = [];
appKey.value = key || '';
if (appKey.value == 'mysql') {
form.format = 'utf8mb4';
}
await getAppService(key).then((res) => {
pObj.services = res.data || [];
form[childKey] = '';
@ -268,6 +286,9 @@ const changeService = (value: string, services: App.AppService[]) => {
});
}
});
if (appKey.value == 'mysql') {
loadOptions(value);
}
updateParam();
};
@ -275,6 +296,22 @@ const toPage = (key: string) => {
window.location.href = '/apps/all?install=' + key;
};
const formatOptions = ref();
const collationOptions = ref();
const appKey = ref('');
const loadOptions = async (database: string) => {
const defaultOptions = [{ format: 'utf8mb4' }, { format: 'utf8mb3' }, { format: 'gbk' }, { format: 'big5' }];
await loadFormatCollations(database).then((res) => {
formatOptions.value = res.data || defaultOptions;
loadCollations();
});
};
const loadCollations = async () => {
collationOptions.value = formatOptions.value?.find((item) => item.format === form.format)?.collations || [];
};
onMounted(() => {
handleParams();
});

View file

@ -417,10 +417,6 @@
</template>
<script lang="ts" setup>
import { searchAppInstalled, installedOp, appInstalledDeleteCheck, getAppIconUrl } from '@/api/modules/app';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import Backups from '@/components/backup/index.vue';
import Uploads from '@/components/upload/index.vue';
import PortJumpDialog from '@/components/port-jump/index.vue';
@ -429,20 +425,25 @@ import AppDelete from './delete/index.vue';
import AppParams from './detail/index.vue';
import AppUpgrade from './upgrade/index.vue';
import AppIgnore from './ignore/index.vue';
import ComposeLogs from '@/components/log/compose/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import { App } from '@/api/interface/app';
import Status from '@/components/status/index.vue';
import { getAge, jumpToPath, toLink } from '@/utils/util';
import { useRouter } from 'vue-router';
import { MsgSuccess } from '@/utils/message';
import TaskLog from '@/components/log/task/index.vue';
import Detail from '@/views/app-store/detail/index.vue';
import IgnoreApp from '@/views/app-store/installed/ignore/create/index.vue';
import { getAgentSettingByKey } from '@/api/modules/setting';
import Tags from '@/views/app-store/components/tag.vue';
import SvgIcon from '@/components/svg-icon/svg-icon.vue';
import MainDiv from '@/components/main-div/index.vue';
import ComposeLogs from '@/components/log/compose/index.vue';
import IgnoreApp from '@/views/app-store/installed/ignore/create/index.vue';
import TerminalDialog from '@/views/container/container/terminal/index.vue';
import { searchAppInstalled, installedOp, appInstalledDeleteCheck, getAppIconUrl } from '@/api/modules/app';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { ElMessageBox } from 'element-plus';
import { App } from '@/api/interface/app';
import { getAge, jumpToPath, toLink } from '@/utils/util';
import { useRouter } from 'vue-router';
import { MsgSuccess } from '@/utils/message';
import { getAgentSettingByKey } from '@/api/modules/setting';
import { routerToFileWithPath, routerToNameWithQuery } from '@/utils/router';
import { useGlobalStore } from '@/composables/useGlobalStore';
const { currentNode, isMaster, currentNodeAddr } = useGlobalStore();

View file

@ -79,6 +79,10 @@
<TaskLog ref="taskLogRef" />
</template>
<script lang="ts" setup>
import Diff from './diff/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { App } from '@/api/interface/app';
import { getAppUpdateVersions, ignoreUpgrade, installedOp } from '@/api/modules/app';
import { getAppStoreConfig } from '@/api/modules/setting';
@ -87,10 +91,7 @@ import { ElMessageBox, FormInstance } from 'element-plus';
import { reactive, ref, onBeforeUnmount } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { Rules } from '@/global/form-rules';
import Diff from './diff/index.vue';
import bus from '@/global/bus';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { v4 as uuidv4 } from 'uuid';
const composeDiffRef = ref();

View file

@ -16,7 +16,7 @@ import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import svgLoader from 'vite-svg-loader';
const prefix = `monaco-editor/esm/vs`;
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
@ -44,7 +44,6 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
scss: {
additionalData: `@use "@/styles/var.scss" as *;`,
silenceDeprecations: ['legacy-js-api'],
api: 'modern',
},
},
},
@ -52,6 +51,9 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
port: viteEnv.VITE_PORT,
open: viteEnv.VITE_OPEN,
host: '0.0.0.0',
sourcemapIgnoreList: (sourcePath) => {
return sourcePath.includes('node_modules');
},
proxy: {
'/api/v2': {
target: 'http://localhost:9999/',
@ -95,12 +97,16 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
svgLoader({
defaultImport: 'url',
}),
monacoEditorPlugin({
languageWorkers: ['editorWorkerService', 'typescript', 'json', 'html', 'css'],
}),
],
esbuild: {
pure: viteEnv.VITE_DROP_CONSOLE ? ['console.log'] : [],
drop: viteEnv.VITE_DROP_CONSOLE && process.env.NODE_ENV === 'production' ? ['debugger'] : [],
},
build: {
sourcemap: false,
outDir: '../core/cmd/server/web',
minify: 'esbuild',
target: 'esnext',
@ -110,15 +116,12 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
manualChunks: {
jsonWorker: [`${prefix}/language/json/json.worker`],
cssWorker: [`${prefix}/language/css/css.worker`],
htmlWorker: [`${prefix}/language/html/html.worker`],
tsWorker: [`${prefix}/language/typescript/ts.worker`],
editorWorker: [`${prefix}/editor/editor.worker`],
},
},
},
},
optimizeDeps: {
include: ['monaco-editor/esm/vs/editor/editor.api'],
exclude: ['monaco-editor'],
},
};
});