2 次代碼提交 a55a6d3586 ... b2d41fd537

作者 SHA1 備註 提交日期
  IlhamTahir b2d41fd537 feat: 喂养计划 11 月之前
  IlhamTahir dfadf3a8f0 feat: 产品选择列表 11 月之前
共有 36 個文件被更改,包括 832 次插入49 次删除
  1. 31 7
      pages.config.ts
  2. 1 5
      src/App.vue
  3. 6 0
      src/api/feeding-plan.ts
  4. 1 1
      src/api/pet.ts
  5. 6 0
      src/api/product.ts
  6. 6 0
      src/components.d.ts
  7. 14 0
      src/components/AddFeedingPlan.vue
  8. 28 0
      src/components/BButton.vue
  9. 102 0
      src/components/BSlider.vue
  10. 40 0
      src/components/ProductItem.vue
  11. 40 0
      src/components/ProductReadonlyItem.vue
  12. 38 0
      src/components/SectionCard.vue
  13. 11 0
      src/layouts/custom.vue
  14. 7 0
      src/model/feeding-plan.ts
  15. 2 0
      src/model/pet.ts
  16. 14 0
      src/model/product.ts
  17. 34 10
      src/pages.json
  18. 53 0
      src/pages/feed-plan-calculator/choose-product.vue
  19. 54 0
      src/pages/feed-plan-calculator/confirm.vue
  20. 67 0
      src/pages/feed-plan-calculator/index.vue
  21. 0 0
      src/pages/feed-questionnaire/components/FeedForm.vue
  22. 6 3
      src/pages/feed-questionnaire/components/FeedQuestionnaire.vue
  23. 0 0
      src/pages/feed-questionnaire/components/FeedSlogan.vue
  24. 4 4
      src/pages/feed-questionnaire/components/FeedStart.vue
  25. 0 0
      src/pages/feed-questionnaire/components/FeedStep.vue
  26. 0 0
      src/pages/feed-questionnaire/components/ProgressBar.vue
  27. 3 3
      src/pages/feed-questionnaire/index.vue
  28. 2 2
      src/pages/start-filing/index.vue
  29. 2 2
      src/pages/userInfo/index.vue
  30. 4 0
      src/static/icons/plus-circle-2.svg
  31. 6 0
      src/static/svg/complete-badge.svg
  32. 60 0
      src/static/svg/plate.svg
  33. 127 1
      src/stores/feeding-plan.ts
  34. 36 0
      src/stores/product.ts
  35. 11 8
      src/uni-pages.d.ts
  36. 16 3
      tailwind.config.ts

+ 31 - 7
pages.config.ts

