feat: 外观设置
This commit is contained in:
parent
4aafda3446
commit
d5ca5eeaf4
@ -1,7 +1,6 @@
|
||||
import { Result } from '@/request/Result'
|
||||
import { get, post, del, put } from '@/request/index'
|
||||
import type { TeamMember } from '@/api/type/team'
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
const prefix = '/display'
|
||||
|
||||
/**
|
||||
@ -23,12 +22,14 @@ const getThemeInfo: () => Promise<Result<any>> = () => {
|
||||
* slogan
|
||||
* }
|
||||
*/
|
||||
const postThemeInfo: (data: any) => Promise<Result<boolean>> = (data) => {
|
||||
return post(`${prefix}/update`, data)
|
||||
const postThemeInfo: (data: any, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
|
||||
data,
|
||||
loading
|
||||
) => {
|
||||
return post(`${prefix}/update`, data, undefined, loading)
|
||||
}
|
||||
|
||||
export default {
|
||||
getThemeInfo,
|
||||
postThemeInfo
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.4409 232.4409"><title>MaxKB</title><path class="cls-1" d="M128.4532,177H98.7785L87.78,187.9985a4.6069,4.6069,0,0,0,3.2576,7.8644h45.1569a4.6069,4.6069,0,0,0,3.2575-7.8644Z"/><path class="cls-1" d="M210.0008,90.7042h-5.85v41.1511h5.85a4.4537,4.4537,0,0,0,4.4537-4.4537V95.1579A4.4537,4.4537,0,0,0,210.0008,90.7042Z"/><path class="cls-1" d="M28.29,90.7042H22.44a4.4538,4.4538,0,0,0-4.4538,4.4537v32.2437a4.4538,4.4538,0,0,0,4.4538,4.4537h5.85Z"/><path class="cls-1" d="M138.8087,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,138.8087,96.1512Z"/><path class="cls-1" d="M95.3622,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,95.3622,96.1512Z"/><path class="cls-1" d="M166.8344,48.8968H65.6064A33.7544,33.7544,0,0,0,31.89,82.6131v57.07A33.7548,33.7548,0,0,0,65.6064,173.4h101.228a33.7549,33.7549,0,0,0,33.7168-33.7168v-57.07A33.7545,33.7545,0,0,0,166.8344,48.8968Zm2.831,90.4457a6.0733,6.0733,0,0,1-6.0732,6.0733H114.2168a43.5922,43.5922,0,0,0-21.3313,5.5757l-16.5647,9.2946v-14.87h-7.472a6.0733,6.0733,0,0,1-6.0733-6.0733v-60.5a6.0733,6.0733,0,0,1,6.0733-6.0733h94.7434a6.0733,6.0733,0,0,1,6.0732,6.0733Z"/></svg>
|
||||
<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.4409 232.4409"><defs><style>.cls-1{fill:#fff;}</style></defs><title>MaxKB</title><path class="cls-1" d="M128.4532,177H98.7785L87.78,187.9985a4.6069,4.6069,0,0,0,3.2576,7.8644h45.1569a4.6069,4.6069,0,0,0,3.2575-7.8644Z"/><path class="cls-1" d="M210.0008,90.7042h-5.85v41.1511h5.85a4.4537,4.4537,0,0,0,4.4537-4.4537V95.1579A4.4537,4.4537,0,0,0,210.0008,90.7042Z"/><path class="cls-1" d="M28.29,90.7042H22.44a4.4538,4.4538,0,0,0-4.4538,4.4537v32.2437a4.4538,4.4538,0,0,0,4.4538,4.4537h5.85Z"/><path class="cls-1" d="M138.8087,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,138.8087,96.1512Z"/><path class="cls-1" d="M95.3622,96.1512a8.33,8.33,0,0,0-8.33,8.33v5.9727a8.33,8.33,0,1,0,16.6607,0v-5.9727A8.33,8.33,0,0,0,95.3622,96.1512Z"/><path class="cls-1" d="M166.8344,48.8968H65.6064A33.7544,33.7544,0,0,0,31.89,82.6131v57.07A33.7548,33.7548,0,0,0,65.6064,173.4h101.228a33.7549,33.7549,0,0,0,33.7168-33.7168v-57.07A33.7545,33.7545,0,0,0,166.8344,48.8968Zm2.831,90.4457a6.0733,6.0733,0,0,1-6.0732,6.0733H114.2168a43.5922,43.5922,0,0,0-21.3313,5.5757l-16.5647,9.2946v-14.87h-7.472a6.0733,6.0733,0,0,1-6.0733-6.0733v-60.5a6.0733,6.0733,0,0,1,6.0733-6.0733h94.7434a6.0733,6.0733,0,0,1,6.0732,6.0733Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@ -955,7 +955,7 @@ export const iconMap: any = {
|
||||
])
|
||||
}
|
||||
},
|
||||
'app-minify': {
|
||||
'app-magnify': {
|
||||
iconReader: () => {
|
||||
return h('i', [
|
||||
h(
|
||||
@ -976,7 +976,7 @@ export const iconMap: any = {
|
||||
])
|
||||
}
|
||||
},
|
||||
'app-magnify': {
|
||||
'app-minify': {
|
||||
iconReader: () => {
|
||||
return h('i', [
|
||||
h(
|
||||
@ -1071,5 +1071,5 @@ export const iconMap: any = {
|
||||
)
|
||||
])
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,9 +3,7 @@
|
||||
<div class="login-container w-full h-full">
|
||||
<el-row class="container w-full h-full">
|
||||
<el-col :xs="0" :sm="0" :md="10" :lg="10" :xl="10" class="left-container">
|
||||
<div class="login-image">
|
||||
<img :src="`../src/assets/theme/${themeImg}.jpg`" class="login-image" />
|
||||
</div>
|
||||
<div class="login-image" :style="loginImageStyle"></div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="14" :lg="14" :xl="14" class="right-container flex-center">
|
||||
<slot></slot>
|
||||
@ -15,12 +13,33 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { getThemeImg } from '@/utils/theme'
|
||||
import useStore from '@/stores'
|
||||
defineOptions({ name: 'LoginLayout' })
|
||||
const props = defineProps({
|
||||
themeImg: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
const { user } = useStore()
|
||||
|
||||
const fileURL = computed(() => {
|
||||
if (user.themeInfo.loginImage) {
|
||||
if (typeof user.themeInfo.loginImage === 'string') {
|
||||
return user.themeInfo.loginImage
|
||||
} else {
|
||||
return URL.createObjectURL(user.themeInfo.loginImage)
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const loginImageStyle = computed(() => {
|
||||
if (user.themeInfo.loginImage) {
|
||||
return {
|
||||
backgroundImage: `url(${fileURL.value})`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
backgroundImage: `url(../src/assets/theme/${getThemeImg(user.themeInfo?.theme)}.jpg)`
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -29,7 +48,9 @@ const props = defineProps({
|
||||
height: 100vh;
|
||||
|
||||
.login-image {
|
||||
object-fit: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
<template>
|
||||
<img v-if="user.themeInfo.loginLogo" :src="fileURL" alt="" height="45px" class="mr-8" />
|
||||
<template v-else>
|
||||
<svg
|
||||
v-if="!isDefaultTheme"
|
||||
viewBox="0 0 122 36"
|
||||
@ -55,6 +57,7 @@
|
||||
</svg>
|
||||
<img v-else src="@/assets/logo/MaxKB-logo.svg" :height="height" />
|
||||
</template>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import useStore from '@/stores'
|
||||
@ -66,9 +69,21 @@ defineProps({
|
||||
default: '36px'
|
||||
}
|
||||
})
|
||||
const { common } = useStore()
|
||||
const { user } = useStore()
|
||||
const isDefaultTheme = computed(() => {
|
||||
return common.isDefaultTheme()
|
||||
return user.isDefaultTheme()
|
||||
})
|
||||
|
||||
const fileURL = computed(() => {
|
||||
if (user.themeInfo.loginLogo) {
|
||||
if (typeof user.themeInfo.loginLogo === 'string') {
|
||||
return user.themeInfo.loginLogo
|
||||
} else {
|
||||
return URL.createObjectURL(user.themeInfo.loginLogo)
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -45,9 +45,9 @@ defineProps({
|
||||
default: '36px'
|
||||
}
|
||||
})
|
||||
const { common } = useStore()
|
||||
const { user } = useStore()
|
||||
const isDefaultTheme = computed(() => {
|
||||
return common.isDefaultTheme()
|
||||
return user.isDefaultTheme()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -8,9 +8,9 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import TopBar from '../top-bar/index.vue'
|
||||
import useStore from '@/stores'
|
||||
const { common } = useStore()
|
||||
const { user } = useStore()
|
||||
const isDefaultTheme = computed(() => {
|
||||
return common.isDefaultTheme()
|
||||
return user.isDefaultTheme()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -35,9 +35,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import useStore from '@/stores'
|
||||
const { common, user } = useStore()
|
||||
const { user } = useStore()
|
||||
const isDefaultTheme = computed(() => {
|
||||
return common.isDefaultTheme()
|
||||
return user.isDefaultTheme()
|
||||
})
|
||||
|
||||
const aboutDialogVisible = ref(false)
|
||||
|
||||
@ -4,7 +4,6 @@ import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { createApp } from 'vue'
|
||||
import { store } from '@/stores'
|
||||
import theme from '@/theme'
|
||||
import directives from '@/directives'
|
||||
import App from './App.vue'
|
||||
import router from '@/router'
|
||||
@ -56,8 +55,6 @@ app.use(ElementPlus, {
|
||||
locale: zhCn
|
||||
})
|
||||
|
||||
app.use(theme)
|
||||
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
app.use(Components)
|
||||
|
||||
@ -4,7 +4,7 @@ import { Role, ComplexPermission } from '@/utils/permission/type'
|
||||
const settingRouter = {
|
||||
path: '/setting',
|
||||
name: 'setting',
|
||||
meta: { icon: 'Setting', title: '系统设置', permission: 'SETTING:READ' },
|
||||
meta: { icon: 'Setting', title: '系统管理', permission: 'SETTING:READ' },
|
||||
redirect: () => {
|
||||
if (hasPermission(new Role('ADMIN'), 'AND')) {
|
||||
return '/user'
|
||||
@ -59,7 +59,7 @@ const settingRouter = {
|
||||
meta: {
|
||||
icon: 'app-setting',
|
||||
iconActive: 'app-setting-active',
|
||||
title: '系统设置',
|
||||
title: '系统管理',
|
||||
activeMenu: '/setting',
|
||||
parentPath: '/setting',
|
||||
parentName: 'setting',
|
||||
|
||||
@ -8,7 +8,6 @@ export interface commonTypes {
|
||||
paginationConfig: any | null
|
||||
search: any
|
||||
device: string
|
||||
theme: string
|
||||
}
|
||||
|
||||
const useCommonStore = defineStore({
|
||||
@ -18,16 +17,9 @@ const useCommonStore = defineStore({
|
||||
// 搜索和分页缓存
|
||||
paginationConfig: {},
|
||||
search: {},
|
||||
device: DeviceType.Desktop,
|
||||
theme: ''
|
||||
device: DeviceType.Desktop
|
||||
}),
|
||||
actions: {
|
||||
isDefaultTheme() {
|
||||
return !this.theme || this.theme === '#3370FF'
|
||||
},
|
||||
setTheme(val: string) {
|
||||
this.theme = val
|
||||
},
|
||||
saveBreadcrumb(data: any) {
|
||||
this.breadcrumb = data
|
||||
},
|
||||
|
||||
@ -2,6 +2,8 @@ import { defineStore } from 'pinia'
|
||||
import type { User } from '@/api/type/user'
|
||||
import UserApi from '@/api/user'
|
||||
import ThemeApi from '@/api/theme'
|
||||
import { useElementPlusTheme } from 'use-element-plus-theme'
|
||||
const { changeTheme } = useElementPlusTheme()
|
||||
|
||||
export interface userStateTypes {
|
||||
userType: number // 1 系统操作者 2 对话用户
|
||||
@ -26,6 +28,13 @@ const useUserStore = defineStore({
|
||||
themeInfo: null
|
||||
}),
|
||||
actions: {
|
||||
isDefaultTheme() {
|
||||
return !this.themeInfo?.theme || this.themeInfo?.theme === '#3370FF'
|
||||
},
|
||||
setTheme(data: any) {
|
||||
changeTheme(data?.['theme'])
|
||||
this.themeInfo = data
|
||||
},
|
||||
isExpire() {
|
||||
return this.isXPack && !this.XPACK_LICENSE_IS_VALID
|
||||
},
|
||||
@ -82,8 +91,14 @@ const useUserStore = defineStore({
|
||||
},
|
||||
|
||||
async theme() {
|
||||
return ThemeApi.getThemeInfo().then((ok) => {
|
||||
return await ThemeApi.getThemeInfo().then((ok) => {
|
||||
this.themeInfo = ok.data
|
||||
changeTheme(this.themeInfo['theme'])
|
||||
window.document.title = this.themeInfo['title'] || 'MaxKB'
|
||||
const link = document.querySelector('link[rel="icon"]') as any
|
||||
if (link) {
|
||||
link['href'] = this.themeInfo['icon'] || '/favicon.ico'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@ -369,8 +369,8 @@ h5 {
|
||||
|
||||
/* tag */
|
||||
.default-tag {
|
||||
background: var(--tag-default-bg);
|
||||
color: var(--tag-default-color);
|
||||
background: var(--el-color-primary-light-7);
|
||||
color: var(--el-color-primary);
|
||||
border: none;
|
||||
}
|
||||
.success-tag {
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import type { InferData } from "./type";
|
||||
const inferData: Array<InferData> = [
|
||||
{
|
||||
key: "primary",
|
||||
value: "#3370FF",
|
||||
},
|
||||
{ key: "success", value: "#67c23a" },
|
||||
{ key: "warning", value: "#e6a23c" },
|
||||
{ key: "danger", value: "#f56c6c" },
|
||||
{ key: "error", value: "#F54A45" },
|
||||
{ key: "info", value: "#909399" },
|
||||
];
|
||||
export default inferData;
|
||||
@ -1,5 +0,0 @@
|
||||
import type { KeyValueData } from './type'
|
||||
const keyValueData: KeyValueData = {
|
||||
'--el-header-padding': '0px'
|
||||
}
|
||||
export default keyValueData
|
||||
@ -1,281 +0,0 @@
|
||||
import type {
|
||||
ThemeSetting,
|
||||
InferData,
|
||||
KeyValueData,
|
||||
UpdateInferData,
|
||||
UpdateKeyValueData
|
||||
} from './type'
|
||||
import { TinyColor } from '@ctrl/tinycolor'
|
||||
// 引入默认推断数据
|
||||
import inferData from './defaultInferData'
|
||||
// 引入默认keyValue数据
|
||||
import keyValueData from './defaultKeyValueData'
|
||||
// 引入设置对象
|
||||
import setting from './setting'
|
||||
import type { App } from 'vue'
|
||||
declare global {
|
||||
interface ChildNode {
|
||||
innerText: string
|
||||
}
|
||||
}
|
||||
class Theme {
|
||||
/**
|
||||
* 主题设置
|
||||
*/
|
||||
themeSetting: ThemeSetting
|
||||
/**
|
||||
* 键值数据
|
||||
*/
|
||||
keyValue: KeyValueData
|
||||
/**
|
||||
* 外推数据
|
||||
*/
|
||||
inferData: Array<InferData>
|
||||
/**
|
||||
*是否是第一次初始化
|
||||
*/
|
||||
isFirstWriteStyle: boolean
|
||||
/**
|
||||
* 混色白
|
||||
*/
|
||||
colorWhite: string
|
||||
/**
|
||||
* 混色黑
|
||||
*/
|
||||
colorBlack: string
|
||||
|
||||
constructor(themeSetting: ThemeSetting, keyValue: KeyValueData, inferData: Array<InferData>) {
|
||||
this.themeSetting = themeSetting
|
||||
this.keyValue = keyValue
|
||||
this.inferData = inferData
|
||||
this.isFirstWriteStyle = true
|
||||
this.colorWhite = '#ffffff'
|
||||
this.colorBlack = '#000000'
|
||||
this.initDefaultTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接
|
||||
* @param setting 主题设置
|
||||
* @param names 需要拼接的所有值
|
||||
* @returns 拼接后的数据
|
||||
*/
|
||||
getVarName = (setting: ThemeSetting, ...names: Array<string>) => {
|
||||
return (
|
||||
setting.startDivision + setting.namespace + setting.division + names.join(setting.division)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换外推数据
|
||||
* @param setting 主题设置对象
|
||||
* @param inferData 外推数据
|
||||
* @returns
|
||||
*/
|
||||
mapInferMainStyle = (setting: ThemeSetting, inferData: InferData) => {
|
||||
const key: string = this.getVarName(
|
||||
setting,
|
||||
inferData.setting ? inferData.setting.type : setting.colorInferSetting.type,
|
||||
inferData.key
|
||||
)
|
||||
return {
|
||||
[key]: inferData.value,
|
||||
...this.mapInferDataStyle(setting, inferData)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 转换外推数据
|
||||
* @param setting 设置
|
||||
* @param inferData 外推数据
|
||||
*/
|
||||
mapInferData = (setting: ThemeSetting, inferData: Array<InferData>) => {
|
||||
return inferData
|
||||
.map((itemData) => {
|
||||
return this.mapInferMainStyle(setting, itemData)
|
||||
})
|
||||
.reduce((pre, next) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
/**
|
||||
* 转换外推数据
|
||||
* @param setting 主题设置对象
|
||||
* @param inferData 外推数据
|
||||
* @returns
|
||||
*/
|
||||
mapInferDataStyle = (setting: ThemeSetting, inferData: InferData) => {
|
||||
const inferSetting = inferData.setting ? inferData.setting : setting.colorInferSetting
|
||||
if (inferSetting.type === 'color') {
|
||||
return Object.keys(inferSetting)
|
||||
.map((key: string) => {
|
||||
if (key === 'light' || key === 'dark') {
|
||||
return inferSetting[key]
|
||||
.map((l: any) => {
|
||||
const varName = this.getVarName(
|
||||
setting,
|
||||
inferSetting.type,
|
||||
inferData.key,
|
||||
key,
|
||||
l.toString()
|
||||
)
|
||||
return {
|
||||
[varName]: new TinyColor(inferData.value)
|
||||
.mix(key === 'light' ? this.colorWhite : this.colorBlack, l * 10)
|
||||
.toHexString()
|
||||
}
|
||||
})
|
||||
.reduce((pre: any, next: any) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
return {}
|
||||
})
|
||||
.reduce((pre, next) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param themeSetting 主题设置
|
||||
* @param keyValueData 键值数据
|
||||
* @returns 映射后的键值数据
|
||||
*/
|
||||
mapKeyValue = (themeSetting: ThemeSetting, keyValueData: KeyValueData) => {
|
||||
return Object.keys(keyValueData)
|
||||
.map((key: string) => {
|
||||
return {
|
||||
[this.updateKeyBySetting(key, themeSetting)]: keyValueData[key]
|
||||
}
|
||||
})
|
||||
.reduce((pre, next) => {
|
||||
return { ...pre, ...next }
|
||||
}, {})
|
||||
}
|
||||
/**
|
||||
* 根据配置文件修改Key
|
||||
* @param key key
|
||||
* @param themeSetting 主题设置
|
||||
* @returns
|
||||
*/
|
||||
updateKeyBySetting = (key: string, themeSetting: ThemeSetting) => {
|
||||
return key.startsWith(themeSetting.startDivision)
|
||||
? key
|
||||
: key.startsWith(themeSetting.namespace)
|
||||
? themeSetting.startDivision + key
|
||||
: key.startsWith(themeSetting.division)
|
||||
? themeSetting.startDivision + themeSetting.namespace
|
||||
: themeSetting.startDivision + themeSetting.namespace + themeSetting.division + key
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param setting 主题设置
|
||||
* @param keyValue 主题键值对数据
|
||||
* @param inferData 外推数据
|
||||
* @returns 合并后的键值对数据
|
||||
*/
|
||||
tokeyValueStyle = () => {
|
||||
return {
|
||||
...this.mapInferData(this.themeSetting, this.inferData),
|
||||
...this.mapKeyValue(this.themeSetting, this.keyValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将keyValue对象转换为S
|
||||
* @param keyValue
|
||||
* @returns
|
||||
*/
|
||||
toString = (keyValue: KeyValueData) => {
|
||||
const inner = Object.keys(keyValue)
|
||||
.map((key: string) => {
|
||||
return key + ':' + keyValue[key] + ';'
|
||||
})
|
||||
.join('')
|
||||
return `@charset "UTF-8";:root{${inner}}`
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elNewStyle 新的变量样式
|
||||
*/
|
||||
writeNewStyle = (elNewStyle: string) => {
|
||||
if (this.isFirstWriteStyle) {
|
||||
const style = document.createElement('style')
|
||||
style.innerText = elNewStyle
|
||||
document.head.appendChild(style)
|
||||
this.isFirstWriteStyle = false
|
||||
} else {
|
||||
if (document.head.lastChild) {
|
||||
document.head.lastChild.innerText = elNewStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据并且写入dom
|
||||
* @param updateInferData 平滑数据修改
|
||||
* @param updateKeyvalueData keyValue数据修改
|
||||
*/
|
||||
updateWrite = (updateInferData?: UpdateInferData, updateKeyvalueData?: UpdateKeyValueData) => {
|
||||
this.update(updateInferData, updateKeyvalueData)
|
||||
const newStyle = this.tokeyValueStyle()
|
||||
const newStyleString = this.toString(newStyle)
|
||||
this.writeNewStyle(newStyleString)
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
* @param inferData
|
||||
* @param keyvalueData
|
||||
*/
|
||||
update = (updateInferData?: UpdateInferData, updateKeyvalueData?: UpdateKeyValueData) => {
|
||||
if (updateInferData) {
|
||||
this.updateInferData(updateInferData)
|
||||
}
|
||||
if (updateKeyvalueData) {
|
||||
this.updateOrCreateKeyValueData(updateKeyvalueData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改外推数据 外推数据只能修改,不能新增
|
||||
* @param inferData
|
||||
*/
|
||||
updateInferData = (updateInferData: UpdateInferData) => {
|
||||
Object.keys(updateInferData).forEach((key) => {
|
||||
const findInfer = this.inferData.find((itemInfer) => {
|
||||
return itemInfer.key === key
|
||||
})
|
||||
if (findInfer) {
|
||||
findInfer.value = updateInferData[key]
|
||||
} else {
|
||||
this.inferData.push({ key, value: updateInferData[key] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认主题
|
||||
*/
|
||||
initDefaultTheme = () => {
|
||||
this.updateWrite()
|
||||
}
|
||||
/**
|
||||
* 修改KeyValue数据
|
||||
* @param keyvalueData keyValue数据
|
||||
*/
|
||||
updateOrCreateKeyValueData = (updateKeyvalueData: UpdateKeyValueData) => {
|
||||
Object.keys(updateKeyvalueData).forEach((key) => {
|
||||
const newKey = this.updateKeyBySetting(key, this.themeSetting)
|
||||
this.keyValue[newKey] = updateKeyvalueData[newKey]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const install = (app: App) => {
|
||||
app.config.globalProperties.theme = new Theme(setting, keyValueData, inferData)
|
||||
}
|
||||
export default { install }
|
||||
@ -1,12 +0,0 @@
|
||||
import type { ThemeSetting } from "./type";
|
||||
const setting: ThemeSetting = {
|
||||
namespace: "el",
|
||||
division: "-",
|
||||
startDivision: "--",
|
||||
colorInferSetting: {
|
||||
light: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
dark: [2],
|
||||
type: "color",
|
||||
},
|
||||
};
|
||||
export default setting;
|
||||
@ -1,71 +0,0 @@
|
||||
interface ThemeSetting {
|
||||
/**
|
||||
*element-ui Namespace
|
||||
*/
|
||||
namespace: string;
|
||||
/**
|
||||
* 数据分隔符
|
||||
*/
|
||||
division: string;
|
||||
/**
|
||||
* 前缀
|
||||
*/
|
||||
startDivision: string;
|
||||
/**
|
||||
* 颜色外推设置
|
||||
*/
|
||||
colorInferSetting: ColorInferSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色混和设置
|
||||
*/
|
||||
interface ColorInferSetting {
|
||||
/**
|
||||
* 与白色混
|
||||
*/
|
||||
light: Array<number>;
|
||||
/**
|
||||
* 与黑色混
|
||||
*/
|
||||
dark: Array<number>;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑数据
|
||||
*/
|
||||
interface KeyValueData {
|
||||
[propName: string]: string;
|
||||
}
|
||||
type UpdateInferData = KeyValueData;
|
||||
|
||||
type UpdateKeyValueData = KeyValueData;
|
||||
/**
|
||||
*平滑数据
|
||||
*/
|
||||
interface InferData {
|
||||
/**
|
||||
* 设置
|
||||
*/
|
||||
setting?: ColorInferSetting | any;
|
||||
/**
|
||||
* 健
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type {
|
||||
KeyValueData,
|
||||
InferData,
|
||||
ThemeSetting,
|
||||
UpdateInferData,
|
||||
UpdateKeyValueData,
|
||||
};
|
||||
44
ui/src/utils/theme.ts
Normal file
44
ui/src/utils/theme.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export const themeList = [
|
||||
{
|
||||
label: '默认',
|
||||
value: '#3370FF',
|
||||
loginBackground: 'default'
|
||||
},
|
||||
{
|
||||
label: '活力橙',
|
||||
value: '#FF8800',
|
||||
loginBackground: 'orange'
|
||||
},
|
||||
{
|
||||
label: '松石绿',
|
||||
value: '#00B69D',
|
||||
loginBackground: 'green'
|
||||
},
|
||||
{
|
||||
label: '商务蓝',
|
||||
value: '#4954E6',
|
||||
loginBackground: 'default'
|
||||
},
|
||||
{
|
||||
label: '神秘紫',
|
||||
value: '#7F3BF5',
|
||||
loginBackground: 'purple'
|
||||
},
|
||||
{
|
||||
label: '胭脂红',
|
||||
value: '#F01D94',
|
||||
loginBackground: 'red'
|
||||
}
|
||||
]
|
||||
|
||||
export function getThemeImg(val: string) {
|
||||
return themeList.filter((v) => v.value === val)?.[0]?.loginBackground || 'default'
|
||||
}
|
||||
|
||||
export const defautSetting = {
|
||||
icon: '',
|
||||
loginLogo: '',
|
||||
loginImage: '',
|
||||
title: 'MaxKB',
|
||||
slogan: '欢迎使用 MaxKB 智能知识库'
|
||||
}
|
||||
@ -64,7 +64,7 @@
|
||||
<div class="mr-16">
|
||||
<el-button link @click="enlarge = !enlarge">
|
||||
<AppIcon
|
||||
:iconName="enlarge ? 'app-magnify' : 'app-minify'"
|
||||
:iconName="enlarge ? 'app-minify' : 'app-magnify'"
|
||||
class="color-secondary"
|
||||
style="font-size: 20px"
|
||||
></AppIcon>
|
||||
|
||||
@ -29,13 +29,6 @@ const tabList = [
|
||||
}
|
||||
]
|
||||
|
||||
// 动态引入组件
|
||||
const loadComponent = async (componentName: string) => {
|
||||
await import(`./component/${componentName}.vue`).then((res) => res.default)
|
||||
}
|
||||
|
||||
const currentComponent = computed(() => loadComponent(activeName.value))
|
||||
|
||||
function handleClick() {}
|
||||
|
||||
onMounted(() => {})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<login-layout v-loading="loading">
|
||||
<LoginContainer subTitle="欢迎使用 MaxKB 智能知识库">
|
||||
<LoginContainer :subTitle="user.themeInfo?.slogan || '欢迎使用 MaxKB 智能知识库'">
|
||||
<h2 class="mb-24">{{ loginMode || '普通登录' }}</h2>
|
||||
<el-form
|
||||
class="login-form"
|
||||
@ -109,8 +109,7 @@ const rules = ref<FormRules<LoginRequest>>({
|
||||
})
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
|
||||
|
||||
const modeList = ref<string[]>(['']);
|
||||
const modeList = ref<string[]>([''])
|
||||
const loginMode = ref('')
|
||||
|
||||
function changeMode(val: string) {
|
||||
@ -135,16 +134,19 @@ const login = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
user.theme()
|
||||
user.asyncGetProfile().then((res) => {
|
||||
if (user.isXPack) {
|
||||
loading.value = true
|
||||
user.getAuthType().then((res) => {
|
||||
modeList.value = [...modeList.value, ...res];
|
||||
}).finally(() => (loading.value = false))
|
||||
user
|
||||
.getAuthType()
|
||||
.then((res) => {
|
||||
modeList.value = [...modeList.value, ...res]
|
||||
})
|
||||
.finally(() => (loading.value = false))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.login-gradient-divider {
|
||||
|
||||
@ -3,14 +3,15 @@
|
||||
<div class="header">
|
||||
<div class="tag flex-between">
|
||||
<div class="flex align-center">
|
||||
<LogoIcon height="24px" class="mr-8" />
|
||||
<span class="ellipsis">{{ title }}</span>
|
||||
<img v-if="props.data.icon" :src="fileURL" alt="" height="20px" class="mr-8" />
|
||||
<img v-else src="@/assets/logo/logo.svg" height="24px" class="mr-8" />
|
||||
<span class="ellipsis">{{ data.title }}</span>
|
||||
</div>
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<login-layout style="height: 530px" :themeImg="themeImg">
|
||||
<LoginContainer :subTitle="slogan" class="login-container">
|
||||
<login-layout style="height: 530px">
|
||||
<LoginContainer :subTitle="data.slogan" class="login-container">
|
||||
<div class="mask"></div>
|
||||
<h2 class="mb-24">{{ '普通登录' }}</h2>
|
||||
<el-form class="login-form">
|
||||
@ -42,18 +43,24 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
themeImg: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
slogan: {
|
||||
type: String,
|
||||
default: '欢迎使用 MaxKB 智能知识库'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'MaxKB'
|
||||
data: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const fileURL = computed(() => {
|
||||
if (props.data.icon) {
|
||||
if (typeof props.data.icon === 'string') {
|
||||
return props.data.icon
|
||||
} else {
|
||||
return URL.createObjectURL(props.data.icon)
|
||||
}
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="theme-setting">
|
||||
<div class="theme-setting" v-loading="loading">
|
||||
<h4 class="p-16-24">外观设置</h4>
|
||||
<el-scrollbar>
|
||||
<div class="p-24 pt-0">
|
||||
@ -8,7 +8,7 @@
|
||||
<el-radio-group
|
||||
v-model="themeForm.theme"
|
||||
class="app-radio-button-group"
|
||||
@change="changeTheme"
|
||||
@change="changeThemeHandle"
|
||||
>
|
||||
<template v-for="(item, index) in themeList" :key="index">
|
||||
<el-radio-button :label="item.label" :value="item.value" />
|
||||
@ -20,19 +20,30 @@
|
||||
<el-card shadow="never" class="layout-bg">
|
||||
<div class="flex-between">
|
||||
<h5 class="mb-16">页面预览</h5>
|
||||
<el-button type="primary" link> 恢复默认 </el-button>
|
||||
<el-button type="primary" link @click="resetForm"> 恢复默认 </el-button>
|
||||
</div>
|
||||
<div class="theme-preview">
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="16">
|
||||
<LoginPreview :themeImg="themeImg" :slogan="themeForm.slogan" :title="themeForm.title" />
|
||||
<LoginPreview :data="themeForm" />
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="theme-form">
|
||||
<el-card shadow="never" class="mb-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">网站 Logo</span>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:on-change="
|
||||
(file: any, fileList: any) => onChange(file, fileList, 'icon')
|
||||
"
|
||||
>
|
||||
<el-button size="small"> 替换图片 </el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-text type="info" size="small"
|
||||
>顶部网站显示的 Logo,建议尺寸 48 x 48,支持 JPG、PNG、SVG,大小不超过
|
||||
@ -42,7 +53,18 @@
|
||||
<el-card shadow="never" class="mb-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">登录 Logo</span>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:on-change="
|
||||
(file: any, fileList: any) => onChange(file, fileList, 'loginLogo')
|
||||
"
|
||||
>
|
||||
<el-button size="small"> 替换图片 </el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-text type="info" size="small"
|
||||
>登录页面右侧 Logo,建议尺寸 204*52,支持 JPG、PNG、SVG,大小不超过
|
||||
@ -52,7 +74,18 @@
|
||||
<el-card shadow="never" class="mb-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">登录背景图</span>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
:on-change="
|
||||
(file: any, fileList: any) => onChange(file, fileList, 'loginImage')
|
||||
"
|
||||
>
|
||||
<el-button size="small"> 替换图片 </el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-text type="info" size="small">
|
||||
左侧背景图,矢量图建议尺寸 576*900,位图建议尺寸1152*1800;支持
|
||||
@ -92,53 +125,34 @@
|
||||
</el-scrollbar>
|
||||
<div class="theme-setting__operate w-full p-16-24">
|
||||
<el-button @click="resetTheme">放弃更新</el-button>
|
||||
<el-button type="primary"> 保存并应用 </el-button>
|
||||
<el-button type="primary" @click="updataTheme(themeFormRef)"> 保存并应用 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import type { FormInstance, FormRules, UploadFiles } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import LoginPreview from './LoginPreview.vue'
|
||||
import { useElementPlusTheme } from 'use-element-plus-theme'
|
||||
import { themeList, defautSetting } from '@/utils/theme'
|
||||
import ThemeApi from '@/api/theme'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import useStore from '@/stores'
|
||||
const { common } = useStore()
|
||||
|
||||
const themeList = [
|
||||
{
|
||||
label: '默认',
|
||||
value: '#3370FF',
|
||||
loginBackground: 'default'
|
||||
},
|
||||
{
|
||||
label: '活力橙',
|
||||
value: '#FF8800',
|
||||
loginBackground: 'orange'
|
||||
},
|
||||
{
|
||||
label: '松石绿',
|
||||
value: '#00B69D',
|
||||
loginBackground: 'green'
|
||||
},
|
||||
{
|
||||
label: '商务蓝',
|
||||
value: '#4954E6',
|
||||
loginBackground: 'default'
|
||||
},
|
||||
{
|
||||
label: '神秘紫',
|
||||
value: '#7F3BF5',
|
||||
loginBackground: 'purple'
|
||||
},
|
||||
{
|
||||
label: '胭脂红',
|
||||
value: '#F01D94',
|
||||
loginBackground: 'red'
|
||||
}
|
||||
]
|
||||
const { user } = useStore()
|
||||
|
||||
onBeforeRouteLeave((to, from) => {
|
||||
user.setTheme(cloneTheme.value)
|
||||
})
|
||||
|
||||
const themeInfo = computed(() => user.themeInfo)
|
||||
|
||||
const themeFormRef = ref<FormInstance>()
|
||||
const themeForm = ref({
|
||||
const loading = ref(false)
|
||||
const cloneTheme = ref(null)
|
||||
const themeForm = ref<any>({
|
||||
theme: '#3370FF',
|
||||
icon: '',
|
||||
loginLogo: '',
|
||||
@ -152,24 +166,67 @@ const rules = reactive<FormRules>({
|
||||
slogan: [{ required: true, message: '请输入欢迎语', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const themeImg = ref('default')
|
||||
const onChange = (file: any, fileList: UploadFiles, attr: string) => {
|
||||
if (attr === 'loginImage') {
|
||||
const isLimit = file?.size / 1024 / 1024 < 5
|
||||
if (!isLimit) {
|
||||
// @ts-ignore
|
||||
MsgError(`文件大小超过 5M`)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const isLimit = file?.size / 1024 < 200
|
||||
if (!isLimit) {
|
||||
// @ts-ignore
|
||||
MsgError(`文件大小超过 200KB`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const { changeTheme } = useElementPlusTheme(themeForm.value.theme)
|
||||
themeForm.value[attr] = file.raw
|
||||
}
|
||||
|
||||
function changeThemeHandle(val: string) {
|
||||
themeForm.value.theme = val
|
||||
user.setTheme(themeForm.value)
|
||||
}
|
||||
|
||||
function resetTheme() {
|
||||
themeForm.value.theme = '#3370FF'
|
||||
changeTheme(themeForm.value.theme)
|
||||
user.setTheme(cloneTheme.value)
|
||||
themeForm.value = cloneDeep(themeInfo.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => themeForm.value.theme,
|
||||
(val) => {
|
||||
if (val) {
|
||||
common.setTheme(val)
|
||||
themeImg.value = themeList.filter((v) => v.value === val)[0].loginBackground
|
||||
function resetForm() {
|
||||
themeForm.value = {
|
||||
theme: themeForm.value.theme,
|
||||
...defautSetting
|
||||
}
|
||||
user.setTheme(themeForm.value)
|
||||
}
|
||||
)
|
||||
|
||||
const updataTheme = async (formEl: FormInstance | undefined, test?: string) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
let fd = new FormData()
|
||||
Object.keys(themeForm.value).map((item) => {
|
||||
fd.append(item, themeForm.value[item])
|
||||
})
|
||||
ThemeApi.postThemeInfo(fd, loading).then((res) => {
|
||||
user.theme()
|
||||
cloneTheme.value = cloneDeep(themeForm.value)
|
||||
MsgSuccess('外观设置成功')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (themeInfo.value) {
|
||||
themeForm.value = themeInfo.value
|
||||
cloneTheme.value = cloneDeep(themeInfo.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<AppAvatar shape="square avatar-blue">
|
||||
<AppAvatar shape="square" class="avatar-blue">
|
||||
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user