OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
@php use Leantime\Domain\Auth\Models\Roles; @endphp
@dispatchEvent('beforeHeadMenu')
<ul class="headmenu pull-right">
@dispatchEvent('insideHeadMenu')
@include('timesheets::partials.stopwatch', [
'onTheClock' => $onTheClock
])
@if ($login::userIsAtLeast("manager", true))
<li class="notificationDropdown appsLink">
<a
class="dropdown-toggle profileHandler newsDropDownHandler"
hx-get="{{ BASE_URL }}/plugins/marketplaceplugins/getLatest"
hx-target="#pluginNewsDropdown"
hx-indicator=".htmx-news-indicator"
hx-trigger="click"
preload="mouseover"
data-toggle='dropdown'
data-tippy-content='{{ __('popover.latest_plugins') }}'
>
<i class="fa-solid fa-puzzle-piece"></i>
</a>
<div class='dropdown-menu tw-p-m tw-h-screen tw-overflow-y-auto' id='pluginNewsDropdown'>
<div class="htmx-indicator htmx-news-indicator">
<x-global::loadingText type="text" count="3" includeHeadline="true" />
</div>
</div>
</li>
@endif
<li class="notificationDropdown">
<a
class="dropdown-toggle profileHandler newsDropDownHandler"
hx-get="{{ BASE_URL }}/notifications/news/get"
hx-target="#newsDropdown"
hx-indicator=".htmx-news-indicator"
hx-trigger="click"
preload="mouseover"
data-toggle='dropdown'
data-tippy-content='{{ __('popover.latest_updates') }}'
>
<span class="fa-solid fa-bolt-lightning"></span>
<span hx-get="{{ BASE_URL }}/notifications/news-badge/get" hx-trigger="load" hx-target="this"></span>
</a>
<div class='dropdown-menu tw-p-m tw-h-screen tw-overflow-y-auto' id='newsDropdown'>
<div class="htmx-indicator htmx-news-indicator">
<x-global::loadingText type="text" count="3" includeHeadline="true" />
</div>
</div>
</li>
<li class="notificationDropdown">
<a
href='javascript:void(0);'
class="dropdown-toggle profileHandler notificationHandler"
data-toggle='dropdown'
data-tippy-content='{{ __('popover.notifications') }}'
>
<span class="fa-solid fa-bell"></span>
@if($newNotificationCount>0)
<span class='notificationCounter'>{{ $newNotificationCount }}</span>
@endif
</a>
<div class='dropdown-menu' id='notificationsDropdown'>
<div class='dropdownTabs'>
<a
href='javascript:void(0);'
class='notifcationTabs active'
id="notificationsListLink"
onclick="toggleNotificationTabs('notifications')"
>Notification ({{ $totalNewNotifications }})</a>
<a
href='javascript:void(0);'
class='notifcationTabs'
id="mentionsListLink"
onclick="toggleNotificationTabs('mentions')"
>Mentions ({{ $totalNewMentions }})</a>
</div>
<div class="scroll-wrapper">
<ul id='notificationsList' class='notifcationViewLists'>
@if ($totalNotificationCount === 0)
<p style='padding: 10px'>{{ __('text.no_notifications') }}</p>
@endif
@foreach ($notifications as $notif)
@if ($notif['type'] == 'mention')
@continue
@endif
<li
@if ($notif['read'] == 0)
class='new'
@endif
data-url="{{ $notif['url'] }}"
data-id="{{ $notif['id'] }}"
>
<a href="{{ $notif['url'] }}">
<span class="notificationProfileImage">
<img src="{{ BASE_URL }}/api/users?profileImage={{ $notif['authorId'] }}"/>
</span>
<span class="notificationDate">
{{ format($notif['datetime'])->date() }}
{{ format($notif['datetime'])->time() }}
</span>
<span class="notificationTitle">{!! strip_tags($tpl->convertRelativePaths($notif['message'])) !!}</span>
</a>
</li>
@endforeach
</ul>
<ul id='mentionsList' style='display:none;' class='notificationViewLists'>
@if ($totalMentionCount === 0)
<p style="padding: 10px">{{ __('text.no_notifications') }}</p>
@endif
@foreach ($notifications as $notif)
@if ($notif['type'] != 'mention')
@continue
@endif
<li
@if ($notif['read'] == 0)
class='new'
@endif
data-url="{{ $notif['url'] }}"
data-id="{{ $notif['id'] }}"
>
<a href="{{ $notif['url'] }}">
<span class="notificationProfileImage">
<img src="{{ BASE_URL }}/api/users?profileImage={{ $notif['authorId'] }}"/>
</span>
<span class="notificationDate">
{{ format($notif['datetime'])->date() }}
{{ format($notif['datetime'])->time() }}
</span>
<span class="notificationTitle">{!! strip_tags($tpl->convertRelativePaths($notif['message'])) !!}</span>
</a>
</li>
@endforeach
</ul>
</div>
</div>
</li>
<li>
<div class="userloggedinfo">
@include("auth::partials.loginInfo")
</div>
@dispatchEvent('afterUser')
</li>
@dispatchEvent('beforeHeadMenuClose')
</ul>
<ul class="headmenu work-modes" style="height: 50px; float: left;">
@dispatchEvent('afterHeadMenuOpen')
<li>
@include('menu::projectSelector')
</li>
<li>
<a
href="{{ BASE_URL }}/dashboard/home"
@if ($menuType == 'personal')
class="active"
@endif
data-tippy-content="{{ __('popover.my_work') }}"
>{!! __('menu.my_work') !!}</a>
</li>
@if ($login::userIsAtLeast("manager", true))
<li>
@if($login::userHasRole("manager"))
<a
href="{{ BASE_URL }}/projects/showAll/"
@if ($menuType == 'company')
class="active"
@endif
data-tippy-content="{{ __('popover.company') }}"
>{!! __('menu.company') !!}</a>
@else
<a
href="{{ BASE_URL }}/setting/editCompanySettings/"
@if ($menuType == 'company')
class="active"
@endif
data-tippy-content="{{ __('popover.company') }}"
>{!! __('menu.company') !!}</a>
@endif
</li>
@endif
</ul>
@dispatchEvent('afterHeadMenu')
@once
@push('scripts')
<script>
function toggleNotificationTabs(active) {
jQuery(".notifcationTabs").removeClass("active");
jQuery('#' + active + 'ListLink').addClass("active");
jQuery('.notifcationViewLists').hide();
jQuery('#' + active + 'List').show();
}
jQuery(document).ready(function () {
jQuery('.notificationHandler').on('click', function () {
leantime.rpc('Notifications.Notifications.markRead', { id: 'all' })
.then(function () {
jQuery(".notifcationViewLists li.new").removeClass("new");
jQuery(".notificationCounter").fadeOut();
})
.catch(function (e) { console.error('Could not mark notifications read', e); });
});
jQuery('.notificationDropdown .dropdown-menu').on('click', function (e) {
e.stopPropagation();
});
jQuery('notificationsDropdown li').click(function () {
const url = jQuery(this).data('url');
const id = jQuery(this).data('id');
window.location.href = url;
})
});
</script>
@endpush
@endonce