@@ -18,7 +18,7 @@ export default defineUniPages({
       },
     },
     {
-      path: 'pages/feed-calculator/index',
+      path: 'pages/feed-questionnaire/index',
       type: 'page',
       style: {
         navigationBarTitleText: '喂养计算器',
@@ -64,33 +64,57 @@ export default defineUniPages({
       type: 'page',
     },
     {
-      path: 'pages/feed-calculator/components/FeedSlogan',
+      path: 'pages/feed-questionnaire/components/FeedSlogan',
       type: 'page',
     },
     {
-      path: 'pages/feed-calculator/components/FeedForm',
+      path: 'pages/feed-questionnaire/components/FeedForm',
       type: 'page',
     },
     {
-      path: 'pages/feed-calculator/components/FeedQuestionnaire',
+      path: 'pages/feed-questionnaire/components/FeedQuestionnaire',
       type: 'page',
     },
     {
-      path: 'pages/feed-calculator/components/FeedStart',
+      path: 'pages/feed-questionnaire/components/FeedStart',
       type: 'page',
     },
     {
-      path: 'pages/feed-calculator/components/FeedStep',
+      path: 'pages/feed-questionnaire/components/FeedStep',
       type: 'page',
     },
     {
-      path: 'pages/feed-calculator/components/ProgressBar',
+      path: 'pages/feed-questionnaire/components/ProgressBar',
       type: 'page',
     },
     {
       path: 'pages/pet-manual/components/CardList',
       type: 'page',
     },
+    {
+      path: 'pages/feed-plan-calculator/index',
+      type: 'page',
+      style: {
+        navigationBarTitleText: '喂养问卷',
+      },
+      layout: 'custom',
+    },
+    {
+      path: 'pages/feed-plan-calculator/choose-product',
+      type: 'page',
+      style: {
+        navigationBarTitleText: '喂养问卷',
+      },
+      layout: 'custom',
+    },
+    {
+      path: 'pages/feed-plan-calculator/confirm',
+      type: 'page',
+      style: {
+        navigationBarTitleText: '喂养问卷',
+      },
+      layout: 'custom',
+    },
   ],
   globalStyle: {
     backgroundColor: '#fff',

+ 1 - 5
src/App.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { useAppStore } from '@/stores/app'
+import {useAppStore} from '@/stores/app'
 
 onLaunch(() => {
   const authStore = useAppStore()
@@ -8,7 +8,3 @@ onLaunch(() => {
   }
 })
 </script>
-
-<template>
-  <TabBar />
-</template>

+ 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() {

+ 6 - 0
src/api/product.ts

@@ -0,0 +1,6 @@
+import type {Product} from '@/model/product'
+import httpClient from '@/api/httpClient'
+
+export function fetchProductList() {
+  return httpClient.get<Product[]>('/products/list')
+}

+ 6 - 0
src/components.d.ts

@@ -7,11 +7,17 @@ export {}
 
 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']
   }

+ 14 - 0
src/components/AddFeedingPlan.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+
+</script>
+
+<template>
+  <view class="w-full h-[135px] bg-white shadow-cell-group rounded-xl flex flex-col justify-center items-center gap-[5px] text-xs text-disabled">
+    <image src="@/static/icons/plus-circle-2.svg" class="w-[47px] h-[47px]" />
+    添加
+  </view>
+</template>
+
+<style scoped>
+
+</style>

+ 28 - 0
src/components/BButton.vue

@@ -0,0 +1,28 @@
+<script setup lang="ts">
+const props = withDefaults(defineProps<{
+  disabled: boolean
+  type: 'primary' | 'default'
+}>(), {
+  disabled: false,
+  type: 'primary',
+})
+</script>
+
+<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 "
+    :disabled="disabled"
+    :class="{
+      '!bg-disabled !text-white': disabled,
+      'bg-primary button-shadow': !disabled && type === 'primary',
+    }"
+  >
+    <slot />
+  </button>
+</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>

+ 11 - 0
src/layouts/custom.vue

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

+ 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 }[]
 }

+ 14 - 0
src/model/product.ts

@@ -0,0 +1,14 @@
+import type { BaseModel } from '@/model/base'
+
+export interface Product extends BaseModel {
+  name: string
+  category: ProductCategory
+  photo: string
+  tags: string[]
+  totalCalories: number
+  totalWeight: number
+}
+export enum ProductCategory {
+  DRY_FOOD = '干粮',
+  WET_FOOD = '湿粮',
+}

+ 34 - 10
src/pages.json

@@ -8,17 +8,41 @@
   "entryPagePath": "pages/home/index",
   "pages": [
     {
-      "path": "pages/feed-calculator/index",
+      "path": "pages/feed-plan/index",
       "type": "page",
       "style": {
-        "navigationBarTitleText": "喂养计算器"
+        "navigationBarTitleText": "喂养计"
       }
     },
     {
-      "path": "pages/feed-plan/index",
+      "path": "pages/feed-plan-calculator/choose-product",
       "type": "page",
       "style": {
-        "navigationBarTitleText": "喂养计划"
+        "navigationBarTitleText": "喂养问卷"
+      },
+      "layout": "custom"
+    },
+    {
+      "path": "pages/feed-plan-calculator/confirm",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "喂养问卷"
+      },
+      "layout": "custom"
+    },
+    {
+      "path": "pages/feed-plan-calculator/index",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "喂养问卷"
+      },
+      "layout": "custom"
+    },
+    {
+      "path": "pages/feed-questionnaire/index",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "喂养计算器"
       }
     },
     {
@@ -62,32 +86,32 @@
       "style": {}
     },
     {
-      "path": "pages/feed-calculator/components/FeedForm",
+      "path": "pages/feed-questionnaire/components/FeedForm",
       "type": "page",
       "style": {}
     },
     {
-      "path": "pages/feed-calculator/components/FeedQuestionnaire",
+      "path": "pages/feed-questionnaire/components/FeedQuestionnaire",
       "type": "page",
       "style": {}
     },
     {
-      "path": "pages/feed-calculator/components/FeedSlogan",
+      "path": "pages/feed-questionnaire/components/FeedSlogan",
       "type": "page",
       "style": {}
     },
     {
-      "path": "pages/feed-calculator/components/FeedStart",
+      "path": "pages/feed-questionnaire/components/FeedStart",
       "type": "page",
       "style": {}
     },
     {
-      "path": "pages/feed-calculator/components/FeedStep",
+      "path": "pages/feed-questionnaire/components/FeedStep",
       "type": "page",
       "style": {}
     },
     {
-      "path": "pages/feed-calculator/components/ProgressBar",
+      "path": "pages/feed-questionnaire/components/ProgressBar",
       "type": "page",
       "style": {}
     },

