Compare commits

..

10 Commits

Author SHA1 Message Date
8537e6d956 feat:更新字体文件 2025-10-16 21:46:49 +08:00
0fa8cfb79f feat:增加Docker部署 2025-10-16 17:35:45 +08:00
80139c54d1 feat:更新朋友圈UI 2025-07-18 17:25:29 +08:00
b4ebc0e35b feat:增加颜色工具类 2025-07-04 15:33:19 +08:00
45a129ea94 feat:更新主题色功能 2025-07-02 22:43:55 +08:00
1d0cb061d0 feat:增加菜谱收藏功能 2025-07-02 19:55:18 +08:00
1037edeed4 feat:增加查询用户菜谱记录功能 2025-07-01 22:42:25 +08:00
b6f7869fe8 feat:更新菜谱UI 2025-07-01 20:02:08 +08:00
be8e105a55 feat:更新个人中心UI 2025-06-30 22:52:28 +08:00
50991bb49a feat:更新UI 2025-06-30 20:04:02 +08:00
40 changed files with 880 additions and 392 deletions

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM nginx:1.27.0
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
RUN chmod -R 755 /usr/share/nginx/html/

23
nginx.conf Normal file
View File

@@ -0,0 +1,23 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /usr/share/nginx/html;
index index.html;
server_name _;
location /food-hub/ {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /food-hub/index.html;
}
location /food-api/ {
proxy_pass http://host.docker.internal:8083/;
}
location / {
try_files $uri $uri/ =404;
}
}

16
pnpm-lock.yaml generated
View File

@@ -22,7 +22,7 @@ importers:
version: 3.5.17(typescript@5.8.3) version: 3.5.17(typescript@5.8.3)
vue3-common: vue3-common:
specifier: git+https://gitee.com/Cxx0822/vue3-common specifier: git+https://gitee.com/Cxx0822/vue3-common
version: git+https://gitee.com/Cxx0822/vue3-common#4cabccc268f404bef9fd13078ff88fc2830b7461(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint-config-standard@16.0.3(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint@7.32.0))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint-plugin-vue@8.7.1(eslint@7.32.0))(eslint@7.32.0)(pinia@2.3.1(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3)) version: git+https://gitee.com/Cxx0822/vue3-common#0d4a3beb7b1bfb068fe6672c25839997cd8d2129(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint-config-standard@16.0.3(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint@7.32.0))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint-plugin-vue@8.7.1(eslint@7.32.0))(eslint@7.32.0)(pinia@2.3.1(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3))
vue3-emoji-picker: vue3-emoji-picker:
specifier: ^1.1.8 specifier: ^1.1.8
version: 1.1.8(typescript@5.8.3) version: 1.1.8(typescript@5.8.3)
@@ -1220,8 +1220,8 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
form-data@4.0.3: form-data@4.0.4:
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
fs.realpath@1.0.0: fs.realpath@1.0.0:
@@ -2022,8 +2022,8 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=5.0.0' typescript: '>=5.0.0'
vue3-common@git+https://gitee.com/Cxx0822/vue3-common#4cabccc268f404bef9fd13078ff88fc2830b7461: vue3-common@git+https://gitee.com/Cxx0822/vue3-common#0d4a3beb7b1bfb068fe6672c25839997cd8d2129:
resolution: {commit: 4cabccc268f404bef9fd13078ff88fc2830b7461, repo: https://gitee.com/Cxx0822/vue3-common, type: git} resolution: {commit: 0d4a3beb7b1bfb068fe6672c25839997cd8d2129, repo: https://gitee.com/Cxx0822/vue3-common, type: git}
version: 1.0.0 version: 1.0.0
peerDependencies: peerDependencies:
pinia: ^2.3.0 pinia: ^2.3.0
@@ -2727,7 +2727,7 @@ snapshots:
axios@1.10.0: axios@1.10.0:
dependencies: dependencies:
follow-redirects: 1.15.9 follow-redirects: 1.15.9
form-data: 4.0.3 form-data: 4.0.4
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
@@ -3284,7 +3284,7 @@ snapshots:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
form-data@4.0.3: form-data@4.0.4:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
combined-stream: 1.0.8 combined-stream: 1.0.8
@@ -4119,7 +4119,7 @@ snapshots:
'@vue/language-core': 2.2.10(typescript@5.8.3) '@vue/language-core': 2.2.10(typescript@5.8.3)
typescript: 5.8.3 typescript: 5.8.3
vue3-common@git+https://gitee.com/Cxx0822/vue3-common#4cabccc268f404bef9fd13078ff88fc2830b7461(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint-config-standard@16.0.3(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint@7.32.0))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint-plugin-vue@8.7.1(eslint@7.32.0))(eslint@7.32.0)(pinia@2.3.1(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3)): vue3-common@git+https://gitee.com/Cxx0822/vue3-common#0d4a3beb7b1bfb068fe6672c25839997cd8d2129(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0)(typescript@5.8.3))(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint-config-standard@16.0.3(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint@7.32.0))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.62.0(eslint@7.32.0)(typescript@5.8.3))(eslint@7.32.0))(eslint-plugin-node@11.1.0(eslint@7.32.0))(eslint-plugin-promise@5.2.0(eslint@7.32.0))(eslint-plugin-vue@8.7.1(eslint@7.32.0))(eslint@7.32.0)(pinia@2.3.1(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3)))(vue-router@4.5.1(vue@3.5.17(typescript@5.8.3)))(vue@3.5.17(typescript@5.8.3)):
dependencies: dependencies:
'@fortawesome/fontawesome-free': 6.7.2 '@fortawesome/fontawesome-free': 6.7.2
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8

View File

@@ -4,33 +4,45 @@ import type { IStatsChart } from 'vue3-common/types'
export const addRecipeApi = (recipe: IRecipe): Promise<boolean> => export const addRecipeApi = (recipe: IRecipe): Promise<boolean> =>
cloudService({ cloudService({
url: '/food-service/food/recipe', url: '/food-api/food/recipe',
method: 'post', method: 'post',
data: recipe data: recipe
}) })
export const updateRecipeApi = (id: number, recipe: IRecipe): Promise<boolean> => export const updateRecipeApi = (id: number, recipe: IRecipe): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/food/recipe/${id}`, url: `/food-api/food/recipe/${id}`,
method: 'put', method: 'put',
data: recipe data: recipe
}) })
export const deleteRecipeApi = (id: number): Promise<boolean> => export const deleteRecipeApi = (id: number): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/food/recipe/${id}`, url: `/food-api/food/recipe/${id}`,
method: 'DELETE' method: 'DELETE'
}) })
export const queryRecipeByIdApi = (id: number): Promise<IRecipe> => export const queryRecipeByIdApi = (id: number): Promise<IRecipe> =>
cloudService({ cloudService({
url: `/food-service/food/recipe/${id}`, url: `/food-api/food/recipe/${id}`,
method: 'get'
})
export const queryRecipeByUserApi = (id: number): Promise<IRecipe[]> =>
cloudService({
url: `/food-api/food/recipe/user/${id}`,
method: 'get'
})
export const queryRecipeUserFavouriteApi = (): Promise<IRecipe[]> =>
cloudService({
url: '/food-api/food/recipe/user/favourite',
method: 'get' method: 'get'
}) })
export const queryRecipeApi = (query: IFoodQuery): Promise<IRecipe[]> => export const queryRecipeApi = (query: IFoodQuery): Promise<IRecipe[]> =>
cloudService({ cloudService({
url: '/food-service/food/recipe', url: '/food-api/food/recipe',
method: 'get', method: 'get',
params: { ...query } params: { ...query }
}) })
@@ -38,70 +50,101 @@ export const queryRecipeApi = (query: IFoodQuery): Promise<IRecipe[]> =>
export const queryRecipeByPageApi = (currentPage: number, pageSize: number, export const queryRecipeByPageApi = (currentPage: number, pageSize: number,
query: IFoodQuery): Promise<any> => query: IFoodQuery): Promise<any> =>
cloudService({ cloudService({
url: '/food-service/food/recipe/page', url: '/food-api/food/recipe/page',
method: 'get', method: 'get',
params: { currentPage, pageSize, ...query } params: { currentPage, pageSize, ...query }
}) })
export const queryFoodNameListApi = (): Promise<string[]> => export const queryFoodNameListApi = (): Promise<string[]> =>
cloudService({ cloudService({
url: '/food-service/food/recipe/name', url: '/food-api/food/recipe/name',
method: 'get' method: 'get'
}) })
export const addRecordApi = (record: IRecord): Promise<boolean> => export const addRecordApi = (record: IRecord): Promise<boolean> =>
cloudService({ cloudService({
url: '/food-service/food/record', url: '/food-api/food/record',
method: 'post', method: 'post',
data: record data: record
}) })
export const updateRecordApi = (id: number, record: IRecord): Promise<boolean> => export const updateRecordApi = (id: number, record: IRecord): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/food/record/${id}`, url: `/food-api/food/record/${id}`,
method: 'put', method: 'put',
data: record data: record
}) })
export const deleteRecordApi = (id: number): Promise<boolean> => export const deleteRecordApi = (id: number): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/food/record/${id}`, url: `/food-api/food/record/${id}`,
method: 'DELETE' method: 'DELETE'
}) })
export const addRecipeCommentApi = (id: number, content: string): Promise<boolean> =>
cloudService({
url: `/food-api/food/recipe/${id}/comment`,
method: 'post',
params: { content }
})
export const addRecipeLikeApi = (id: number): Promise<boolean> =>
cloudService({
url: `/food-api/food/recipe/${id}/like`,
method: 'post'
})
export const deleteRecipeLikeApi = (id: number): Promise<boolean> =>
cloudService({
url: `/food-api/food/recipe/${id}/like`,
method: 'delete'
})
export const addRecipeFavouriteApi = (id: number): Promise<boolean> =>
cloudService({
url: `/food-api/food/recipe/${id}/favourite`,
method: 'post'
})
export const deleteRecipeFavouriteApi = (id: number): Promise<boolean> =>
cloudService({
url: `/food-api/food/recipe/${id}/favourite`,
method: 'delete'
})
export const queryRecordApi = (startDate: string, endDate: string): Promise<IRecord[]> => export const queryRecordApi = (startDate: string, endDate: string): Promise<IRecord[]> =>
cloudService({ cloudService({
url: '/food-service/food/record', url: '/food-api/food/record',
method: 'get', method: 'get',
params: { startDate, endDate } params: { startDate, endDate }
}) })
export const queryCategoryApi = (): Promise<string[]> => export const queryCategoryApi = (): Promise<string[]> =>
cloudService({ cloudService({
url: '/food-service/food/category', url: '/food-api/food/category',
method: 'get' method: 'get'
}) })
export const queryStatsApi = (): Promise<IStats> => export const queryStatsApi = (): Promise<IStats> =>
cloudService({ cloudService({
url: '/food-service/food/stats', url: '/food-api/food/stats',
method: 'get' method: 'get'
}) })
export const queryRecordStatsApi = (): Promise<IStatsChart[]> => export const queryRecordStatsApi = (): Promise<IStatsChart[]> =>
cloudService({ cloudService({
url: '/food-service/food/stats/record', url: '/food-api/food/stats/record',
method: 'get' method: 'get'
}) })
export const queryCategoryStatsApi = (): Promise<IStatsChart[]> => export const queryCategoryStatsApi = (): Promise<IStatsChart[]> =>
cloudService({ cloudService({
url: '/food-service/food/stats/category', url: '/food-api/food/stats/category',
method: 'get' method: 'get'
}) })
export const queryRankStatsApi = (): Promise<IStatsChart[]> => export const queryRankStatsApi = (): Promise<IStatsChart[]> =>
cloudService({ cloudService({
url: '/food-service/food/stats/rank', url: '/food-api/food/stats/rank',
method: 'get' method: 'get'
}) })