View File

@@ -0,0 +1,64 @@
@php
/**
* @todo Move this to Composer, or find a better
* way to add filters for all passed variables
*/
use Leantime\Domain\Auth\Models\Roles;
$settingsLink = $tpl->dispatchTplFilter(
'settingsLink',
$settingsLink,
['type' => $menuType]
);
@endphp
@dispatchEvent('beforeMenu')
<ul class="nav nav-tabs nav-stacked">
@dispatchEvent('afterMenuOpen')
@if ($allAvailableProjects
|| !session()->has("currentProject")
|| $menuType == "personal"
|| $menuType == "company")
<li class="dropdown scrollableMenu">
<ul style="display:block;">
@foreach ($menuStructure as $key => $menuItem)
@includeIf("menu::partials.leftnav.".$menuItem['type'], ["menuItem" => $menuItem, "module" => $module, "action" => $action])
@endforeach
@if ($login::userIsAtLeast(Roles::$manager) && $menuType != 'company' && $menuType != 'personal' && $menuType != 'projecthub')
<li class="fixedMenuPoint {{ $module == $settingsLink['module'] && $action == $settingsLink['action'] ? 'active' : '' }}">
<a href="{{ BASE_URL }}/{{ $settingsLink['module'] }}/{{ $settingsLink['action'] }}/{{ session("currentProject") }}">
{!! $settingsLink['label'] !!}
</a>
</li>
@endif
</ul>
</li>
@endif
@dispatchEvent('beforeMenuClose')
</ul>
@dispatchEvent('afterMenuClose')
@once
@push('scripts')
<script>
jQuery(document).ready(function () {
leantime.menuController.initProjectSelector();
leantime.menuController.initLeftMenuHamburgerButton();
});
</script>
@endpush
@endonce

