1014 lines
25 KiB
Vue
1014 lines
25 KiB
Vue
<!--
|
||
Shared order-detail body for retail / omde / tamin list modals.
|
||
Shows customer summary + item rows with product images. In editMode, a
|
||
product→variant picker sits beside the line list (search products, then
|
||
pick a variant from that product).
|
||
-->
|
||
<template>
|
||
<div v-if="order" class="od" :class="{ 'od--edit': editMode }">
|
||
<a-alert
|
||
v-if="order.revisionStatus === 'AWAITING_CUSTOMER'"
|
||
type="warning"
|
||
show-icon
|
||
class="od__alert"
|
||
:message="order.confirmed_str || 'در انتظار تأیید مشتری'"
|
||
description="تا تأیید مشتری (یا اعمال/لغو توسط شما) ارقام فعلی سفارش تغییر نمیکند. پیامک برای مشتری ارسال شده است."
|
||
/>
|
||
|
||
<section class="od__summary" aria-label="خلاصه سفارش">
|
||
<div class="od__who">
|
||
<div class="od__avatar" aria-hidden="true">
|
||
{{ initials }}
|
||
</div>
|
||
<div class="od__whoText">
|
||
<h3 class="od__name">{{ customerName }}</h3>
|
||
<a v-if="order.customer?.phone" class="od__phone" :href="`tel:${order.customer.phone}`">
|
||
{{ order.customer.phone }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<div class="od__meta">
|
||
<div class="od__metaItem">
|
||
<span class="od__metaLabel">تاریخ ثبت</span>
|
||
<span class="od__metaValue">{{ formatDate(order.caj) }}</span>
|
||
</div>
|
||
<div class="od__metaItem">
|
||
<span class="od__metaLabel">وضعیت</span>
|
||
<a-tag color="blue">{{ order.sts_str }}</a-tag>
|
||
</div>
|
||
<div v-if="order.orderNumber" class="od__metaItem">
|
||
<span class="od__metaLabel">شماره سفارش</span>
|
||
<span class="od__metaValue od__mono">{{ order.orderNumber }}</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="od__workspace">
|
||
<section class="od__items" aria-label="آیتمهای سفارش">
|
||
<header class="od__itemsHead">
|
||
<h4 class="od__itemsTitle">آیتمهای سفارش</h4>
|
||
<span class="od__itemsCount">{{ order.items?.length || 0 }} قلم</span>
|
||
</header>
|
||
|
||
<ul v-if="order.items?.length" class="od__list">
|
||
<li v-for="(item, index) in order.items" :key="item.id || index" class="od__row">
|
||
<div class="od__art">
|
||
<img
|
||
v-if="item.image && !broken[itemKey(item, index)]"
|
||
:src="item.image"
|
||
:alt="item.title || ''"
|
||
@error="markBroken(itemKey(item, index))"
|
||
/>
|
||
<div v-else class="od__artEmpty" aria-hidden="true">
|
||
<PictureOutlined />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="od__body">
|
||
<div class="od__titles">
|
||
<h5 class="od__product">{{ item.title || 'محصول' }}</h5>
|
||
<span v-if="item.variant_title" class="od__variant">{{ item.variant_title }}</span>
|
||
</div>
|
||
|
||
<div v-if="!editMode" class="od__chips">
|
||
<span class="od__chip">
|
||
<b>{{ item.qty }}</b>
|
||
عدد
|
||
</span>
|
||
<span class="od__chip">
|
||
<b>{{ formatGrams(item.grams) }}</b>
|
||
گرم
|
||
</span>
|
||
<span v-if="item.wagePercent != null" class="od__chip">
|
||
<b>{{ formatWage(item.wagePercent) }}</b>
|
||
٪ اجرت
|
||
</span>
|
||
<span v-if="Number(item.flatRialCharge) > 0" class="od__chip od__chip--accent">
|
||
<b>{{ priceUnit(item.flatRialCharge) }}</b>
|
||
</span>
|
||
</div>
|
||
|
||
<div v-else class="od__edit">
|
||
<a-form-item label="وزن (گرم)" class="od__field">
|
||
<a-input-number v-model:value="item.grams" :min="0" class="w-full" />
|
||
</a-form-item>
|
||
<a-form-item label="تعداد" class="od__field">
|
||
<a-input-number v-model:value="item.qty" :min="1" class="w-full" />
|
||
</a-form-item>
|
||
<a-form-item label="اجرت ریالی" class="od__field">
|
||
<a-input-number v-model:value="item.flatRialCharge" :min="0" class="w-full" />
|
||
</a-form-item>
|
||
<a-button size="small" danger class="od__remove" @click="emit('remove-line', index)">
|
||
حذف آیتم
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
<a-empty v-else description="آیتمی در این سفارش نیست" />
|
||
</section>
|
||
|
||
<aside v-if="editMode" class="od__picker" aria-label="افزودن محصول">
|
||
<header class="od__pickerHead">
|
||
<div>
|
||
<h4 class="od__pickerTitle">افزودن به سفارش</h4>
|
||
<p class="od__pickerHint">اول محصول را پیدا کنید، بعد تنوع را انتخاب کنید</p>
|
||
</div>
|
||
<ol class="od__steps" aria-hidden="true">
|
||
<li :class="{ 'is-active': !selectedProduct, 'is-done': !!selectedProduct }">۱ محصول</li>
|
||
<li :class="{ 'is-active': !!selectedProduct }">۲ تنوع</li>
|
||
</ol>
|
||
</header>
|
||
|
||
<a-form-item label="جستجوی محصول" class="od__field">
|
||
<a-input-search
|
||
v-model:value="productSearch"
|
||
allow-clear
|
||
placeholder="نام یا کد محصول…"
|
||
:loading="productsLoading"
|
||
enter-button="جستجو"
|
||
@search="onProductSearchSubmit"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<div
|
||
v-if="categories.length"
|
||
class="od__catChips"
|
||
role="tablist"
|
||
aria-label="دستهبندی محصولات"
|
||
>
|
||
<button
|
||
type="button"
|
||
class="od__catChip"
|
||
:class="{ 'is-active': !selectedCategoryId }"
|
||
role="tab"
|
||
:aria-selected="!selectedCategoryId"
|
||
@click="selectCategory(null)"
|
||
>
|
||
همه
|
||
</button>
|
||
<button
|
||
v-for="cat in categories"
|
||
:key="cat.id"
|
||
type="button"
|
||
class="od__catChip"
|
||
:class="{ 'is-active': selectedCategoryId === cat.id }"
|
||
role="tab"
|
||
:aria-selected="selectedCategoryId === cat.id"
|
||
@click="selectCategory(cat.id)"
|
||
>
|
||
{{ cat.title }}
|
||
</button>
|
||
</div>
|
||
|
||
<a-spin :spinning="productsLoading">
|
||
<div v-if="products.length" class="od__products" role="listbox" aria-label="محصولات">
|
||
<button
|
||
v-for="p in products"
|
||
:key="p.id"
|
||
type="button"
|
||
class="od__productCard"
|
||
:class="{ 'is-selected': selectedProduct?.id === p.id }"
|
||
role="option"
|
||
:aria-selected="selectedProduct?.id === p.id"
|
||
@click="selectProduct(p)"
|
||
>
|
||
<div class="od__productThumb">
|
||
<img
|
||
v-if="productThumb(p) && !broken[`p-${p.id}`]"
|
||
:src="productThumb(p)!"
|
||
:alt="p.title"
|
||
@error="markBroken(`p-${p.id}`)"
|
||
/>
|
||
<div v-else class="od__artEmpty" aria-hidden="true">
|
||
<PictureOutlined />
|
||
</div>
|
||
</div>
|
||
<div class="od__productMeta">
|
||
<span class="od__productName">{{ p.title }}</span>
|
||
<span class="od__productSub">
|
||
{{ p.variants_count ?? p.variants_all?.length ?? 0 }} تنوع
|
||
<template v-if="p.product_code"> · {{ p.product_code }}</template>
|
||
</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<a-empty
|
||
v-else-if="!productsLoading"
|
||
class="od__pickerEmpty"
|
||
:description="productsEmptyHint"
|
||
/>
|
||
</a-spin>
|
||
|
||
<div v-if="selectedProduct" class="od__variantsBlock">
|
||
<div class="od__variantsHead">
|
||
<h5 class="od__variantsTitle">تنوعهای «{{ selectedProduct.title }}»</h5>
|
||
<a-button type="link" size="small" @click="clearProduct">عوض کردن محصول</a-button>
|
||
</div>
|
||
|
||
<div v-if="activeVariants.length" class="od__variants" role="listbox" aria-label="تنوعها">
|
||
<button
|
||
v-for="v in activeVariants"
|
||
:key="v.id"
|
||
type="button"
|
||
class="od__variantCard"
|
||
:class="{ 'is-selected': localVariantId === String(v.id) }"
|
||
role="option"
|
||
:aria-selected="localVariantId === String(v.id)"
|
||
@click="localVariantId = String(v.id)"
|
||
>
|
||
<div class="od__variantTop">
|
||
<span class="od__variantName">{{ v.title || 'تنوع' }}</span>
|
||
<a-tag v-if="localVariantId === String(v.id)" color="blue">انتخابشده</a-tag>
|
||
</div>
|
||
<div class="od__variantFacts">
|
||
<span><b>{{ formatGrams(v.grams) }}</b> گرم</span>
|
||
<span v-if="v.percentage_pay != null"><b>{{ formatWage(v.percentage_pay) }}</b>٪ اجرت</span>
|
||
<span v-if="v.sku" class="od__mono">{{ v.sku }}</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<a-empty v-else description="این محصول تنوع فعالی ندارد" />
|
||
|
||
<div class="od__addBar">
|
||
<a-form-item label="تعداد" class="od__field od__addQty">
|
||
<a-input-number v-model:value="localQty" :min="1" class="w-full" />
|
||
</a-form-item>
|
||
<a-button type="primary" class="od__addBtn" :disabled="!localVariantId" @click="onAdd">
|
||
افزودن به سفارش
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
<a-empty v-else />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, ref, watch } from 'vue';
|
||
import { PictureOutlined } from '@ant-design/icons-vue';
|
||
import { message } from 'ant-design-vue';
|
||
import api from '../../api/client';
|
||
import { jdate, priceUnit } from '../../utils/filters';
|
||
|
||
export type AddLinePayload = {
|
||
variantId: string;
|
||
qty: number;
|
||
title?: string;
|
||
variantTitle?: string;
|
||
grams?: number;
|
||
wagePercent?: number;
|
||
flatRialCharge?: number;
|
||
image?: string | null;
|
||
};
|
||
|
||
const props = defineProps<{
|
||
order: Record<string, any> | undefined;
|
||
editMode?: boolean;
|
||
addVariantId?: string;
|
||
addQty?: number;
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'remove-line', index: number): void;
|
||
(e: 'add-line', payload: AddLinePayload): void;
|
||
(e: 'update:addVariantId', value: string): void;
|
||
(e: 'update:addQty', value: number): void;
|
||
}>();
|
||
|
||
const localVariantId = ref(props.addVariantId ?? '');
|
||
const localQty = ref(props.addQty ?? 1);
|
||
const productsLoading = ref(false);
|
||
const products = ref<Record<string, any>[]>([]);
|
||
const productSearch = ref('');
|
||
const selectedProduct = ref<Record<string, any> | null>(null);
|
||
const categories = ref<{ id: string; title: string }[]>([]);
|
||
const selectedCategoryId = ref<string | null>(null);
|
||
let searchTimer: ReturnType<typeof setTimeout> | null = null;
|
||
|
||
/** Broken/missing files (empty local uploads dir, SPA HTML fallback, …). */
|
||
const broken = reactive<Record<string, boolean>>({});
|
||
|
||
const activeVariants = computed(() => {
|
||
const all = selectedProduct.value?.variants_all ?? [];
|
||
return all.filter((v: any) => v.is_active !== false);
|
||
});
|
||
|
||
const productsEmptyHint = computed(() => {
|
||
if (productSearch.value.trim() || selectedCategoryId.value) return 'محصولی پیدا نشد';
|
||
return 'برای شروع، محصول را جستجو کنید یا یک دسته انتخاب کنید';
|
||
});
|
||
|
||
function formatGrams(v: unknown) {
|
||
const n = parseFloat(String(v));
|
||
if (!Number.isFinite(n)) return String(v ?? '—');
|
||
return n % 1 === 0 ? String(n) : n.toFixed(3);
|
||
}
|
||
|
||
function productThumb(p: Record<string, any>): string | null {
|
||
return p.thumb?.full || p.images?.[0]?.url || null;
|
||
}
|
||
|
||
async function loadCategories() {
|
||
try {
|
||
const { data } = await api.get('product/categories');
|
||
const rows = Array.isArray(data) ? data : Array.isArray(data?.rows) ? data.rows : [];
|
||
categories.value = rows
|
||
.map((c: any) => ({ id: String(c.id), title: String(c.title ?? '').trim() }))
|
||
.filter((c: { id: string; title: string }) => c.id && c.title);
|
||
} catch (e) {
|
||
console.log(e);
|
||
categories.value = [];
|
||
}
|
||
}
|
||
|
||
async function loadProducts(search = productSearch.value) {
|
||
productsLoading.value = true;
|
||
try {
|
||
const filters: { field: string; op: string; value: unknown }[] = [
|
||
{ field: 'isActive', op: '=', value: true },
|
||
];
|
||
if (selectedCategoryId.value) {
|
||
filters.push({ field: 'categoryId', op: '=', value: selectedCategoryId.value });
|
||
}
|
||
const { data } = await api.post('products', {
|
||
page: 1,
|
||
limit: 24,
|
||
search: String(search ?? '').trim() || undefined,
|
||
filters,
|
||
});
|
||
products.value = Array.isArray(data?.rows) ? data.rows : [];
|
||
} catch (e) {
|
||
console.log(e);
|
||
message.error('دریافت لیست محصولات ممکن نشد');
|
||
} finally {
|
||
productsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function onProductSearchSubmit(q: string) {
|
||
productSearch.value = q;
|
||
void loadProducts(q);
|
||
}
|
||
|
||
function selectCategory(id: string | null) {
|
||
if (selectedCategoryId.value === id) return;
|
||
selectedCategoryId.value = id;
|
||
clearProduct();
|
||
void loadProducts();
|
||
}
|
||
|
||
function selectProduct(p: Record<string, any>) {
|
||
selectedProduct.value = p;
|
||
localVariantId.value = '';
|
||
const first = (p.variants_all ?? []).find((v: any) => v.is_active !== false);
|
||
if (first) localVariantId.value = String(first.id);
|
||
}
|
||
|
||
function clearProduct() {
|
||
selectedProduct.value = null;
|
||
localVariantId.value = '';
|
||
}
|
||
|
||
function itemKey(item: Record<string, any>, index: number) {
|
||
return String(item.id || item.productVariantId || index);
|
||
}
|
||
|
||
function markBroken(key: string) {
|
||
broken[key] = true;
|
||
}
|
||
|
||
watch(
|
||
() => props.order?.id,
|
||
() => {
|
||
for (const k of Object.keys(broken)) delete broken[k];
|
||
},
|
||
);
|
||
|
||
watch(
|
||
() => props.addVariantId,
|
||
(v) => {
|
||
if (v !== undefined) localVariantId.value = v;
|
||
},
|
||
);
|
||
watch(
|
||
() => props.addQty,
|
||
(v) => {
|
||
if (v !== undefined) localQty.value = v;
|
||
},
|
||
);
|
||
watch(localVariantId, (v) => emit('update:addVariantId', v));
|
||
watch(localQty, (v) => emit('update:addQty', Number(v) || 1));
|
||
|
||
watch(productSearch, (q) => {
|
||
if (searchTimer) clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(() => {
|
||
void loadProducts(q);
|
||
}, 320);
|
||
});
|
||
|
||
watch(
|
||
() => props.editMode,
|
||
(on) => {
|
||
if (on) {
|
||
void loadCategories();
|
||
void loadProducts('');
|
||
} else {
|
||
clearProduct();
|
||
products.value = [];
|
||
productSearch.value = '';
|
||
selectedCategoryId.value = null;
|
||
categories.value = [];
|
||
}
|
||
},
|
||
);
|
||
|
||
const customerName = computed(() => {
|
||
const c = props.order?.customer;
|
||
if (!c) return '—';
|
||
const name = `${c.fname ?? ''} ${c.lname ?? ''}`.trim();
|
||
return name || c.fullName || '—';
|
||
});
|
||
|
||
const initials = computed(() => {
|
||
const parts = customerName.value.split(/\s+/).filter(Boolean);
|
||
if (!parts.length || parts[0] === '—') return '؟';
|
||
return parts
|
||
.slice(0, 2)
|
||
.map((p: string) => p[0])
|
||
.join('');
|
||
});
|
||
|
||
function formatDate(v: string | null | undefined) {
|
||
return v ? jdate(v) : '—';
|
||
}
|
||
|
||
function formatWage(v: unknown) {
|
||
const n = parseFloat(String(v));
|
||
if (!Number.isFinite(n)) return String(v ?? '—');
|
||
return n % 1 === 0 ? String(n) : n.toFixed(2);
|
||
}
|
||
|
||
function onAdd() {
|
||
const variantId = String(localVariantId.value ?? '').trim();
|
||
if (!variantId) {
|
||
message.warning('یک تنوع انتخاب کنید');
|
||
return;
|
||
}
|
||
if (!selectedProduct.value) {
|
||
message.warning('اول یک محصول انتخاب کنید');
|
||
return;
|
||
}
|
||
const row = activeVariants.value.find((v: any) => String(v.id) === variantId);
|
||
const thumb = row?.image?.url || productThumb(selectedProduct.value);
|
||
emit('add-line', {
|
||
variantId,
|
||
qty: Number(localQty.value) || 1,
|
||
title: selectedProduct.value.title ?? 'محصول',
|
||
variantTitle: row?.title ?? variantId,
|
||
grams: row?.grams != null ? Number(row.grams) : 0,
|
||
wagePercent: row?.percentage_pay != null ? Number(row.percentage_pay) : undefined,
|
||
flatRialCharge: row?.rial_pay != null ? Number(row.rial_pay) : 0,
|
||
image: thumb ?? null,
|
||
});
|
||
message.success('آیتم به سفارش اضافه شد');
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.od__alert {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.od__summary {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px 24px;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
padding: 14px 16px;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 12px;
|
||
background: #fafafa;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.od__who {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.od__avatar {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
flex: none;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #1d39c4;
|
||
background: #f0f5ff;
|
||
border: 1px solid #d6e4ff;
|
||
}
|
||
|
||
.od__whoText {
|
||
min-width: 0;
|
||
}
|
||
|
||
.od__name {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.od__phone {
|
||
display: inline-block;
|
||
margin-top: 2px;
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
direction: ltr;
|
||
unicode-bidi: plaintext;
|
||
}
|
||
|
||
.od__meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px 20px;
|
||
}
|
||
|
||
.od__metaItem {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.od__metaLabel {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.od__metaValue {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.od__mono {
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.od__workspace {
|
||
display: block;
|
||
}
|
||
|
||
.od--edit .od__workspace {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
|
||
gap: 16px;
|
||
align-items: start;
|
||
}
|
||
|
||
.od__itemsHead {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.od__itemsTitle {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.od__itemsCount {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.od__list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
max-height: min(58vh, 560px);
|
||
overflow: auto;
|
||
padding-inline-end: 4px;
|
||
}
|
||
|
||
.od__row {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
}
|
||
|
||
.od__art {
|
||
width: 72px;
|
||
height: 72px;
|
||
flex: none;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.od__art img,
|
||
.od__productThumb img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
|
||
.od__artEmpty {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: grid;
|
||
place-items: center;
|
||
color: rgba(0, 0, 0, 0.25);
|
||
font-size: 22px;
|
||
}
|
||
|
||
.od__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.od__titles {
|
||
min-width: 0;
|
||
}
|
||
|
||
.od__product {
|
||
margin: 0;
|
||
font-size: 13.5px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
line-height: 1.45;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.od__variant {
|
||
display: block;
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.od__chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.od__chip {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
padding: 3px 9px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
background: #f5f5f5;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.od__chip b {
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.od__chip--accent {
|
||
color: #531dab;
|
||
background: #f9f0ff;
|
||
border-color: #efdbff;
|
||
}
|
||
|
||
.od__chip--accent b {
|
||
color: #531dab;
|
||
}
|
||
|
||
.od__edit {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||
gap: 8px 12px;
|
||
align-items: end;
|
||
}
|
||
|
||
.od__field {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.od__remove {
|
||
justify-self: start;
|
||
}
|
||
|
||
/* ── Picker panel ───────────────────────────────────────── */
|
||
.od__picker {
|
||
position: sticky;
|
||
top: 0;
|
||
border: 1px solid #e6ebf5;
|
||
border-radius: 14px;
|
||
background: linear-gradient(180deg, #f7f9fc 0%, #fff 48px);
|
||
padding: 14px;
|
||
box-shadow: 0 8px 24px -18px rgba(20, 30, 60, 0.35);
|
||
max-height: min(70vh, 680px);
|
||
overflow: auto;
|
||
}
|
||
|
||
.od__pickerHead {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.od__pickerTitle {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.od__pickerHint {
|
||
margin: 4px 0 0;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.od__steps {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.od__steps li {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 6px 8px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.35);
|
||
background: #eef1f6;
|
||
}
|
||
|
||
.od__steps li.is-active {
|
||
color: #1d39c4;
|
||
background: #e6f0ff;
|
||
}
|
||
|
||
.od__steps li.is-done {
|
||
color: #389e0d;
|
||
background: #f6ffed;
|
||
}
|
||
|
||
.od__catChips {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin: 10px 0 2px;
|
||
padding-bottom: 2px;
|
||
overflow-x: auto;
|
||
scrollbar-width: thin;
|
||
}
|
||
|
||
.od__catChip {
|
||
flex: none;
|
||
padding: 5px 12px;
|
||
border-radius: 999px;
|
||
border: 1px solid #e6ebf5;
|
||
background: #fff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.65);
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
|
||
}
|
||
|
||
.od__catChip:hover {
|
||
border-color: #91caff;
|
||
color: #1d39c4;
|
||
}
|
||
|
||
.od__catChip.is-active {
|
||
border-color: #1677ff;
|
||
background: #e6f4ff;
|
||
color: #0958d9;
|
||
}
|
||
|
||
.od__products {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
max-height: 220px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.od__productCard {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
width: 100%;
|
||
padding: 8px;
|
||
border-radius: 12px;
|
||
border: 1px solid #eef0f4;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
text-align: right;
|
||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
||
}
|
||
|
||
.od__productCard:hover {
|
||
border-color: #91caff;
|
||
background: #fafcff;
|
||
}
|
||
|
||
.od__productCard.is-selected {
|
||
border-color: #1677ff;
|
||
background: #f0f5ff;
|
||
box-shadow: 0 0 0 1px rgba(22, 119, 255, 0.2);
|
||
}
|
||
|
||
.od__productThumb {
|
||
width: 48px;
|
||
height: 48px;
|
||
flex: none;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.od__productMeta {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.od__productName {
|
||
font-size: 12.5px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.od__productSub {
|
||
font-size: 11px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
}
|
||
|
||
.od__pickerEmpty {
|
||
margin: 12px 0 4px;
|
||
}
|
||
|
||
.od__variantsBlock {
|
||
margin-top: 14px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e6ebf5;
|
||
}
|
||
|
||
.od__variantsHead {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.od__variantsTitle {
|
||
margin: 0;
|
||
font-size: 12.5px;
|
||
font-weight: 800;
|
||
color: rgba(0, 0, 0, 0.75);
|
||
}
|
||
|
||
.od__variants {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.od__variantCard {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border-radius: 12px;
|
||
border: 1px solid #eef0f4;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
text-align: right;
|
||
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||
}
|
||
|
||
.od__variantCard:hover {
|
||
border-color: #91caff;
|
||
}
|
||
|
||
.od__variantCard.is-selected {
|
||
border-color: #1677ff;
|
||
background: #f0f5ff;
|
||
box-shadow: 0 0 0 1px rgba(22, 119, 255, 0.18);
|
||
}
|
||
|
||
.od__variantTop {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.od__variantName {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.od__variantFacts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
font-size: 11.5px;
|
||
color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.od__variantFacts b {
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.78);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
.od__addBar {
|
||
display: grid;
|
||
grid-template-columns: 100px 1fr;
|
||
gap: 10px;
|
||
align-items: end;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.od__addBtn {
|
||
height: 36px;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.od--edit .od__workspace {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.od__picker {
|
||
position: static;
|
||
max-height: none;
|
||
}
|
||
|
||
.od__list {
|
||
max-height: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 575px) {
|
||
.od__summary {
|
||
padding: 12px;
|
||
}
|
||
|
||
.od__art {
|
||
width: 56px;
|
||
height: 56px;
|
||
}
|
||
|
||
.od__row {
|
||
padding: 10px;
|
||
}
|
||
|
||
.od__addBar {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|