Bläddra i källkod

feat: 喂养计划

IlhamTahir 11 månader sedan
förälder
incheckning
b2d41fd537

+ 9 - 1
pages.config.ts

@@ -1,4 +1,4 @@
-import {defineUniPages} from '@uni-helper/vite-plugin-uni-pages'
+import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
 
 export default defineUniPages({
   easycom: {
@@ -107,6 +107,14 @@ export default defineUniPages({
       },
       layout: 'custom',
     },
+    {
+      path: 'pages/feed-plan-calculator/confirm',
+      type: 'page',
+      style: {
+        navigationBarTitleText: '喂养问卷',
+      },
+      layout: 'custom',
+    },
   ],
   globalStyle: {
     backgroundColor: '#fff',

+ 6 - 0
src/api/feeding-plan.ts

@@ -0,0 +1,6 @@
+import type { CreateFeedingPlanRequest, FeedingPlan } from '@/model/pet'
+import httpClient from '@/api/httpClient'
+
+export async function createFeedingPlan(createFeedingPlanRequest: CreateFeedingPlanRequest) {
+  return httpClient.post<FeedingPlan>('/feeding-plans', createFeedingPlanRequest)
+}

+ 1 - 1
src/api/pet.ts

@@ -2,7 +2,7 @@ import type { CreatePetRequest, Pet } from '@/model/pet'
 import httpClient from '@/api/httpClient'
 
 export function createPet(createPetRequest: CreatePetRequest) {
-  return httpClient.post('/pets', createPetRequest)
+  return httpClient.post<Pet>('/pets', createPetRequest)
 }
 
 export function getOwnPets() {

+ 4 - 0
src/components.d.ts

@@ -9,11 +9,15 @@ declare module 'vue' {
   export interface GlobalComponents {
     AddFeedingPlan: typeof import('./components/AddFeedingPlan.vue')['default']
     BButton: typeof import('./components/BButton.vue')['default']
+    BSlider: typeof import('./components/BSlider.vue')['default']
     Cell: typeof import('./components/Cell.vue')['default']
     CellGroup: typeof import('./components/CellGroup.vue')['default']
     PickerDate: typeof import('./components/PickerDate.vue')['default']
     PickerItem: typeof import('./components/PickerItem.vue')['default']
     PopupInput: typeof import('./components/PopupInput.vue')['default']
+    ProductItem: typeof import('./components/ProductItem.vue')['default']
+    ProductReadonlyItem: typeof import('./components/ProductReadonlyItem.vue')['default']
+    SectionCard: typeof import('./components/SectionCard.vue')['default']
     TabBar: typeof import('./components/TabBar.vue')['default']
     TitleBar: typeof import('./components/TitleBar.vue')['default']
   }

+ 5 - 3
src/components/BButton.vue

@@ -10,11 +10,11 @@ const props = withDefaults(defineProps<{
 
 <template>
   <button
-    class="px-[60px] py-[5px]  bg-primary border-none outline-none text-white rounded-3xl flex items-center justify-center text-lg disabled:bg-[red] after:border-none"
+    class="px-[60px] py-[5px]  bg-primary border-none outline-none text-white rounded-3xl flex items-center justify-center text-lg disabled:bg-[red] after:border-none "
     :disabled="disabled"
     :class="{
       '!bg-disabled !text-white': disabled,
-      'bg-primary': !disabled && type === 'primary',
+      'bg-primary button-shadow': !disabled && type === 'primary',
     }"
   >
     <slot />
@@ -22,5 +22,7 @@ const props = withDefaults(defineProps<{
 </template>
 
 <style scoped>
-
+.button-shadow {
+  box-shadow: 0 4px 12px 0 rgba(69, 69, 229, 0.30);
+}
 </style>

+ 102 - 0
src/components/BSlider.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+const props = withDefaults(defineProps<{
+  modelValue: number
+  splitNumber?: number
+  step?: number
+  readonly?: boolean
+}>(), {
+  splitNumber: 9,
+  step: 10,
+  readonly: false,
+})
+
+const emit = defineEmits<{
+  'update:modelValue': [number]
+  'change': [number]
+}>()
+
+// 将内部状态通过 update:modelValue 通知父组件
+function updateValue(val: number) {
+  // 限制取值在 [0, 100] 之间
+  val = Math.max(0, Math.min(val, 100))
+
+  // 通知父组件 (v-model)
+  emit && emit('update:modelValue', val)
+  emit && emit('change', val)
+}
+
+const trackLeft = ref(0) // 轨道的X起点
+const trackWidth = ref(0) // 轨道的宽度
+
+onMounted(() => {
+  const instance = getCurrentInstance()
+  if (!instance)
+    return
+
+  uni.createSelectorQuery().in(instance.proxy).select('.slider-wrapper').boundingClientRect((data) => {
+    if (Array.isArray(data))
+      return
+    trackLeft.value = data.left || 0
+    trackWidth.value = data.width || 0
+  }).exec()
+})
+
+function onTouchStart(e: Event) {
+  e.preventDefault()
+}
+
+function onTouchMove(e: any) {
+  if (props.readonly)
+    return
+  // 1. 获取当前触点X坐标
+  const pageX = e.touches[0].pageX
+
+  // 2. 计算相对于轨道左侧的距离
+  const delta = pageX - trackLeft.value
+
+  // 3. 转化为 0~1 的进度,再*100变成百分比
+  let percent = (delta / trackWidth.value) * 100
+
+  // 4. 对 percent 进行 step 吸附(step默认为10,即0%、10%、20%等)
+  if (props.step > 0) {
+    const stepCount = Math.round(percent / props.step) // 离哪个step更近
+    percent = stepCount * props.step
+  }
+
+  // 5. 更新 currentValue 并emit通知父组件
+  updateValue(percent)
+}
+
+function onTouchEnd(e: Event) {
+  e.preventDefault()
+}
+</script>
+
+<template>
+  <view
+    class="h-[8px] slider-wrapper rounded-full bg-[#E7E7E7] relative flex items-center" @touchstart="onTouchStart"
+    @touchmove="onTouchMove"
+    @touchend="onTouchEnd"
+  >
+    <view class="w-full h-full flex justify-around items-center absolute left-0 top-0 z-0">
+      <view v-for="i of splitNumber" :key="i" class="w-1 h-1.5 bg-disabled" />
+    </view>
+    <view
+      class="h-full absolute left-0 top-0 z-1 bg-primary  rounded-full" :style="{
+        width: `${modelValue}%`,
+      }"
+    />
+    <view
+      v-if="!readonly"
+      class="w-[14px] h-[14px] rounded-full slider-thumb bg-[white] absolute left-0 z-2" :style="{
+        left: `calc(${modelValue}% - 7px)`,
+      }"
+    />
+  </view>
+</template>
+
+<style scoped>
+.slider-thumb {
+  box-shadow: 0 0 6px 0 rgba(69, 69, 229, 0.53);
+}
+</style>

+ 40 - 0
src/components/ProductItem.vue

@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import type { FeedingPlanProduct } from '@/model/feeding-plan'
+
+defineProps<{
+  data: FeedingPlanProduct
+}>()
+</script>
+
+<template>
+  <view class="pl-2.5 py-4 pr-5 rounded-xl bg-white shadow-cell-group flex items-center">
+    <image :src="data.product.photo" class="w-[120px] h-[120px]" />
+    <view class="flex-1 flex flex-col justify-between h-[80px] py-2">
+      <view class="font-semibold">
+        {{ data.product.name }}
+      </view>
+      <view class="flex gap-2">
+        <view v-for="tag in data.product.tags" :key="tag" class="px-2 py-0.5 bg-[#f3f3f3] rounded-0.75 text-xs">
+          {{ tag }}
+        </view>
+      </view>
+    </view>
+    <view class="text-right">
+      <view class="text-xs font-semibold">
+        每日
+      </view>
+      <view class="text-xl font-bold mb-4">
+        {{ data.dailyUsageWeight }}<text class="text-xs font-medium">
+          g
+        </text>
+      </view>
+      <view class="text-[9px] text-[#CDCDCD]">
+        {{ data.product.totalCalories }}Kcal
+      </view>
+    </view>
+  </view>
+</template>
+
+<style scoped>
+
+</style>

+ 40 - 0
src/components/ProductReadonlyItem.vue

@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import type { FeedingPlanProduct } from '@/model/feeding-plan'
+
+defineProps<{
+  data: FeedingPlanProduct
+}>()
+</script>
+
+<template>
+  <view class="py-4 pl-2 pr-8 w-full flex gap-3 relative items-center border-b-[1px] border-[#E7E7E7] last-of-type:border-0">
+    <image :src="data.product.photo" class="w-[80px] h-[80px]" />
+    <view class="flex-1 flex flex-col justify-between h-[80px] py-2">
+      <view class="font-semibold">
+        {{ data.product.name }}
+      </view>
+      <view class="flex gap-2">
+        <view v-for="tag in data.product.tags" :key="tag" class="px-2 py-0.5 bg-[#f3f3f3] rounded-0.75 text-xs">
+          {{ tag }}
+        </view>
+      </view>
+    </view>
+    <view class="text-right">
+      <view class="text-xs font-semibold">
+        每日
+      </view>
+      <view class="text-xl font-bold mb-4">
+        {{ data.dailyUsageWeight }}<text class="text-xs font-medium">
+          g
+        </text>
+      </view>
+      <view class="text-[9px] text-[#CDCDCD]">
+        {{ data.product.totalCalories }}Kcal
+      </view>
+    </view>
+  </view>
+</template>
+
+<style scoped>
+
+</style>

+ 38 - 0
src/components/SectionCard.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+const props = defineProps<{
+  title: string
+  subtitle?: string
+  onOperationClick?: () => void
+  operationTitle?: string
+}>()
+
+function handleTap() {
+  if (props.onOperationClick) {
+    props.onOperationClick()
+  }
+}
+</script>
+
+<template>
+  <view class="w-full  relative rounded-3 overflow-hidden shadow-cell-group bg-[#fff]">
+    <view v-if="operationTitle" class="w-[111px] h-[44px] bg-[#1a1a1a] absolute right-0 top-0" @tap="handleTap">
+      <view class="text-[#999] text-xs absolute right-4 leading-[34px]">
+        {{ operationTitle }}
+      </view>
+    </view>
+    <image class="w-[343px] h-[179px] absolute right-0 top-0 " src="@/static/image/shape-bg.png" />
+    <view class="pl-4 relative flex gap-1 h-[44px] items-center text-xs">
+      <view>{{ title }}</view>
+      <view v-if="subtitle" class="text-[#D9D9D9]">
+        {{ subtitle }}
+      </view>
+    </view>
+    <view class="relative">
+      <slot />
+    </view>
+  </view>
+</template>
+
+<style scoped>
+
+</style>

+ 1 - 1
src/layouts/custom.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts"></script>
 
 <template>
-  <view class="w-screen h-screen">
+  <view class="w-screen h-screen overflow-y-auto pb-safe-bottom relative bg-regular">
     <slot />
   </view>
 </template>

+ 7 - 0
src/model/feeding-plan.ts

@@ -0,0 +1,7 @@
+import type { Product } from '@/model/product'
+
+export interface FeedingPlanProduct {
+  percentage: number
+  dailyUsageWeight: number
+  product: Product
+}

+ 2 - 0
src/model/pet.ts

@@ -60,4 +60,6 @@ export interface FeedingPlan extends BaseModel {
 }
 
 export interface CreateFeedingPlanRequest extends Omit<FeedingPlan, keyof BaseModel> {
+  petId: string
+  products: { id: string, dailyUsageWeight: number }[]
 }

+ 1 - 1
src/model/product.ts

@@ -1,4 +1,4 @@
-import type {BaseModel} from '@/model/base'
+import type { BaseModel } from '@/model/base'
 
 export interface Product extends BaseModel {
   name: string

+ 8 - 0
src/pages.json

@@ -22,6 +22,14 @@
       },
       "layout": "custom"
     },
+    {
+      "path": "pages/feed-plan-calculator/confirm",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "喂养问卷"
+      },
+      "layout": "custom"
+    },
     {
       "path": "pages/feed-plan-calculator/index",
       "type": "page",

+ 10 - 1
src/pages/feed-plan-calculator/choose-product.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import type { Product } from '@/model/product'
+import { useFeedingPlanStore } from '@/stores/feeding-plan'
 import { useProductStore } from '@/stores/product'
 
 const productStore = useProductStore()
@@ -6,6 +8,13 @@ const productStore = useProductStore()
 onMounted(() => {
   productStore.fetchAllProducts()
 })
+
+const feedingPlanStore = useFeedingPlanStore()
+
+function selectProduct(product: Product) {
+  feedingPlanStore.addProductToSelected(product)
+  uni.navigateBack()
+}
 </script>
 
 <template>
@@ -22,7 +31,7 @@ onMounted(() => {
       </view>
     </view>
     <view class="w-full bg-white rounded-tr-xl shadow-cell-group">
-      <view v-for="product in productStore.productList" :key="product.id" class="py-4 px-8 flex gap-3 items-center border-b-1">
+      <view v-for="product in productStore.productList" :key="product.id" class="py-4 px-8 flex gap-3 items-center border-b-1" @tap="() => selectProduct(product)">
         <image :src="product.photo" class="w-[80px] h-[80px]" />
         <view class="h-full flex flex-col gap-4">
           <view class="font-semibold">

+ 54 - 0
src/pages/feed-plan-calculator/confirm.vue

@@ -0,0 +1,54 @@
+<script setup lang="ts">
+import ProductReadonlyItem from '@/components/ProductReadonlyItem.vue'
+
+const feedingPlanStore = useFeedingPlanStore()
+
+async function handleConfirm() {
+  await feedingPlanStore.confirm()
+  uni.navigateTo({
+    url: '/pages/feed-plan/index',
+  })
+}
+</script>
+
+<template>
+  <view class="p-4 flex flex-col items-center gap-4">
+    <view v-if="feedingPlanStore.pet" class="shadow-cell-group w-full px-4 py-8 bg-[white] rounded-3 flex items-center gap-4">
+      <image class="w-[64px] h-[64px] rounded-full" src="@/static/image/pet-default-avatar.png" />
+      <view class="flex-1">
+        <view class="font-semibold mb-1.5">
+          {{ feedingPlanStore.pet.name }}
+        </view>
+        <view class="flex gap-2 flex-wrap">
+          <view v-for="petTag in feedingPlanStore.petTags" :key="petTag.key" class="px-2 py-0.5 bg-[#f3f3f3] rounded-0.75 text-[#000] text-xs">
+            {{ petTag.value }}
+          </view>
+        </view>
+      </view>
+      <image class="w-[99px] h-[101px]" src="@/static/svg/complete-badge.svg" />
+    </view>
+    <SectionCard v-if="feedingPlanStore.selectedProducts.length" class="w-full" title="产品比例" subtitle="FEEDING PLAN" operation-title="重新编辑">
+      <view v-for="(item, index) in feedingPlanStore.selectedProducts" :key="item.product.id" class="p-4 flex flex-col gap-3">
+        <view class="w-full flex justify-between">
+          <view class="font-semibold">
+            {{ item.product.name }}
+          </view>
+          <view class="w-[48px] h-[28px] flex items-center justify-center rounded-1 bg-[#f3f3f3] font-semibold">
+            {{ feedingPlanStore.selectedProducts[index].percentage }}%
+          </view>
+        </view>
+        <BSlider readonly :model-value="item.percentage" @change="(value) => feedingPlanStore.changePercentage(item.product.id, value)" />
+      </view>
+    </SectionCard>
+    <view v-if="feedingPlanStore.selectedProducts.length" class="w-full  relative rounded-3 overflow-hidden shadow-cell-group bg-[#fff]">
+      <ProductReadonlyItem v-for="item in feedingPlanStore.selectedProducts" :key="item.product.id" :data="item" />
+    </view>
+    <BButton class="fixed bottom-6 mb-safe-bottom z-3" @tap="handleConfirm">
+      确认
+    </BButton>
+  </view>
+</template>
+
+<style scoped>
+
+</style>

+ 29 - 5
src/pages/feed-plan-calculator/index.vue

@@ -1,15 +1,25 @@
 <script setup lang="ts">
 import AddFeedingPlan from '@/components/AddFeedingPlan.vue'
+import ProductItem from '@/components/ProductItem.vue'
+import { useFeedingPlanStore } from '@/stores/feeding-plan'
 
 function goToChooseProductPage() {
   uni.navigateTo({
     url: '/pages/feed-plan-calculator/choose-product',
   })
 }
+
+const feedingPlanStore = useFeedingPlanStore()
+
+function handleNext() {
+  uni.navigateTo({
+    url: '/pages/feed-plan-calculator/confirm',
+  })
+}
 </script>
 
 <template>
-  <view class="w-full h-full bg-regular flex flex-col items-center">
+  <view class="w-full bg-regular flex flex-col items-center">
     <view class="mt-[54px] flex flex-col gap-1 mb-[60px] items-center">
       <view class="gradient-text text-[48px] font-bold">
         400<text class="text-[24px] font-semibold">
@@ -19,16 +29,30 @@ function goToChooseProductPage() {
       <view class="font-semibold">
         总热量
       </view>
-      <view class="text-xs text-disabled font-semibold">
+      <view v-if="!feedingPlanStore.selectedProducts.length" class="text-xs text-disabled font-semibold">
         (未添加产品)
       </view>
     </view>
     <image src="@/static/svg/plate.svg" class="w-[289px] h-[176px] mb-[50px]" />
-    <view class="w-full px-4 mb-[52px]">
+    <view class="w-full px-4 mb-[52px] flex flex-col gap-3">
+      <SectionCard v-if="feedingPlanStore.selectedProducts.length" title="产品比例" subtitle="FEEDING PLAN" operation-title="推荐比例">
+        <view v-for="(item, index) in feedingPlanStore.selectedProducts" :key="item.product.id" class="p-4 flex flex-col gap-3">
+          <view class="w-full flex justify-between">
+            <view class="font-semibold">
+              {{ item.product.name }}
+            </view>
+            <view class="w-[48px] h-[28px] flex items-center justify-center rounded-1 bg-[#f3f3f3] font-semibold">
+              {{ feedingPlanStore.selectedProducts[index].percentage }}%
+            </view>
+          </view>
+          <BSlider :model-value="item.percentage" @change="(value) => feedingPlanStore.changePercentage(item.product.id, value)" />
+        </view>
+      </SectionCard>
+      <ProductItem v-for="item in feedingPlanStore.selectedProducts" :key="item.product.id" :data="item" />
       <AddFeedingPlan @tap="goToChooseProductPage" />
     </view>
-    <BButton disabled>
-      确认
+    <BButton class="fixed bottom-6 mb-safe-bottom z-3" :disabled="!feedingPlanStore.selectedProducts.length" @tap="handleNext">
+      下一步
     </BButton>
   </view>
 </template>

+ 3 - 0
src/pages/feed-questionnaire/components/FeedQuestionnaire.vue

@@ -70,6 +70,9 @@ function handleAnswer(answer: PetCard) {
 
 async function confirm() {
   await feedingPlanStore.persistentPet()
+  uni.navigateTo({
+    url: '/pages/feed-plan-calculator/index',
+  })
 }
 </script>
 

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 6 - 0
src/static/svg/complete-badge.svg


+ 127 - 1
src/stores/feeding-plan.ts

@@ -1,9 +1,13 @@
+import type { FeedingPlanProduct } from '@/model/feeding-plan'
+import type { Product } from '@/model/product'
+import { createFeedingPlan } from '@/api/feeding-plan'
 import { createPet } from '@/api/pet'
 import {
   type CreateFeedingPlanRequest,
   type CreatePetRequest,
   FeedingGoal,
   Gender,
+  type Pet,
   PetBodyType,
   PetType,
 } from '@/model/pet'
@@ -20,6 +24,8 @@ export const useFeedingPlanStore = defineStore('feeding-plan', () => {
     { value: Gender.FEMALE, label: '女孩' },
   ]
 
+  const dailyCalories = ref(400)
+
   const pet = ref <CreatePetRequest>({
     birthday: new Date().toISOString().split('T')[0],
     bodyType: PetBodyType.IDEAL,
@@ -34,6 +40,8 @@ export const useFeedingPlanStore = defineStore('feeding-plan', () => {
     weight: 0,
   })
 
+  const savedPet = ref<Pet | null>(null)
+
   const petTags = computed(() => {
     return [
       { key: 'age', value: `${new Date().getFullYear() - new Date(pet.value.birthday).getFullYear()}岁` },
@@ -45,6 +53,8 @@ export const useFeedingPlanStore = defineStore('feeding-plan', () => {
   const feedingPlan = ref<CreateFeedingPlanRequest>({
     feedingGoal: FeedingGoal.LOSE,
     targetWeight: pet.value.weight - 1,
+    petId: savedPet.value?.id || '',
+    products: [],
   })
 
   const feedingGoalOptions = [
@@ -59,11 +69,122 @@ export const useFeedingPlanStore = defineStore('feeding-plan', () => {
   }
 
   const persistentPet = async () => {
-    await createPet({
+    savedPet.value = await createPet({
       ...pet.value,
     })
   }
 
+  /**
+   * 用户选中的商品列表,每个含有 product 信息和 percentage 百分比
+   */
+  const selectedProducts = ref<FeedingPlanProduct[]>([])
+
+  const arrangeDailyConsumeWeight = () => {
+    selectedProducts.value.forEach((item) => {
+      item.dailyUsageWeight = Math.floor((dailyCalories.value * item.percentage / 100) / item.product.totalCalories * item.product.totalWeight)
+    })
+  }
+  /**
+   * 将所有已选商品的 percentage【平分】为 100 / total,
+   * 并用 Math.floor 取整后,将多余 remainder 逐个分配(防止总和小于 100)。
+   */
+  function recalculatePercentage() {
+    const total = selectedProducts.value.length
+    if (total === 0)
+      return
+
+    // 1. 先把 100 整除
+    const base = Math.floor(100 / total) // 每个至少分到多少
+    const remainder = 100 - base * total // 还剩多少未分配
+
+    // 2. 所有人先设置为 base
+    selectedProducts.value.forEach((item) => {
+      item.percentage = base
+    })
+
+    // 3. 余数 remainder 逐个+1 分配给前 remainder 个
+    //    如果你不在意 1% 的误差,可以省略这一步
+    for (let i = 0; i < remainder; i++) {
+      selectedProducts.value[i].percentage += 1
+    }
+  }
+
+  /**
+   * 添加一个新商品
+   * - 如果已存在则直接 return
+   * - 如果不存在则 push 到 selectedProducts,并平均分配
+   */
+  function addProductToSelected(product: Product) {
+    // 如果已存在,不重复添加
+    if (selectedProducts.value.some(item => item.product.id === product.id)) {
+      return
+    }
+    // 新商品默认 0%(稍后 re calc 会统一分配)
+    selectedProducts.value.push({
+      product,
+      percentage: 0,
+      dailyUsageWeight: 0,
+    })
+    // 重新平分所有商品的比例
+    recalculatePercentage()
+    arrangeDailyConsumeWeight()
+  }
+
+  /**
+   * 给除 exceptId 之外的商品,分摊 -delta;
+   * 也就是说,如果 delta 是正,其他商品就要减掉 delta;如果 delta 是负,其他商品就要加上 -delta。
+   *
+   * 注意:这里用 `-=` 实现把 diff 分摊给其他产品
+   */
+  function modifyPercentageExceptId(exceptId: string, delta: number) {
+    selectedProducts.value.forEach((item) => {
+      if (item.product.id !== exceptId) {
+        item.percentage -= delta
+      }
+    })
+  }
+
+  /**
+   * 修改指定 productId 的 percentage
+   * @param productId 产品的id
+   * @param newVal 用户想要设置的新值 (0~100)
+   */
+  function changePercentage(productId: string, newVal: number) {
+    const product = selectedProducts.value.find(item => item.product.id === productId)
+    if (product) {
+      const oldVal = product.percentage
+      const diff = newVal - oldVal
+
+      // 先更新本产品的新值
+      product.percentage = newVal
+
+      // 计算需要从(或给)其他商品分配的增量
+      // total-1 防止只有一个商品时出现除以0
+      const total = selectedProducts.value.length
+      if (total > 1) {
+        const deltaPerProduct = diff / (total - 1)
+        // 其他商品分摊 -deltaPerProduct (实际在函数里 -= deltaPerProduct)
+        modifyPercentageExceptId(productId, deltaPerProduct)
+      }
+      arrangeDailyConsumeWeight()
+    }
+  }
+
+  const confirm = async () => {
+    if (savedPet.value === null) {
+      return
+    }
+    feedingPlan.value.petId = savedPet.value.id
+
+    feedingPlan.value.products = selectedProducts.value.map((item) => {
+      return {
+        id: item.product.id,
+        dailyUsageWeight: item.dailyUsageWeight,
+      }
+    })
+    await createFeedingPlan(feedingPlan.value)
+  }
+
   return {
     pet,
     petTypeOptions,
@@ -73,5 +194,10 @@ export const useFeedingPlanStore = defineStore('feeding-plan', () => {
     petTags,
     feedingPlan,
     feedingGoalOptions,
+    selectedProducts,
+    addProductToSelected,
+    changePercentage,
+    dailyCalories,
+    confirm,
   }
 })

+ 1 - 0
src/uni-pages.d.ts

@@ -6,6 +6,7 @@
 interface NavigateToOptions {
   url: "/pages/feed-plan/index" |
        "/pages/feed-plan-calculator/choose-product" |
+       "/pages/feed-plan-calculator/confirm" |
        "/pages/feed-plan-calculator/index" |
        "/pages/feed-questionnaire/index" |
        "/pages/home/index" |

+ 9 - 3
tailwind.config.ts

@@ -1,6 +1,6 @@
-import type {Config} from 'tailwindcss'
-import {isMp, isQuickapp} from '@uni-helper/uni-env'
-import {basePreset, elementPlusPreset, miniprogramBasePreset} from 'tailwind-extensions'
+import type { Config } from 'tailwindcss'
+import { isMp, isQuickapp } from '@uni-helper/uni-env'
+import { basePreset, elementPlusPreset, miniprogramBasePreset } from 'tailwind-extensions'
 
 const presets: Config['presets'] = [basePreset]
 if (isMp || isQuickapp) {
@@ -23,6 +23,12 @@ const theme: Config['theme'] = {
     backgroundColor: {
       regular: '#F5F6F7',
     },
+    spacing: {
+      'safe-top': 'env(safe-area-inset-top)',
+      'safe-bottom': 'env(safe-area-inset-bottom)',
+      'safe-left': 'env(safe-area-inset-left)',
+      'safe-right': 'env(safe-area-inset-right)',
+    },
   },
   boxShadow: {
     'cell-group': '0px 4px 12px 0px rgba(0, 0, 0, 0.10)',

Vissa filer visades inte eftersom för många filer har ändrats