View File

@@ -3,45 +3,51 @@ import type { IMoment } from '@/types/food'
export const addMomentApi = (moment: IMoment): Promise<boolean> => export const addMomentApi = (moment: IMoment): Promise<boolean> =>
cloudService({ cloudService({
url: '/food-service/moment', url: '/food-api/moment',
method: 'post', method: 'post',
data: moment data: moment
}) })
export const updateMomentApi = (id: number, moment: IMoment): Promise<boolean> => export const updateMomentApi = (id: number, moment: IMoment): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/moment/${id}`, url: `/food-api/moment/${id}`,
method: 'put', method: 'put',
data: moment data: moment
}) })
export const deleteMomentApi = (id: number): Promise<boolean> => export const deleteMomentApi = (id: number): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/moment/${id}`, url: `/food-api/moment/${id}`,
method: 'delete' method: 'delete'
}) })
export const queryMomentApi = (): Promise<IMoment[]> => export const queryMomentListApi = (): Promise<IMoment[]> =>
cloudService({ cloudService({
url: '/food-service/moment', url: '/food-api/moment',
method: 'get' method: 'get'
}) })
export const addCommentApi = (id: number, content: string): Promise<boolean> => export const queryMomentByIdApi = (id: number): Promise<IMoment[]> =>
cloudService({ cloudService({
url: `/food-service/moment/${id}/comment`, url: `/food-api/moment/${id}`,
method: 'get'
})
export const addMomentCommentApi = (id: number, content: string): Promise<boolean> =>
cloudService({
url: `/food-api/moment/${id}/comment`,
method: 'post', method: 'post',
params: { content } params: { content }
}) })
export const addLikeApi = (id: number): Promise<boolean> => export const addMomentLikeApi = (id: number): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/moment/${id}/like`, url: `/food-api/moment/${id}/like`,
method: 'post' method: 'post'
}) })
export const deleteLikeApi = (id: number): Promise<boolean> => export const deleteMomentLikeApi = (id: number): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/moment/${id}/like`, url: `/food-api/moment/${id}/like`,
method: 'delete' method: 'delete'
}) })

View File

@@ -3,14 +3,14 @@ import type { ISession } from '@/types/auth'
export const loginApi = (username: string, password: string): Promise<ISession> => export const loginApi = (username: string, password: string): Promise<ISession> =>
cloudService({ cloudService({
url: '/food-service/session', url: '/food-api/session',
method: 'post', method: 'post',
params: { username, password } params: { username, password }
}) })
export const logoutApi = (username: string): Promise<ISession> => export const logoutApi = (username: string): Promise<ISession> =>
cloudService({ cloudService({
url: '/food-service/session', url: '/food-api/session',
method: 'delete', method: 'delete',
params: { username } params: { username }
}) })

View File

