feat(system): 支持自定义登录页品牌

This commit is contained in:
kuaifan 2026-09-04 08:18:03 +00:00
parent ac43f630d3
commit f43977f07e
21 changed files with 125 additions and 32 deletions

View File

@ -45,7 +45,7 @@ class SystemController extends AbstractController
* @apiParam {String} type * @apiParam {String} type
* - get: 获取(默认) * - get: 获取(默认)
* - all: 获取所有(需要管理员权限) * - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'task_user_limit', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local'] * - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'task_user_limit', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'login_logo', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local']
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
@ -89,6 +89,7 @@ class SystemController extends AbstractController
'user_private_chat_mute', 'user_private_chat_mute',
'user_group_chat_mute', 'user_group_chat_mute',
'system_alias', 'system_alias',
'login_logo',
'system_welcome', 'system_welcome',
'image_compress', 'image_compress',
'image_quality', 'image_quality',
@ -114,6 +115,10 @@ class SystemController extends AbstractController
if ($all['system_alias'] == config('app.name')) { if ($all['system_alias'] == config('app.name')) {
$all['system_alias'] = ''; $all['system_alias'] = '';
} }
if (isset($all['login_logo'])) {
$loginLogo = $all['login_logo'];
$all['login_logo'] = $loginLogo ? Base::unFillUrl(is_array($loginLogo) ? $loginLogo[0]['path'] : $loginLogo) : '';
}
if ($all['system_welcome'] == '欢迎您,{username}') { if ($all['system_welcome'] == '欢迎您,{username}') {
$all['system_welcome'] = ''; $all['system_welcome'] = '';
} }
@ -130,6 +135,7 @@ class SystemController extends AbstractController
} }
// //
$setting['system_alias'] = ($setting['system_alias'] ?? '') ?: config('app.name'); $setting['system_alias'] = ($setting['system_alias'] ?? '') ?: config('app.name');
$setting['login_logo'] = Base::fillUrl($setting['login_logo'] ?? '');
$setting['reg'] = $setting['reg'] ?: 'open'; $setting['reg'] = $setting['reg'] ?: 'open';
$setting['reg_identity'] = $setting['reg_identity'] ?: 'normal'; $setting['reg_identity'] = $setting['reg_identity'] ?: 'normal';
$setting['temp_account_alias'] = $setting['temp_account_alias'] ?: ''; $setting['temp_account_alias'] = $setting['temp_account_alias'] ?: '';

View File

