This commit is contained in:
xds
2026-03-22 14:26:45 +03:00
parent 33694d68db
commit 466a27907a
28 changed files with 1334 additions and 71 deletions

View File

@@ -16,7 +16,7 @@
<button
v-for="mat in materialsByCategory(cat)"
:key="mat.id"
@click="selectMaterial(mat.id)"
@click="selectMaterial(mat)"
:class="[
'flex flex-col rounded-lg border-2 p-3.5 text-left transition-all',
store.materialId === mat.id
@@ -29,6 +29,18 @@
<span class="text-xs font-medium text-gray-500">{{ mat.price_per_gram }} &#8381;/г</span>
</div>
<p class="mt-1 text-xs leading-relaxed text-gray-500">{{ mat.description }}</p>
<!-- Color palette -->
<div v-if="mat.color_options && mat.color_options.length" class="mt-2 flex flex-wrap gap-1">
<span
v-for="c in mat.color_options"
:key="c.hex || c"
class="h-4 w-4 rounded-full border border-gray-300"
:style="{ backgroundColor: c.hex || c }"
:title="c.name || c"
></span>
</div>
<div class="mt-2 flex flex-wrap gap-1.5">
<span v-if="mat.properties.food_safe" class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-[10px] font-medium text-green-700">
Food safe
@@ -43,11 +55,35 @@
</button>
</div>
</div>
<!-- Color selection (shown when material is selected) -->
<div v-if="selectedMaterial && selectedMaterial.color_options && selectedMaterial.color_options.length" class="mt-5 rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="mb-3 text-sm font-semibold text-gray-700">Выберите цвет</h3>
<div class="flex flex-wrap gap-2">
<button
v-for="c in selectedMaterial.color_options"
:key="c.hex || c"
@click="selectColor(c)"
:class="[
'flex items-center gap-2 rounded-lg border-2 px-3 py-1.5 text-xs font-medium transition-all',
store.color === (c.name || c)
? 'border-primary-500 bg-white ring-1 ring-primary-500'
: 'border-gray-200 bg-white hover:border-gray-300',
]"
>
<span
class="h-5 w-5 rounded-full border border-gray-300 flex-shrink-0"
:style="{ backgroundColor: c.hex || c }"
></span>
<span class="text-gray-700">{{ c.name || c }}</span>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { computed, onMounted } from 'vue'
import { useCalculatorStore } from '../stores/calculator'
import { useMaterialsStore } from '../stores/materials'
@@ -63,8 +99,19 @@ function materialsByCategory(cat) {
return materialsStore.materials.filter((m) => m.category === cat)
}
function selectMaterial(id) {
store.materialId = id
const selectedMaterial = computed(() => {
if (!store.materialId) return null
return materialsStore.materials.find((m) => m.id === store.materialId)
})
function selectMaterial(mat) {
store.materialId = mat.id
store.color = null
store.result = null
}
function selectColor(c) {
store.color = c.name || c
store.result = null
}

View File

@@ -1,52 +1,147 @@
<template>
<form @submit.prevent="submitOrder" class="space-y-4">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Имя *</label>
<input v-model="form.client_name" required class="input-field" placeholder="Иван Петров" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Телефон *</label>
<input v-model="form.client_phone" required class="input-field" placeholder="+79001234567" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Email</label>
<input v-model="form.client_email" type="email" class="input-field" placeholder="ivan@example.com" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Компания</label>
<input v-model="form.client_company" class="input-field" placeholder="ООО Технопарк" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Способ получения</label>
<select v-model="form.delivery_method" class="input-field">
<option value="pickup">Самовывоз</option>
<option value="delivery">Доставка</option>
</select>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Комментарий</label>
<textarea v-model="form.comment" rows="3" class="input-field" placeholder="Дополнительные пожелания"></textarea>
<div class="space-y-5">
<!-- Auth section -->
<div v-if="!clientStore.isAuthenticated" class="rounded-lg border border-gray-200 bg-gray-50 p-4">
<div class="flex gap-2 mb-4">
<button
@click="authMode = 'login'"
:class="['rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', authMode === 'login' ? 'bg-primary-600 text-white' : 'bg-white text-gray-600 border border-gray-200']"
>Войти</button>
<button
@click="authMode = 'register'"
:class="['rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', authMode === 'register' ? 'bg-primary-600 text-white' : 'bg-white text-gray-600 border border-gray-200']"
>Регистрация</button>
<button
@click="authMode = 'guest'"
:class="['rounded-lg px-4 py-1.5 text-sm font-medium transition-colors', authMode === 'guest' ? 'bg-gray-700 text-white' : 'bg-white text-gray-600 border border-gray-200']"
>Без регистрации</button>
</div>
<!-- Login form -->
<form v-if="authMode === 'login'" @submit.prevent="handleLogin" class="space-y-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Email</label>
<input v-model="authForm.email" type="email" required class="input-field" placeholder="your@email.com" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Пароль</label>
<input v-model="authForm.password" type="password" required class="input-field" />
</div>
<p v-if="authError" class="text-sm text-red-600">{{ authError }}</p>
<button type="submit" :disabled="authLoading" class="btn-primary text-sm">
{{ authLoading ? 'Вход...' : 'Войти' }}
</button>
</form>
<!-- Register form -->
<form v-if="authMode === 'register'" @submit.prevent="handleRegister" class="space-y-3">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Имя *</label>
<input v-model="authForm.name" required class="input-field" placeholder="Иван Петров" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Email *</label>
<input v-model="authForm.email" type="email" required class="input-field" placeholder="your@email.com" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Пароль *</label>
<input v-model="authForm.password" type="password" required minlength="6" class="input-field" />
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Телефон</label>
<input v-model="authForm.phone" class="input-field" placeholder="+79001234567" />
</div>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600">Компания</label>
<input v-model="authForm.company" class="input-field" placeholder="ООО Технопарк" />
</div>
<p v-if="authError" class="text-sm text-red-600">{{ authError }}</p>
<button type="submit" :disabled="authLoading" class="btn-primary text-sm">
{{ authLoading ? 'Регистрация...' : 'Зарегистрироваться' }}
</button>
</form>
<!-- Guest info -->
<p v-if="authMode === 'guest'" class="text-sm text-gray-500">
Вы можете оформить заказ без регистрации. Отслеживание заказа будет доступно только по номеру заказа.
</p>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<!-- Logged in info -->
<div v-else class="flex items-center justify-between rounded-lg border border-green-200 bg-green-50 px-4 py-3">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-sm font-medium text-green-800">{{ clientStore.user?.name }}</span>
<span class="text-xs text-green-600">{{ clientStore.user?.email }}</span>
</div>
<button @click="clientStore.logout()" class="text-xs text-green-700 hover:text-green-900 underline">Выйти</button>
</div>
<button type="submit" :disabled="loading" class="btn-primary w-full">
<svg v-if="loading" class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{{ loading ? 'Оформляем...' : 'Оформить заказ' }}
</button>
</form>
<!-- Order form -->
<form @submit.prevent="submitOrder" class="space-y-4">
<template v-if="!clientStore.isAuthenticated">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Имя *</label>
<input v-model="form.client_name" required class="input-field" placeholder="Иван Петров" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Телефон *</label>
<input v-model="form.client_phone" required class="input-field" placeholder="+79001234567" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Email</label>
<input v-model="form.client_email" type="email" class="input-field" placeholder="ivan@example.com" />
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Компания</label>
<input v-model="form.client_company" class="input-field" placeholder="ООО Технопарк" />
</div>
</template>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Способ получения</label>
<select v-model="form.delivery_method" class="input-field">
<option value="pickup">Самовывоз</option>
<option value="delivery">Доставка</option>
</select>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700">Комментарий</label>
<textarea v-model="form.comment" rows="3" class="input-field" placeholder="Дополнительные пожелания"></textarea>
</div>
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
<button type="submit" :disabled="loading" class="btn-primary w-full">
<svg v-if="loading" class="mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
{{ loading ? 'Оформляем...' : 'Оформить заказ' }}
</button>
</form>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { reactive, ref, watch } from 'vue'
import api from '../api/client'
import { useClientStore } from '../stores/client'
const props = defineProps({ calculationId: String })
const emit = defineEmits(['success'])
const clientStore = useClientStore()
const authMode = ref('login')
const authForm = reactive({ email: '', password: '', name: '', phone: '', company: '' })
const authError = ref('')
const authLoading = ref(false)
const form = reactive({
client_name: '',
client_phone: '',
@@ -59,14 +154,70 @@ const form = reactive({
const loading = ref(false)
const error = ref('')
// Pre-fill form from client data
watch(() => clientStore.user, (u) => {
if (u) {
form.client_name = u.name || ''
form.client_phone = u.phone || ''
form.client_email = u.email || ''
form.client_company = u.company || ''
}
}, { immediate: true })
async function handleLogin() {
authLoading.value = true
authError.value = ''
try {
await clientStore.login(authForm.email, authForm.password)
} catch (e) {
authError.value = e.response?.data?.detail || 'Ошибка входа'
} finally {
authLoading.value = false
}
}
async function handleRegister() {
authLoading.value = true
authError.value = ''
try {
await clientStore.register({
email: authForm.email,
password: authForm.password,
name: authForm.name,
phone: authForm.phone || null,
company: authForm.company || null,
})
} catch (e) {
authError.value = e.response?.data?.detail || 'Ошибка регистрации'
} finally {
authLoading.value = false
}
}
async function submitOrder() {
loading.value = true
error.value = ''
try {
const { data } = await api.post('/orders', {
const payload = {
calculation_id: props.calculationId,
...form,
})
delivery_method: form.delivery_method,
comment: form.comment,
}
if (clientStore.isAuthenticated) {
payload.client_name = clientStore.user.name
payload.client_phone = clientStore.user.phone || '+70000000000'
payload.client_email = clientStore.user.email
payload.client_company = clientStore.user.company
payload.client_token = clientStore.token
} else {
payload.client_name = form.client_name
payload.client_phone = form.client_phone
payload.client_email = form.client_email
payload.client_company = form.client_company
}
const { data } = await api.post('/orders', payload)
emit('success', data)
} catch (e) {
error.value = e.response?.data?.detail || 'Ошибка оформления заказа'

View File

@@ -55,6 +55,25 @@
/>
</div>
<!-- Multicolor -->
<div>
<label class="flex items-center gap-3 cursor-pointer rounded-lg border-2 p-3 transition-all"
:class="store.multicolor ? 'border-primary-500 bg-primary-50' : 'border-gray-200 hover:border-gray-300'"
>
<input
type="checkbox"
:checked="store.multicolor"
@change="toggleMulticolor"
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<div>
<span class="text-sm font-medium text-gray-900">Многоцветная печать</span>
<span class="ml-2 text-xs text-gray-400">(AMS, +30% к стоимости)</span>
<p class="text-xs text-gray-500 mt-0.5">Печать несколькими цветами на Bambu Lab AMS</p>
</div>
</label>
</div>
<!-- Post-processing -->
<div>
<label class="mb-2.5 block text-sm font-medium text-gray-700">Постобработка</label>
@@ -88,6 +107,11 @@ const postProcessingOptions = [
{ value: 'acetone_smoothing', label: 'Ацетоновая обработка (ABS)', price: '400 ₽/шт' },
]
function toggleMulticolor() {
store.multicolor = !store.multicolor
store.result = null
}
function togglePP(value) {
const idx = store.settings.post_processing.indexOf(value)
if (idx > -1) {