+ 53 - 0
src/pages/feed-plan-calculator/choose-product.vue

@@ -0,0 +1,53 @@
+<script setup lang="ts">
+import type { Product } from '@/model/product'
+import { useFeedingPlanStore } from '@/stores/feeding-plan'
+import { useProductStore } from '@/stores/product'
+
+const productStore = useProductStore()
+
+onMounted(() => {
+  productStore.fetchAllProducts()
+})
+
+const feedingPlanStore = useFeedingPlanStore()
+
+function selectProduct(product: Product) {
+  feedingPlanStore.addProductToSelected(product)
+  uni.navigateBack()
+}
+</script>
+
+<template>
+  <view class="w-full h-full overflow-y-auto bg-regular px-4 py-5">
+    <view class="w-full flex gap-2">
+      <view
+        v-for="category in productStore.categoryList" :key="category.value" class="rounded-t-xl px-4 py-[9px] font-semibold" :class="{
+          'bg-white ': category.value === productStore.currentProductCategory,
+          'bg-disabled text-white': category.value !== productStore.currentProductCategory,
+        }"
+        @tap="() => productStore.switchCurrentProductCategory(category.value)"
+      >
+        {{ category.label }}
+      </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" @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">
+            {{ product.name }}
+          </view>
+          <view class="flex gap-2">
+            <view v-for="tag in product.tags" :key="tag" class="px-2 py-0.5 bg-[#f3f3f3] rounded-0.75">
+              {{ tag }}
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style scoped>
+
+</style>

+ 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>

+ 67 - 0
src/pages/feed-plan-calculator/index.vue