@@ -3,20 +3,20 @@ import type { ISession, IUser } from '@/types/auth'
export const registerApi = (username: string, password: string): Promise<ISession> => export const registerApi = (username: string, password: string): Promise<ISession> =>
cloudService({ cloudService({
url: '/food-service/user', url: '/food-api/user',
method: 'post', method: 'post',
params: { username, password } params: { username, password }
}) })
export const queryUserApi = (id: number): Promise<IUser> => export const queryUserApi = (id: number): Promise<IUser> =>
cloudService({ cloudService({
url: `/food-service/user/${id}`, url: `/food-api/user/${id}`,
method: 'get' method: 'get'
}) })
export const updateUserApi = (id: number, user: IUser): Promise<boolean> => export const updateUserApi = (id: number, user: IUser): Promise<boolean> =>
cloudService({ cloudService({
url: `/food-service/user/${id}`, url: `/food-api/user/${id}`,
method: 'put', method: 'put',
data: user data: user
}) })

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="life-record-card card-item"> <div class="life-record-card card-item">
<el-carousel v-if="props.foodItem?.recordList.length > 0" <el-carousel v-if="props.item?.recordList.length > 0"
arrow="always" @change="changeFoodEvent"> arrow="always" @change="changeFoodEvent">
<el-carousel-item v-for="(item, index) in getRecord(props.foodItem?.recordList)" <el-carousel-item v-for="(item, index) in getRecord(props.item?.recordList)"
:key="index"> :key="index">
<el-image :src="serviceFileRootPath + item.imageUrl" <el-image :src="serviceFileRootPath + item.imageUrl"
fit="contain" alt="暂无成果"> fit="contain" alt="暂无成果">
@@ -21,32 +21,50 @@
<div class="top-container"> <div class="top-container">
<div class="info-list"> <div class="info-list">
<div class="info-item"> <div class="info-item">
<span class="title">{{ props.foodItem.name }}</span> <span class="title">{{ props.item.name }}</span>
</div> </div>
</div> </div>
<div class="button-info"> <div class="button-info">
<el-button type="primary" :icon="Edit" circle :size="'small'" <el-button v-if="isCurrentUser"
type="primary" :icon="Edit" circle :size="'small'"
@click="editClick"/> @click="editClick"/>
<el-button type="primary" :icon="Collection" circle :size="'small'" <el-button type="primary" :icon="Collection" circle :size="'small'"
@click="viewClick()" /> @click="viewClick()" />
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'" <el-button v-if="isCurrentUser"
type="danger" :icon="DeleteFilled" circle :size="'small'"
@click="deleteClick()" /> @click="deleteClick()" />
</div> </div>
</div> </div>
<div class="bottom-container"> <div class="bottom-container">
<div class="info-list"> <div class="info-list">
<div class="info-item category-item"> <el-avatar v-if="!isCurrentUser" :src="avatarUrl" :size="20" fit="contain"/>
<span v-if="!isCurrentUser" class="user">{{ props.item.username }}</span>
<div v-if="isCurrentUser" class="info-item category-item">
<i class="fa-solid fa-utensils"></i> <i class="fa-solid fa-utensils"></i>
{{ props.foodItem.category }} {{ props.item.category }}
</div>
<div v-if="isCurrentUser" class="info-item like-item">
<i class="fa-regular fa-thumbs-up"></i>
{{ props.item.likeCount }}
</div>
<div v-if="isCurrentUser" class="info-item comment-item">
<i class="fa-regular fa-comment"></i>
{{ props.item.commentCount }}
</div>
<div v-if="isCurrentUser" class="info-item start-item">
<i class="fa-solid fa-star"></i>
{{ props.item.favouriteCount }}
</div> </div>
</div> </div>
<div v-if="props.foodItem?.recordList.length > 0" class="info-list"> <div v-if="props.item?.recordList.length > 0" class="info-list">
<div class="info-item"> <div class="info-item">
<i class="fa-regular fa-calendar-check"></i> <i class="fa-regular fa-calendar-check"></i>
{{ formatDate(props.foodItem.recordList[foodCardInfo.currentFoodIndex].date) }} {{ formatDate(props.item.recordList[foodCardInfo.currentFoodIndex].date) }}
</div> </div>
</div> </div>
@@ -61,7 +79,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Edit, Collection, DeleteFilled } from '@element-plus/icons-vue' import { Edit, Collection, DeleteFilled } from '@element-plus/icons-vue'
import { useFoodStore } from '@/store/food' import { useFoodStore } from '@/store/food'
import { defineProps, onMounted, reactive } from 'vue' import { useUserStore } from '@/store/user'
import { computed, defineProps, onMounted, reactive } from 'vue'
import type { PropType } from 'vue' import type { PropType } from 'vue'
import type { IRecipe, IRecord } from '@/types/food.ts' import type { IRecipe, IRecord } from '@/types/food.ts'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -71,6 +90,7 @@ import { serviceFileRootPath } from '@/utils'
const router = useRouter() const router = useRouter()
const foodStore = useFoodStore() const foodStore = useFoodStore()
const userStore = useUserStore()
const foodCardInfo = reactive({ const foodCardInfo = reactive({
recommendRate: 0, recommendRate: 0,
@@ -78,34 +98,44 @@ const foodCardInfo = reactive({
}) })
const props = defineProps({ const props = defineProps({
foodItem: { item: {
required: true, required: true,
type: Object as PropType<IRecipe> type: Object as PropType<IRecipe>
} }
}) })
onMounted(() => { onMounted(() => {
foodCardInfo.recommendRate = props.foodItem?.recommendRate as number foodCardInfo.recommendRate = props.item?.recommendRate as number
}) })
const avatarUrl = computed(() => {
if (props.item?.avatar) {
return serviceFileRootPath + props.item.avatar
} else {
return 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
}
})
const isCurrentUser = computed(() => props.item?.userId === userStore.getUserId())
const getRecord = (recordList: IRecord[]) => { const getRecord = (recordList: IRecord[]) => {
return recordList.filter((item) => item.imageUrl) return recordList.filter((item) => item.imageUrl)
} }
const editClick = async () => { const editClick = async () => {
foodStore.recipeApiType = 'UPDATE' foodStore.recipeApiType = 'UPDATE'
await foodStore.queryRecipe(props.foodItem.id as number) await foodStore.queryRecipe(props.item.id as number)
await router.push('/record/recipeForm') await router.push('/record/recipeForm')
} }
const viewClick = async () => { const viewClick = async () => {
await router.push({ name: 'RecordDetailId', params: { id: props.foodItem?.id } }) await router.push({ name: 'RecordDetailId', params: { id: props.item?.id } })
} }
const deleteClick = () => { const deleteClick = () => {
foodStore.recipeApiType = 'DELETE' foodStore.recipeApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该菜谱: ${props.foodItem?.name}?`).then(() => { commonElMessageBox(`请确认是否删除该菜谱: ${props.item?.name}?`).then(() => {
foodStore.handleRecipeApi(props.foodItem as IRecipe) foodStore.handleRecipeApi(props.item as IRecipe)
}) })
} }

View File

@@ -21,9 +21,6 @@ const foodStore = useFoodStore()
const appStore = useAppStore() const appStore = useAppStore()
onMounted(async () => { onMounted(async () => {
foodStore.queryDateType = 'month'
await foodStore.refreshInfo()
const { month } = foodStore.queryRecord const { month } = foodStore.queryRecord
calendarRef.value.move(new Date(month)) calendarRef.value.move(new Date(month))
}) })
@@ -86,7 +83,7 @@ const dateClick = (day) => {
*/ */
const changeMonthClick = async (value) => { const changeMonthClick = async (value) => {
foodStore.queryRecord.month = value[0].id foodStore.queryRecord.month = value[0].id
await foodStore.refreshInfo() await foodStore.queryRecordList()
} }
</script> </script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="search-container">
<el-input v-if="expanded" ref="inputRef"
v-model="searchText" placeholder="搜索菜友菜谱"
style="width: 200px;"
show-word-limit :maxlength="10" clearable
@keyup.enter="handleSearch" @blur="handleBlur"
/>
<el-button v-else :icon="Search"
circle size="small" @click="expandSearch"/>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { Search } from '@element-plus/icons-vue'
const expanded = ref(false)
const searchText = ref('')
const inputRef = ref(null)
// 展开搜索框
const expandSearch = () => {
expanded.value = true
// 等待DOM更新后自动聚焦输入框
nextTick(() => {
inputRef.value?.focus()
})
}
// 处理搜索
const handleSearch = () => {
if (searchText.value.trim()) {
console.log('搜索内容:', searchText.value)
// 这里可以触发搜索逻辑
}
}
// 输入框失去焦点时的处理
const handleBlur = () => {
if (!searchText.value.trim()) {
expanded.value = false
}
}
</script>
<style lang="scss">
</style>

View File

@@ -7,24 +7,17 @@
@change="queryFoodSegmented"/> @change="queryFoodSegmented"/>
</div> </div>
<div class="food-list"> <recipe-list />
<el-empty v-if="foodStore.recipeList.length === 0" description="暂无记录"/>
<div v-else class="list">
<food-card v-for="(item, index) in foodStore.recipeList"
:key="index" :food-item="item"/>
</div>
</div>
<arrive-bottom v-if="foodStore.isScrollEnd"/> <arrive-bottom v-if="foodStore.isScrollEnd"/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import FoodCard from '@/components/Food/FoodCard.vue' import RecipeList from '@/components/Food/RecipeList.vue'
import ArriveBottom from '@/components/ArriveButton.vue' import ArriveBottom from '@/components/ArriveButton.vue'
import { useFoodStore } from '@/store/food.ts' import { useFoodStore } from '@/store/food.ts'
import { onBeforeUnmount, onMounted, ref } from 'vue' import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ICategory } from '@/types' import type { ICategory } from '@/types'
const foodRecipeRef = ref() const foodRecipeRef = ref()
@@ -33,10 +26,7 @@ const foodStore = useFoodStore()
const queryCategoryList = ref([] as ICategory[]) const queryCategoryList = ref([] as ICategory[])
onMounted(async () => { onMounted(async () => {
await foodStore.queryCategory()
initCategoryList()
// foodStore.foodRecipeList = [] // foodStore.foodRecipeList = []
await foodStore.refreshInfo()
// foodRecipeRef.value.addEventListener('scroll', handleScroll) // foodRecipeRef.value.addEventListener('scroll', handleScroll)
}) })
@@ -44,6 +34,10 @@ onBeforeUnmount(() => {
// foodRecipeRef.value.removeEventListener('scroll', handleScroll) // foodRecipeRef.value.removeEventListener('scroll', handleScroll)
}) })
watch(() => foodStore.isRefresh, () => {
initCategoryList()
})
const initCategoryList = () => { const initCategoryList = () => {
queryCategoryList.value = [] queryCategoryList.value = []
queryCategoryList.value.push({ queryCategoryList.value.push({
@@ -87,13 +81,5 @@ const handleScroll = () => {
.query-item { .query-item {
padding: 5px 10px; padding: 5px 10px;
} }
.food-list {
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
}
} }
</style> </style>

View File

@@ -1,16 +1,27 @@
<template> <template>
<div class="food-timeline"> <div class="food-timeline">
<div class="button-container">
<el-button :icon="CaretLeft" circle
type="primary" size="small"
@click="previousYearClick"/>
<el-date-picker <el-date-picker
v-model="currentYear" v-model="foodStore.queryRecord.year"
type="year" placeholder="请选择年份" type="year" placeholder="请选择年份"
:editable="false" :editable="false"
style="width: 100px;" style="width: 100px;"
@change="changeDateEvent" @change="changeDateEvent"
/> />
<el-button :icon="CaretRight" circle
type="primary" size="small"
@click="nextYearClick"/>
</div>
<el-timeline v-if="foodStore.recordList.length > 0"> <el-timeline v-if="foodStore.recordList.length > 0">
<el-timeline-item v-for="(item, index) in foodStore.recordList" <el-timeline-item v-for="(item, index) in foodStore.recordList"
:key="index" :timestamp="formatDate(item.date)" placement="top"> :key="index" :timestamp="formatDate(item.date)"
placement="top" color='var(--el-color-primary)'>
<div class="timeline-info card-item"> <div class="timeline-info card-item">
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
<el-image :src="serviceFileRootPath + item.imageUrl" <el-image :src="serviceFileRootPath + item.imageUrl"
@@ -30,22 +41,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CaretLeft, CaretRight } from '@element-plus/icons-vue'
import { useFoodStore } from '@/store/food.ts' import { useFoodStore } from '@/store/food.ts'
import { onMounted, ref } from 'vue' import { formatDate, getLastDate, getNextDate } from 'vue3-common/utils/dateUtil'
import { formatDate } from 'vue3-common/utils/dateUtil'
import { serviceFileRootPath } from '@/utils' import { serviceFileRootPath } from '@/utils'
const currentYear = ref(new Date())
const foodStore = useFoodStore() const foodStore = useFoodStore()
onMounted(async () => {
foodStore.queryDateType = 'year'
await foodStore.refreshInfo()
})
const changeDateEvent = async (value: Date) => { const changeDateEvent = async (value: Date) => {
foodStore.queryRecord.year = String(value.getFullYear()) foodStore.queryRecord.year = String(value.getFullYear())
await foodStore.refreshInfo() await foodStore.queryRecordList()
}
const previousYearClick = async () => {
const lastYear = getLastDate(foodStore.queryRecord.year, 1, 'years')
foodStore.queryRecord.year = String(new Date(lastYear).getFullYear())
await foodStore.queryRecordList()
}
const nextYearClick = async () => {
const lastYear = getNextDate(foodStore.queryRecord.year, 1, 'years')
foodStore.queryRecord.year = String(new Date(lastYear).getFullYear())
await foodStore.queryRecordList()
} }
</script> </script>
@@ -54,13 +71,21 @@ const changeDateEvent = async (value: Date) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.el-date-editor { .button-container {
align-self: center; align-self: center;
display: flex;
align-items: center;
gap: 10px;
} }
.el-timeline { .el-timeline {
padding: 10px; padding: 10px;
.el-timeline-item__timestamp {
font-weight: bold;
color: #000000;
}
.timeline-info { .timeline-info {
padding: 10px; padding: 10px;
display: flex; display: flex;

View File

@@ -0,0 +1,28 @@
<template>
<div class="food-list">
<el-empty v-if="foodStore.recipeList.length === 0" description="暂无记录"/>
<div v-else class="list">
<food-card v-for="(item, index) in foodStore.recipeList"
:key="index" :item="item"/>
</div>
</div>
</template>
<script setup lang="ts">
import FoodCard from '@/components/Food/FoodCard.vue'
import { useFoodStore } from '@/store/food'
const foodStore = useFoodStore()
</script>
<style lang="scss">
.food-list {
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
}
</style>

View File

@@ -48,7 +48,7 @@
<div /> <div />
<div class="login-type"> <div class="login-type">
<el-button @click="authStore.changeLoginType('phone')">手机号登录</el-button> <el-button>手机号登录</el-button>
<el-button>扫码登录</el-button> <el-button>扫码登录</el-button>
</div> </div>
@@ -58,8 +58,7 @@
<div class="type-list"> <div class="type-list">
<i class="fa-brands fa-weixin" style="color: #3CB034"></i> <i class="fa-brands fa-weixin" style="color: #3CB034"></i>
<i class="fa-brands fa-qq" style="color: #29A4D9"></i> <i class="fa-brands fa-qq" style="color: #29A4D9"></i>
<i class="fa-solid fa-envelope" style="color: #4971B8" <i class="fa-solid fa-envelope" style="color: #4971B8"></i>
@click="authStore.changeLoginType('email')"></i>
<i class="fa-brands fa-github" style="color: #555555"></i> <i class="fa-brands fa-github" style="color: #555555"></i>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="moment-card card-item"> <div class="moment-card card-item">
<el-avatar :src="avatarUrl" fit="contain" @click="viewProfileClick"/> <el-avatar :src="avatarUrl" :size="35" fit="contain" @click="viewProfileClick"/>
<div class="moment-content"> <div class="moment-content">
<span class="username">{{ props.item.username }}</span> <span class="username">{{ props.item.username }}</span>
@@ -9,6 +9,7 @@
<div class="moment-image-list"> <div class="moment-image-list">
<el-image v-for="(item, index) in props.item.imageList" :key="index" <el-image v-for="(item, index) in props.item.imageList" :key="index"
:src="serviceFileRootPath + item" fit="cover" :src="serviceFileRootPath + item" fit="cover"
:initial-index="index"
:preview-src-list=previewList /> :preview-src-list=previewList />
</div> </div>
@@ -124,9 +125,9 @@ const editClick = () => {
const handleLikeClick = () => { const handleLikeClick = () => {
if (isLike.value) { if (isLike.value) {
foodStore.deleteLike(props.item?.id as number) foodStore.deleteMomentLike(props.item?.id as number)
} else { } else {
foodStore.addLike(props.item?.id as number) foodStore.addMomentLike(props.item?.id as number)
} }
} }
@@ -135,7 +136,7 @@ const onSelectEmoji = (emoji) => {
} }
const sendCommentClick = async () => { const sendCommentClick = async () => {
await foodStore.addComment(props.item?.id as number, momentInfo.comment) await foodStore.addMomentComment(props.item?.id as number, momentInfo.comment)
momentInfo.comment = '' momentInfo.comment = ''
momentInfo.isShowComment = false momentInfo.isShowComment = false
@@ -150,61 +151,3 @@ const viewProfileClick = async () => {
} }
} }
</script> </script>
<style lang="scss">
.moment-card {
padding: 10px;
display: grid;
grid-template-columns: 50px 1fr;
gap: 10px;
.moment-content {
display: flex;
flex-direction: column;
gap: 10px;
p {
margin: 0;
}
.username {
font-weight: bold;
font-size: larger;
}
.moment-image-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.button-container {
display: flex;
gap: 20px;
}
.moment-comment {
display: grid;
grid-template-columns: 1fr 40px 60px;
justify-items: center;
gap: 5px;
}
.moment-comment-list {
padding: 10px;
background-color: var(--color-comment-bg);
display: flex;
flex-direction: column;
gap: 5px;
.moment-comment-item {
.name {
font-weight: bold;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="food-moment"
v-loading="foodStore.isLoading" element-loading-text="加载中">
<el-empty v-if="foodStore.momentList.length === 0" description="暂无记录"/>
<div v-else class="moment-list">
<moment-card v-for="(item, index) in foodStore.momentList"
:key="index" :item="item"/>
</div>
</div>
</template>
<script setup lang="ts">
import MomentCard from '@/components/Moment/MomentCard.vue'
import { useFoodStore } from '@/store/food'
const foodStore = useFoodStore()
</script>
<style lang="scss">
@use "@/styles/moment.module.scss";
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="profile-card"> <div class="profile-card card-item">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<el-avatar :src="avatarUrl" alt="用户头像" :size="80" <el-avatar :src="avatarUrl" alt="用户头像" :size="80"
fit="contain" @click="previewVisible = true" fit="contain" @click="previewVisible = true"
@@ -75,16 +75,16 @@ const avatarUrl = computed(() => {
<style lang="scss"> <style lang="scss">
.profile-card { .profile-card {
background: linear-gradient(135deg, #6e8efb, #a777e3); // background: linear-gradient(135deg, #6e8efb, #a777e3);
border-radius: 10px; // border-radius: 10px;
padding: 20px; padding: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); // box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
text-align: center; text-align: center;
color: white; // color: white;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 15px; gap: 10px;
.avatar-wrapper { .avatar-wrapper {
position: relative; position: relative;

View File

@@ -18,7 +18,8 @@
<span class="title">📊排行榜</span> <span class="title">📊排行榜</span>
</div> </div>
<div class="chart rank-chart"> <div class="chart rank-chart">
<food-rank /> <food-rank v-if="foodStore.rankStats.length > 0"/>
<el-empty v-else description="暂无数据"/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,11 +13,13 @@
<span class="title-container">{{ title }}</span> <span class="title-container">{{ title }}</span>
<div class="right-container"> <div v-if="!appStore.isShowMobileBack" class="right-container">
<svg-icon v-if="appStore.isWebSocket" <!-- <svg-icon v-if="appStore.isWebSocket"-->
name="home-websocket" style="color: #FFFFFF;"></svg-icon> <!-- name="home-websocket" style="color: #FFFFFF;"></svg-icon>-->
<el-button type="info" :icon="Setting" size="small" <food-query />
circle color="#FFFFFF" @click="openSettingClick"/>
<el-button :icon="Setting" size="small"
circle @click="openSettingClick"/>
<el-drawer <el-drawer
v-model="appStore.isShowSetting" v-model="appStore.isShowSetting"
@@ -30,6 +32,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import FoodQuery from '@/components/Food/FoodQuery.vue'
import { SvgIcon } from 'vue3-common' import { SvgIcon } from 'vue3-common'
import MobileSetting from '@/components/MobileSetting.vue' import MobileSetting from '@/components/MobileSetting.vue'
import { Back } from '@element-plus/icons-vue' import { Back } from '@element-plus/icons-vue'

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil' import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import { useDark, useToggle } from '@vueuse/core' import { useDark, useToggle } from '@vueuse/core'
import { darken, lighten } from '@/utils/colorUtils'
export const useAppStore = defineStore('ipp', { export const useAppStore = defineStore('ipp', {
state: () => ({ state: () => ({
@@ -14,7 +15,7 @@ export const useAppStore = defineStore('ipp', {
isDark: false, isDark: false,
isWebSocket: false, isWebSocket: false,
serverIp: '14.103.235.151', serverIp: '14.103.235.151',
version: 'v1.0.0.2025062802' version: 'v1.0.0.2025071801'
}), }),
actions: { actions: {
initApp() { initApp() {
@@ -71,9 +72,18 @@ export const useAppStore = defineStore('ipp', {
getThemeColor(): string { getThemeColor(): string {
return getLocalStorage('theme-color') as string || '#009FA8' return getLocalStorage('theme-color') as string || '#009FA8'
}, },
setThemeColor(value: string) { setThemeColor(color: string) {
document.documentElement.style.setProperty('--el-color-primary', value) const { style } = document.documentElement
setLocalStorage('theme-color', value)
// 1. 设置主色
style.setProperty('--el-color-primary', color)
for (let i = 1; i <= 9; i++) {
style.setProperty(`--el-color-primary-light-${i}`, lighten(color, i / 10))
style.setProperty(`--el-color-primary-dark-${i}`, darken(color, i / 10))
}
setLocalStorage('theme-color', color)
} }
} }
}) })

View File

@@ -4,30 +4,22 @@ import type {
IStats, IRecord, IMoment, IFoodSegment IStats, IRecord, IMoment, IFoodSegment
} from '@/types/food' } from '@/types/food'
import { import {
addRecipeApi, addRecipeApi, addRecordApi, deleteRecipeApi, deleteRecordApi,
addRecordApi, queryFoodNameListApi, queryRecipeByIdApi, queryRecordApi, queryStatsApi,
deleteRecipeApi, updateRecipeApi, updateRecordApi, queryCategoryApi, queryRecordStatsApi,
deleteRecordApi, queryCategoryStatsApi, queryRankStatsApi,
queryFoodNameListApi, queryRecipeApi, queryRecipeByUserApi, deleteRecipeLikeApi,
queryRecipeByIdApi, addRecipeLikeApi, addRecipeCommentApi, addRecipeFavouriteApi, deleteRecipeFavouriteApi, queryRecipeUserFavouriteApi
queryRecordApi,
queryStatsApi,
updateRecipeApi,
updateRecordApi,
queryCategoryApi,
queryRecordStatsApi,
queryCategoryStatsApi, queryRankStatsApi, queryRecipeApi
} from '@/apis/food.ts' } from '@/apis/food.ts'
import type { IDialog, IHandleApi, IStatsChart } from 'vue3-common/types' import type { IDialog, IHandleApi, IStatsChart } from 'vue3-common/types'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil' import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
import { import {
addCommentApi, addMomentLikeApi,
addLikeApi, addMomentApi, addMomentCommentApi,
addMomentApi, deleteMomentLikeApi,
deleteLikeApi, deleteMomentApi, queryMomentByIdApi,
deleteMomentApi, queryMomentListApi,
queryMomentApi,
updateMomentApi updateMomentApi
} from '@/apis/moment' } from '@/apis/moment'
@@ -40,9 +32,19 @@ export const useFoodStore = defineStore('food', {
category: '', category: '',
recommendRate: 0, recommendRate: 0,
remark: '', remark: '',
isShare: false,
username: '',
userId: 0,
avatar: '',
materialList: [], materialList: [],
stepList: [], stepList: [],
recordList: [] recordList: [],
likeCount: 0,
likeList: [],
favouriteCount: 0,
favouriteList: [],
commentCount: 0,
commentList: []
} as IRecipe, } as IRecipe,
recordList: [] as IRecord[], recordList: [] as IRecord[],
currentRecord: { currentRecord: {
@@ -105,7 +107,8 @@ export const useFoodStore = defineStore('food', {
recordStats: [] as IStatsChart[], recordStats: [] as IStatsChart[],
categoryStats: [] as IStatsChart[], categoryStats: [] as IStatsChart[],
rankStats: [] as IStatsChart[], rankStats: [] as IStatsChart[],
isRefresh: false isRefresh: false,
isLoading: false
}), }),
actions: { actions: {
async queryRecipeList() { async queryRecipeList() {
@@ -113,16 +116,34 @@ export const useFoodStore = defineStore('food', {
// const result = await queryFoodRecipeByPageApi(currentPage, pageSize, this.queryRecipe) // const result = await queryFoodRecipeByPageApi(currentPage, pageSize, this.queryRecipe)
// this.foodRecipeList.push(...result.records) // this.foodRecipeList.push(...result.records)
// this.pageInfo.totalSize = result.total // this.pageInfo.totalSize = result.total
this.isLoading = true
this.recipeList = await queryRecipeApi(this.queryRecipeSegment) this.recipeList = await queryRecipeApi(this.queryRecipeSegment)
this.isRefresh = !this.isRefresh
this.isLoading = false
},
async queryRecipeByUser(id: number) {
this.isLoading = true
this.recipeList = await queryRecipeByUserApi(id)
this.isLoading = false
},
async queryRecipeUserFavourite() {
this.isLoading = true
this.recipeList = await queryRecipeUserFavouriteApi()
this.isLoading = false
}, },
async queryRecordList() { async queryRecordList() {
const { year, month } = this.queryRecord const { year, month } = this.queryRecord
const date = this.queryDateType === 'year' ? year : month const date = this.queryDateType === 'year' ? year : month
this.queryDateList = getStartEndDate(date, this.queryDateType) this.queryDateList = getStartEndDate(date, this.queryDateType)
this.isLoading = true
this.recordList = await queryRecordApi(this.queryDateList[0], this.queryDateList[1]) this.recordList = await queryRecordApi(this.queryDateList[0], this.queryDateList[1])
this.isRefresh = !this.isRefresh
this.isLoading = false
}, },
async queryMomentList() { async queryMomentList() {
this.momentList = await queryMomentApi() this.isLoading = true
this.momentList = await queryMomentListApi()
this.isLoading = false
}, },
async queryFoodNameList() { async queryFoodNameList() {
this.foodNameList = await queryFoodNameListApi() this.foodNameList = await queryFoodNameListApi()
@@ -130,6 +151,11 @@ export const useFoodStore = defineStore('food', {
async queryRecipe(id: number) { async queryRecipe(id: number) {
this.currentRecipe = await queryRecipeByIdApi(id) this.currentRecipe = await queryRecipeByIdApi(id)
}, },
async queryMoment(id: number) {
this.isLoading = true
this.momentList = await queryMomentByIdApi(id)
this.isLoading = false
},
async queryStats() { async queryStats() {
this.stats = await queryStatsApi() this.stats = await queryStatsApi()
this.recordStats = await queryRecordStatsApi() this.recordStats = await queryRecordStatsApi()
@@ -157,7 +183,7 @@ export const useFoodStore = defineStore('food', {
default: default:
break break
} }
await this.refreshInfo() await this.queryRecipeList()
}, },
async handleRecordApi(record: IRecord) { async handleRecordApi(record: IRecord) {
switch (this.recordApiType) { switch (this.recordApiType) {
@@ -176,7 +202,7 @@ export const useFoodStore = defineStore('food', {
default: default:
break break
} }
await this.refreshInfo() await this.queryRecordList()
}, },
async handleMomentApi(moment: IMoment) { async handleMomentApi(moment: IMoment) {
switch (this.momentApiType) { switch (this.momentApiType) {
@@ -195,31 +221,51 @@ export const useFoodStore = defineStore('food', {
default: default:
break break
} }
await queryMomentApi()
},
async addComment(id: number, content: string) {
await addCommentApi(id, content)
await this.queryMomentList() await this.queryMomentList()
}, },
async addLike(id: number) { async addMomentComment(id: number, content: string) {
await addLikeApi(id) await addMomentCommentApi(id, content)
await this.queryMomentList() await this.queryMomentList()
}, },
async deleteLike(id: number) { async addMomentLike(id: number) {
await deleteLikeApi(id) await addMomentLikeApi(id)
await this.queryMomentList() await this.queryMomentList()
}, },
async refreshInfo() { async deleteMomentLike(id: number) {
await this.queryRecipeList() await deleteMomentLikeApi(id)
await this.queryRecordList() await this.queryMomentList()
this.isRefresh = !this.isRefresh },
async addRecipeComment(id: number, content: string) {
await addRecipeCommentApi(id, content)
await this.queryRecipe(this.currentRecipe.id as number)
},
async addRecipeLike(id: number) {
await addRecipeLikeApi(id)
await this.queryRecipe(this.currentRecipe.id as number)
},
async deleteRecipeLike(id: number) {
await deleteRecipeLikeApi(id)
await this.queryRecipe(this.currentRecipe.id as number)
},
async addRecipeFavourite(id: number) {
await addRecipeFavouriteApi(id)
await this.queryRecipe(this.currentRecipe.id as number)
},
async deleteRecipeFavourite(id: number) {
await deleteRecipeFavouriteApi(id)
await this.queryRecipe(this.currentRecipe.id as number)
}, },
getEmptyRecipe(): IRecipe { getEmptyRecipe(): IRecipe {
return { return {
commentCount: 0,
commentList: [],
likeCount: 0,
likeList: [],
category: '', category: '',
materialList: [], materialList: [],
name: '', name: '',
recommendRate: 0, recommendRate: 0,
isShare: false,
remark: '', remark: '',
stepList: [], stepList: [],
recordList: [] recordList: []

View File

@@ -79,7 +79,7 @@ span, input {
@font-face { @font-face {
font-family: 'CustomFont'; font-family: 'CustomFont';
src: url('@/assets/font/霞鹜臻楷.ttf') format('truetype'); src: url('@/assets/font/custom.woff2') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }

View File

@@ -201,6 +201,28 @@
} }
} }
} }
.button-container {
margin-top: 10px;
align-self: center;
display: flex;
gap: 20px;
}
.food-comment-list {
padding: 10px;
display: flex;
flex-direction: column;
gap: 5px;
.food-comment-item {
.name {
font-weight: bold;
}
}
}
} }
.back-button { .back-button {

View File

@@ -47,18 +47,35 @@ $card-image-height: 200px;
.info-item { .info-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 3px; gap: 5px;
.title { .title {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
} }
.user {
// font-size: 0.9rem;
color: #9C9C9C;
}
} }
.category-item { .category-item {
color: #0d9488; color: #0d9488;
} }
.like-item {
color: #FF4D4F;
}
.comment-item {
color: #03A9F4;
}
.start-item {
color: #FFAB40;
}
.location-item { .location-item {
color: #0d9488; color: #0d9488;
} }

View File

@@ -77,7 +77,7 @@ $tabBar-height: 70px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 10px;
} }
} }

View File

@@ -0,0 +1,67 @@
.food-moment {
display: flex;
flex-direction: column;
gap: 10px;
.moment-list {
display: flex;
flex-direction: column;
gap: 10px;
.moment-card {
padding: 10px;
display: grid;
grid-template-columns: 35px 1fr;
gap: 5px;
.moment-content {
display: flex;
flex-direction: column;
gap: 10px;
p {
margin: 0;
}
.username {
font-weight: bold;
font-size: larger;
}
.moment-image-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.button-container {
display: flex;
gap: 20px;
}
.moment-comment {
display: grid;
grid-template-columns: 1fr 40px 60px;
justify-items: center;
gap: 5px;
}
.moment-comment-list {
padding: 10px;
background-color: var(--color-comment-bg);
display: flex;
flex-direction: column;
gap: 5px;
.moment-comment-item {
.name {
font-weight: bold;
}
}
}
}
}
}
}

View File

@@ -18,6 +18,14 @@ export interface IStep {
imageUrl: string; imageUrl: string;
} }
export interface IComment {
id: number;
username: string;
avatar: string;
content: string;
date: string;
}
/** /**
* 成果信息 * 成果信息
*/ */
@@ -51,6 +59,10 @@ export interface IRecipe {
* 备注 * 备注
*/ */
remark: string; remark: string;
isShare: boolean;
userId?: number;
username?: string;
avatar?: string;
/** /**
* 食材 * 食材
*/ */
@@ -63,6 +75,12 @@ export interface IRecipe {
* 成果 * 成果
*/ */
recordList: IRecord[]; recordList: IRecord[];
likeList?: number[];
likeCount?: number;
favouriteList?: number[];
favouriteCount?: number;
commentList?: IComment[];
commentCount?: number;
} }
/** /**
@@ -78,14 +96,6 @@ export interface IFoodQuery {
category: string; category: string;
} }
export interface IComment {
id: number;
username: string;
avatar: string;
content: string;
date: string;
}
export interface IMoment { export interface IMoment {
id?: number; id?: number;
userId?: number; userId?: number;

69
src/utils/colorUtils.ts Normal file
View File

@@ -0,0 +1,69 @@
/**
* 将十六进制颜色转换为RGB数组
* @param color 十六进制颜色字符串(如:#FF0000 或 FFFFFF
* @returns RGB数值数组 [r, g, b]
*/
export const hex2Rgb = (color: string): [number, number, number] => {
const cleanColor = color.replace('#', '')
const result = cleanColor.match(/../g)?.map((val) => parseInt(val, 16)) ?? [0, 0, 0]
// 确保结果长度为3并处理无效输入
if (result.length !== 3) {
throw new Error('Invalid hex color format')
}
return [result[0], result[1], result[2]]
}
/**
* 将RGB颜色转换为十六进制字符串
* @param r 红色值 (0-255)
* @param g 绿色值 (0-255)
* @param b 蓝色值 (0-255)
* @returns 十六进制颜色字符串(如:#FF0000
*/
export const rgb2Hex = (r: number, g: number, b: number): string => {
const validateComponent = (val: number) => Math.max(0, Math.min(255, Math.round(val)))
const hexs = [
validateComponent(r).toString(16).padStart(2, '0'),
validateComponent(g).toString(16).padStart(2, '0'),
validateComponent(b).toString(16).padStart(2, '0')
]
return `#${hexs.join('')}`
}
/**
* 调亮颜色
* @param color 十六进制颜色字符串
* @param level 亮度调整级别 (0-1)
* @returns 调亮后的十六进制颜色
*/
export const lighten = (color: string, level: number): string => {
const [r, g, b] = hex2Rgb(color)
const validateLevel = Math.max(0, Math.min(1, level))
const newR = Math.floor((255 - r) * validateLevel + r)
const newG = Math.floor((255 - g) * validateLevel + g)
const newB = Math.floor((255 - b) * validateLevel + b)
return rgb2Hex(newR, newG, newB)
}
/**
* 调暗颜色
* @param color 十六进制颜色字符串
* @param level 暗度调整级别 (0-1)
* @returns 调暗后的十六进制颜色
*/
export const darken = (color: string, level: number): string => {
const [r, g, b] = hex2Rgb(color)
const validateLevel = Math.max(0, Math.min(1, level))
const newR = Math.floor(r * (1 - validateLevel))
const newG = Math.floor(g * (1 - validateLevel))
const newB = Math.floor(b * (1 - validateLevel))
return rgb2Hex(newR, newG, newB)
}

View File

@@ -1,12 +1,12 @@
/** /**
* 文件服务器地址 * 文件服务器地址
*/ */
export const fileServiceUrl = '/food-service/file' export const fileServiceUrl = '/food-api/file'
/** /**
* 服务器文件路径根地址 * 服务器文件路径根地址
*/ */
export const serviceFileRootPath = '/food-service/' export const serviceFileRootPath = '/food-api/'
/** /**
* 获取assets静态资源 * 获取assets静态资源

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="moment-form common-mobile-view"> <div class="moment-form common-mobile-view">
<el-form ref="formRef" :model="foodStore.currentMoment" <el-form ref="formRef" :model="foodStore.currentMoment"
:rules="rules" label-width="60"> :rules="rules" label-width="55">
<el-form-item label="内容" prop="content"> <el-form-item label="内容" prop="content">
<el-input v-model="foodStore.currentMoment.content" <el-input v-model="foodStore.currentMoment.content"
type="textarea" :rows="3" type="textarea" :rows="3"
@@ -12,7 +12,8 @@
<el-form-item label="图片"> <el-form-item label="图片">
<upload-image v-model="foodStore.currentMoment.imageList" <upload-image v-model="foodStore.currentMoment.imageList"
:service-url="fileServiceUrl" :service-url="fileServiceUrl"
:image-limit="4" :image-limit="9"
:multiple="true"
:service-file-root-path="serviceFileRootPath" :service-file-root-path="serviceFileRootPath"
:image-path="foodStore.imagePath"/> :image-path="foodStore.imagePath"/>
</el-form-item> </el-form-item>

View File

@@ -1,20 +1,15 @@
<template> <template>
<div class="common-mobile-view food-moment"> <div class="common-mobile-view">
<el-empty v-if="foodStore.momentList.length === 0" description="暂无朋友圈记录"/> <moment-list />
<div v-else class="moment-list">
<moment-card v-for="(item, index) in foodStore.momentList"
:key="index" :item="item"/>
<el-button type="primary" circle :icon="Refresh" <el-button type="primary" circle :icon="Refresh"
class="refresh-button" @click="foodStore.queryMomentList()"/> class="refresh-button" @click="foodStore.queryMomentList()"/>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MomentList from '@/components/Moment/MomentList.vue'
import { Refresh } from '@element-plus/icons-vue' import { Refresh } from '@element-plus/icons-vue'
import MomentCard from '@/components/Moment/MomentCard.vue'
import { useFoodStore } from '@/store/food' import { useFoodStore } from '@/store/food'
import { onMounted } from 'vue' import { onMounted } from 'vue'
@@ -26,21 +21,9 @@ onMounted(async () => {
</script> </script>
<style lang="scss"> <style lang="scss">
.food-moment {
display: flex;
flex-direction: column;
gap: 10px;
.moment-list {
display: flex;
flex-direction: column;
gap: 10px;
.refresh-button { .refresh-button {
position: fixed; position: fixed;
top: 60px; top: 60px;
right: 20px; right: 20px;
} }
}
}
</style> </style>

View File

@@ -1,22 +1,48 @@
<template> <template>
<div class="moment-profile-view common-mobile-view"> <div class="moment-profile-view common-mobile-view">
<profile-card /> <profile-card />
<el-tabs v-model="activeName"
stretch class="profile-tabs"
@tab-click="handleClick">
<el-tab-pane label="朋友圈" name="moment">
<moment-list />
</el-tab-pane>
<el-tab-pane label="菜谱记录" name="recipe">
<recipe-list />
</el-tab-pane>
</el-tabs>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import RecipeList from '@/components/Food/RecipeList.vue'
import ProfileCard from '@/components/Profile/ProfileCard.vue' import ProfileCard from '@/components/Profile/ProfileCard.vue'
import { useUserStore } from '@/store/user.ts' import { useFoodStore } from '@/store/food.ts'
import { useAppStore } from '@/store/app.ts' import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import MomentList from '@/components/Moment/MomentList.vue'
import { onMounted } from 'vue' import type { TabsPaneContext } from 'element-plus'
const appStore = useAppStore() const foodStore = useFoodStore()
const router = useRouter()
onMounted(() => { const activeName = ref('moment')
onMounted(async () => {
await foodStore.queryMoment(foodStore.currentMomentUserId)
}) })
const handleClick = async (tab: TabsPaneContext) => {
switch (tab.paneName) {
case 'moment':
await foodStore.queryMoment(foodStore.currentMomentUserId)
break
case 'recipe':
await foodStore.queryRecipeByUser(foodStore.currentMomentUserId)
break
default:
break
}
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -2,6 +2,10 @@
<div class="profile-view common-mobile-view"> <div class="profile-view common-mobile-view">
<profile-card /> <profile-card />
<el-tabs v-model="activeName"
stretch class="profile-tabs"
@tab-click="handleClick">
<el-tab-pane label="基本资料" name="basic">
<div class="menu-card card-item"> <div class="menu-card card-item">
<div class="menu-item" @click="infoClick"> <div class="menu-item" @click="infoClick">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
@@ -19,19 +23,26 @@
<div class="menu-divider"></div> <div class="menu-divider"></div>
<div class="menu-item">
<i class="fa-solid fa-comment-dots"></i>
<span>反馈意见</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="passwordClick"> <div class="menu-item" @click="passwordClick">
<i class="fas fa-key"></i> <i class="fas fa-key"></i>
<span>更改密码</span> <span>更改密码</span>
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</div> </div>
</div>
</el-tab-pane>
<el-tab-pane label="朋友圈" name="moment">
<moment-list />
</el-tab-pane>
<el-tab-pane label="菜谱收藏" name="recipe">
<recipe-list />
</el-tab-pane>
<el-tab-pane label="系统设置" name="system">
<div class="menu-card card-item">
<div class="menu-item">
<i class="fa-solid fa-comment-dots"></i>
<span>反馈意见</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-divider"></div> <div class="menu-divider"></div>
@@ -57,6 +68,8 @@
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</div> </div>
</div> </div>
</el-tab-pane>
</el-tabs>
<div class="version">版本号 {{ appStore.version }}</div> <div class="version">版本号 {{ appStore.version }}</div>
</div> </div>
@@ -64,18 +77,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import ProfileCard from '@/components/Profile/ProfileCard.vue' import ProfileCard from '@/components/Profile/ProfileCard.vue'
import RecipeList from '@/components/Food/RecipeList.vue'
import MomentList from '@/components/Moment/MomentList.vue'
import { useUserStore } from '@/store/user.ts' import { useUserStore } from '@/store/user.ts'
import { useAppStore } from '@/store/app.ts' import { useAppStore } from '@/store/app.ts'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import { useFoodStore } from '@/store/food.ts'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { commonElMessageBox } from 'vue3-common/utils/elUtil' import { commonElMessageBox } from 'vue3-common/utils/elUtil'
import { onMounted } from 'vue' import { onMounted, ref } from 'vue'
import type { TabsPaneContext } from 'element-plus'
const userStore = useUserStore() const userStore = useUserStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const appStore = useAppStore() const appStore = useAppStore()
const foodStore = useFoodStore()
const router = useRouter() const router = useRouter()
const activeName = ref('basic')
onMounted(() => { onMounted(() => {
userStore.queryUser(userStore.getUserId() as number) userStore.queryUser(userStore.getUserId() as number)
}) })
@@ -89,6 +109,19 @@ const passwordClick = () => {
// router.push('/mobile-home/profile/password') // router.push('/mobile-home/profile/password')
} }
const handleClick = async (tab: TabsPaneContext) => {
switch (tab.paneName) {
case 'moment':
await foodStore.queryMoment(userStore.getUserId())
break
case 'recipe':
await foodStore.queryRecipeUserFavourite()
break
default:
break
}
}
const refreshClick = async () => { const refreshClick = async () => {
await router.push('/') await router.push('/')
location.reload() location.reload()

View File

@@ -12,28 +12,67 @@
<food-others-data/> <food-others-data/>
</div> </div>
<div class="button-container">
<el-badge :value="foodStore.currentRecipe.likeList.length" :max="9">
<el-button :type="isLike ? 'danger' : 'default'"
circle @click="handleLikeClick()">
<i class="fa-regular fa-thumbs-up"></i>
</el-button>
</el-badge>
<el-badge v-if="!isCurrentUser"
:value="foodStore.currentRecipe.favouriteList.length" :max="9">
<el-button v-if="!isCurrentUser"
:type="isFavourite ? 'danger' : 'default'"
circle @click="handleFavouriteClick()">
<i class="fa-regular fa-star"></i>
</el-button>
</el-badge>
</div>
</section> </section>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useFoodStore } from '@/store/food.ts'
import { useAppStore } from '@/store/app.ts'
import { useRoute } from 'vue-router'
import FoodBasicData from '@/components/Food/FoodBasicData.vue' import FoodBasicData from '@/components/Food/FoodBasicData.vue'
import FoodMaterialData from '@/components/Food/FoodMaterialData.vue' import FoodMaterialData from '@/components/Food/FoodMaterialData.vue'
import FoodStepData from '@/components/Food/FoodStepData.vue' import FoodStepData from '@/components/Food/FoodStepData.vue'
import FoodOthersData from '@/components/Food/FoodOthersData.vue' import FoodOthersData from '@/components/Food/FoodOthersData.vue'
import { onMounted } from 'vue' import { computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.ts'
import { useFoodStore } from '@/store/food.ts'
import { useAppStore } from '@/store/app.ts'
import { useRoute } from 'vue-router'
const foodStore = useFoodStore() const foodStore = useFoodStore()
const appStore = useAppStore() const appStore = useAppStore()
const route = useRoute() const route = useRoute()
const userStore = useUserStore()
onMounted(() => { onMounted(() => {
appStore.isShowMobileBack = true appStore.isShowMobileBack = true
foodStore.queryRecipe(route.params.id as number) foodStore.queryRecipe(route.params.id as number)
}) })
const isLike = computed(() => foodStore.currentRecipe.likeList.includes(userStore.getUserId()))
const isFavourite = computed(() => foodStore.currentRecipe.favouriteList.includes(userStore.getUserId()))
const isCurrentUser = computed(() => foodStore.currentRecipe.userId === userStore.getUserId())
const handleLikeClick = () => {
if (isLike.value) {
foodStore.deleteRecipeLike(foodStore.currentRecipe.id as number)
} else {
foodStore.addRecipeLike(foodStore.currentRecipe.id as number)
}
}
const handleFavouriteClick = () => {
if (isFavourite.value) {
foodStore.deleteRecipeFavourite(foodStore.currentRecipe.id as number)
} else {
foodStore.addRecipeFavourite(foodStore.currentRecipe.id as number)
}
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,32 +1,18 @@
<template> <template>
<div class="common-mobile-view food-record"> <div class="common-mobile-view food-record">
<div class="food-segment card-item"> <el-tabs v-model="foodStore.recordSegment"
<el-input stretch class="record-tabs"
v-model="foodInfo.query" @tab-click="handleClick">
placeholder="请输入菜谱名称" :prefix-icon="Search" clearable <el-tab-pane label="菜谱" name="recipe">
:maxlength="20" show-word-limit <food-recipe v-loading="foodStore.isLoading" element-loading-text="加载中"/>
/> </el-tab-pane>
<el-tab-pane label="日历" name="daily">
<div class="query-item"> <food-daily v-loading="foodStore.isLoading" element-loading-text="加载中"/>
<span>查看类型</span> </el-tab-pane>
<el-segmented v-model="foodStore.recordSegment" <el-tab-pane label="时间轴" name="timeline">
:options="recordSegmentList"/> <food-timeline v-loading="foodStore.isLoading" element-loading-text="加载中"/>
</div> </el-tab-pane>
</div> </el-tabs>
<hr style="width: 100%;"/>
<div v-if="foodStore.recordSegment === 'recipe'" class="food-calendar">
<food-recipe />
</div>
<div v-if="foodStore.recordSegment === 'daily'" class="food-calendar">
<food-daily />
</div>
<div v-if="foodStore.recordSegment === 'timeline'" class="food-calendar">
<food-timeline />
</div>
</div> </div>
</template> </template>
@@ -34,28 +20,35 @@
import FoodRecipe from '@/components/Food/FoodRecipe.vue' import FoodRecipe from '@/components/Food/FoodRecipe.vue'
import FoodDaily from '@/components/Food/FoodDaily.vue' import FoodDaily from '@/components/Food/FoodDaily.vue'
import FoodTimeline from '@/components/Food/FoodTimeline.vue' import FoodTimeline from '@/components/Food/FoodTimeline.vue'
import { Search } from '@element-plus/icons-vue'
import { reactive } from 'vue'
import { useFoodStore } from '@/store/food' import { useFoodStore } from '@/store/food'
import type { TabsPaneContext } from 'element-plus'
import { onMounted } from 'vue'
const foodStore = useFoodStore() const foodStore = useFoodStore()
const foodInfo = reactive({ onMounted(async () => {
query: '' await foodStore.queryCategory()
await foodStore.queryRecipeList()
}) })
const recordSegmentList = [{ const handleClick = async (tab: TabsPaneContext) => {
label: '菜谱', switch (tab.paneName) {
value: 'recipe' case 'recipe':
}, await foodStore.queryCategory()
{ await foodStore.queryRecipeList()
label: '日历', break
value: 'daily' case 'daily':
}, foodStore.queryDateType = 'month'
{ await foodStore.queryRecordList()
label: '时间轴', break
value: 'timeline' case 'timeline':
}] foodStore.queryDateType = 'year'
await foodStore.queryRecordList()
break
default:
break
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -63,12 +56,5 @@ const recordSegmentList = [{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
.food-segment {
padding: 5px 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
} }
</style> </style>

View File

@@ -1,15 +1,26 @@
<template> <template>
<div class="food-recipe-form common-mobile-view"> <div class="food-recipe-form common-mobile-view">
<h2 style="text-align: center; margin: 0;">
{{ foodStore.recipeApiType === 'ADD' ? '新增菜谱' : '编辑菜谱' }}
</h2>
<el-steps :active="currentStepIndex"
align-center finish-status="success">
<el-step title="基础信息" />
<el-step title="食材信息" />
<el-step title="步骤信息" />
</el-steps>
<el-form ref="formRef" :model="foodStore.currentRecipe" <el-form ref="formRef" :model="foodStore.currentRecipe"
:rules="rules" label-width="60px"> :rules="rules" label-width="60px">
<el-form-item label="名称" prop="name"> <el-form-item v-if="currentStepIndex === 0" label="名称" prop="name">
<el-input v-model="foodStore.currentRecipe.name" autocomplete="off" <el-input v-model="foodStore.currentRecipe.name" autocomplete="off"
placeholder="请输入名称" clearable placeholder="请输入名称" clearable
show-word-limit maxlength="20"> show-word-limit maxlength="20">
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="菜系" prop="category"> <el-form-item v-if="currentStepIndex === 0" label="菜系" prop="category">
<el-select v-model="foodStore.currentRecipe.category" <el-select v-model="foodStore.currentRecipe.category"
placeholder="请选择或输入菜系" allow-create filterable> placeholder="请选择或输入菜系" allow-create filterable>
<el-option v-for="(item, index) in foodCategoryList" <el-option v-for="(item, index) in foodCategoryList"
@@ -17,32 +28,42 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="推荐" prop="recommendRate"> <el-form-item v-if="currentStepIndex === 0" label="推荐" prop="recommendRate">
<el-rate v-model="foodStore.currentRecipe.recommendRate" <el-rate v-model="foodStore.currentRecipe.recommendRate"
:colors="foodStore.rateColorList"/> :colors="foodStore.rateColorList"/>
</el-form-item> </el-form-item>
<el-form-item label="食材"> <el-form-item v-if="currentStepIndex === 0" label="共享">
<food-material-form /> <el-radio-group v-model="foodStore.currentRecipe.isShare">
<el-radio :value="false"></el-radio>
<el-radio :value="true"></el-radio>
</el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="步骤"> <el-form-item v-if="currentStepIndex === 0" label="备注">
<food-step-form />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="foodStore.currentRecipe.remark" autocomplete="off" <el-input v-model="foodStore.currentRecipe.remark" autocomplete="off"
placeholder="请输入备注" clearable placeholder="请输入备注" clearable
type="textarea" :rows="3" type="textarea" :rows="3"
show-word-limit maxlength="50"> show-word-limit maxlength="50">
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item v-if="currentStepIndex === 1" label="食材">
<food-material-form />
</el-form-item>
<el-form-item v-if="currentStepIndex === 2" label="步骤">
<food-step-form />
</el-form-item>
</el-form> </el-form>
<div class="button-container"> <div class="button-container">
<el-button type="primary" @click="confirmClick">确认</el-button> <el-button v-if="currentStepIndex !== 0"
<el-button v-if="foodStore.recipeApiType === 'UPDATE'" type="primary" @click="currentStepIndex--">上一步</el-button>
type="danger" @click="deleteClick">删除</el-button> <el-button v-if="currentStepIndex !== 2"
type="primary" @click="currentStepIndex++">下一步</el-button>
<el-button v-if="currentStepIndex === 2"
type="primary" @click="confirmClick">确认</el-button>
<el-button type="info" @click="cancelClick">取消</el-button> <el-button type="info" @click="cancelClick">取消</el-button>
</div> </div>
@@ -60,7 +81,6 @@ import { useFoodStore } from '@/store/food'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { foodRules } from '@/utils/element/elRules' import { foodRules } from '@/utils/element/elRules'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
const rules = reactive<FormRules>(foodRules) const rules = reactive<FormRules>(foodRules)
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
@@ -68,6 +88,8 @@ const foodStore = useFoodStore()
const foodCategoryList = ['家常菜', '淮扬菜', '甜点', '其他'] const foodCategoryList = ['家常菜', '淮扬菜', '甜点', '其他']
const currentStepIndex = ref(0)
/** /**
* 点击确认按钮 * 点击确认按钮
*/ */
@@ -81,14 +103,6 @@ const confirmClick = () => {
const cancelClick = () => { const cancelClick = () => {
history.back() history.back()
} }
const deleteClick = () => {
commonElMessageBox('是否确认删除该菜谱?').then(() => {
foodStore.recipeApiType = 'DELETE'
foodStore.handleRecipeApi(foodStore.currentRecipe)
history.back()
})
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,5 +1,9 @@
<template> <template>
<div class="food-record-form common-mobile-view"> <div class="food-record-form common-mobile-view">
<h2 style="text-align: center; margin: 0;">
{{ foodStore.recordApiType === 'ADD' ? '新增记录' : '编辑记录' }}
</h2>
<el-form ref="formRef" :model="foodStore.currentRecord" <el-form ref="formRef" :model="foodStore.currentRecord"
:rules="rules" label-width="80"> :rules="rules" label-width="80">
<el-form-item label="菜谱名称" prop="name"> <el-form-item label="菜谱名称" prop="name">

View File

@@ -12,7 +12,7 @@ export default defineConfig({
} }
} }
}, },
base: './', base: '/food-hub/',
resolve: { resolve: {
// 配置路径别名 // 配置路径别名
alias: { alias: {
@@ -20,12 +20,11 @@ export default defineConfig({
} }
}, },
server: { server: {
port: 5175,
proxy: { proxy: {
'/food-service': { '/food-api': {
target: 'http://127.0.0.1:8100', target: 'http://127.0.0.1:8083',
changeOrigin: true, changeOrigin: true,
rewrite: (item) => item.replace(/^\/food-service/, '') rewrite: (item) => item.replace(/^\/food-api/, '')
} }
} }
} }