View File

@@ -0,0 +1,53 @@
@php
$lastClient = '';
@endphp
<ul id="{{ $prefix }}-projectSelectorlist-group-{{ $parent }}" class="level-{{ $level }} projectGroup">
@foreach($projects as $project)
@php
$parentState = session("usersettings.submenuToggle.".$prefix.'-projectSelectorlist-group-'.$project['clientId'], 'closed');
@endphp
@if(
!session()->exists("usersettings.projectSelectFilter.client")
|| session("usersettings.projectSelectFilter.client") == $project["clientId"]
|| session("usersettings.projectSelectFilter.client") == 0
|| session("usersettings.projectSelectFilter.client") == ""
)
@if ($lastClient != $project['clientName'])
@php
$lastClient = $project['clientName']
@endphp
@if(!$loop->first)
</ul>
@endif
<li class='projectLineItem clientIdHead-{{$project['clientId'] }}'>
<a href="javascript:void(0);"
class="toggler {{ $parentState }}"
id="{{ $prefix }}-toggler-{{ $project["clientId"] }}"
onclick="leantime.menuController.toggleProjectDropDownList('{{ $project["clientId"] }}', '', '{{ $prefix }}')">
@if($parentState == 'closed')
<i class="fa fa-angle-right"></i>
@else
<i class="fa fa-angle-down"></i>
@endif
</a>
<a href="javascript:void(0)">
{{ $project['clientName'] }}
</a>
<ul id="{{ $prefix }}-projectSelectorlist-group-{{ $project['clientId'] }}" class="level-1 projectGroup {{ $parentState }}">
@endif
<li class="projectLineItem hasSubtitle {{ session("currentProject") == $project['id'] ? "active" : '' }}" >
@include('menu::partials.projectLink')
<div class="clear"></div>
</li>
@endif
@endforeach
</ul>

View File

@@ -0,0 +1,5 @@
<li class="fixedMenuPoint {{ $module == $settingsLink['module'] && $action == $settingsLink['action'] ? 'active' : '' }}">
<a href="@if(isset($settingsLink['url'])) {{ $settingsLink['url'] }} @else {{ BASE_URL }}/{{ $settingsLink['module'] }}/{{ $settingsLink['action'] }} @endif">
{!! __($settingsLink['label']) !!}
</a>
</li>

View File

@@ -0,0 +1,5 @@
<li class="title">
<a href="javascript:void(0);">
<strong>{!! __($menuItem['title']) !!}</strong>
</a>
</li>

View File

@@ -0,0 +1,25 @@
@if(!isset($menuItem['role']) || $login::userIsAtLeast($menuItem['role'] ?? 'editor'))
<li
@if(
$module == $menuItem['module']
&& (!isset($menuItem['active']) || in_array($action, $menuItem['active']))
)
class='active'
@endif
>
<a href="{{ BASE_URL . $menuItem['href'] }}"
data-tippy-content="{{ strip_tags(__($menuItem['tooltip'])) }}"
data-tippy-placement="right"
preload="mouseover"
@if(isset($menuItem['attributes']))
@foreach($menuItem['attributes'] as $key => $value)
{{ $key }}="{{ $value }}"
@endforeach
@endif
>
{!! $menuItem['title'] !!}
</a>
</li>
@endif

View File

@@ -0,0 +1 @@
<li class="separator"></li>

View File

