二次开发:补全 PRO 功能 + 优化体验

- 新增站点自定义(标题/Favicon/登录页描述)
- 新增在线自定义 CSS/JS 编辑器
- 扩展备份迁移支持面板配置导出导入
- 默认账号改为 admin/1234
- 设置按钮改为橙色更醒目
- 修复登录页误报"登录过期"弹窗
- 修复 i18n 双重 apps 块导致翻译失效

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yuming
2026-05-15 14:29:03 +08:00
parent 25f46209d9
commit d6daf1dce6
24 changed files with 587 additions and 46 deletions
+86
View File
@@ -0,0 +1,86 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目简介
Sun-Panel 是服务器/NAS 导航面板应用,前后端分离架构:
- **前端**Vue 3 + TypeScript + Vite + Naive UI + Pinia
- **后端**Go + Gin + GORM,支持 SQLite(默认)或 MySQL,可选 Redis
## 常用命令
### 前端
```bash
pnpm dev # 启动开发服务器,端口 1002
pnpm build # 生产构建(类型检查 + Vite 打包)
pnpm lint # ESLint 检查
pnpm lint:fix # 自动修复
pnpm type-check # 仅 TypeScript 类型检查
```
### 后端
```bash
cd service
go run main.go # 启动,默认端口 3002
```
### Docker 全量构建
```bash
docker build -t sun-panel .
```
## 架构要点
### 前后端通信
开发时,Vite 将 `/api/*``/uploads/*` 代理到 `http://127.0.0.1:3002`(由 `.env``VITE_APP_API_BASE_URL` 控制)。生产部署时前后端同端口,Go 直接 serve 静态文件。
### 前端分层
```
src/api/ → API 调用函数(按模块分:panel/ system/ 等)
src/store/ → Pinia 状态(auth/user/panel/admin/notice/moduleConfig/app
src/views/ → 页面组件(home/login/exception
src/components/ → UI 组件(apps/ common/ deskModule/
src/utils/request/ → axios 封装,含 token 拦截器
src/hooks/ → Composition APIuseTheme/useLanguage/useBasicLayout/useIconRender
src/locales/ → 国际化(zh-CN.json / en-US.json
```
### 后端分层
```
service/router/ → 路由注册,入口 A_ENTER.go
service/api/api_v1/ → Handler 层,中间件在 middleware/
service/lib/ → 业务逻辑(user/cache/monitor/siteFavicon 等)
service/models/ → GORM 模型
service/global/ → 全局变量(Db/Logger/Redis 等)
service/initialize/ → 启动初始化流程,入口 A_ENTER.go
```
后端每个子目录都以 `A_ENTER.go` 作为该模块的入口聚合文件。
### 路由分组(后端)
- `/system/*` — 登录/用户/系统设置,需 LoginInterceptor
- `/panel/*` — 面板数据/图标管理,需 LoginInterceptor
- `/openness/*` — 无需认证的公开接口
### 配置文件
后端使用 INI 格式配置,示例见 `service/assets/conf.example.ini`。关键字段:
- `database_drive``sqlite`(默认)或 `mysql`
- `cache_drive` / `queue_drive``memory`(默认)或 `redis`
- `source_path`:上传文件存储路径,默认 `./uploads`
## 开发注意
- 前端路径别名 `@` 指向 `src/`
- 提交前 Husky + lint-staged 会自动对 `.ts/.tsx/.vue` 运行 `eslint --fix`
- TypeScript 开启 `strict``noUnusedLocals`,避免引入 `any`
- 国际化新增文案需同时更新 `zh-CN.json``en-US.json`
+1 -1
View File
@@ -4,7 +4,7 @@ FROM node AS web_image
# 华为源
# RUN npm config set registry https://repo.huaweicloud.com/repository/npm/
RUN npm install pnpm -g
RUN npm install pnpm@8 -g
WORKDIR /build
+3
View File
@@ -68,5 +68,8 @@
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
},
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "vue-demi"]
}
}
+9 -7
View File
@@ -1,11 +1,13 @@
package system
type ApiSystem struct {
About About
LoginApi LoginApi
UserApi UserApi
FileApi FileApi
NoticeApi NoticeApi
ModuleConfigApi ModuleConfigApi
MonitorApi MonitorApi
About About
LoginApi LoginApi
UserApi UserApi
FileApi FileApi
NoticeApi NoticeApi
ModuleConfigApi ModuleConfigApi
MonitorApi MonitorApi
SiteCustomizeApi SiteCustomizeApi
CustomStyleApi CustomStyleApi
}
+33
View File
@@ -0,0 +1,33 @@
package system
import (
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/global"
"sun-panel/lib/cmn/systemSetting"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type CustomStyleApi struct{}
// 获取自定义 CSS/JS(无需登录)
func (a *CustomStyleApi) Get(c *gin.Context) {
cfg := systemSetting.CustomStyle{}
global.SystemSetting.GetValueByInterface(systemSetting.CUSTOM_STYLE, &cfg)
apiReturn.SuccessData(c, cfg)
}
// 保存自定义 CSS/JS(管理员)
func (a *CustomStyleApi) Set(c *gin.Context) {
cfg := systemSetting.CustomStyle{}
if err := c.ShouldBindBodyWith(&cfg, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
if err := global.SystemSetting.Set(systemSetting.CUSTOM_STYLE, cfg); err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
apiReturn.Success(c)
}
@@ -0,0 +1,33 @@
package system
import (
"sun-panel/api/api_v1/common/apiReturn"
"sun-panel/global"
"sun-panel/lib/cmn/systemSetting"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type SiteCustomizeApi struct{}
// 获取站点自定义配置(无需登录,登录页使用)
func (a *SiteCustomizeApi) Get(c *gin.Context) {
cfg := systemSetting.SiteCustomize{}
global.SystemSetting.GetValueByInterface(systemSetting.SITE_CUSTOMIZE, &cfg)
apiReturn.SuccessData(c, cfg)
}
// 保存站点自定义配置(管理员)
func (a *SiteCustomizeApi) Set(c *gin.Context) {
cfg := systemSetting.SiteCustomize{}
if err := c.ShouldBindBodyWith(&cfg, binding.JSON); err != nil {
apiReturn.ErrorParamFomat(c, err.Error())
return
}
if err := global.SystemSetting.Set(systemSetting.SITE_CUSTOMIZE, cfg); err != nil {
apiReturn.ErrorDatabase(c, err.Error())
return
}
apiReturn.Success(c)
}
+2 -2
View File
@@ -135,13 +135,13 @@ func NotFoundAndCreateUser(db *gorm.DB) error {
if err != gorm.ErrRecordNotFound {
return err
}
username := "admin@sun.cc"
username := "admin"
fUser.Mail = username
fUser.Username = username
fUser.Name = username
fUser.Status = 1
fUser.Role = 1
fUser.Password = cmn.PasswordEncryption("12345678")
fUser.Password = cmn.PasswordEncryption("1234")
if errCreate := db.Create(&fUser).Error; errCreate != nil {
return errCreate
@@ -15,6 +15,8 @@ const (
DISCLAIMER = "disclaimer" // 免责声明 储存类型:字符串
WEB_ABOUT_DESCRIPTION = "web_about_description" // 关于的描述信息
PANEL_PUBLIC_USER_ID = "panel_public_user_id" // 公开访问模式用户id *uint|null
SITE_CUSTOMIZE = "site_customize" // 站点自定义配置
CUSTOM_STYLE = "custom_style" // 自定义 CSS/JS
)
type SystemSettingCache struct {
@@ -43,6 +45,19 @@ type ApplicationSetting struct {
WebSiteUrl string `json:"webSiteUrl"` // 站点地址
}
// 站点自定义配置
type SiteCustomize struct {
SiteTitle string `json:"siteTitle"` // 站点标题(显示在登录页)
FaviconUrl string `json:"faviconUrl"` // Favicon 图标 URL
LoginDescription string `json:"loginDescription"` // 登录页描述文字
}
// 自定义 CSS/JS
type CustomStyle struct {
Css string `json:"css"` // 自定义 CSS 内容
Js string `json:"js"` // 自定义 JS 内容
}
var (
ErrorNoExists = errors.New("no exists")
)
+2
View File
@@ -10,4 +10,6 @@ func Init(routerGroup *gin.RouterGroup) {
InitNoticeRouter(routerGroup)
InitModuleConfigRouter(routerGroup)
InitMonitorRouter(routerGroup)
InitSiteCustomizeRouter(routerGroup)
InitCustomStyleRouter(routerGroup)
}
+19
View File
@@ -0,0 +1,19 @@
package system
import (
"sun-panel/api/api_v1"
"sun-panel/api/api_v1/middleware"
"github.com/gin-gonic/gin"
)
func InitCustomStyleRouter(router *gin.RouterGroup) {
api := api_v1.ApiGroupApp.ApiSystem.CustomStyleApi
// 无需登录
router.POST("/system/customStyle/get", api.Get)
// 管理员才能修改
rAdmin := router.Group("", middleware.LoginInterceptor, middleware.AdminInterceptor)
rAdmin.POST("/system/customStyle/set", api.Set)
}
+19
View File
@@ -0,0 +1,19 @@
package system
import (
"sun-panel/api/api_v1"
"sun-panel/api/api_v1/middleware"
"github.com/gin-gonic/gin"
)
func InitSiteCustomizeRouter(router *gin.RouterGroup) {
api := api_v1.ApiGroupApp.ApiSystem.SiteCustomizeApi
// 无需登录(登录页读取)
router.POST("/system/siteCustomize/get", api.Get)
// 管理员才能修改
rAdmin := router.Group("", middleware.LoginInterceptor, middleware.AdminInterceptor)
rAdmin.POST("/system/siteCustomize/set", api.Set)
}
+25
View File
@@ -1,11 +1,36 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { NConfigProvider } from 'naive-ui'
import { NaiveProvider } from '@/components/common'
import { useTheme } from '@/hooks/useTheme'
import { useLanguage } from '@/hooks/useLanguage'
import { getCustomStyle } from '@/api/system/customStyle'
import type { CustomStyle } from '@/api/system/customStyle'
const { theme, themeOverrides } = useTheme()
const { language } = useLanguage()
// 注入自定义 CSS/JS
onMounted(async () => {
try {
const res = await getCustomStyle<CustomStyle>()
if (res.code === 0 && res.data) {
if (res.data.css) {
const style = document.createElement('style')
style.id = 'sun-panel-custom-css'
style.textContent = res.data.css
document.head.appendChild(style)
}
if (res.data.js) {
const script = document.createElement('script')
script.id = 'sun-panel-custom-js'
script.textContent = res.data.js
document.body.appendChild(script)
}
}
}
catch {}
})
</script>
<template>
+19
View File
@@ -0,0 +1,19 @@
import { post } from '@/utils/request'
export interface CustomStyle {
css: string
js: string
}
export function getCustomStyle<T>() {
return post<T>({
url: '/system/customStyle/get',
})
}
export function setCustomStyle<T>(data: CustomStyle) {
return post<T>({
url: '/system/customStyle/set',
data,
})
}
+20
View File
@@ -0,0 +1,20 @@
import { post } from '@/utils/request'
export interface SiteCustomize {
siteTitle: string
faviconUrl: string
loginDescription: string
}
export function getSiteCustomize<T>() {
return post<T>({
url: '/system/siteCustomize/get',
})
}
export function setSiteCustomize<T>(data: SiteCustomize) {
return post<T>({
url: '/system/siteCustomize/set',
data,
})
}
+70
View File
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NButton, NForm, NFormItem, NInput, NText, useMessage } from 'naive-ui'
import { getCustomStyle, setCustomStyle } from '@/api/system/customStyle'
import type { CustomStyle } from '@/api/system/customStyle'
import { t } from '@/locales'
const ms = useMessage()
const saving = ref(false)
const form = ref<CustomStyle>({
css: '',
js: '',
})
onMounted(async () => {
try {
const res = await getCustomStyle<CustomStyle>()
if (res.code === 0 && res.data)
form.value = res.data
}
catch {}
})
async function handleSave() {
saving.value = true
try {
const res = await setCustomStyle<any>(form.value)
if (res.code === 0)
ms.success(t('apps.customStyle.saveSuccess'))
}
catch {}
finally {
saving.value = false
}
}
</script>
<template>
<div class="p-4">
<NText depth="3" class="block mb-3 text-sm">
{{ t('apps.customStyle.tip') }}
</NText>
<NForm label-placement="top">
<NFormItem :label="t('apps.customStyle.cssLabel')">
<NInput
v-model:value="form.css"
type="textarea"
:rows="10"
:placeholder="t('apps.customStyle.cssPlaceholder')"
style="font-family: monospace; font-size: 13px;"
/>
</NFormItem>
<NFormItem :label="t('apps.customStyle.jsLabel')">
<NInput
v-model:value="form.js"
type="textarea"
:rows="10"
:placeholder="t('apps.customStyle.jsPlaceholder')"
style="font-family: monospace; font-size: 13px;"
/>
</NFormItem>
<NFormItem>
<NButton type="primary" :loading="saving" @click="handleSave">
{{ t('common.save') }}
</NButton>
</NFormItem>
</NForm>
</div>
</template>
+47 -10
View File
@@ -8,6 +8,7 @@ import { ConfigVersionLowError, FormatError, exportJson, importJsonString } from
import { get as getAbout } from '@/api/system/about'
import { edit as addGroup, getList as getGroupList } from '@/api/panel/itemIconGroup'
import { addMultiple as addMultipleIcons, getListByGroupId } from '@/api/panel/itemIcon'
import { get as getUserConfig, set as setUserConfig } from '@/api/panel/userConfig'
import { t } from '@/locales'
@@ -28,8 +29,8 @@ const debug = ref(false)
const importObj = ref<ImportJsonResult | null> (null)
const importItems = ref<string[]>(['icons']) // 当前软件版本支持导入导出的项目
const checkedItems = ref<string[]>(['icons']) // 当前准备导入的项目
const importItems = ref<string[]>(['icons', 'userConfig']) // 当前软件版本支持导入导出的项目
const checkedItems = ref<string[]>(['icons', 'userConfig']) // 当前准备导入的项目
// 导入图标
async function importIcons(): Promise<string | null> {
@@ -210,36 +211,70 @@ function importCheck() {
}
}
// 导出面板配置
async function exportUserConfig(): Promise<Panel.userConfig | null> {
try {
const res = await getUserConfig<Panel.userConfig>()
if (res.code === 0)
return res.data
}
catch {}
return null
}
// 导入面板配置
async function importUserConfig(): Promise<string | null> {
const cfg = importObj.value?.getUserConfig()
if (!cfg)
return null
try {
const res = await setUserConfig<any>(cfg)
if (res.code !== 0)
return res.msg
}
catch (error) {
if (error instanceof Error)
return error.message
}
return null
}
// 开始导出
async function handleStartExport() {
loading.value = true
// console.log('要导出的项目', checkedItems.value)
// 获取软件版本号
const exportResult = exportJson(version.value)
if (checkedItems.value.includes('icons')) {
console.log('export icons ...')
const iconGroups = await exportIcons()
exportResult.addIconsData(iconGroups)
console.log('export icons finish', iconGroups)
}
// console.log('导出结果')
if (checkedItems.value.includes('userConfig')) {
const cfg = await exportUserConfig()
if (cfg)
exportResult.addUserConfigData(cfg)
}
jsonData.value = exportResult.string()
exportResult.exportFile()
loading.value = false
exportRoundModalShow.value = false
// ms.success(t('common.success'))
}
// 开始导入
async function handleStartImport() {
loading.value = true
if (checkedItems.value.includes('icons')) {
console.log('export icons ...')
const errMsg = await importIcons()
if (errMsg !== null)
ms.success(`${t('common.failed')}:${errMsg}`)
ms.error(`${t('common.failed')}:${errMsg}`)
}
if (checkedItems.value.includes('userConfig')) {
const errMsg = await importUserConfig()
if (errMsg !== null)
ms.error(`${t('common.failed')}:${errMsg}`)
}
loading.value = false
@@ -325,6 +360,7 @@ async function handleStartImport() {
<NSpace justify="center" style="margin-top: 20px;">
<NCheckboxGroup v-model:value="checkedItems">
<NCheckbox v-if="importItems.includes('icons')" value="icons" :label="$t('apps.exportImport.moduleIcon')" />
<NCheckbox v-if="importItems.includes('userConfig')" value="userConfig" :label="$t('apps.exportImport.moduleUserConfig')" />
<NCheckbox v-if="importItems.includes('style')" value="style" :label="$t('apps.exportImport.moduleStyle')" />
</NCheckboxGroup>
</NSpace>
@@ -345,6 +381,7 @@ async function handleStartImport() {
<NSpace justify="center" style="margin-top: 20px;">
<NCheckboxGroup v-model:value="checkedItems">
<NCheckbox v-if="importItems.includes('icons')" value="icons" :label="$t('apps.exportImport.moduleIcon')" />
<NCheckbox v-if="importItems.includes('userConfig')" value="userConfig" :label="$t('apps.exportImport.moduleUserConfig')" />
<NCheckbox v-if="importItems.includes('style')" value="style" :label="$t('apps.exportImport.moduleStyle')" />
</NCheckboxGroup>
</NSpace>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui'
import { getSiteCustomize, setSiteCustomize } from '@/api/system/siteCustomize'
import type { SiteCustomize } from '@/api/system/siteCustomize'
import { t } from '@/locales'
const ms = useMessage()
const saving = ref(false)
const form = ref<SiteCustomize>({
siteTitle: '',
faviconUrl: '',
loginDescription: '',
})
onMounted(async () => {
try {
const res = await getSiteCustomize<SiteCustomize>()
if (res.code === 0 && res.data)
form.value = res.data
}
catch {}
})
async function handleSave() {
saving.value = true
try {
const res = await setSiteCustomize<any>(form.value)
if (res.code === 0)
ms.success(t('apps.siteCustomize.saveSuccess'))
}
catch {}
finally {
saving.value = false
}
}
</script>
<template>
<div class="p-4">
<NForm label-placement="top">
<NFormItem :label="t('apps.siteCustomize.siteTitle')">
<NInput
v-model:value="form.siteTitle"
:placeholder="t('apps.siteCustomize.siteTitlePlaceholder')"
/>
</NFormItem>
<NFormItem :label="t('apps.siteCustomize.faviconUrl')">
<NInput
v-model:value="form.faviconUrl"
:placeholder="t('apps.siteCustomize.faviconUrlPlaceholder')"
/>
</NFormItem>
<NFormItem :label="t('apps.siteCustomize.loginDescription')">
<NInput
v-model:value="form.loginDescription"
type="textarea"
:rows="3"
:placeholder="t('apps.siteCustomize.loginDescriptionPlaceholder')"
/>
</NFormItem>
<NFormItem>
<NButton type="primary" :loading="saving" @click="handleSave">
{{ t('common.save') }}
</NButton>
</NFormItem>
</NForm>
</div>
</template>
+20
View File
@@ -90,6 +90,7 @@
"fileModified": "The file has been modified, import with caution",
"import": "Import configuration",
"moduleIcon": "Icon configuration",
"moduleUserConfig": "Panel configuration",
"moduleStyle": "Style configuration",
"selectExportData": "Select the configuration data to export",
"selectImportData": "Select the configuration data to import",
@@ -125,6 +126,25 @@
"dark": "Dark ",
"light": "Light "
}
},
"siteCustomize": {
"appName": "Site Customize",
"siteTitle": "Site Title",
"siteTitlePlaceholder": "Leave blank to use default title Sun-Panel",
"faviconUrl": "Favicon URL",
"faviconUrlPlaceholder": "Leave blank to use default icon",
"loginDescription": "Login Page Description",
"loginDescriptionPlaceholder": "Leave blank to hide",
"saveSuccess": "Saved successfully"
},
"customStyle": {
"appName": "Custom CSS/JS",
"cssLabel": "Custom CSS",
"jsLabel": "Custom JS",
"cssPlaceholder": "/* Enter custom CSS here */",
"jsPlaceholder": "// Enter custom JS here",
"saveSuccess": "Saved successfully",
"tip": "Refresh the page after saving to apply changes"
}
},
"common": {
+20
View File
@@ -90,6 +90,7 @@
"fileModified": "文件被修改过,谨慎导入",
"import": "导入配置",
"moduleIcon": "图标配置",
"moduleUserConfig": "面板配置",
"moduleStyle": "样式配置",
"selectExportData": "请选择要导出的配置数据",
"selectImportData": "请选择要导入的配置数据",
@@ -125,6 +126,25 @@
"dark": "深色 ",
"light": "浅色 "
}
},
"siteCustomize": {
"appName": "站点自定义",
"siteTitle": "站点标题",
"siteTitlePlaceholder": "留空则使用默认标题 Sun-Panel",
"faviconUrl": "Favicon 图标地址",
"faviconUrlPlaceholder": "留空则使用默认图标",
"loginDescription": "登录页描述",
"loginDescriptionPlaceholder": "留空不显示",
"saveSuccess": "保存成功"
},
"customStyle": {
"appName": "自定义 CSS/JS",
"cssLabel": "自定义 CSS",
"jsLabel": "自定义 JS",
"cssPlaceholder": "/* 在此输入自定义 CSS */",
"jsPlaceholder": "// 在此输入自定义 JS",
"saveSuccess": "保存成功",
"tip": "修改后刷新页面生效"
}
},
"common": {
+13 -2
View File
@@ -25,7 +25,7 @@ export interface JsonStructure {
exportTime: string
appVersion: string
icons?: any
// styleConfig: Panel.panelConfig
userConfig?: Panel.userConfig
md5: string
}
@@ -49,6 +49,7 @@ export interface IconGroup {
interface ExportJsonResult {
addIconsData(datas: IconGroup[]): ExportJsonResult
addUserConfigData(data: Panel.userConfig): ExportJsonResult
exportFile(): void
string(): string
}
@@ -75,6 +76,12 @@ export function exportJson(appVersion?: string): ExportJsonResult {
return this
},
// 添加用户面板配置
addUserConfigData(data: Panel.userConfig) {
jsonData.userConfig = data
return this
},
// 导出json文件
exportFile() {
generateMD5AndUpdate()
@@ -103,7 +110,8 @@ export interface ImportJsonResult {
isPassCheckConfigVersionBest: () => boolean // 验证程序的导入版本驱动是否为最佳 当配置文件和驱动版本相等的时候为最佳,否则不匹配过新或者过旧
jsonStruct: JsonStructure // 根据实际情况提供更具体的类型定义
hasProperty: (key: string) => boolean
geticons: () => IconGroup[] // 根据实际情况提供更具体的类型定义
geticons: () => IconGroup[]
getUserConfig: () => Panel.userConfig | null
}
// 导入json数据
@@ -140,6 +148,9 @@ export function importJsonString(jsonString: string): ImportJsonResult | null {
geticons: (): IconGroup[] => {
return jsonStruct.icons || []
},
getUserConfig: (): Panel.userConfig | null => {
return jsonStruct.userConfig || null
},
}
}
+12 -12
View File
@@ -35,19 +35,19 @@ function http<T = any>(
return res.data
if (res.data.code === 1001) {
// 避免重复弹窗
if (loginMessageShow === false) {
loginMessageShow = true
message.warning(t('api.loginExpires'), {
// message.warning('登录过期', {
onLeave() {
loginMessageShow = false
},
})
}
router.push({ path: '/login' })
authStore.removeToken()
// 已在登录页不弹提示,避免带旧 token 进入时误报
if (router.currentRoute.value.path !== '/login') {
if (loginMessageShow === false) {
loginMessageShow = true
message.warning(t('api.loginExpires'), {
onLeave() {
loginMessageShow = false
},
})
}
router.push({ path: '/login' })
}
return res.data
}
+19 -8
View File
@@ -92,15 +92,26 @@ function handleResize() {
}
onMounted(() => {
const adminApp: App = {
name: t('adminSettingUsers.appName'),
componentName: 'Users',
icon: 'lucide-users',
auth: 1,
if (authStore.userInfo?.role === 1) {
apps.value.push({
name: t('adminSettingUsers.appName'),
componentName: 'Users',
icon: 'lucide-users',
auth: 1,
})
apps.value.push({
name: t('apps.siteCustomize.appName'),
componentName: 'SiteCustomize',
icon: 'mdi-earth-settings',
auth: 1,
})
apps.value.push({
name: t('apps.customStyle.appName'),
componentName: 'CustomStyle',
icon: 'mdi-code-braces',
auth: 1,
})
}
// 初始化
if (authStore.userInfo?.role === 1)
apps.value.push(adminApp)
window.addEventListener('resize', handleResize)
handleResize()
+1 -1
View File
@@ -524,7 +524,7 @@ function handleAddItem(itemIconGroupId?: number) {
</template>
</NButton>
<NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN" color="#2a2a2a6b" @click="settingModalShow = !settingModalShow">
<NButton v-if="authStore.visitMode === VisitMode.VISIT_MODE_LOGIN" color="#f97316" @click="settingModalShow = !settingModalShow">
<template #icon>
<SvgIcon class="text-white font-xl" icon="majesticons-applications" />
</template>
+29 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { NButton, NCard, NForm, NFormItem, NGradientText, NInput, NSelect, useMessage } from 'naive-ui'
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { login } from '@/api'
import { useAppStore, useAuthStore } from '@/store'
import { SvgIcon } from '@/components/common'
@@ -8,14 +8,37 @@ import { router } from '@/router'
import { t } from '@/locales'
import { languageOptions } from '@/utils/defaultData'
import type { Language } from '@/store/modules/app/helper'
import { getSiteCustomize } from '@/api/system/siteCustomize'
import type { SiteCustomize } from '@/api/system/siteCustomize'
// const userStore = useUserStore()
const authStore = useAuthStore()
const appStore = useAppStore()
const ms = useMessage()
const loading = ref(false)
const languageValue = ref<Language>(appStore.language)
const siteCustomize = ref<SiteCustomize>({
siteTitle: '',
faviconUrl: '',
loginDescription: '',
})
onMounted(async () => {
try {
const res = await getSiteCustomize<SiteCustomize>()
if (res.code === 0 && res.data) {
siteCustomize.value = res.data
// 动态设置 favicon
if (res.data.faviconUrl) {
const link = document.querySelector<HTMLLinkElement>('link[rel~="icon"]')
if (link)
link.href = res.data.faviconUrl
}
}
}
catch {}
})
// const isShowCaptcha = ref<boolean>(false)
// const isShowRegister = ref<boolean>(false)
@@ -75,9 +98,12 @@ function handleChangeLanuage(value: Language) {
<div class="login-title ">
<NGradientText :size="30" type="success" class="!font-bold">
{{ $t('common.appName') }}
{{ siteCustomize.siteTitle || $t('common.appName') }}
</NGradientText>
</div>
<div v-if="siteCustomize.loginDescription" class="text-center text-slate-400 text-sm mb-3">
{{ siteCustomize.loginDescription }}
</div>
<NForm :model="form" label-width="100px" @keydown.enter="handleSubmit">
<NFormItem>
<NInput v-model:value="form.username" :placeholder="$t('login.usernamePlaceholder')">