@@ -0,0 +1,67 @@
+<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 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">
+          Kcal
+        </text>
+      </view>
+      <view class="font-semibold">
+        总热量
+      </view>
+      <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] 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 class="fixed bottom-6 mb-safe-bottom z-3" :disabled="!feedingPlanStore.selectedProducts.length" @tap="handleNext">
+      下一步
+    </BButton>
+  </view>
+</template>
+
+<style scoped>
+.gradient-text {
+  background: linear-gradient(180deg, #151515 0%, #7B7B7B 100%);
+  background-clip: text;
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+</style>

+ 0 - 0
src/pages/feed-calculator/components/FeedForm.vue → src/pages/feed-questionnaire/components/FeedForm.vue


+ 6 - 3
src/pages/feed-calculator/components/FeedQuestionnaire.vue → src/pages/feed-questionnaire/components/FeedQuestionnaire.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 import type { FeedFormQuestions, PetCard, StepInfo, UserList } from '@/model/pet-manual'
-import FeedForm from '@/pages/feed-calculator/components/FeedForm.vue'
-import FeedSlogan from '@/pages/feed-calculator/components/FeedSlogan.vue'
-import ProgressBar from '@/pages/feed-calculator/components/ProgressBar.vue'
+import FeedForm from '@/pages/feed-questionnaire/components/FeedForm.vue'
+import FeedSlogan from '@/pages/feed-questionnaire/components/FeedSlogan.vue'
+import ProgressBar from '@/pages/feed-questionnaire/components/ProgressBar.vue'
 import avator from '@/static/image/pet-parameters/avatar.png'
 
 const props = defineProps<{
@@ -70,6 +70,9 @@ function handleAnswer(answer: PetCard) {
 
 async function confirm() {
   await feedingPlanStore.persistentPet()
+  uni.navigateTo({
+    url: '/pages/feed-plan-calculator/index',
+  })
 }
 </script>
 

+ 0 - 0
src/pages/feed-calculator/components/FeedSlogan.vue → src/pages/feed-questionnaire/components/FeedSlogan.vue


+ 4 - 4
src/pages/feed-calculator/components/FeedStart.vue → src/pages/feed-questionnaire/components/FeedStart.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import type { StepInfo } from '@/model/pet-manual'
-import FeedStep from '@/pages/feed-calculator/components/FeedStep.vue'
+import type {StepInfo} from '@/model/pet-manual'
+import FeedStep from '@/pages/feed-questionnaire/components/FeedStep.vue'
 import feedTitle from '@/static/image/feed-plan/feed-title.png'
 
 const props = defineProps<{
@@ -24,9 +24,9 @@ function handleReady() {
   </view>
   <FeedFlogan />
   <view class="flex items-center justify-center">
-    <button class="w-[176px] h-[47px] flex items-center justify-center bg-[#4545E5] border-none text-[white] rounded-3xl mt-[87px]" @tap="handleReady">
+    <BButton class="w-[176px] h-[47px] flex items-center justify-center bg-[#4545E5] border-none text-[white] rounded-3xl mt-[87px]" @tap="handleReady">
       准备好了
-    </button>
+    </BButton>
   </view>
 </template>
 

+ 0 - 0
src/pages/feed-calculator/components/FeedStep.vue → src/pages/feed-questionnaire/components/FeedStep.vue


+ 0 - 0
src/pages/feed-calculator/components/ProgressBar.vue → src/pages/feed-questionnaire/components/ProgressBar.vue


+ 3 - 3
src/pages/feed-calculator/index.vue → src/pages/feed-questionnaire/index.vue

@@ -1,9 +1,9 @@
 <script setup lang="ts">
 import type { FeedFormQuestions, UserList } from '@/model/pet-manual'
 import { PetBodyType } from '@/model/pet'
-import FeedQuestionnaire from '@/pages/feed-calculator/components/FeedQuestionnaire.vue'
-import FeedStart from '@/pages/feed-calculator/components/FeedStart.vue'
-import FeedStep from '@/pages/feed-calculator/components/FeedStep.vue'
+import FeedQuestionnaire from '@/pages/feed-questionnaire/components/FeedQuestionnaire.vue'
+import FeedStart from '@/pages/feed-questionnaire/components/FeedStart.vue'
+import FeedStep from '@/pages/feed-questionnaire/components/FeedStep.vue'
 import extremelyObese from '@/static/image/body-type/extremely-obese.svg'
 import extremelyThin from '@/static/image/body-type/extremely-thin.svg'
 import ideal from '@/static/image/body-type/ideal.svg'

+ 2 - 2
src/pages/start-filing/index.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import PickerItem from '@/components/PickerItem.vue'
-import FeedSlogan from '@/pages/feed-calculator/components/FeedSlogan.vue'
+import FeedSlogan from '@/pages/feed-questionnaire/components/FeedSlogan.vue'
 import edit from '@/static/image/start-filing/edit.png'
 import { useFeedingPlanStore } from '@/stores/feeding-plan'
 import ToolApi from '@/utils'
@@ -9,7 +9,7 @@ const safeHeight = ToolApi.getSafeHeight()
 
 const feedSloganBottom = ref<number>(13)
 function handleNext() {
-  uni.navigateTo({ url: '/pages/feed-calculator/index' })
+  uni.navigateTo({ url: '/pages/feed-questionnaire/index' })
 }
 
 const { pet, petTypeOptions, genderOptions } = useFeedingPlanStore()

+ 2 - 2
src/pages/userInfo/index.vue

@@ -31,9 +31,9 @@ import union from '@/static/image/user-info/union.png'
       <uni-list-item show-arrow title="地址" right-text="杭州" />
     </uni-card>
     <view class=" w-full flex justify-center items-center mt-[80px]">
-      <button class="w-[176px] h-[47px] font-normal bg-[#4545E5] text-[18px] rounded-[69px] text-[#fff] flex justify-center items-center">
+      <BButton class="w-[176px] h-[47px] font-normal bg-[#4545E5] text-[18px] rounded-[69px] text-[#fff] flex justify-center items-center">
         确认
-      </button>
+      </BButton>
     </view>
     <view class="mt-[20px] flex justify-center items-center">
       <image

+ 4 - 0
src/static/icons/plus-circle-2.svg

@@ -0,0 +1,4 @@
+<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M21.8214 25.1766V35.248H25.1786V25.1766H35.25V21.8194H25.1786V11.748H21.8214V21.8194H11.75V25.1766H21.8214Z" fill="#D9D9D9"/>
+    <path d="M23.5 46.998C36.4787 46.998 47 36.4767 47 23.498C47 10.5193 36.4787 -0.00201416 23.5 -0.00201416C10.5213 -0.00201416 0 10.5193 0 23.498C0 36.4767 10.5213 46.998 23.5 46.998ZM23.5 43.6408C12.3754 43.6408 3.35714 34.6226 3.35714 23.498C3.35714 12.3734 12.3754 3.35513 23.5 3.35513C34.6246 3.35513 43.6429 12.3734 43.6429 23.498C43.6429 34.6226 34.6246 43.6408 23.5 43.6408Z" fill="#D9D9D9"/>
+</svg>

File diff suppressed because it is too large
+ 6 - 0
src/static/svg/complete-badge.svg


+ 60 - 0
src/static/svg/plate.svg

@@ -0,0 +1,60 @@
+<svg width="314" height="202" viewBox="0 0 314 202" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <g clip-path="url(#clip0_259_3139)">
+        <g filter="url(#filter0_d_259_3139)">
+            <path d="M301.8 164.36C301.8 177.5 236.93 188.16 156.9 188.16C76.87 188.16 12 177.5 12 164.36L42.78 95.93C42.78 106.29 93.81 115.52 156.9 115.52C219.99 115.52 271.03 106.29 271.03 95.93L301.8 164.36Z" fill="url(#paint0_linear_259_3139)"/>
+        </g>
+        <g filter="url(#filter1_d_259_3139)">
+            <path d="M156.9 115.52C219.987 115.52 271.13 107.121 271.13 96.76C271.13 86.3991 219.987 78 156.9 78C93.8124 78 42.6699 86.3991 42.6699 96.76C42.6699 107.121 93.8124 115.52 156.9 115.52Z" fill="#5B5BF4"/>
+        </g>
+        <path d="M156.9 109.72C214.802 109.72 261.74 103.918 261.74 96.76C261.74 89.6024 214.802 83.8 156.9 83.8C98.9985 83.8 52.0601 89.6024 52.0601 96.76C52.0601 103.918 98.9985 109.72 156.9 109.72Z" fill="#4545E5"/>
+        <path d="M164.63 132.34H144.5L138.3 171.5H159.26C166.24 171.5 172.11 167.43 173.3 159.95C174.17 154.45 171.25 152.42 169.81 151.43C171.15 150.6 174.62 148.13 175.33 143.68C176.4 136.92 172.78 132.35 164.64 132.35L164.63 132.34ZM163.75 159.4C163.49 161.05 162.32 162.86 159.68 162.86H153.19H149.28L150.36 156.05H154.27V155.99H160.77C163.41 155.99 164.01 157.75 163.75 159.4ZM165.61 144.17C165.37 145.71 164.28 147.36 161.64 147.36H155.65H151.74L152.75 141H156.66L162.65 140.99C165.29 140.99 165.85 142.64 165.61 144.18V144.17Z" fill="white"/>
+    </g>
+    <g filter="url(#filter2_d_259_3139)">
+        <path d="M157.24 47.59V16.67" stroke="#4545E5" stroke-width="11.34" stroke-miterlimit="10" stroke-linecap="round"/>
+        <path d="M214.62 55.85L236.48 33.98" stroke="#4545E5" stroke-width="11.34" stroke-miterlimit="10" stroke-linecap="round"/>
+        <path d="M99.86 55.85L78 33.98" stroke="#4545E5" stroke-width="11.34" stroke-miterlimit="10" stroke-linecap="round"/>
+    </g>
+    <defs>
+        <filter id="filter0_d_259_3139" x="0" y="84.93" width="313.8" height="116.23" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+            <feFlood flood-opacity="0" result="BackgroundImageFix"/>
+            <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+            <feOffset dy="1"/>
+            <feGaussianBlur stdDeviation="6"/>
+            <feComposite in2="hardAlpha" operator="out"/>
+            <feColorMatrix type="matrix" values="0 0 0 0 0.270588 0 0 0 0 0.270588 0 0 0 0 0.898039 0 0 0 0.6 0"/>
+            <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_259_3139"/>
+            <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_259_3139" result="shape"/>
+        </filter>
+        <filter id="filter1_d_259_3139" x="30.6699" y="67" width="252.46" height="61.52" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+            <feFlood flood-opacity="0" result="BackgroundImageFix"/>
+            <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+            <feOffset dy="1"/>
+            <feGaussianBlur stdDeviation="6"/>
+            <feComposite in2="hardAlpha" operator="out"/>
+            <feColorMatrix type="matrix" values="0 0 0 0 0.270588 0 0 0 0 0.270588 0 0 0 0 0.898039 0 0 0 0.6 0"/>
+            <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_259_3139"/>
+            <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_259_3139" result="shape"/>
+        </filter>
+        <filter id="filter2_d_259_3139" x="60.3301" y="0" width="193.82" height="74.52" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+            <feFlood flood-opacity="0" result="BackgroundImageFix"/>
+            <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+            <feOffset dy="1"/>
+            <feGaussianBlur stdDeviation="6"/>
+            <feComposite in2="hardAlpha" operator="out"/>
+            <feColorMatrix type="matrix" values="0 0 0 0 0.270588 0 0 0 0 0.270588 0 0 0 0 0.898039 0 0 0 0.6 0"/>
+            <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_259_3139"/>
+            <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_259_3139" result="shape"/>
+        </filter>
+        <linearGradient id="paint0_linear_259_3139" x1="131.64" y1="246.3" x2="199.46" y2="27.12" gradientUnits="userSpaceOnUse">
+            <stop stop-color="#6191FF"/>
+            <stop offset="0.13" stop-color="#597CF7"/>
+            <stop offset="0.32" stop-color="#5063EF"/>
+            <stop offset="0.52" stop-color="#4A52E9"/>
+            <stop offset="0.74" stop-color="#4648E6"/>
+            <stop offset="1" stop-color="#4545E5"/>
+        </linearGradient>
+        <clipPath id="clip0_259_3139">
+            <rect width="314" height="135" fill="white" transform="translate(0 67)"/>
+        </clipPath>
+    </defs>
+</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,
   }
 })

+ 36 - 0
src/stores/product.ts

@@ -0,0 +1,36 @@
+import {fetchProductList} from '@/api/product'
+import {type Product, ProductCategory} from '@/model/product'
+import {defineStore} from 'pinia'
+
+export const useProductStore = defineStore('product', () => {
+  const allProducts = ref<Product[]>([])
+  const currentProductCategory = ref<ProductCategory>(ProductCategory.WET_FOOD)
+
+  const categoryList = [
+    { label: '湿粮', value: ProductCategory.WET_FOOD },
+    { label: '干粮', value: ProductCategory.DRY_FOOD },
+
+  ]
+
+  const productList = computed<Product[]>(() => {
+    return allProducts.value.filter(product => product.category === currentProductCategory.value)
+  })
+
+  const switchCurrentProductCategory = (category: ProductCategory) => {
+    currentProductCategory.value = category
+  }
+
+  const fetchAllProducts = async () => {
+    allProducts.value = await fetchProductList()
+  }
+
+  return {
+    allProducts,
+    currentProductCategory,
+    productList,
+    categoryList,
+    switchCurrentProductCategory,
+    fetchAllProducts,
+
+  }
+})

+ 11 - 8
src/uni-pages.d.ts

@@ -4,20 +4,23 @@
 // Generated by vite-plugin-uni-pages
 
 interface NavigateToOptions {
-  url: "/pages/feed-calculator/index" |
-       "/pages/feed-plan/index" |
+  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" |
        "/pages/me/index" |
        "/pages/pet-manual/index" |
        "/pages/setting/index" |
        "/pages/start-filing/index" |
        "/pages/userInfo/index" |
-       "/pages/feed-calculator/components/FeedForm" |
-       "/pages/feed-calculator/components/FeedQuestionnaire" |
-       "/pages/feed-calculator/components/FeedSlogan" |
-       "/pages/feed-calculator/components/FeedStart" |
-       "/pages/feed-calculator/components/FeedStep" |
-       "/pages/feed-calculator/components/ProgressBar" |
+       "/pages/feed-questionnaire/components/FeedForm" |
+       "/pages/feed-questionnaire/components/FeedQuestionnaire" |
+       "/pages/feed-questionnaire/components/FeedSlogan" |
+       "/pages/feed-questionnaire/components/FeedStart" |
+       "/pages/feed-questionnaire/components/FeedStep" |
+       "/pages/feed-questionnaire/components/ProgressBar" |
        "/pages/pet-manual/components/CardList";
 }
 interface RedirectToOptions extends NavigateToOptions {}

+ 16 - 3
tailwind.config.ts

@@ -13,9 +13,22 @@ else {
 }
 
 const theme: Config['theme'] = {
-  colors: {
-    primary: '#4545E5',
-    default: '#828282',
+
+  extend: {
+    colors: {
+      primary: '#4545E5',
+      default: '#828282',
+      disabled: '#D9D9D9',
+    },
+    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)',

Some files were not shown because too many files changed in this diff