@@ -0,0 +1,27 @@
@if(!isset($menuItem['role']) || $login::userIsAtLeast($menuItem['role'] ?? 'editor'))
<li class="submenuToggle">
<a href="javascript:void(0);"
@if ( $menuItem['visual'] !== 'always' )
onclick="leantime.menuController.toggleSubmenu('{{ $menuItem['id'] }}')"
@endif
>
<i class="submenuCaret fa fa-angle-{{ $menuItem['visual'] == 'closed' ? 'right' : 'down' }}"
id="submenu-icon-{{ $menuItem['id'] }}"></i>
<strong>{!! __($menuItem['title']) !!}</strong>
</a>
</li>
<ul id="submenu-{{ $menuItem['id'] }}" class="submenu {{ $menuItem['visual'] == 'closed' ? 'closed' : 'open' }}">
@foreach ($menuItem['submenu'] as $subkey => $submenuItem)
@switch ($submenuItem['type'])
@case('header')
@include("menu::partials.leftnav.header", ["menuItem" => $submenuItem, "module" => $module, "action" => $action])
@break
@case('item')
@include("menu::partials.leftnav.item", ["menuItem" => $submenuItem, "module" => $module, "action" => $action])
@endswitch
@endforeach
</ul>
@endif

View File

@@ -0,0 +1,19 @@
<ul class="level-0 noGroup">
@foreach($projects as $project)
@if(
!session()->exists("usersettings.projectSelectFilter.client")
|| session("usersettings.projectSelectFilter.client") == $project["clientId"]
|| session("usersettings.projectSelectFilter.client") == 0
|| session("usersettings.projectSelectFilter.client") == ""
)
<li class="projectLineItem hasSubtitle {{ session("currentProject") ?? 0 == $project['id'] ? "active" : '' }}" >
@include('menu::partials.projectLink')
<div class="clear"></div>
</li>
@endif
@endforeach
</ul>

View File

@@ -0,0 +1,46 @@
@php
$groupState = session("usersettings.submenuToggle.".$prefix.'-projectSelectorlist-group-'.$parent, 'closed');
@endphp
<ul id="{{ $prefix }}-projectSelectorlist-group-{{ $parent }}" class="level-{{ $level }} projectGroup {{ $groupState }}">
@foreach($projects as $project)
@if(
!session()->exists("usersettings.projectSelectFilter.client")
|| session("usersettings.projectSelectFilter.client") == $project["clientId"]
|| session("usersettings.projectSelectFilter.client") == 0
|| session("usersettings.projectSelectFilter.client") == ""
|| $project["clientId"] == ''
)
<li class="projectLineItem hasSubtitle {{ session("currentProject") == $project['id'] ? "active" : '' }}" >
@php
$parentState = session("usersettings.submenuToggle.".$prefix.'-projectSelectorlist-group-'.$project['id'], 'closed');
@endphp
@if((empty($project['children']) || count($project['children']) ==0))
<span class="toggler"></span>
@endif
@if(!empty($project['children']) && count($project['children']) >0)
<a href="javascript:void(0);" class="toggler {{ $parentState }}" id="{{ $prefix }}-toggler-{{ $project["id"] }}" onclick="leantime.menuController.toggleProjectDropDownList('{{ $project["id"] }}', '', '{{ $prefix }}')">
@if($parentState == 'closed')
<i class="fa fa-angle-right"></i>
@else
<i class="fa fa-angle-down"></i>
@endif
</a>
@endif
@include('menu::partials.projectLink')
<div class="clear"></div>
{{-- Depth cap: the hierarchy is cycle-guarded server-side, but a runaway
tree (e.g. mutated by a plugin filter) must never recurse unbounded --}}
@if(!empty($project['children']) && count($project['children']) >0 && $level < 20)
@include('menu::partials.projectGroup', ['projects' => $project['children'], 'parent' => $project['id'], 'level'=> $level+1, 'prefix' => $prefix, "currentProject"=>$currentProject])
@endif
</li>
@endif
@endforeach
</ul>

View File

