feat(marketplace): склад участка переосмыслен как доска полок
Плоский список «позиция + поле Полка + кнопки» был непонятен: не было ощущения, ГДЕ что лежит и зачем раскладка. Переделано в доску: - колонка «Поступило» — принятое, ещё не разложенное; - колонки-полки — что физически лежит на каждой полке; - раскладка = перетащить карточку на полку (нативный DnD) или меню «⋮»; - «+ Полка» создаёт новую полку; снять с полки = вернуть в «Поступило»; - этикетка и раскладка по количеству — в меню карточки; - штрих-код показан в карточке; отдельный лист — только для печати. Цель страницы теперь читается: разложить так, чтобы при выдаче заказчику сразу найти, на какой полке заказ. Backend без изменений (assignInventoryShelf/splitInventory/generateInventoryLabel), без новых зависимостей (DnD на HTML5). Канон: токены --p-*, убран хардкод #b3261e. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+329
-114
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Loading } from 'quasar'
|
||||
import { Zeus } from '@coopenomics/sdk'
|
||||
@@ -18,11 +18,16 @@ import {
|
||||
} from '../api'
|
||||
|
||||
/**
|
||||
* Стол ПВЗ, «Раскладка и маркировка». Имущество попадает на склад на приёмке
|
||||
* (статус RECEIVED) — здесь оператор организует его физически: назначает полку
|
||||
* (свободная строка), при необходимости раскладывает одну принятую позицию по
|
||||
* нескольким полкам (split), и опционально наклеивает штрих-код для быстрого
|
||||
* поиска. Штрих-код не обязателен — склад работает и без него (один холодильник).
|
||||
* Стол ПВЗ, «Склад участка». Имущество, принятое кооперативом (RECEIVED), лежит
|
||||
* на складе КУ — здесь оператор организует его физически как доску полок:
|
||||
*
|
||||
* - колонка «Поступило» — принятое, ещё не разложенное;
|
||||
* - колонки-полки — что лежит на каждой полке;
|
||||
* - раскладка = перетащить карточку на полку (DnD) или через меню «⋮»;
|
||||
* - «Разложить» дробит позицию по количеству на несколько полок;
|
||||
* - «Этикетка» наклеивает штрих-код для поиска сканером (по желанию).
|
||||
*
|
||||
* Цель — при выдаче заказчику за секунды найти, на какой полке лежит заказ.
|
||||
*/
|
||||
|
||||
const route = useRoute()
|
||||
@@ -33,26 +38,59 @@ const braname = computed(() => store.activeBraname ?? '')
|
||||
const items = ref<MarketplaceInventoryItemView[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// Сначала непромаркированные/без полки (нужно разложить), затем остальное.
|
||||
const RECEIVED = Zeus.MarketplaceInventoryStatus.RECEIVED
|
||||
const LABELED = Zeus.MarketplaceInventoryStatus.LABELED
|
||||
|
||||
const onWarehouse = computed(() =>
|
||||
[...items.value]
|
||||
.filter((i) => i.status === RECEIVED || i.status === LABELED)
|
||||
.sort((a, b) => {
|
||||
// Сперва то, чем ещё нужно заняться: без полки, затем без штрих-кода.
|
||||
const aw = (a.shelf ? 0 : 2) + (a.barcode_value ? 0 : 1)
|
||||
const bw = (b.shelf ? 0 : 2) + (b.barcode_value ? 0 : 1)
|
||||
return bw - aw
|
||||
}),
|
||||
// Имущество на складе (принятое/промаркированное) — то, что раскладываем.
|
||||
const boardItems = computed(() =>
|
||||
items.value.filter((i) => i.status === RECEIVED || i.status === LABELED),
|
||||
)
|
||||
|
||||
const labeledItems = computed(() => items.value.filter((i) => !!i.barcode_value))
|
||||
|
||||
// Непромаркированный RECEIVED-пул одного заказа — куски, которые можно собрать
|
||||
// с полок и разложить заново (merge/split/перенос). Промаркированные единицы
|
||||
// (со штрих-кодом) фиксированы и в пул не входят.
|
||||
// Полки-плейсхолдеры, созданные оператором, но пока пустые (нет позиций).
|
||||
// Живут до перезагрузки — после reload остаются лишь полки с позициями.
|
||||
const extraShelves = ref<string[]>([])
|
||||
|
||||
const shelfNames = computed(() => {
|
||||
const set = new Set<string>()
|
||||
for (const i of boardItems.value) if (i.shelf) set.add(i.shelf)
|
||||
for (const s of extraShelves.value) set.add(s)
|
||||
return [...set].sort((a, b) => a.localeCompare(b, 'ru'))
|
||||
})
|
||||
|
||||
interface BoardColumn {
|
||||
key: string
|
||||
title: string
|
||||
icon: string
|
||||
shelf: string | null
|
||||
items: MarketplaceInventoryItemView[]
|
||||
}
|
||||
|
||||
const INBOX_KEY = '__inbox__'
|
||||
|
||||
const columns = computed<BoardColumn[]>(() => {
|
||||
const cols: BoardColumn[] = [
|
||||
{
|
||||
key: INBOX_KEY,
|
||||
title: 'Поступило',
|
||||
icon: 'inbox',
|
||||
shelf: null,
|
||||
items: boardItems.value.filter((i) => !i.shelf),
|
||||
},
|
||||
]
|
||||
for (const name of shelfNames.value) {
|
||||
cols.push({
|
||||
key: name,
|
||||
title: name,
|
||||
icon: 'shelves',
|
||||
shelf: name,
|
||||
items: boardItems.value.filter((i) => i.shelf === name),
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
// ── Перераскладка (split) по количеству: непромаркированный пул заказа ──
|
||||
function orderPool(item: MarketplaceInventoryItemView): MarketplaceInventoryItemView[] {
|
||||
return items.value.filter(
|
||||
(i) => i.order_id === item.order_id && i.status === RECEIVED && !i.barcode_value,
|
||||
@@ -61,15 +99,10 @@ function orderPool(item: MarketplaceInventoryItemView): MarketplaceInventoryItem
|
||||
function orderPoolTotal(item: MarketplaceInventoryItemView): number {
|
||||
return orderPool(item).reduce((a, p) => a + p.quantity_per_label, 0)
|
||||
}
|
||||
// Перераскладка доступна для непромаркированной позиции, если в заказе ≥2 ед.
|
||||
// (есть что распределить или собрать обратно).
|
||||
function canRedistribute(item: MarketplaceInventoryItemView): boolean {
|
||||
return !item.barcode_value && orderPoolTotal(item) >= 2
|
||||
}
|
||||
|
||||
// Черновик полки на строку — чтобы сохранять только по явному действию.
|
||||
const shelfDraft = reactive<Record<string, string>>({})
|
||||
|
||||
async function load(): Promise<void> {
|
||||
if (!braname.value.trim()) {
|
||||
items.value = []
|
||||
@@ -78,7 +111,6 @@ async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = await fetchInventoryByBraname(braname.value.trim())
|
||||
for (const i of items.value) shelfDraft[i.id] = i.shelf ?? ''
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось загрузить склад участка')
|
||||
} finally {
|
||||
@@ -86,18 +118,54 @@ async function load(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveShelf(item: MarketplaceInventoryItemView): Promise<void> {
|
||||
const next = (shelfDraft[item.id] ?? '').trim()
|
||||
if (next === (item.shelf ?? '')) return
|
||||
// ── Перекладка позиции на полку (и снятие — shelf=null) ──
|
||||
async function moveToShelf(item: MarketplaceInventoryItemView, shelf: string | null): Promise<void> {
|
||||
const next = shelf?.trim() ? shelf.trim() : null
|
||||
if ((item.shelf ?? null) === next) return
|
||||
try {
|
||||
await assignInventoryShelf({ inventory_id: item.id, shelf: next || null })
|
||||
SuccessAlert(next ? `Полка «${next}» сохранена` : 'Полка очищена')
|
||||
await assignInventoryShelf({ inventory_id: item.id, shelf: next })
|
||||
SuccessAlert(next ? `Переложено на полку «${next}»` : 'Снято с полки')
|
||||
await load()
|
||||
} catch (e) {
|
||||
FailAlert(e, 'Не удалось сохранить полку')
|
||||
FailAlert(e, 'Не удалось переложить позицию')
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drag & drop карточек между колонками ──
|
||||
const dragId = ref<string | null>(null)
|
||||
const dragOverKey = ref<string | null>(null)
|
||||
|
||||
function onDragStart(item: MarketplaceInventoryItemView): void {
|
||||
dragId.value = item.id
|
||||
}
|
||||
function onDragEnd(): void {
|
||||
dragId.value = null
|
||||
dragOverKey.value = null
|
||||
}
|
||||
function onDrop(col: BoardColumn): void {
|
||||
const item = items.value.find((i) => i.id === dragId.value)
|
||||
dragOverKey.value = null
|
||||
dragId.value = null
|
||||
if (item) void moveToShelf(item, col.shelf)
|
||||
}
|
||||
|
||||
// ── Создание новой (пустой) полки ──
|
||||
const newShelfOpen = ref(false)
|
||||
const newShelfName = ref('')
|
||||
function openNewShelf(): void {
|
||||
newShelfName.value = ''
|
||||
newShelfOpen.value = true
|
||||
}
|
||||
function createShelf(): void {
|
||||
const name = newShelfName.value.trim()
|
||||
if (!name) return
|
||||
if (!extraShelves.value.includes(name) && !shelfNames.value.includes(name)) {
|
||||
extraShelves.value.push(name)
|
||||
}
|
||||
newShelfOpen.value = false
|
||||
}
|
||||
|
||||
// ── Маркировка штрих-кодом ──
|
||||
async function makeLabel(item: MarketplaceInventoryItemView): Promise<void> {
|
||||
Loading.show({ message: 'Генерирую этикетку…' })
|
||||
try {
|
||||
@@ -111,7 +179,7 @@ async function makeLabel(item: MarketplaceInventoryItemView): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Раскладка по полкам (split) ──
|
||||
// ── Раскладка по количеству на несколько полок (split/merge/move) ──
|
||||
const splitDialogOpen = ref(false)
|
||||
const splitTarget = ref<MarketplaceInventoryItemView | null>(null)
|
||||
const splitRows = ref<{ quantity: number | null; shelf: string }[]>([])
|
||||
@@ -119,7 +187,6 @@ const splitRows = ref<{ quantity: number | null; shelf: string }[]>([])
|
||||
const splitTotal = computed(() =>
|
||||
splitRows.value.reduce((a, r) => a + (Number(r.quantity) || 0), 0),
|
||||
)
|
||||
// Цель раскладки — суммарное количество непромаркированного пула заказа.
|
||||
const splitPoolTotal = computed(() =>
|
||||
splitTarget.value ? orderPoolTotal(splitTarget.value) : 0,
|
||||
)
|
||||
@@ -133,9 +200,6 @@ const splitValid = computed(
|
||||
|
||||
function openSplit(item: MarketplaceInventoryItemView): void {
|
||||
splitTarget.value = item
|
||||
// Предзаполняем строками из текущих кусков пула (кол-во + полка) — оператор
|
||||
// видит, как заказ сейчас лежит на полках, и правит как нужно. Один кусок —
|
||||
// добавляем пустую строку, чтобы предложить раскладку.
|
||||
const pool = orderPool(item)
|
||||
splitRows.value = pool.map((p) => ({
|
||||
quantity: p.quantity_per_label as number | null,
|
||||
@@ -191,7 +255,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template lang="pug">
|
||||
q-page.place(role='region', aria-label='Раскладка и маркировка')
|
||||
q-page.place(role='region', aria-label='Склад участка')
|
||||
OperatorBranchBar
|
||||
|
||||
EmptyState(
|
||||
@@ -211,60 +275,114 @@ q-page.place(role='region', aria-label='Раскладка и маркировк
|
||||
RefreshButton(:loading='loading', @refresh='load')
|
||||
|
||||
PageHint.no-print(storage-key='mp:operator-labeling:banner-dismissed')
|
||||
| Принятое имущество лежит на складе сразу. Назначьте каждой позиции полку
|
||||
| (свободная строка), при необходимости разложите одну позицию по нескольким
|
||||
| полкам и — по желанию — наклейте штрих-код для быстрого поиска. Штрих-код
|
||||
| не обязателен: если у вас один холодильник, можно обойтись без него.
|
||||
| Разложите принятое имущество по полкам — чтобы при выдаче заказчику сразу
|
||||
| найти, где что лежит. Перетащите карточку на полку (или меню «⋮ → на полку»).
|
||||
| Штрих-код — по желанию, для поиска сканером.
|
||||
|
||||
.place__loading.no-print(v-if='loading && !items.length')
|
||||
q-spinner(size='28px', color='primary')
|
||||
|
||||
EmptyState.no-print(
|
||||
v-if='!onWarehouse.length',
|
||||
v-else-if='!boardItems.length',
|
||||
title='На складе пусто',
|
||||
body='Здесь появятся принятые позиции — после приёмки партии на столе «Приёмка партии».'
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name='inventory_2', size='48px')
|
||||
|
||||
.place__list.no-print(v-else)
|
||||
.place__item(v-for='item in onWarehouse', :key='item.id')
|
||||
.place__item-info
|
||||
.place__item-name {{ item.product_name_snapshot || 'Товар по предложению' }}
|
||||
.place__item-meta
|
||||
| Принято {{ item.quantity_per_label }} ед. · {{ item.orderer_account_snapshot }}
|
||||
BaseBadge(v-if='item.barcode_value', variant='pos') Промаркировано
|
||||
BaseBadge(v-else, variant='neutral') Без штрих-кода
|
||||
//- Штрих-код в той же строке, что и имущество с полкой — не оторван.
|
||||
.place__item-barcode(v-if='item.barcode_value')
|
||||
BarcodeDisplay(:code='item.barcode_value', size='sm')
|
||||
.place__item-controls
|
||||
BaseInput.place__shelf(
|
||||
v-model='shelfDraft[item.id]',
|
||||
label='Полка',
|
||||
placeholder='A-12',
|
||||
dense,
|
||||
@blur='saveShelf(item)',
|
||||
@keydown.enter='saveShelf(item)'
|
||||
)
|
||||
BaseButton(
|
||||
v-if='!item.barcode_value',
|
||||
variant='ghost',
|
||||
size='sm',
|
||||
:disabled='!canRedistribute(item)',
|
||||
@click='openSplit(item)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='call_split', size='16px')
|
||||
| Разложить
|
||||
BaseButton(
|
||||
v-if='!item.barcode_value',
|
||||
variant='primary',
|
||||
size='sm',
|
||||
@click='makeLabel(item)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='qr_code_2', size='16px')
|
||||
| Этикетка
|
||||
.place__board.no-print(v-else)
|
||||
.place__col(
|
||||
v-for='col in columns',
|
||||
:key='col.key',
|
||||
:class='{ "place__col--inbox": col.key === "__inbox__", "is-over": dragOverKey === col.key }',
|
||||
@dragover.prevent='dragOverKey = col.key',
|
||||
@dragleave='dragOverKey = (dragOverKey === col.key ? null : dragOverKey)',
|
||||
@drop='onDrop(col)'
|
||||
)
|
||||
.place__col-head
|
||||
q-icon(:name='col.icon', size='18px')
|
||||
span.place__col-title {{ col.title }}
|
||||
BaseBadge(variant='neutral') {{ col.items.length }}
|
||||
|
||||
//- Печатный лист этикеток (только при печати; на экране штрих-код в строке).
|
||||
.place__col-body
|
||||
.place__empty-drop(v-if='!col.items.length')
|
||||
| {{ col.key === '__inbox__' ? 'Всё разложено' : 'Перетащите сюда' }}
|
||||
|
||||
.place__card(
|
||||
v-for='item in col.items',
|
||||
:key='item.id',
|
||||
draggable='true',
|
||||
:class='{ "is-dragging": dragId === item.id }',
|
||||
@dragstart='onDragStart(item)',
|
||||
@dragend='onDragEnd'
|
||||
)
|
||||
.place__card-top
|
||||
.place__card-info
|
||||
.place__card-name {{ item.product_name_snapshot || 'Товар по предложению' }}
|
||||
.place__card-meta {{ item.quantity_per_label }} ед. · {{ item.orderer_account_snapshot }}
|
||||
BaseButton(variant='ghost', size='sm', icon-only, aria-label='Действия')
|
||||
template(#icon-left)
|
||||
q-icon(name='more_vert', size='18px')
|
||||
q-menu(anchor='bottom right', self='top right')
|
||||
q-list(dense, style='min-width: 200px')
|
||||
q-item-label(header) Переложить на полку
|
||||
q-item(
|
||||
v-for='name in shelfNames.filter((n) => n !== item.shelf)',
|
||||
:key='name',
|
||||
clickable,
|
||||
v-close-popup,
|
||||
@click='moveToShelf(item, name)'
|
||||
)
|
||||
q-item-section(avatar)
|
||||
q-icon(name='shelves', size='18px')
|
||||
q-item-section {{ name }}
|
||||
q-item(clickable, v-close-popup, @click='openNewShelf')
|
||||
q-item-section(avatar)
|
||||
q-icon(name='add', size='18px')
|
||||
q-item-section На новую полку…
|
||||
q-item(
|
||||
v-if='item.shelf',
|
||||
clickable,
|
||||
v-close-popup,
|
||||
@click='moveToShelf(item, null)'
|
||||
)
|
||||
q-item-section(avatar)
|
||||
q-icon(name='inbox', size='18px')
|
||||
q-item-section Снять с полки
|
||||
q-separator
|
||||
q-item(
|
||||
v-if='canRedistribute(item)',
|
||||
clickable,
|
||||
v-close-popup,
|
||||
@click='openSplit(item)'
|
||||
)
|
||||
q-item-section(avatar)
|
||||
q-icon(name='call_split', size='18px')
|
||||
q-item-section Разложить по количеству
|
||||
q-item(
|
||||
v-if='!item.barcode_value',
|
||||
clickable,
|
||||
v-close-popup,
|
||||
@click='makeLabel(item)'
|
||||
)
|
||||
q-item-section(avatar)
|
||||
q-icon(name='qr_code_2', size='18px')
|
||||
q-item-section Наклеить этикетку
|
||||
|
||||
.place__card-badges
|
||||
BaseBadge(v-if='item.barcode_value', variant='pos') Промаркировано
|
||||
BaseBadge(v-else, variant='neutral') Без штрих-кода
|
||||
|
||||
BarcodeDisplay(v-if='item.barcode_value', :code='item.barcode_value', size='sm')
|
||||
|
||||
//- Колонка-кнопка создания новой полки.
|
||||
.place__col.place__col--add
|
||||
BaseButton(variant='ghost', @click='openNewShelf')
|
||||
template(#icon-left)
|
||||
q-icon(name='add', size='18px')
|
||||
| Полка
|
||||
|
||||
//- Печатный лист этикеток (только при печати; на экране штрих-код в карточке).
|
||||
.place__grid.print-only(v-if='labeledItems.length')
|
||||
.place__label(v-for='item in labeledItems', :key='item.id')
|
||||
.place__label-name.ellipsis {{ item.product_name_snapshot }}
|
||||
@@ -273,6 +391,21 @@ q-page.place(role='region', aria-label='Раскладка и маркировк
|
||||
template(v-if='item.shelf') · Полка: {{ item.shelf }}
|
||||
BarcodeDisplay(v-if='item.barcode_value', :code='item.barcode_value', size='md')
|
||||
|
||||
//- Новая полка.
|
||||
BaseDialog(v-model='newShelfOpen', title='Новая полка', size='sm')
|
||||
.place__new-shelf
|
||||
BaseInput(
|
||||
v-model='newShelfName',
|
||||
label='Название полки',
|
||||
placeholder='A-12',
|
||||
autofocus,
|
||||
@keydown.enter='createShelf'
|
||||
)
|
||||
template(#footer)
|
||||
BaseButton(variant='ghost', size='sm', @click='newShelfOpen = false') Отмена
|
||||
BaseButton(variant='primary', size='sm', :disabled='!newShelfName.trim()', @click='createShelf') Создать
|
||||
|
||||
//- Раскладка по количеству.
|
||||
BaseDialog(v-model='splitDialogOpen', title='Разложить по полкам', size='md')
|
||||
.place__split(v-if='splitTarget')
|
||||
.place__split-head
|
||||
@@ -322,52 +455,134 @@ q-page.place(role='region', aria-label='Раскладка и маркировк
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__list {
|
||||
&__loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--p-6, 24px);
|
||||
}
|
||||
|
||||
// Доска полок — горизонтальный ряд колонок с прокруткой.
|
||||
&__board {
|
||||
display: flex;
|
||||
gap: var(--p-3, 12px);
|
||||
align-items: flex-start;
|
||||
overflow-x: auto;
|
||||
padding-bottom: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__col {
|
||||
flex: 0 0 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3, 12px);
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md, 12px);
|
||||
background: var(--p-surface);
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
background: var(--p-surface-2);
|
||||
padding: var(--p-3, 12px);
|
||||
max-height: calc(100vh - 220px);
|
||||
transition: border-color 0.12s ease, background 0.12s ease;
|
||||
|
||||
&.is-over {
|
||||
border-color: var(--p-primary);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
|
||||
&--inbox {
|
||||
background: var(--p-surface);
|
||||
}
|
||||
|
||||
&--add {
|
||||
border-style: dashed;
|
||||
background: transparent;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-info {
|
||||
min-width: 0;
|
||||
&__col-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-1, 4px);
|
||||
}
|
||||
|
||||
&__item-name {
|
||||
font-size: var(--p-fs-body, 14px);
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
gap: var(--p-2, 8px);
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
&__item-meta {
|
||||
&__col-title {
|
||||
flex: 1 1 auto;
|
||||
font-weight: 600;
|
||||
font-size: var(--p-fs-body, 14px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&__col-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__empty-drop {
|
||||
border: 1px dashed var(--p-line);
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
padding: var(--p-4, 16px);
|
||||
text-align: center;
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-3);
|
||||
}
|
||||
|
||||
&__card {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-sm, 8px);
|
||||
background: var(--p-surface);
|
||||
padding: var(--p-3, 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
cursor: grab;
|
||||
|
||||
&.is-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&__card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__card-info {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__card-name {
|
||||
font-size: var(--p-fs-body, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&__card-meta {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__item-controls {
|
||||
&__card-badges {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--p-2, 8px);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--p-1, 4px);
|
||||
}
|
||||
|
||||
&__shelf {
|
||||
width: 120px;
|
||||
&__new-shelf {
|
||||
padding-top: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__split {
|
||||
@@ -387,10 +602,6 @@ q-page.place(role='region', aria-label='Раскладка и маркировк
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__item-barcode {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__split-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -417,7 +628,7 @@ q-page.place(role='region', aria-label='Раскладка и маркировк
|
||||
color: var(--p-ink-2);
|
||||
|
||||
&--bad {
|
||||
color: var(--p-danger, #b3261e);
|
||||
color: var(--p-neg);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -451,6 +662,10 @@ q-page.place(role='region', aria-label='Раскладка и маркировк
|
||||
@media (max-width: 768px) {
|
||||
.place {
|
||||
padding: var(--p-4, 16px);
|
||||
|
||||
&__col {
|
||||
flex-basis: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user