@ -75,6 +75,7 @@ class IndexController extends InvokeController
} }
return response()->view('main', [ return response()->view('main', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'), 'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'login_logo' => Base::fillUrl(Base::settingFind('system', 'login_logo')),
'version' => Base::getVersion(), 'version' => Base::getVersion(),
'style' => $style, 'style' => $style,
'script' => $script, 'script' => $script,

1
electron/build.js vendored
View File

@ -347,6 +347,7 @@ async function startBuild(data) {
// system info // system info
const systemInfo = { const systemInfo = {
title: data.name, title: data.name,
loginLogo: "",
debug: "no", debug: "no",
version: config.version, version: config.version,
origin: "./", origin: "./",

View File

@ -1778,7 +1778,7 @@ WiFi签到延迟时长为±1分钟。
最多添加5个时间范围 最多添加5个时间范围
系统别名 系统别名
用于网页默认标题、邮件发送等 用于网页标题、邮件发送,以及 Web 端和客户端登录页的欢迎语。
打包权限 打包权限
允许所有人 允许所有人
@ -2739,3 +2739,5 @@ WebDAV 路径冲突
上传与媒体处理 上传与媒体处理
至少保留一项 至少保留一项
确定要删除该优先级吗? 确定要删除该优先级吗?
登录页 Logo
用于 Web 端和客户端登录页,未设置时使用默认 Logo。

View File

@ -25380,16 +25380,16 @@
"ru": "Системный псевдоним" "ru": "Системный псевдоним"
}, },
{ {
"key": "用于网页默认标题、邮件发送等", "key": "用于网页标题、邮件发送,以及 Web 端和客户端登录页的欢迎语。",
"zh": "", "zh": "",
"zh-CHT": "用於網頁默認標題、郵件發送等", "zh-CHT": "用於網頁標題、郵件發送,以及 Web 端和客戶端登入頁的歡迎語。",
"en": "Used for web default titles, email sending, etc.", "en": "Used for webpage titles, email delivery, and the welcome message on Web and client login pages.",
"ko": "웹 기본 제목, 이메일 전송 등에 사용", "ko": "웹 페이지 제목, 이메일 발송, Web 및 클라이언트 로그인 페이지의 환영 문구에 사용됩니다.",
"ja": "ウェブのデフォルトタイトル、メール送信などに使用されます", "ja": "Webページのタイトル、メール送信、Web版およびクライアント版のログインページのウェルカムメッセージに使用されます。",
"de": "Wird für Web-Standardtitel, E-Mail-Versand usw. verwendet", "de": "Wird für Webseitentitel, den E-Mail-Versand und die Begrüßung auf den Anmeldeseiten der Web- und Client-Version verwendet.",
"fr": "Utilisé pour les titres par défaut des pages Web, l'envoi d'e-mails, etc.", "fr": "Utilisé pour les titres des pages Web, l'envoi d'e-mails et le message de bienvenue des pages de connexion Web et client.",
"id": "Digunakan untuk judul default web, pengiriman email, dll.", "id": "Digunakan untuk judul halaman web, pengiriman email, dan pesan sambutan pada halaman login Web dan klien.",
"ru": "Используется для веб-страниц по умолчанию, отправки электронной почты и т. д." "ru": "Используется для заголовков веб-страниц, отправки электронной почты и приветствия на страницах входа веб-версии и клиента."
}, },
{ {
"key": "打包下载【(%T1)】文件夹", "key": "打包下载【(%T1)】文件夹",
@ -39586,5 +39586,29 @@
"fr": "Afficher dans le dossier", "fr": "Afficher dans le dossier",
"id": "Tampilkan di Folder", "id": "Tampilkan di Folder",
"ru": "Показать в папке" "ru": "Показать в папке"
},
{
"key": "登录页 Logo",
"zh": "",
"zh-CHT": "登錄頁 Logo",
"en": "Login Page Logo",
"ko": "로그인 페이지 로고",
"ja": "ログインページのロゴ",
"de": "Logo der Anmeldeseite",
"fr": "Logo de la page de connexion",
"id": "Logo Halaman Masuk",
"ru": "Логотип страницы входа"
},
{
"key": "用于 Web 端和客户端登录页,未设置时使用默认 Logo。",
"zh": "",
"zh-CHT": "用於 Web 端和客戶端登入頁,未設定時使用預設 Logo。",
"en": "Used on Web and client login pages. The default logo is used when not configured.",
"ko": "Web 및 클라이언트 로그인 페이지에 사용되며, 설정하지 않으면 기본 로고가 사용됩니다.",
"ja": "Web版およびクライアント版のログインページで使用されます。未設定の場合はデフォルトのロゴが使用されます。",
"de": "Wird auf den Anmeldeseiten der Web- und Client-Version verwendet. Ohne Konfiguration wird das Standardlogo verwendet.",
"fr": "Utilisé sur les pages de connexion Web et client. Le logo par défaut est utilisé si aucun logo n'est configuré.",
"id": "Digunakan pada halaman login Web dan klien. Logo default digunakan jika tidak dikonfigurasi.",
"ru": "Используется на страницах входа веб-версии и клиента. Если не настроено, используется логотип по умолчанию."
} }
] ]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ negative:
- SYSTEM_SETTING=disabled 时所有系统设置都不能改(演示环境常用) - SYSTEM_SETTING=disabled 时所有系统设置都不能改(演示环境常用)
- 自动归档天数限制为 1-100 天,超出会报错 - 自动归档天数限制为 1-100 天,超出会报错
- 邀请码留空会自动生成随机码,不能完全关闭"凭码注册"模式 - 邀请码留空会自动生成随机码,不能完全关闭"凭码注册"模式
last_verified: v1.8.89 last_verified: v1.9.18
--- ---
# 通用设置 # 通用设置
@ -66,7 +66,8 @@ last_verified: v1.8.89
- `file_upload_limit` — 单文件上传大小MB - `file_upload_limit` — 单文件上传大小MB
**外观** **外观**
- `system_alias` / `system_welcome` — 系统名称别名与欢迎语 - `system_alias` / `system_welcome` — 系统名称别名与欢迎语;系统别名同时显示在 Web、桌面端和移动端登录页的 `Welcome` 标题中
- `login_logo` — 登录页 Logo支持上传、预览和删除删除后恢复内置 DooTask Logo
- 系统别名与部署的默认应用名称相同时,服务端使用空值存储以表示“沿用默认名称”,但保存响应和后续读取仍显示实际默认名称 - 系统别名与部署的默认应用名称相同时,服务端使用空值存储以表示“沿用默认名称”,但保存响应和后续读取仍显示实际默认名称
## 操作步骤 ## 操作步骤

View File

@ -20,7 +20,7 @@ negative:
- 账号被停用disable_at 非空)会提示「帐号已停用」,需联系管理员 - 账号被停用disable_at 非空)会提示「帐号已停用」,需联系管理员
- 开启「注册需邮箱验证」时,未验证邮箱的账号无法登录,必须先完成验证([[user-account.email-verify.howto]] - 开启「注册需邮箱验证」时,未验证邮箱的账号无法登录,必须先完成验证([[user-account.email-verify.howto]]
- 多次失败后系统会强制要求填验证码([[user-account.login-codeimg.howto]] - 多次失败后系统会强制要求填验证码([[user-account.login-codeimg.howto]]
last_verified: v1.7.90 last_verified: v1.9.18
--- ---
# 登录账号 # 登录账号
@ -28,6 +28,7 @@ last_verified: v1.7.90
## 入口 ## 入口
- 登录页:`/login` - 登录页:`/login`
- 客户端启动时自动跳转 - 客户端启动时自动跳转
- Web、桌面端和移动端登录页会显示管理员配置的登录 Logo 和系统别名;未配置时显示内置 DooTask 品牌
## 支持的登录方式 ## 支持的登录方式
DooTask 同时支持以下登录方式(在登录页可切换): DooTask 同时支持以下登录方式(在登录页可切换):

View File

@ -2,7 +2,7 @@
<div class="page-login"> <div class="page-login">
<PageTitle :title="$L('登录')"/> <PageTitle :title="$L('登录')"/>
<div class="login-body"> <div class="login-body">
<div class="login-logo no-dark-content"></div> <div class="login-logo no-dark-content" :style="loginLogo ? {'background-image': 'url(' + loginLogo + ')'} : null"></div>
<div class="login-box"> <div class="login-box">
<div class="login-mode-switch"> <div class="login-mode-switch">
<div class="login-mode-switch-box"> <div class="login-mode-switch-box">
@ -245,24 +245,31 @@ export default {
'themeConf', 'themeConf',
'themeList', 'themeList',
'systemConfig',
]), ]),
currentLanguage() { currentLanguage() {
return languageList[languageName] || 'Language' return languageList[languageName] || 'Language'
}, },
systemAlias() {
return this.systemConfig.system_alias || window.systemInfo.title || "DooTask"
},
loginLogo() {
return this.systemConfig.login_logo || window.systemInfo.loginLogo || ''
},
welcomeTitle() { welcomeTitle() {
if (this.loginMode == 'qrcode') { if (this.loginMode == 'qrcode') {
return this.$L("扫码登录") return this.$L("扫码登录")
} }
const title = window.systemInfo.title || "DooTask"; return "Welcome " + this.systemAlias
return "Welcome " + title
}, },
subTitle() { subTitle() {
const title = window.systemInfo.title || "DooTask";
if (this.loginMode == 'qrcode') { if (this.loginMode == 'qrcode') {
return this.$L('请使用(*)移动端扫描二维码。', title) return this.$L('请使用(*)移动端扫描二维码。', this.systemAlias)
} }
if (this.loginType=='reg') { if (this.loginType=='reg') {
return this.$L('输入您的信息以创建帐户。') return this.$L('输入您的信息以创建帐户。')

View File

@ -285,7 +285,16 @@
<div style="width: 220px;"> <div style="width: 220px;">
<Input v-model="formDatum.system_alias"/> <Input v-model="formDatum.system_alias"/>
</div> </div>
<div class="form-tip">{{$L('用于网页默认标题、邮件发送等')}}</div> <div class="form-tip">{{$L('用于网页标题、邮件发送,以及 Web 端和客户端登录页的欢迎语。')}}</div>
</FormItem>
<FormItem :label="$L('登录页 Logo')" prop="login_logo">
<ImgUpload
v-model="formDatum.login_logo"
:num="1"
:width="512"
:height="512"
whcut="percentage"/>
<div class="form-tip">{{$L('用于 Web 端和客户端登录页,未设置时使用默认 Logo。')}}</div>
</FormItem> </FormItem>
<FormItem :label="$L('欢迎词')" prop="system_welcome"> <FormItem :label="$L('欢迎词')" prop="system_welcome">
<div style="width: 220px;"> <div style="width: 220px;">
@ -359,9 +368,10 @@ import UserSelect from "../../../../components/UserSelect.vue";
import SystemColumnTemplate from "./SystemColumnTemplate.vue"; import SystemColumnTemplate from "./SystemColumnTemplate.vue";
import SystemFileSetting from "./SystemFileSetting.vue"; import SystemFileSetting from "./SystemFileSetting.vue";
import SystemTaskPriority from "./SystemTaskPriority.vue"; import SystemTaskPriority from "./SystemTaskPriority.vue";
import ImgUpload from "../../../../components/ImgUpload.vue";
const SCOPE_FIELDS = { const SCOPE_FIELDS = {
general: ['system_alias', 'system_welcome'], general: ['system_alias', 'login_logo', 'system_welcome'],
account: ['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy'], account: ['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy'],
project: ['project_invite', 'project_add_permission', 'project_add_userids', 'department_owner_project_view'], project: ['project_invite', 'project_add_permission', 'project_add_userids', 'department_owner_project_view'],
task: ['auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'task_user_limit', 'unclaimed_task_reminder', 'unclaimed_task_reminder_time', 'task_ai_auto_analyze'], task: ['auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'task_user_limit', 'unclaimed_task_reminder', 'unclaimed_task_reminder_time', 'task_ai_auto_analyze'],
@ -372,7 +382,7 @@ const SCOPE_FIELDS = {
export default { export default {
name: 'SystemSetting', name: 'SystemSetting',
components: {SystemTaskPriority, SystemFileSetting, SystemColumnTemplate, UserSelect}, components: {ImgUpload, SystemTaskPriority, SystemFileSetting, SystemColumnTemplate, UserSelect},
props: { props: {
scope: { scope: {

View File

@ -23,6 +23,7 @@
}; };
window.systemInfo = { window.systemInfo = {
title: "{{$system_alias}}", title: "{{$system_alias}}",
loginLogo: "{{$login_logo}}",
debug: "{{config('app.debug') ? 'yes' : 'no'}}", debug: "{{config('app.debug') ? 'yes' : 'no'}}",
version: "{{ $version }}", version: "{{ $version }}",
origin: window.location.origin + "/", origin: window.location.origin + "/",

View File

@ -0,0 +1,39 @@
<?php
namespace Tests\Feature;
use App\Module\Base;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class SystemBrandingTest extends TestCase
{
use DatabaseTransactions;
public function test_public_system_setting_exposes_login_branding(): void
{
Base::setting('system', [
'system_alias' => 'Example Team',
'login_logo' => 'uploads/user/picture/1/202609/example.png',
], true);
$response = $this->getJson('/api/system/setting')
->assertOk()
->assertJsonPath('ret', 1)
->assertJsonPath('data.system_alias', 'Example Team');
$this->assertStringEndsWith(
'/uploads/user/picture/1/202609/example.png',
$response->json('data.login_logo')
);
$page = $this->get('/login')
->assertOk()
->assertSee('Example Team', false);
$this->assertStringContainsString(
'/uploads/user/picture/1/202609/example.png',
str_replace('\/', '/', $page->getContent())
);
}
}