@@ -0,0 +1,21 @@
<a href='{{ BASE_URL }}/projects/changeCurrentProject/{{ $project["id"] }}'
@if(strlen($project["name"]) > 25)
data-tippy-content='{{ $project["name"] }}'
@endif >
<span class='projectAvatar'>
@if(isset($projectTypeAvatars[$project["type"]]) && $projectTypeAvatars[$project["type"]] != "avatar")
<span class="{{ $projectTypeAvatars[$project["type"]] }}"></span>
@else
<img src='{{ BASE_URL }}/api/projects?projectAvatar={{ $project["id"] }}&v={{ format($project['modified'])->timestamp() }}' />
@endif
</span>
<span class='projectName'>
@if($project["clientName"] != '')
<small>{{ $project["clientName"] }}</small><br />
@else
<small>{{ __('projectType.'.$project["type"] ?? 'project') }}</small><br />
@endif
{{ $project["name"] }}
</span>
</a>

View File

@@ -0,0 +1,49 @@
<div class="projectListFilter">
<form
hx-target="#mainProjectSelector"
hx-swap="outerHTML"
hx-trigger="change">
<i class="fas fa-filter"></i>
<select data-placeholder="" title=""
hx-post="{{ BASE_URL }}/hx/menu/projectSelector/update-menu"
hx-target="#mainProjectSelector"
hx-swap="outerHTML"
hx-indicator=".htmx-indicator, .htmx-loaded-content"
name="client">
<option value="" data-placeholder="true">All Clients</option>
@foreach ($clients as $client)
@if($client['id'] > 0)
<option value='{{ $client['id'] }}'
@if (isset($projectSelectFilter['client']) && $projectSelectFilter['client'] == $client['id'])
selected='selected'
@endif
>{{ $client['name'] }}</option>
@endif
@endforeach
</select>
<i class="fa-solid fa-diagram-project"></i>
<select data-placeholder="" name="groupBy"
hx-post="{{ BASE_URL }}/hx/menu/projectSelector/update-menu"
hx-target="#mainProjectSelector"
hx-indicator=".htmx-indicator, .htmx-loaded-content"
hx-swap="outerHTML">
@foreach ($projectSelectGroupOptions as $key => $group)
<option value='{{ $key }}'
{{ $projectSelectFilter["groupBy"] == $key ? " selected='selected' " : "" }}
>{{ $group }}</option>
@endforeach
</select>
<input type="hidden" name="activeTab" value="" />
</form>
</div>
<div class="htmx-indicator tw-ml-m tw-mr-m tw-pt-l">
<x-global::loadingText type="project" count="5" includeHeadline="false"/>
</div>

View File

