diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7b9cc83..0af817fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
* 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
+* 功能新增:允许在管理后台设置首页显示的导航菜单
* 功能新增:增加 Suno 文生音乐页面功能
## v4.1.0
diff --git a/api/core/types/config.go b/api/core/types/config.go
index 1158b684..027ea14a 100644
--- a/api/core/types/config.go
+++ b/api/core/types/config.go
@@ -228,5 +228,6 @@ type SystemConfig struct {
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片
+ IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
Copyright string `json:"copyright"` // 版权信息
}
diff --git a/api/handler/menu_handler.go b/api/handler/menu_handler.go
index 647ed1e0..39de1c78 100644
--- a/api/handler/menu_handler.go
+++ b/api/handler/menu_handler.go
@@ -27,9 +27,15 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
// List 数据列表
func (h *MenuHandler) List(c *gin.Context) {
+ index := h.GetBool(c, "index")
var items []model.Menu
var list = make([]vo.Menu, 0)
- res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items)
+ session := h.DB.Session(&gorm.Session{})
+ session = session.Where("enabled", true)
+ if index {
+ session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
+ }
+ res := session.Order("sort_num ASC").Find(&items)
if res.Error == nil {
for _, item := range items {
var product vo.Menu
diff --git a/web/public/images/transparent-bg.png b/web/public/images/transparent-bg.png
new file mode 100644
index 00000000..8b1a375c
Binary files /dev/null and b/web/public/images/transparent-bg.png differ
diff --git a/web/src/assets/css/index.styl b/web/src/assets/css/index.styl
new file mode 100644
index 00000000..8a47c12a
--- /dev/null
+++ b/web/src/assets/css/index.styl
@@ -0,0 +1,116 @@
+.index-page {
+ margin: 0
+ overflow hidden
+ color #ffffff
+ display flex
+ justify-content center
+ align-items baseline
+ padding-top 150px
+
+ .color-bg {
+ position absolute
+ top 0
+ left 0
+ width 100vw
+ height 100vh
+ }
+
+ .image-bg {
+ filter: blur(8px);
+ background-size: cover;
+ background-position: center;
+ }
+
+ .shadow {
+ box-shadow rgba(0, 0, 0, 0.3) 0px 0px 3px
+
+ &:hover {
+ box-shadow rgba(0, 0, 0, 0.3) 0px 0px 8px
+ }
+ }
+
+ .menu-box {
+ position absolute
+ top 0
+ width 100%
+ display flex
+
+ .el-menu {
+ padding 0 30px
+ width 100%
+ display flex
+ justify-content space-between
+ background none
+ border none
+
+ .menu-item {
+ display flex
+ padding 20px 0
+
+ color #ffffff
+
+ .title {
+ font-size 24px
+ padding 10px 10px 0 10px
+ }
+
+ .el-image {
+ height 50px
+ }
+
+ .el-button {
+ margin-left 10px
+
+ span {
+ margin-left 5px
+ }
+ }
+ }
+ }
+ }
+
+ .content {
+ text-align: center;
+ position relative
+
+ h1 {
+ font-size: 5rem;
+ margin-bottom: 1rem;
+ }
+
+ p {
+ font-size: 1.5rem;
+ margin-bottom: 2rem;
+ }
+
+ .navs {
+ display flex
+ max-width 900px
+ padding 20px
+
+ .nav-item {
+ width 200px
+ .el-button {
+ width 100%
+ padding: 25px 20px;
+ font-size: 1.3rem;
+ transition: all 0.3s ease;
+
+ .iconfont {
+ font-size 24px
+ margin-right 10px
+ position relative
+ top -2px
+ }
+ }
+ }
+ }
+ }
+
+ .footer {
+ .el-link__inner {
+ color #ffffff
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css
index ef2d1a57..10b4f5de 100644
--- a/web/src/assets/iconfont/iconfont.css
+++ b/web/src/assets/iconfont/iconfont.css
@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
- src: url('iconfont.woff2?t=1721292490257') format('woff2'),
- url('iconfont.woff?t=1721292490257') format('woff'),
- url('iconfont.ttf?t=1721292490257') format('truetype');
+ src: url('iconfont.woff2?t=1721356513025') format('woff2'),
+ url('iconfont.woff?t=1721356513025') format('woff'),
+ url('iconfont.ttf?t=1721356513025') format('truetype');
}
.iconfont {
@@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale;
}
+.icon-app:before {
+ content: "\e64f";
+}
+
.icon-pause:before {
content: "\e693";
}
diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js
index 25288e87..1e83327f 100644
--- a/web/src/assets/iconfont/iconfont.js
+++ b/web/src/assets/iconfont/iconfont.js
@@ -1 +1 @@
-window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}}(window);
\ No newline at end of file
+window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}}(window);
\ No newline at end of file
diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json
index c48278e4..d1b11fab 100644
--- a/web/src/assets/iconfont/iconfont.json
+++ b/web/src/assets/iconfont/iconfont.json
@@ -5,6 +5,13 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
+ {
+ "icon_id": "1503777",
+ "name": "应用",
+ "font_class": "app",
+ "unicode": "e64f",
+ "unicode_decimal": 58959
+ },
{
"icon_id": "7156146",
"name": "暂停",
diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf
index 5e3d71bc..18b71dce 100644
Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ
diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff
index 5156107d..5663d176 100644
Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ
diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2
index d2db2973..7418be6e 100644
Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ
diff --git a/web/src/components/FooterBar.vue b/web/src/components/FooterBar.vue
index 671adf31..c30955ca 100644
--- a/web/src/components/FooterBar.vue
+++ b/web/src/components/FooterBar.vue
@@ -1,13 +1,15 @@
@@ -23,7 +25,12 @@ const version = ref(process.env.VUE_APP_VERSION)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
const copyRight = ref('')
const license = ref({})
-
+const props = defineProps({
+ textColor: {
+ type: String,
+ default: '#ffffff'
+ },
+});
// 获取系统配置
httpGet("/api/config/get?key=system").then(res => {
@@ -56,8 +63,10 @@ httpGet("/api/config/license").then(res => {
padding 20px;
width 100%
- .el-link {
- color #409eff
+ a {
+ &:hover {
+ text-decoration underline
+ }
}
}
}
diff --git a/web/src/views/Index.vue b/web/src/views/Index.vue
index fc805497..319e7929 100644
--- a/web/src/views/Index.vue
+++ b/web/src/views/Index.vue
@@ -1,6 +1,6 @@
-
+
-
欢迎使用 {{ title }}
-
{{ slogan }}
-
-
- AI 对话
-
-
-
- MJ 绘画
-
+
欢迎使用 {{ title }}
+
{{ slogan }}
-
-
- SD 绘画
-
-
-
- 思维导图
-
-
+
+
+
+
+
+ {{item.name}}
+
+
+
+
-
+
@@ -82,23 +75,82 @@ const logo = ref("")
const slogan = ref("")
const license = ref({})
const winHeight = window.innerHeight - 150
-const bgImgUrl = ref('')
const isLogin = ref(false)
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL)
+const navs = ref([])
+const btnColors = ref([
+ {bgColor: "#fff143", textColor: "#50616D"},
+ {bgColor: "#eaff56", textColor: "#50616D"},
+ {bgColor: "#bddd22", textColor: "#50616D"},
+ {bgColor: "#1bd1a5", textColor: "#50616D"},
+ {bgColor: "#e0eee8", textColor: "#50616D"},
+ {bgColor: "#7bcfa6", textColor: "#50616D"},
+ {bgColor: "#bce672", textColor: "#50616D"},
+ {bgColor: "#44cef6", textColor: "#ffffff"},
+ {bgColor: "#70f3ff", textColor: "#50616D"},
+ {bgColor: "#fffbf0", textColor: "#50616D"},
+ {bgColor: "#d6ecf0", textColor: "#50616D"},
+ {bgColor: "#88ada6", textColor: "#50616D"},
+ {bgColor: "#30dff3", textColor: "#50616D"},
+ {bgColor: "#d3e0f3", textColor: "#50616D"},
+ {bgColor: "#e9e7ef", textColor: "#50616D"},
+ {bgColor: "#eacd76", textColor: "#50616D"},
+ {bgColor: "#f2be45", textColor: "#50616D"},
+ {bgColor: "#549688", textColor: "#ffffff"},
+ {bgColor: "#758a99", textColor: "#ffffff"},
+ {bgColor: "#41555d", textColor: "#ffffff"},
+ {bgColor: "#21aa93", textColor: "#ffffff"},
+ {bgColor: "#0aa344", textColor: "#ffffff"},
+ {bgColor: "#f05654", textColor: "#ffffff"},
+ {bgColor: "#db5a6b", textColor: "#ffffff"},
+ {bgColor: "#db5a6b", textColor: "#ffffff"},
+ {bgColor: "#8d4bbb", textColor: "#ffffff"},
+ {bgColor: "#426666", textColor: "#ffffff"},
+ {bgColor: "#177cb0", textColor: "#ffffff"},
+ {bgColor: "#395260", textColor: "#ffffff"},
+ {bgColor: "#519a73", textColor: "#ffffff"},
+ {bgColor: "#75878a", textColor: "#ffffff"},
+])
+const iconMap =ref(
+ {
+ "/chat": "icon-chat",
+ "/mj": "icon-mj",
+ "/sd": "icon-sd",
+ "/dalle": "icon-dalle",
+ "/images-wall": "icon-image",
+ "/suno": "icon-suno",
+ "/xmind": "icon-xmind",
+ "/apps": "icon-app",
+ "/member": "icon-vip-user",
+ "/invite": "icon-share",
+ }
+)
+const bgStyle = {}
+const color = btnColors.value[Math.floor(Math.random() * btnColors.value.length)]
+const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true})
onMounted(() => {
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title
logo.value = res.data.logo
- if (res.data.index_bg_url) {
- bgImgUrl.value = res.data.index_bg_url
+ if (res.data.index_bg_url === 'color') {
+ // 随机选取一种颜色
+ theme.value.bgColor = color.bgColor
+ theme.value.btnBgColor = color.bgColor
+ theme.value.textColor = color.textColor
+ theme.value.btnTextColor = color.textColor
+ // 设置背景颜色
+ bgStyle.backgroundColor = theme.value.bgColor
+ bgStyle.backgroundImage = "/images/transparent-bg.png"
+ theme.value.imageBg = false
+ } else if (res.data.index_bg_url) {
+ bgStyle.backgroundImage = res.data.index_bg_url
} else {
- bgImgUrl.value = "/images/index-bg.jpg"
- }
- if (res.data.slogan) {
- slogan.value = res.data.slogan
+ bgStyle.backgroundImage = "/images/index-bg.jpg"
}
+
+ slogan.value = res.data.slogan
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
@@ -106,7 +158,13 @@ onMounted(() => {
httpGet("/api/config/license").then(res => {
license.value = res.data
}).catch(e => {
- ElMessage.error("获取 License 配置:" + e.message)
+ ElMessage.error("获取 License 配置失败:" + e.message)
+ })
+
+ httpGet("/api/menu/list?index=1").then(res => {
+ navs.value = res.data
+ }).catch(e => {
+ ElMessage.error("获取导航菜单失败:" + e.message)
})
checkSession().then(() => {
@@ -117,107 +175,5 @@ onMounted(() => {
diff --git a/web/src/views/admin/SysConfig.vue b/web/src/views/admin/SysConfig.vue
index 973daf38..505cfb89 100644
--- a/web/src/views/admin/SysConfig.vue
+++ b/web/src/views/admin/SysConfig.vue
@@ -50,6 +50,38 @@
使用动态背景
+ 使用纯色背景
+
+
+
+
+
@@ -407,6 +439,7 @@ const models = ref([])
const openAIModels = ref([])
const notice = ref("")
const license = ref({is_active: false})
+const menus = ref([])
onMounted(() => {
// 加载系统配置
@@ -431,6 +464,12 @@ onMounted(() => {
ElMessage.error("获取模型失败:" + e.message)
})
+ httpGet('/api/admin/menu/list').then(res => {
+ menus.value = res.data
+ }).catch(e => {
+ ElMessage.error("获取模型失败:" + e.message)
+ })
+
fetchLicense()
})