修复bug
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
appDiningProfileConfigGet,
|
||||
appDiningProfileGet,
|
||||
appDiningProfileSavePost,
|
||||
} from '@/service'
|
||||
const { t } = useI18n()
|
||||
|
||||
type Option = { id: string; label: string }
|
||||
type Field = {
|
||||
fieldKey: string
|
||||
fieldName: string
|
||||
groupKey: string
|
||||
groupName: string
|
||||
sortNum: number
|
||||
isRequired: boolean
|
||||
multiple: boolean
|
||||
options: Option[]
|
||||
}
|
||||
type Group = {
|
||||
key: string
|
||||
title: string
|
||||
sortNum: number
|
||||
fields: Field[]
|
||||
}
|
||||
type SelectedMap = Record<string, string[]>
|
||||
type ProfileMap = Record<string, unknown>
|
||||
|
||||
const PREF_FLAG_KEY = 'ai_diet_pref_done'
|
||||
const PREF_DATA_KEY = 'ai_diet_pref_data'
|
||||
|
||||
const loading = ref(false)
|
||||
const groups = ref<Group[]>([])
|
||||
const selectedMap = ref<SelectedMap>({})
|
||||
|
||||
function getFallbackGroups(): Group[] {
|
||||
return [
|
||||
{
|
||||
key: 'taste',
|
||||
title: t('pages.ai.dietPreference.fallback.groupTaste'),
|
||||
sortNum: 1,
|
||||
fields: [
|
||||
{
|
||||
fieldKey: 'taste_like',
|
||||
fieldName: t('pages.ai.dietPreference.fallback.tasteLike'),
|
||||
groupKey: 'taste',
|
||||
groupName: t('pages.ai.dietPreference.fallback.groupTaste'),
|
||||
sortNum: 1,
|
||||
isRequired: false,
|
||||
multiple: false,
|
||||
options: [
|
||||
{ id: 'zkw', label: t('pages.ai.dietPreference.fallback.optionHeavy') },
|
||||
{ id: 'qd', label: t('pages.ai.dietPreference.fallback.optionLight') },
|
||||
{ id: 'ml', label: t('pages.ai.dietPreference.fallback.optionSpicy') },
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldKey: 'taste_taboo',
|
||||
fieldName: t('pages.ai.dietPreference.fallback.tasteTaboo'),
|
||||
groupKey: 'taste',
|
||||
groupName: t('pages.ai.dietPreference.fallback.groupTaste'),
|
||||
sortNum: 2,
|
||||
isRequired: false,
|
||||
multiple: true,
|
||||
options: [
|
||||
{ id: 'seafood', label: t('pages.ai.dietPreference.fallback.optionSeafood') },
|
||||
{ id: 'organ', label: t('pages.ai.dietPreference.fallback.optionOrgan') },
|
||||
{ id: 'onion', label: t('pages.ai.dietPreference.fallback.optionOnion') },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const hasGroups = computed(() => groups.value.length > 0)
|
||||
|
||||
function goPreference() {
|
||||
const current = getCurrentPages?.().slice(-1)[0]
|
||||
if (current?.route === 'pages/ai/diet-preference/index') return
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai/diet-preference/index',
|
||||
})
|
||||
}
|
||||
|
||||
function parseOptionsJson(optionsJson: unknown): Option[] {
|
||||
if (!optionsJson) return []
|
||||
let rawList: any[] = []
|
||||
if (Array.isArray(optionsJson)) {
|
||||
rawList = optionsJson
|
||||
} else if (typeof optionsJson === 'string') {
|
||||
try {
|
||||
rawList = JSON.parse(optionsJson)
|
||||
} catch (e) {
|
||||
rawList = []
|
||||
}
|
||||
}
|
||||
return rawList.map((opt, idx) => ({
|
||||
id: String(opt?.k ?? opt?.key ?? opt?.id ?? idx),
|
||||
label: String(opt?.v ?? opt?.value ?? opt?.name ?? `选项${idx + 1}`),
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizeConfig(data: any): Group[] {
|
||||
const list = Array.isArray(data) ? data : Array.isArray(data?.list) ? data.list : []
|
||||
if (!list.length) return getFallbackGroups()
|
||||
|
||||
const groupMap = new Map<string, Group>()
|
||||
list.forEach((item: any, idx: number) => {
|
||||
const groupKey = String(item?.groupKey ?? `group_${idx}`)
|
||||
const groupName = String(item?.groupName ?? `分组${idx + 1}`)
|
||||
const groupSort = Number(item?.sortNum ?? idx)
|
||||
const fieldKey = String(item?.fieldKey ?? `${groupKey}_field_${idx}`)
|
||||
const options = parseOptionsJson(item?.optionsJson)
|
||||
if (!options.length) return
|
||||
|
||||
const field: Field = {
|
||||
fieldKey,
|
||||
fieldName: String(item?.fieldName ?? fieldKey),
|
||||
groupKey,
|
||||
groupName,
|
||||
sortNum: Number(item?.sortNum ?? idx),
|
||||
isRequired: Number(item?.isRequired ?? 0) === 1,
|
||||
multiple: Number(item?.multiSelect ?? 0) === 1,
|
||||
options,
|
||||
}
|
||||
|
||||
if (!groupMap.has(groupKey)) {
|
||||
groupMap.set(groupKey, {
|
||||
key: groupKey,
|
||||
title: groupName,
|
||||
sortNum: groupSort,
|
||||
fields: [field],
|
||||
})
|
||||
} else {
|
||||
groupMap.get(groupKey)!.fields.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
const groupsBySort = Array.from(groupMap.values())
|
||||
.map((group) => ({
|
||||
...group,
|
||||
fields: group.fields.sort((a, b) => a.sortNum - b.sortNum),
|
||||
}))
|
||||
.sort((a, b) => a.sortNum - b.sortNum)
|
||||
|
||||
return groupsBySort.length ? groupsBySort : getFallbackGroups()
|
||||
}
|
||||
|
||||
function collectSelectedKeys(profileData: any): Record<string, Set<string>> {
|
||||
const map: Record<string, Set<string>> = {}
|
||||
if (!profileData || typeof profileData !== 'object') return map
|
||||
|
||||
const normalizeValues = (val: unknown): string[] => {
|
||||
if (Array.isArray(val)) return val.map((v) => String(v))
|
||||
if (val == null || val === '') return []
|
||||
// 兼容后端返回 "a,b,c" 形式
|
||||
if (typeof val === 'string' && val.includes(',')) {
|
||||
return val.split(',').map((v) => v.trim()).filter(Boolean)
|
||||
}
|
||||
return [String(val)]
|
||||
}
|
||||
|
||||
Object.keys(profileData).forEach((key) => {
|
||||
map[key] = new Set(normalizeValues(profileData[key]))
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
function initSelectionByGroups(profileData: any) {
|
||||
const selectedFromServer = collectSelectedKeys(profileData)
|
||||
const nextMap: SelectedMap = {}
|
||||
groups.value.forEach((group) => {
|
||||
group.fields.forEach((field) => {
|
||||
const matched =
|
||||
selectedFromServer[field.fieldKey] ||
|
||||
selectedFromServer[field.fieldName] ||
|
||||
selectedFromServer[field.fieldKey.toLowerCase()]
|
||||
if (matched && matched.size > 0) {
|
||||
nextMap[field.fieldKey] = field.options
|
||||
.map((o) => o.id)
|
||||
.filter((id) => matched.has(id) || matched.has(String(id)))
|
||||
} else {
|
||||
nextMap[field.fieldKey] = []
|
||||
}
|
||||
})
|
||||
})
|
||||
selectedMap.value = nextMap
|
||||
}
|
||||
|
||||
function toggleOption(field: Field, optionId: string) {
|
||||
const arr = selectedMap.value[field.fieldKey] || []
|
||||
const idx = arr.indexOf(optionId)
|
||||
if (field.multiple) {
|
||||
if (idx >= 0) arr.splice(idx, 1)
|
||||
else arr.push(optionId)
|
||||
} else {
|
||||
selectedMap.value[field.fieldKey] = idx >= 0 ? [] : [optionId]
|
||||
return
|
||||
}
|
||||
selectedMap.value[field.fieldKey] = [...arr]
|
||||
}
|
||||
|
||||
function isSelected(groupKey: string, optionId: string) {
|
||||
return (selectedMap.value[groupKey] || []).includes(optionId)
|
||||
}
|
||||
|
||||
function buildSavePayload() {
|
||||
const fields: ProfileMap = {}
|
||||
groups.value.forEach((group) => {
|
||||
group.fields.forEach((field) => {
|
||||
const values = selectedMap.value[field.fieldKey] || []
|
||||
if (!values.length) return
|
||||
fields[field.fieldKey] = field.multiple ? values : values[0]
|
||||
})
|
||||
})
|
||||
return { fields }
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [configRes, profileRes] = await Promise.all([
|
||||
appDiningProfileConfigGet({}),
|
||||
appDiningProfileGet({}),
|
||||
])
|
||||
groups.value = normalizeConfig(configRes?.data)
|
||||
initSelectionByGroups(profileRes?.data)
|
||||
} catch (e) {
|
||||
groups.value = getFallbackGroups()
|
||||
initSelectionByGroups({})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleComplete() {
|
||||
try {
|
||||
const body = buildSavePayload()
|
||||
await appDiningProfileSavePost({ body })
|
||||
uni.setStorageSync(PREF_DATA_KEY, selectedMap.value)
|
||||
uni.setStorageSync(PREF_FLAG_KEY, true)
|
||||
uni.redirectTo({
|
||||
url: '/pages/ai/chat/index',
|
||||
})
|
||||
} catch (e) {
|
||||
// 保存失败时沿用项目全局错误提示
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page">
|
||||
<navbar :title="t('navbar-ai-diet-preference')">
|
||||
</navbar>
|
||||
<view class="header">
|
||||
<view class="header-title" style="display: flex; align-items: center;">
|
||||
<image src="/static/app/images/ai-diet-preference-title.png" class="w-44rpx h-44rpx ml-8rpx shrink-0"></image>
|
||||
<text class="title">{{ t('pages.ai.dietPreference.title') }}</text>
|
||||
</view>
|
||||
<text class="sub">{{ t('pages.ai.dietPreference.sub') }}</text>
|
||||
</view>
|
||||
|
||||
<view style="padding: 22rpx;">
|
||||
|
||||
<view v-if="loading" class="section">
|
||||
<view class="section-title">{{ t('pages.ai.dietPreference.loadingConfig') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!hasGroups" class="section">
|
||||
<view class="section-title">{{ t('pages.ai.dietPreference.empty') }}</view>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="section" v-for="group in groups" :key="group.key">
|
||||
<view class="section-title">{{ group.title }}</view>
|
||||
<view class="field-block" v-for="field in group.fields" :key="field.fieldKey">
|
||||
<view class="field-title">
|
||||
<text>{{ field.fieldName }}</text>
|
||||
<text v-if="field.isRequired" class="required">*</text>
|
||||
<text class="mode-tag">{{ field.multiple ? t('pages.ai.dietPreference.multi') : t('pages.ai.dietPreference.single') }}</text>
|
||||
</view>
|
||||
<view class="chips">
|
||||
<view
|
||||
v-for="opt in field.options"
|
||||
:key="opt.id"
|
||||
class="chip"
|
||||
:class="isSelected(field.fieldKey, opt.id) ? 'chip--active' : ''"
|
||||
@click="toggleOption(field, opt.id)"
|
||||
>
|
||||
<text>{{ opt.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="footer">
|
||||
<button class="btn" @click="handleComplete">
|
||||
{{ t('pages.ai.dietPreference.submit') }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page {
|
||||
background: #f2f2f2;
|
||||
min-height: 100vh;
|
||||
// padding: 24rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #EFFDF5;
|
||||
// border-radius: 20rpx;
|
||||
padding: 22rpx 22rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #065E46;
|
||||
display: block;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.sub {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #027857;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 22rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 22rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.field-block + .field-block {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.field-title {
|
||||
margin-bottom: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #e23636;
|
||||
}
|
||||
|
||||
.mode-tag {
|
||||
color: #999;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 14rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f6f6f6;
|
||||
color: #333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: #14181b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 28rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
border-radius: 16rpx;
|
||||
background: #14181b;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
padding: 18rpx 0;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
font-size: 24rpx;
|
||||
color: #14181b;
|
||||
padding: 8rpx 0 8rpx 16rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user