OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
@@ -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
|
||||
@@ -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)· {{ $milestoneSummary['inProgress'] }} {{ __("goalcanvas.summary_in_progress") }} @endif
|
||||
@if ($milestoneSummary['notStarted'] > 0)· {{ $milestoneSummary['notStarted'] }} {{ __("goalcanvas.summary_not_started") }} @endif
|
||||
@if ($milestoneSummary['done'] > 0)· {{ $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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user