add local install script
This commit is contained in:
5
install.sh
Executable file
5
install.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
npm run build
|
||||
ssh air rm -rf /pkg/gnu/unitprint/web-app/dist
|
||||
rsync -avz dist/ air:/pkg/gnu/unitprint/web-app/dist/
|
||||
ssh air nginx -s reload
|
||||
@ -2,15 +2,10 @@
|
||||
<script>
|
||||
let { store, onBack = () => {} } = $props();
|
||||
|
||||
const currentStatus = $derived.by(() => {
|
||||
if (!store.job) return 'idle';
|
||||
|
||||
const f = store.job.files?.find((x) => x.status === 'printing');
|
||||
if (f) return 'printing';
|
||||
|
||||
const q = store.job.files?.find((x) => x.status === 'queued');
|
||||
return q ? 'queued' : store.job.phase;
|
||||
});
|
||||
function norm(v) {
|
||||
if (!v) return '';
|
||||
return String(v).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
}
|
||||
|
||||
const OFFICE_EXT = ['DOC', 'DOCX', 'ODT', 'XLS', 'XLSX', 'ODS', 'PPT', 'PPTX', 'ODP'];
|
||||
|
||||
@ -31,116 +26,76 @@
|
||||
return OFFICE_EXT.includes(e) ? e : null;
|
||||
}
|
||||
|
||||
const jobPhase = $derived(norm(store.job?.phase));
|
||||
const jobFiles = $derived(store.job?.files ?? []);
|
||||
|
||||
const currentStatus = $derived.by(() => {
|
||||
if (!store.job) return 'idle';
|
||||
|
||||
if (jobFiles.some((f) => norm(f.status) === 'printing')) {
|
||||
return 'printing';
|
||||
}
|
||||
|
||||
if (jobFiles.some((f) => norm(f.status) === 'queued')) {
|
||||
return 'queued';
|
||||
}
|
||||
|
||||
return jobPhase;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (!store.jobActive) return;
|
||||
const int = setInterval(() => store.refreshJob(), 1000);
|
||||
|
||||
const int = setInterval(() => {
|
||||
store.refreshJob();
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(int);
|
||||
});
|
||||
|
||||
const canCancel = $derived(
|
||||
store.job?.files?.some((f) => f.status === 'queued' || f.status === 'printing') ?? false
|
||||
);
|
||||
|
||||
const jobPhase = $derived(store.job?.phase);
|
||||
const jobFiles = $derived(store.job?.files ?? []);
|
||||
|
||||
/**
|
||||
* Двусторонняя печать определяется по настройкам файлов,
|
||||
* которые всё ещё лежат в store.files после отправки задания.
|
||||
*/
|
||||
const isDuplex = $derived(
|
||||
store.files.some((f) => f.sides === 'Двусторонняя')
|
||||
jobFiles.some((f) => ['queued', 'printing'].includes(norm(f.status)))
|
||||
);
|
||||
|
||||
/**
|
||||
* Текущие запросы сервера на пользовательские действия.
|
||||
* Явные состояния, при которых нужны пользовательские кнопки.
|
||||
* Здесь больше не завязываемся на hasPrintedFiles / firstSideFinished.
|
||||
*/
|
||||
const hasClearPhase = $derived(jobPhase === 'awaiting_clear_output');
|
||||
const hasFlip = $derived(
|
||||
jobPhase === 'awaiting_flip' ||
|
||||
jobFiles.some((f) => f.status === 'awaiting_flip')
|
||||
);
|
||||
const hasPickup = $derived(
|
||||
jobPhase === 'awaiting_pickup' ||
|
||||
jobFiles.some((f) => f.status === 'awaiting_pickup')
|
||||
const showStart = $derived(
|
||||
store.jobActive && jobPhase === 'waiting_start'
|
||||
);
|
||||
|
||||
/**
|
||||
* Проверяем, есть ли уже напечатанные или отправленные файлы.
|
||||
* Если нет — значит это первая печать и лоток выдачи принтера пуст.
|
||||
*/
|
||||
const hasPrintedFiles = $derived(
|
||||
store.job?.files?.some(f => ['done', 'sent', 'awaiting_flip', 'awaiting_pickup'].includes(f.status)) ?? false
|
||||
);
|
||||
|
||||
/**
|
||||
* Нам важно запомнить, что первый проход уже был,
|
||||
* иначе при печати второй стороны инструкция очистки может появиться снова.
|
||||
*/
|
||||
let hadFlip = $state(false);
|
||||
let hadPickup = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!store.job) return;
|
||||
|
||||
if (
|
||||
jobPhase === 'awaiting_flip' ||
|
||||
jobFiles.some((f) => f.status === 'awaiting_flip')
|
||||
) {
|
||||
hadFlip = true;
|
||||
}
|
||||
|
||||
if (
|
||||
jobPhase === 'awaiting_pickup' ||
|
||||
jobFiles.some((f) => f.status === 'awaiting_pickup')
|
||||
) {
|
||||
hadPickup = true;
|
||||
}
|
||||
});
|
||||
|
||||
const firstSideFinished = $derived(
|
||||
hadFlip ||
|
||||
hadPickup ||
|
||||
jobPhase === 'finished' ||
|
||||
jobPhase === 'cancelled' ||
|
||||
jobFiles.some((f) => f.status === 'done')
|
||||
);
|
||||
|
||||
/**
|
||||
* Инструкция очистки лотка:
|
||||
* - если сервер явно просит awaiting_clear_output И есть уже напечатанные файлы;
|
||||
* - для двусторонней печати — ДО первого прохода,
|
||||
* пока ещё не было flip/pickup/done И есть напечатанные файлы
|
||||
* (т.е. файл не первый в очереди и лоток уже занят).
|
||||
*/
|
||||
const showClearOutput = $derived(
|
||||
store.jobActive &&
|
||||
(
|
||||
jobPhase === 'waiting_start' ||
|
||||
showStart ||
|
||||
jobPhase === 'awaiting_clear_output' ||
|
||||
jobFiles.some((f) => f.status === 'awaiting_clear_output')
|
||||
jobFiles.some((f) => norm(f.status) === 'awaiting_clear_output')
|
||||
)
|
||||
);
|
||||
|
||||
const showFlip = $derived(
|
||||
store.jobActive &&
|
||||
hasFlip &&
|
||||
(
|
||||
jobPhase === 'awaiting_flip' ||
|
||||
jobFiles.some((f) => norm(f.status) === 'awaiting_flip')
|
||||
) &&
|
||||
jobPhase !== 'finished' &&
|
||||
jobPhase !== 'cancelled'
|
||||
);
|
||||
|
||||
const showPickup = $derived(
|
||||
store.jobActive &&
|
||||
hasPickup &&
|
||||
(
|
||||
jobPhase === 'awaiting_pickup' ||
|
||||
jobFiles.some((f) => norm(f.status) === 'awaiting_pickup')
|
||||
) &&
|
||||
!showFlip &&
|
||||
jobPhase !== 'finished' &&
|
||||
jobPhase !== 'cancelled'
|
||||
);
|
||||
|
||||
const clearActionAvailable = $derived(
|
||||
showClearOutput &&
|
||||
(jobPhase === 'awaiting_clear_output' || jobPhase === 'waiting_start')
|
||||
);
|
||||
const clearActionAvailable = $derived(showClearOutput);
|
||||
|
||||
const flipActionAvailable = $derived(
|
||||
showFlip && jobPhase === 'awaiting_flip'
|
||||
@ -151,28 +106,64 @@
|
||||
);
|
||||
|
||||
const clearButtonLabel = $derived(
|
||||
jobPhase === 'waiting_start'
|
||||
? 'Начать печать'
|
||||
: 'Убрал листы — продолжать'
|
||||
showStart ? 'Начать печать' : 'Убрал листы — продолжать'
|
||||
);
|
||||
|
||||
/**
|
||||
* Автостарт.
|
||||
*
|
||||
* Если сервер вернул задание в waiting_start, а фронт по какой-то причине
|
||||
* не стартовал его сам, стартуем здесь.
|
||||
*
|
||||
* Если тебе нужен ТОЛЬКО ручной старт, удали этот $effect целиком.
|
||||
*/
|
||||
let autoStartedJobId = '';
|
||||
|
||||
$effect(() => {
|
||||
const jobId = store.job?.job_id;
|
||||
|
||||
if (!store.jobActive) return;
|
||||
if (!jobId) return;
|
||||
if (jobPhase !== 'waiting_start') return;
|
||||
if (autoStartedJobId === jobId) return;
|
||||
|
||||
autoStartedJobId = jobId;
|
||||
|
||||
setTimeout(() => {
|
||||
if (
|
||||
store.job?.job_id === jobId &&
|
||||
store.jobActive &&
|
||||
norm(store.job?.phase) === 'waiting_start'
|
||||
) {
|
||||
store.advanceJob();
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
|
||||
/**
|
||||
* В какой карточке файла показывать инструкцию.
|
||||
* Инструкция всегда живёт ВНУТРИ карточки соответствующего файла,
|
||||
* а не отдельным блоком над списком.
|
||||
*/
|
||||
const flipTarget = $derived(
|
||||
showFlip ? (jobFiles.find((f) => f.status === 'awaiting_flip') ?? null) : null
|
||||
showFlip
|
||||
? (jobFiles.find((f) => norm(f.status) === 'awaiting_flip') ?? null)
|
||||
: null
|
||||
);
|
||||
|
||||
const pickupTarget = $derived(
|
||||
showPickup ? (jobFiles.find((f) => f.status === 'awaiting_pickup') ?? null) : null
|
||||
showPickup
|
||||
? (jobFiles.find((f) => norm(f.status) === 'awaiting_pickup') ?? null)
|
||||
: null
|
||||
);
|
||||
|
||||
const clearTarget = $derived.by(() => {
|
||||
if (!showClearOutput) return null;
|
||||
|
||||
return (
|
||||
jobFiles.find((f) => f.status === 'awaiting_clear_output') ??
|
||||
(jobPhase !== 'waiting_start' ? jobFiles.find((f) => f.status === 'printing') : undefined) ??
|
||||
jobFiles.find((f) => f.status === 'queued') ??
|
||||
jobFiles.find((f) => norm(f.status) === 'awaiting_clear_output') ??
|
||||
(jobPhase !== 'waiting_start'
|
||||
? jobFiles.find((f) => norm(f.status) === 'printing')
|
||||
: undefined) ??
|
||||
jobFiles.find((f) => norm(f.status) === 'queued') ??
|
||||
null
|
||||
);
|
||||
});
|
||||
@ -200,12 +191,14 @@
|
||||
|
||||
const step = (now) => {
|
||||
hold = Math.min(1, (now - t0) / HOLD_MS);
|
||||
|
||||
if (hold >= 1) {
|
||||
raf = 0;
|
||||
hold = 0;
|
||||
store.cancelJob();
|
||||
return;
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
@ -232,34 +225,35 @@
|
||||
|
||||
<main class="list">
|
||||
{#if store.job}
|
||||
{#each store.job.files as f, i (i + '-' + f.name)}
|
||||
{#each jobFiles as f, i (i + '-' + f.name)}
|
||||
{@const b = badge(f.name)}
|
||||
{@const st = norm(f.status)}
|
||||
{@const hint = hintFor(f)}
|
||||
{@const isActive = f.status === 'awaiting_flip'
|
||||
|| f.status === 'awaiting_clear_output'
|
||||
|| f.status === 'awaiting_pickup'
|
||||
|| f.status === 'printing'
|
||||
|| hint !== null}
|
||||
{@const isActive =
|
||||
['awaiting_flip', 'awaiting_clear_output', 'awaiting_pickup', 'printing'].includes(st) ||
|
||||
hint !== null}
|
||||
|
||||
<section class="file-card" class:current={isActive}>
|
||||
<div class="file-row">
|
||||
{#if b}<span class="type-badge">{b}</span>{/if}
|
||||
|
||||
<span class="file-name" title={f.name}>{f.name}</span>
|
||||
<span class="file-status st-{f.status}">
|
||||
{STATUS_LABEL[f.status] ?? f.status}
|
||||
|
||||
<span class="file-status st-{st}">
|
||||
{STATUS_LABEL[st] ?? f.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if hint === 'clear'}
|
||||
<ol class="pickup-hint clear-hint">
|
||||
{#if jobPhase !== 'waiting_start'}
|
||||
{#if !showStart}
|
||||
<li>Уберите распечатанные листы из выходного лотка</li>
|
||||
{/if}
|
||||
|
||||
{#if clearActionAvailable}
|
||||
<li>
|
||||
Нажмите кнопку ниже
|
||||
{jobPhase === 'waiting_start' ? 'для начала печати' : 'для продолжения'}
|
||||
{showStart ? 'для начала печати' : 'для продолжения'}
|
||||
</li>
|
||||
{:else}
|
||||
<li>Дождитесь завершения первого прохода</li>
|
||||
@ -268,6 +262,7 @@
|
||||
{:else if hint === 'flip'}
|
||||
<ol class="pickup-hint flip-hint">
|
||||
<li>Не переворачивая, положите листы в слот ручной подачи</li>
|
||||
|
||||
{#if flipActionAvailable}
|
||||
<li>Нажмите кнопку ниже для печати обратной стороны</li>
|
||||
{:else}
|
||||
@ -277,6 +272,7 @@
|
||||
{:else if hint === 'pickup'}
|
||||
<ol class="pickup-hint">
|
||||
<li>Дождитесь завершения печати</li>
|
||||
|
||||
{#if pickupActionAvailable}
|
||||
<li>Нажмите кнопку ниже для подтверждения</li>
|
||||
{:else}
|
||||
@ -308,16 +304,20 @@
|
||||
{/if}
|
||||
|
||||
{#if store.jobActive}
|
||||
{#if pickupActionAvailable}
|
||||
<button class="next-btn" onclick={store.advanceJob}>
|
||||
{#if showStart}
|
||||
<button class="next-btn" onclick={() => store.advanceJob()}>
|
||||
{clearButtonLabel}
|
||||
</button>
|
||||
{:else if pickupActionAvailable}
|
||||
<button class="next-btn" onclick={() => store.advanceJob()}>
|
||||
Печать закончена — далее
|
||||
</button>
|
||||
{:else if flipActionAvailable}
|
||||
<button class="next-btn next-btn-flip" onclick={store.advanceJob}>
|
||||
<button class="next-btn next-btn-flip" onclick={() => store.advanceJob()}>
|
||||
Переложил — печатать обратную сторону
|
||||
</button>
|
||||
{:else if clearActionAvailable}
|
||||
<button class="next-btn next-btn-clear" onclick={store.advanceJob}>
|
||||
<button class="next-btn next-btn-clear" onclick={() => store.advanceJob()}>
|
||||
{clearButtonLabel}
|
||||
</button>
|
||||
{:else}
|
||||
@ -479,7 +479,6 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Подсказки живут ВНУТРИ карточки файла, текст — по левому краю */
|
||||
.pickup-hint {
|
||||
margin: 0;
|
||||
padding-left: 22px;
|
||||
|
||||
Reference in New Issue
Block a user