feat: 外观设置

This commit is contained in:
wangdan-fit2cloud 2024-07-16 17:32:27 +08:00
parent 4aafda3446
commit d5ca5eeaf4
25 changed files with 381 additions and 619 deletions

View File

@ -1,7 +1,6 @@
import { Result } from '@/request/Result' import { Result } from '@/request/Result'
import { get, post, del, put } from '@/request/index' import { get, post, del, put } from '@/request/index'
import type { TeamMember } from '@/api/type/team' import type { Ref } from 'vue'
const prefix = '/display' const prefix = '/display'
/** /**
@ -23,12 +22,14 @@ const getThemeInfo: () => Promise<Result<any>> = () => {
* slogan * slogan
* } * }
*/ */
const postThemeInfo: (data: any) => Promise<Result<boolean>> = (data) => { const postThemeInfo: (data: any, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
return post(`${prefix}/update`, data) data,
loading
) => {
return post(`${prefix}/update`, data, undefined, loading)
} }
export default { export default {
getThemeInfo, getThemeInfo,
postThemeInfo postThemeInfo
} }

View File

@ -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

View File

@ -955,7 +955,7 @@ export const iconMap: any = {
]) ])
} }
}, },
'app-minify': { 'app-magnify': {
iconReader: () => { iconReader: () => {
return h('i', [ return h('i', [
h( h(
@ -976,7 +976,7 @@ export const iconMap: any = {
]) ])
} }
}, },
'app-magnify': { 'app-minify': {
iconReader: () => { iconReader: () => {
return h('i', [ return h('i', [
h( h(
@ -1071,5 +1071,5 @@ export const iconMap: any = {
) )
]) ])
} }
}, }
} }

View File

@ -3,9 +3,7 @@
<div class="login-container w-full h-full"> <div class="login-container w-full h-full">
<el-row class="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"> <el-col :xs="0" :sm="0" :md="10" :lg="10" :xl="10" class="left-container">
<div class="login-image"> <div class="login-image" :style="loginImageStyle"></div>
<img :src="`../src/assets/theme/${themeImg}.jpg`" class="login-image" />
</div>
</el-col> </el-col>
<el-col :xs="24" :sm="24" :md="14" :lg="14" :xl="14" class="right-container flex-center"> <el-col :xs="24" :sm="24" :md="14" :lg="14" :xl="14" class="right-container flex-center">
<slot></slot> <slot></slot>
@ -15,12 +13,33 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <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' }) defineOptions({ name: 'LoginLayout' })
const props = defineProps({ const { user } = useStore()
themeImg: {
type: String, const fileURL = computed(() => {
default: 'default' 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> </script>
@ -29,7 +48,9 @@ const props = defineProps({
height: 100vh; height: 100vh;
.login-image { .login-image {
object-fit: cover; background-repeat: no-repeat;
background-position: center;
background-size: cover;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

View File

@ -1,4 +1,6 @@
<template> <template>
<img v-if="user.themeInfo.loginLogo" :src="fileURL" alt="" height="45px" class="mr-8" />
<template v-else>
<svg <svg
v-if="!isDefaultTheme" v-if="!isDefaultTheme"
viewBox="0 0 122 36" viewBox="0 0 122 36"
@ -55,6 +57,7 @@
</svg> </svg>
<img v-else src="@/assets/logo/MaxKB-logo.svg" :height="height" /> <img v-else src="@/assets/logo/MaxKB-logo.svg" :height="height" />
</template> </template>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import useStore from '@/stores' import useStore from '@/stores'
@ -66,9 +69,21 @@ defineProps({
default: '36px' default: '36px'
} }
}) })
const { common } = useStore() const { user } = useStore()
const isDefaultTheme = computed(() => { 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -45,9 +45,9 @@ defineProps({
default: '36px' default: '36px'
} }
}) })
const { common } = useStore() const { user } = useStore()
const isDefaultTheme = computed(() => { const isDefaultTheme = computed(() => {
return common.isDefaultTheme() return user.isDefaultTheme()
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -8,9 +8,9 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import TopBar from '../top-bar/index.vue' import TopBar from '../top-bar/index.vue'
import useStore from '@/stores' import useStore from '@/stores'
const { common } = useStore() const { user } = useStore()
const isDefaultTheme = computed(() => { const isDefaultTheme = computed(() => {
return common.isDefaultTheme() return user.isDefaultTheme()
}) })
</script> </script>

View File

@ -35,9 +35,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import useStore from '@/stores' import useStore from '@/stores'
const { common, user } = useStore() const { user } = useStore()
const isDefaultTheme = computed(() => { const isDefaultTheme = computed(() => {
return common.isDefaultTheme() return user.isDefaultTheme()
}) })
const aboutDialogVisible = ref(false) const aboutDialogVisible = ref(false)

View File

@ -4,7 +4,6 @@ import * as ElementPlusIcons from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import { createApp } from 'vue' import { createApp } from 'vue'
import { store } from '@/stores' import { store } from '@/stores'
import theme from '@/theme'
import directives from '@/directives' import directives from '@/directives'
import App from './App.vue' import App from './App.vue'
import router from '@/router' import router from '@/router'
@ -56,8 +55,6 @@ app.use(ElementPlus, {
locale: zhCn locale: zhCn
}) })
app.use(theme)
app.use(router) app.use(router)
app.use(i18n) app.use(i18n)
app.use(Components) app.use(Components)

View File

@ -4,7 +4,7 @@ import { Role, ComplexPermission } from '@/utils/permission/type'
const settingRouter = { const settingRouter = {
path: '/setting', path: '/setting',
name: 'setting', name: 'setting',
meta: { icon: 'Setting', title: '系统设置', permission: 'SETTING:READ' }, meta: { icon: 'Setting', title: '系统管理', permission: 'SETTING:READ' },
redirect: () => { redirect: () => {
if (hasPermission(new Role('ADMIN'), 'AND')) { if (hasPermission(new Role('ADMIN'), 'AND')) {
return '/user' return '/user'
@ -59,7 +59,7 @@ const settingRouter = {
meta: { meta: {
icon: 'app-setting', icon: 'app-setting',
iconActive: 'app-setting-active', iconActive: 'app-setting-active',
title: '系统设置', title: '系统管理',
activeMenu: '/setting', activeMenu: '/setting',
parentPath: '/setting', parentPath: '/setting',
parentName: 'setting', parentName: 'setting',

View File

@ -8,7 +8,6 @@ export interface commonTypes {
paginationConfig: any | null paginationConfig: any | null
search: any search: any
device: string device: string
theme: string
} }
const useCommonStore = defineStore({ const useCommonStore = defineStore({
@ -18,16 +17,9 @@ const useCommonStore = defineStore({
// 搜索和分页缓存 // 搜索和分页缓存
paginationConfig: {}, paginationConfig: {},
search: {}, search: {},
device: DeviceType.Desktop, device: DeviceType.Desktop
theme: ''
}), }),
actions: { actions: {
isDefaultTheme() {
return !this.theme || this.theme === '#3370FF'
},
setTheme(val: string) {
this.theme = val
},
saveBreadcrumb(data: any) { saveBreadcrumb(data: any) {
this.breadcrumb = data this.breadcrumb = data
}, },

View File

@ -2,6 +2,8 @@ import { defineStore } from 'pinia'
import type { User } from '@/api/type/user' import type { User } from '@/api/type/user'
import UserApi from '@/api/user' import UserApi from '@/api/user'
import ThemeApi from '@/api/theme' import ThemeApi from '@/api/theme'
import { useElementPlusTheme } from 'use-element-plus-theme'
const { changeTheme } = useElementPlusTheme()
export interface userStateTypes { export interface userStateTypes {
userType: number // 1 系统操作者 2 对话用户 userType: number // 1 系统操作者 2 对话用户
@ -26,6 +28,13 @@ const useUserStore = defineStore({
themeInfo: null themeInfo: null
}), }),
actions: { actions: {
isDefaultTheme() {
return !this.themeInfo?.theme || this.themeInfo?.theme === '#3370FF'
},
setTheme(data: any) {
changeTheme(data?.['theme'])
this.themeInfo = data
},
isExpire() { isExpire() {
return this.isXPack && !this.XPACK_LICENSE_IS_VALID return this.isXPack && !this.XPACK_LICENSE_IS_VALID
}, },
@ -82,8 +91,14 @@ const useUserStore = defineStore({
}, },
async theme() { async theme() {
return ThemeApi.getThemeInfo().then((ok) => { return await ThemeApi.getThemeInfo().then((ok) => {
this.themeInfo = ok.data 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'
}
}) })
}, },

View File

@ -369,8 +369,8 @@ h5 {
/* tag */ /* tag */
.default-tag { .default-tag {
background: var(--tag-default-bg); background: var(--el-color-primary-light-7);
color: var(--tag-default-color); color: var(--el-color-primary);
border: none; border: none;
} }
.success-tag { .success-tag {

View File

@ -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;

View File

@ -1,5 +0,0 @@
import type { KeyValueData } from './type'
const keyValueData: KeyValueData = {
'--el-header-padding': '0px'
}
export default keyValueData

View File

@ -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 }

View File

@ -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;

View File

@ -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
View 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 智能知识库'
}

View File

@ -64,7 +64,7 @@
<div class="mr-16"> <div class="mr-16">
<el-button link @click="enlarge = !enlarge"> <el-button link @click="enlarge = !enlarge">
<AppIcon <AppIcon
:iconName="enlarge ? 'app-magnify' : 'app-minify'" :iconName="enlarge ? 'app-minify' : 'app-magnify'"
class="color-secondary" class="color-secondary"
style="font-size: 20px" style="font-size: 20px"
></AppIcon> ></AppIcon>

View File

@ -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() {} function handleClick() {}
onMounted(() => {}) onMounted(() => {})

View File

@ -1,6 +1,6 @@
<template> <template>
<login-layout v-loading="loading"> <login-layout v-loading="loading">
<LoginContainer subTitle="欢迎使用 MaxKB 智能知识库"> <LoginContainer :subTitle="user.themeInfo?.slogan || '欢迎使用 MaxKB 智能知识库'">
<h2 class="mb-24">{{ loginMode || '普通登录' }}</h2> <h2 class="mb-24">{{ loginMode || '普通登录' }}</h2>
<el-form <el-form
class="login-form" class="login-form"
@ -109,8 +109,7 @@ const rules = ref<FormRules<LoginRequest>>({
}) })
const loginFormRef = ref<FormInstance>() const loginFormRef = ref<FormInstance>()
const modeList = ref<string[]>([''])
const modeList = ref<string[]>(['']);
const loginMode = ref('') const loginMode = ref('')
function changeMode(val: string) { function changeMode(val: string) {
@ -135,16 +134,19 @@ const login = () => {
} }
onMounted(() => { onMounted(() => {
user.theme()
user.asyncGetProfile().then((res) => { user.asyncGetProfile().then((res) => {
if (user.isXPack) { if (user.isXPack) {
loading.value = true loading.value = true
user.getAuthType().then((res) => { user
modeList.value = [...modeList.value, ...res]; .getAuthType()
}).finally(() => (loading.value = false)) .then((res) => {
modeList.value = [...modeList.value, ...res]
})
.finally(() => (loading.value = false))
} }
}) })
}) })
</script> </script>
<style lang="scss" scope> <style lang="scss" scope>
.login-gradient-divider { .login-gradient-divider {

View File

@ -3,14 +3,15 @@
<div class="header"> <div class="header">
<div class="tag flex-between"> <div class="tag flex-between">
<div class="flex align-center"> <div class="flex align-center">
<LogoIcon height="24px" class="mr-8" /> <img v-if="props.data.icon" :src="fileURL" alt="" height="20px" class="mr-8" />
<span class="ellipsis">{{ title }}</span> <img v-else src="@/assets/logo/logo.svg" height="24px" class="mr-8" />
<span class="ellipsis">{{ data.title }}</span>
</div> </div>
<el-icon><Close /></el-icon> <el-icon><Close /></el-icon>
</div> </div>
</div> </div>
<login-layout style="height: 530px" :themeImg="themeImg"> <login-layout style="height: 530px">
<LoginContainer :subTitle="slogan" class="login-container"> <LoginContainer :subTitle="data.slogan" class="login-container">
<div class="mask"></div> <div class="mask"></div>
<h2 class="mb-24">{{ '普通登录' }}</h2> <h2 class="mb-24">{{ '普通登录' }}</h2>
<el-form class="login-form"> <el-form class="login-form">
@ -42,18 +43,24 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
themeImg: { data: {
type: String, type: Object,
default: 'default' default: null
}, }
slogan: { })
type: String,
default: '欢迎使用 MaxKB 智能知识库' const fileURL = computed(() => {
}, if (props.data.icon) {
title: { if (typeof props.data.icon === 'string') {
type: String, return props.data.icon
default: 'MaxKB' } else {
return URL.createObjectURL(props.data.icon)
}
} else {
return ''
} }
}) })
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="theme-setting"> <div class="theme-setting" v-loading="loading">
<h4 class="p-16-24">外观设置</h4> <h4 class="p-16-24">外观设置</h4>
<el-scrollbar> <el-scrollbar>
<div class="p-24 pt-0"> <div class="p-24 pt-0">
@ -8,7 +8,7 @@
<el-radio-group <el-radio-group
v-model="themeForm.theme" v-model="themeForm.theme"
class="app-radio-button-group" class="app-radio-button-group"
@change="changeTheme" @change="changeThemeHandle"
> >
<template v-for="(item, index) in themeList" :key="index"> <template v-for="(item, index) in themeList" :key="index">
<el-radio-button :label="item.label" :value="item.value" /> <el-radio-button :label="item.label" :value="item.value" />
@ -20,19 +20,30 @@
<el-card shadow="never" class="layout-bg"> <el-card shadow="never" class="layout-bg">
<div class="flex-between"> <div class="flex-between">
<h5 class="mb-16">页面预览</h5> <h5 class="mb-16">页面预览</h5>
<el-button type="primary" link> 恢复默认 </el-button> <el-button type="primary" link @click="resetForm"> 恢复默认 </el-button>
</div> </div>
<div class="theme-preview"> <div class="theme-preview">
<el-row :gutter="8"> <el-row :gutter="8">
<el-col :span="16"> <el-col :span="16">
<LoginPreview :themeImg="themeImg" :slogan="themeForm.slogan" :title="themeForm.title" /> <LoginPreview :data="themeForm" />
</el-col> </el-col>
<el-col :span="8"> <el-col :span="8">
<div class="theme-form"> <div class="theme-form">
<el-card shadow="never" class="mb-8"> <el-card shadow="never" class="mb-8">
<div class="flex-between mb-8"> <div class="flex-between mb-8">
<span class="lighter">网站 Logo</span> <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-button size="small"> 替换图片 </el-button>
</el-upload>
</div> </div>
<el-text type="info" size="small" <el-text type="info" size="small"
>顶部网站显示的 Logo建议尺寸 48 x 48支持 JPGPNGSVG大小不超过 >顶部网站显示的 Logo建议尺寸 48 x 48支持 JPGPNGSVG大小不超过
@ -42,7 +53,18 @@
<el-card shadow="never" class="mb-8"> <el-card shadow="never" class="mb-8">
<div class="flex-between mb-8"> <div class="flex-between mb-8">
<span class="lighter">登录 Logo</span> <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-button size="small"> 替换图片 </el-button>
</el-upload>
</div> </div>
<el-text type="info" size="small" <el-text type="info" size="small"
>登录页面右侧 Logo建议尺寸 204*52支持 JPGPNGSVG大小不超过 >登录页面右侧 Logo建议尺寸 204*52支持 JPGPNGSVG大小不超过
@ -52,7 +74,18 @@
<el-card shadow="never" class="mb-8"> <el-card shadow="never" class="mb-8">
<div class="flex-between mb-8"> <div class="flex-between mb-8">
<span class="lighter">登录背景图</span> <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-button size="small"> 替换图片 </el-button>
</el-upload>
</div> </div>
<el-text type="info" size="small"> <el-text type="info" size="small">
左侧背景图矢量图建议尺寸 576*900位图建议尺寸1152*1800支持 左侧背景图矢量图建议尺寸 576*900位图建议尺寸1152*1800支持
@ -92,53 +125,34 @@
</el-scrollbar> </el-scrollbar>
<div class="theme-setting__operate w-full p-16-24"> <div class="theme-setting__operate w-full p-16-24">
<el-button @click="resetTheme">放弃更新</el-button> <el-button @click="resetTheme">放弃更新</el-button>
<el-button type="primary"> 保存并应用 </el-button> <el-button type="primary" @click="updataTheme(themeFormRef)"> 保存并应用 </el-button>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, watch } from 'vue' import { ref, reactive, onMounted, computed, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus' import { onBeforeRouteLeave } from 'vue-router'
import type { FormInstance, FormRules, UploadFiles } from 'element-plus'
import { cloneDeep } from 'lodash'
import LoginPreview from './LoginPreview.vue' 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' import useStore from '@/stores'
const { common } = useStore()
const themeList = [ const { user } = useStore()
{
label: '默认', onBeforeRouteLeave((to, from) => {
value: '#3370FF', user.setTheme(cloneTheme.value)
loginBackground: 'default' })
},
{ const themeInfo = computed(() => user.themeInfo)
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 themeFormRef = ref<FormInstance>() const themeFormRef = ref<FormInstance>()
const themeForm = ref({ const loading = ref(false)
const cloneTheme = ref(null)
const themeForm = ref<any>({
theme: '#3370FF', theme: '#3370FF',
icon: '', icon: '',
loginLogo: '', loginLogo: '',
@ -152,24 +166,67 @@ const rules = reactive<FormRules>({
slogan: [{ required: true, message: '请输入欢迎语', trigger: 'blur' }] 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() { function resetTheme() {
themeForm.value.theme = '#3370FF' user.setTheme(cloneTheme.value)
changeTheme(themeForm.value.theme) themeForm.value = cloneDeep(themeInfo.value)
} }
watch( function resetForm() {
() => themeForm.value.theme, themeForm.value = {
(val) => { theme: themeForm.value.theme,
if (val) { ...defautSetting
common.setTheme(val)
themeImg.value = themeList.filter((v) => v.value === val)[0].loginBackground
} }
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template> <template>
<AppAvatar shape="square avatar-blue"> <AppAvatar shape="square" class="avatar-blue">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" /> <img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar> </AppAvatar>
</template> </template>