@@ -0,0 +1,126 @@
@props([
'redirect' => 'dashboard/show',
'currentProject'
])
<div class="dropdown-menu projectselector" id="mainProjectSelector">
@if ($menuType == 'project' || $menuType == 'default')
<div class="head">
<span class="sub">{{ __("menu.current_project") }}</span><br />
<span class="title">{{ session("currentProjectName") }}</span>
</div>
@else
<div class="projectSelectorFooter" style="border:none; border-bottom:1px solid var(--main-border-color)">
<ul class="selectorList projectList">
<li>
<a href="{{ BASE_URL }}/projects/showMy"><strong><i class="fa-solid fa-house-flag"></i> Open Project Hub</strong></a>
</li>
@if ($login::userIsAtLeast("manager"))
@dispatchEvent('beforeProjectCreateLink')
<li><a href="{{ $startSomethingUrl }}">
<span class="fancyLink">
{!! __('menu.create_something_new') !!}
</span>
</a>
</li>
@dispatchEvent('afterProjectCreateLink')
@endif
</ul>
</div>
@endif
<div class="tabbedwidget tab-primary projectSelectorTabs">
<ul class="tabs">
<li><a href="#myProjects">{{ __('menu.projectselector.my_projects') }}</a></li>
<li><a href="#favorites">{{ __('menu.projectselector.favorites') }}</a></li>
<li><a href="#recentProjects">{{ __('menu.projectselector.recent') }}</a></li>
<li><a href="#allProjects">{{ __('menu.projectselector.all_projects') }}</a></li>
</ul>
<div id="myProjects" class="scrollingTab">
@include('menu::partials.projectListFilter', ['clients' => $clients, 'projectSelectFilter' => $projectSelectFilter])
<ul class="selectorList projectList htmx-loaded-content">
@if($projectSelectFilter["groupBy"] == "client")
@include('menu::partials.clientGroup', ['projects' => $allAssignedProjects, 'parent' => 0, 'level'=> 0, "prefix" => "myClientProjects", "currentProject"=>$currentProject])
@elseif($projectSelectFilter["groupBy"] == "structure")
@include('menu::partials.projectGroup', ['projects' => $projectHierarchy, 'parent' => 0, 'level'=> 0, "prefix" => "myProjects", "currentProject"=>$currentProject])
@else
@include('menu::partials.noGroup', ['projects' => $allAssignedProjects, "currentProject"=>$currentProject])
@endif
</ul>
</div>
<div id="allProjects" class="scrollingTab">
@include('menu::partials.projectListFilter', ['clients' => $clients, 'projectSelectFilter' => $projectSelectFilter])
<ul class="selectorList projectList htmx-loaded-content">
@if($projectSelectFilter["groupBy"] == "client")
@include('menu::partials.clientGroup', ['projects' => $allAvailableProjects, 'parent' => 0, 'level'=> 0, "prefix" => "allClientProjects", "currentProject"=>$currentProject])
@elseif($projectSelectFilter["groupBy"] == "structure")
@include('menu::partials.projectGroup', ['projects' => $allAvailableProjectsHierarchy, 'parent' => 0, 'level'=> 0, "prefix" => "allProjects", "currentProject"=>$currentProject])
@else
@include('menu::partials.noGroup', ['projects' => $allAvailableProjects, "currentProject"=>$currentProject])
@endif
</ul>
</div>
<div id="recentProjects" class="scrollingTab">
<ul class="selectorList projectList">
@if(count($recentProjects) >= 1)
@include('menu::partials.noGroup', ['projects' => $recentProjects])
@else
<li class='nav-header'></li>
<li><span class='info'>
{{ __("menu.you_dont_have_projects") }}
</span>
</li>
@endif
</ul>
</div>
<div id="favorites" class="scrollingTab">
<ul class="selectorList projectList">
@if(count($favoriteProjects) >= 1)
@include('menu::partials.noGroup', ['projects' => $favoriteProjects])
@else
<li><span class='info'>
{{ __("text.you_have_not_favorited_any_projects") }}
</span>
</li>
@endif
</ul>
</div>
</div>
@if ($menuType == 'project' || $menuType == 'default')
<div class="projectSelectorFooter">
<ul class="selectorList projectList">
@if ($login::userIsAtLeast("manager"))
@dispatchEvent('beforeProjectCreateLink')
<li><a href="{{ $startSomethingUrl }}">
<span class="fancyLink">
{!! __('menu.create_something_new') !!}
</span>
</a>
</li>
@dispatchEvent('afterProjectCreateLink')
@endif
<li>
<a href="{{ BASE_URL }}/projects/showMy"><i class="fa-solid fa-circle-nodes"></i> Project Hub</a>
</li>
</ul>
</div>
@endif
</div>
<script>
jQuery(document).ready(function () {
leantime.menuController.initProjectSelector();
});
</script>

View File

@@ -0,0 +1,21 @@
<a href="{{ BASE_URL }}/projects/showMy"
class="dropdown-toggle bigProjectSelector {{ $menuType == "project" ? "active" : "" }}"
data-toggle="dropdown">
@if ($menuType == 'project' || $menuType == 'default')
<span class="projectAvatar {{ $currentProjectType }}">
@if(isset($projectTypeAvatars[$currentProjectType]) && $projectTypeAvatars[$currentProjectType] != "avatar")
<span class="{{ $projectTypeAvatars[$currentProjectType] }}"></span>
@else
<img src="{{ BASE_URL }}/api/projects?projectAvatar={{ $currentProject['id'] ?? -1 }}&v={{ format($currentProject['modified'] ?? '')->timestamp() }}"/>
@endif
</span>
{{ $currentProject['name'] ?? "" }}&nbsp;
@else
{!! __('menu.projects') !!}
@endif
<i class="fa fa-caret-down" aria-hidden="true"></i>
</a>
@include('menu::partials.projectSelector', [])