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,32 @@
{{--
Milestone chip row for a goal (board + dashboard).
Expects: $milestones = [{ id, headline, color, percentDone, statusType }, ...]
The fill is the milestone's OWN color growing with progress — deliberately
not a status color (status lives on the goal card, never the chips).
Each chip links to its milestone; stopPropagation keeps the click from
also opening the goal card behind it.
--}}
@if (! empty($milestones))
{{-- This partial is included once per goal card; emit the shared chip CSS
only on the first include so a board of N cards doesn't ship N copies. --}}
@once
<style>
.goalMsBoardRow{scrollbar-width:thin;scrollbar-color:#9aa7ad transparent;}
.goalMsBoardRow::-webkit-scrollbar{height:8px;}
.goalMsBoardRow::-webkit-scrollbar-thumb{background:#9aa7ad;border-radius:10px;border:2px solid transparent;background-clip:padding-box;}
.goalMsBoardRow::-webkit-scrollbar-track{background:transparent;}
.goalMsBoardChip{text-decoration:none;color:inherit;cursor:pointer;transition:border-color .12s;}
.goalMsBoardChip:hover{border-color:var(--primary-color,#004666)!important;}
</style>
@endonce
<div class="goalMsBoardRow" style="display:flex;gap:6px;overflow-x:auto;padding-bottom:4px;margin-top:6px;">
@foreach ($milestones as $ms)
<a href="#/tickets/editMilestone/{{ (int) $ms['id'] }}" onclick="event.stopPropagation();" class="goalMsBoardChip" title="{{ __('links.edit_milestone') }}: {{ $ms['headline'] }}"
style="position:relative;flex:0 0 auto;min-width:120px;max-width:180px;height:32px;border-radius:8px;border:1px solid var(--main-border-color,#e4e7ec);background:var(--secondary-background,#f2f4f7);overflow:hidden;display:flex;align-items:center;padding:0 9px;">
<span style="position:absolute;left:0;top:0;bottom:0;width:{{ (int) $ms['percentDone'] }}%;background:{{ $ms['color'] }};opacity:.18;border-right:2px solid {{ $ms['color'] }};"></span>
<span style="position:relative;z-index:1;flex:1;min-width:0;font-size:11.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ $ms['headline'] }}</span>
<span style="position:relative;z-index:1;flex:none;font-size:10px;font-weight:600;opacity:.65;margin-left:5px;">{{ (int) $ms['percentDone'] }}%</span>
</a>
@endforeach
</div>
@endif

View File

@@ -0,0 +1,62 @@
{{--
Goal editor linked-milestones MANAGEMENT list (summary + one row per
milestone). Progress bars deliberately live on the Progress tab, not here
(review 2026-08-03: chips carrying percent duplicated the Progress view)
this list is for linking/unlinking, RA line-item style.
Lives in its own partial so the row-remove hx-post can re-render the WHOLE
section (hx-target="#goalMsSection" outerHTML): the summary count stays
correct with the removal.
Expects: $id, $goalMilestones, $milestoneSummary, $milestones (project
milestone options drives the "link existing" button visibility).
--}}
<div id="goalMsSection">
<div class="gv-ms-head">
@if (($milestoneSummary['total'] ?? 0) > 0)
<span class="gv-ms-summary"><b>{{ $milestoneSummary['total'] }}</b> {{ $milestoneSummary['total'] == 1 ? __("goalcanvas.summary_milestone_one") : __("goalcanvas.summary_milestones") }}
@if ($milestoneSummary['inProgress'] > 0)&middot; {{ $milestoneSummary['inProgress'] }} {{ __("goalcanvas.summary_in_progress") }} @endif
@if ($milestoneSummary['notStarted'] > 0)&middot; {{ $milestoneSummary['notStarted'] }} {{ __("goalcanvas.summary_not_started") }} @endif
@if ($milestoneSummary['done'] > 0)&middot; {{ $milestoneSummary['done'] }} {{ __("goalcanvas.summary_done") }} @endif
</span>
@endif
<span class="gv-ms-actions">
@if ($login::userIsAtLeast($roles::$editor))
<button type="button" class="gv-ms-act helperTooltip" onclick="leantime.goalCanvasController.toggleMilestoneSelectors('new');" data-tippy-content="{{ __('goalcanvas.ms_new') }}" title="{{ __('goalcanvas.ms_new') }}" aria-label="{{ __('goalcanvas.ms_new') }}"><i class="fa fa-plus" aria-hidden="true"></i></button>
@if (count($milestones) > 0)
<button type="button" class="gv-ms-act helperTooltip" onclick="leantime.goalCanvasController.toggleMilestoneSelectors('existing');" data-tippy-content="{{ __('goalcanvas.ms_link') }}" title="{{ __('goalcanvas.ms_link') }}" aria-label="{{ __('goalcanvas.ms_link') }}"><i class="fa fa-link" aria-hidden="true"></i></button>
@endif
@endif
<i class="fa fa-question-circle-o helperTooltip" aria-hidden="true" data-tippy-content="{{ __("tooltip.link_milestones_tooltip") }}"></i>
</span>
</div>
@if (count($goalMilestones) > 0)
<div class="gv-ms-list">
@foreach ($goalMilestones as $ms)
@php
$msDue = trim((string) ($ms['editTo'] ?? ''));
$msDue = ($msDue === '' || str_starts_with($msDue, '0000-00-00')) ? null : $msDue;
@endphp
{{-- Management row only status signals (dots/bars) live on
the Progress tab (review 2026-08-04). --}}
<div class="gv-ms-item">
<a class="gv-ms-name" href="#/tickets/editMilestone/{{ (int) $ms['id'] }}" title="{{ __('links.edit_milestone') }}: {{ $ms['headline'] }}">{{ $ms['headline'] }}</a>
@if ($msDue !== null)
<span class="gv-ms-due">{{ __('label.due') }} {{ format($msDue)->date() }}</span>
@endif
@if ($login::userIsAtLeast($roles::$editor))
<button type="button"
hx-post="{{ BASE_URL }}/goalcanvas/editCanvasItem/{{ $id }}"
hx-vals='{"removeMilestone": {{ (int) $ms['id'] }}}'
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
hx-target="#goalMsSection"
hx-swap="outerHTML"
class="delete gv-ms-remove"
aria-label="{{ __("links.remove") }}: {{ $ms['headline'] }}" title="{{ __("links.remove") }}"><i class="fa fa-close" aria-hidden="true"></i></button>
@endif
</div>
@endforeach
</div>
@endif
</div>

View File

@@ -0,0 +1,103 @@
{{--
Goal metric readout RA planbar language: filled progress + striped
remaining, quarter score marks, a position marker at the current value,
start/goal anchored under the bar's ends, and an explicit "N to go".
The number IS the input (RA inline pattern): blur-when-changed hx-posts
to goalProgress/updateValue, which re-renders this partial into #gvReadout.
Expects: $canvasItem (id, metricType, startValue, currentValue, endValue,
setting, description). Shared by the dialog and the HxController.
--}}
@php
$roType = $canvasItem['metricType'] ?: 'number';
$roStart = (float) ($canvasItem['startValue'] ?? 0);
$roCur = (float) ($canvasItem['currentValue'] ?? 0);
$roGoal = (float) ($canvasItem['endValue'] ?? 0);
$roHasRange = ($roGoal - $roStart) != 0;
$roPct = $roHasRange ? max(0, min(100, (($roCur - $roStart) / ($roGoal - $roStart)) * 100)) : 0;
$roFmt = function ($v) use ($roType) {
$v = (float) $v;
if ($roType === 'percent') {
return rtrim(rtrim(number_format($v, 2), '0'), '.').'%';
}
if ($roType === 'currency') {
return '$'.number_format($v, $v == floor($v) ? 0 : 2);
}
return rtrim(rtrim(number_format($v, 2), '0'), '.');
};
$roReached = $roHasRange && $roPct >= 100;
// Works for decreasing goals too (goal < start): distance left to travel.
$roRemaining = abs($roGoal - $roCur);
// linkAndReport current values are computed from children; viewers can't
// write — both render the number as static text instead of an input.
$roEditable = $login::userIsAtLeast($roles::$editor) && ($canvasItem['setting'] ?? '') !== 'linkAndReport';
// Inline blur-save needs a PERSISTED goal: on the create form (id empty)
// an hx-post with itemId=0 would swap the readout back to zeros and wipe
// the value the user just typed — new goals get a plain input that
// submits with the create form instead.
$roLive = (int) ($canvasItem['id'] ?? 0) > 0;
@endphp
<div class="gv-metric-bar" id="gvReadout">
{{-- WHAT is being measured without it the tab is a bare number. --}}
@if (trim((string) ($canvasItem['description'] ?? '')) !== '')
<div class="gv-mb-metric">{{ $canvasItem['description'] }}</div>
@endif
<div class="gv-mb-top">
@if ($roEditable && $roLive)
<input class="gv-mb-input" type="number" step="0.01" name="currentValue"
value="{{ $roCur == floor($roCur) ? (int) $roCur : $roCur }}"
aria-label="{{ __('goalcanvas.update_current') }}"
data-tippy-content="{{ __('goalcanvas.update_current') }}"
hx-post="{{ BASE_URL }}/hx/goalcanvas/goalProgress/updateValue"
hx-vals='{"itemId": {{ (int) $canvasItem['id'] }}}'
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
hx-trigger="blur changed"
hx-target="#gvReadout"
hx-swap="outerHTML">
@elseif ($roEditable)
{{-- New goal: same editable number, no HTMX the value rides the
create form's own submit. --}}
<input class="gv-mb-input" type="number" step="0.01" name="currentValue"
value="{{ $roCur == floor($roCur) ? (int) $roCur : $roCur }}"
aria-label="{{ __('goalcanvas.update_current') }}"
data-tippy-content="{{ __('goalcanvas.update_current') }}">
@else
<span class="gv-mb-now" @if (($canvasItem['setting'] ?? '') === 'linkAndReport') data-tippy-content="{{ __('text.current_value_calculated_from_children') }}" @endif>{{ $roFmt($roCur) }}</span>
{{-- Static readout (linkAndReport / viewer) submits no currentValue,
and EditCanvasItem defaults an absent one to '' — which would
blank the stored metric when the user saves other fields. Round
the stored value back through the form to preserve it. --}}
<input type="hidden" name="currentValue" value="{{ $roCur == floor($roCur) ? (int) $roCur : $roCur }}">
@endif
{{-- How far away it ISN'T the missing half of every progress bar. --}}
@if ($roHasRange)
<span class="gv-mb-togo">
@if ($roReached)
{{ __('goalcanvas.goal_reached') }}
@else
{{ sprintf(__('goalcanvas.to_go'), $roFmt($roRemaining)) }}
@endif
</span>
@endif
</div>
{{-- Scored track (RA planbar language) + resolved % the % column
width/gap matches the milestone rows so both right edges align. --}}
<div class="gv-mb-barrow">
<div class="gv-mb-scalewrap">
<div class="gv-track gv-track--scored" aria-hidden="true">
<div class="gv-fill" style="width:{{ $roPct }}%"></div>
<span class="gv-tick" style="left:25%"></span>
<span class="gv-tick" style="left:50%"></span>
<span class="gv-tick" style="left:75%"></span>
@if ($roPct > 0 && $roPct < 100)
<span class="gv-marker" style="left:{{ $roPct }}%"></span>
@endif
</div>
{{-- The totals live where they ARE on the scale: start under the
left end, the goal under the right end. --}}
<div class="gv-scale"><span>{{ $roFmt($roStart) }}</span><span>{{ $roFmt($roGoal) }}</span></div>
</div>
<span class="gv-mb-pct">{{ (int) round($roPct) }}%</span>
</div>
</div>