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)
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:
specifier: ^1.1.8
version: 1.1.8(typescript@5.8.3)
@@ -1220,8 +1220,8 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
form-data@4.0.3:
resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fs.realpath@1.0.0:
@@ -2022,8 +2022,8 @@ packages:
peerDependencies:
typescript: '>=5.0.0'
vue3-common@git+https://gitee.com/Cxx0822/vue3-common#4cabccc268f404bef9fd13078ff88fc2830b7461:
resolution: {commit: 4cabccc268f404bef9fd13078ff88fc2830b7461, repo: https://gitee.com/Cxx0822/vue3-common, type: git}
vue3-common@git+https://gitee.com/Cxx0822/vue3-common#0d4a3beb7b1bfb068fe6672c25839997cd8d2129:
resolution: {commit: 0d4a3beb7b1bfb068fe6672c25839997cd8d2129, repo: https://gitee.com/Cxx0822/vue3-common, type: git}
version: 1.0.0
peerDependencies:
pinia: ^2.3.0
@@ -2727,7 +2727,7 @@ snapshots:
axios@1.10.0:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.3
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
@@ -3284,7 +3284,7 @@ snapshots:
dependencies:
is-callable: 1.2.7
form-data@4.0.3:
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
@@ -4119,7 +4119,7 @@ snapshots:
'@vue/language-core': 2.2.10(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:
'@fortawesome/fontawesome-free': 6.7.2
'@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> =>
cloudService({
url: '/food-service/food/recipe',
url: '/food-api/food/recipe',
method: 'post',
data: recipe
})
export const updateRecipeApi = (id: number, recipe: IRecipe): Promise<boolean> =>
cloudService({
url: `/food-service/food/recipe/${id}`,
url: `/food-api/food/recipe/${id}`,
method: 'put',
data: recipe
})
export const deleteRecipeApi = (id: number): Promise<boolean> =>
cloudService({
url: `/food-service/food/recipe/${id}`,
url: `/food-api/food/recipe/${id}`,
method: 'DELETE'
})
export const queryRecipeByIdApi = (id: number): Promise<IRecipe> =>
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'
})
export const queryRecipeApi = (query: IFoodQuery): Promise<IRecipe[]> =>
cloudService({
url: '/food-service/food/recipe',
url: '/food-api/food/recipe',
method: 'get',
params: { ...query }
})
@@ -38,70 +50,101 @@ export const queryRecipeApi = (query: IFoodQuery): Promise<IRecipe[]> =>
export const queryRecipeByPageApi = (currentPage: number, pageSize: number,
query: IFoodQuery): Promise<any> =>
cloudService({
url: '/food-service/food/recipe/page',
url: '/food-api/food/recipe/page',
method: 'get',
params: { currentPage, pageSize, ...query }
})
export const queryFoodNameListApi = (): Promise<string[]> =>
cloudService({
url: '/food-service/food/recipe/name',
url: '/food-api/food/recipe/name',
method: 'get'
})
export const addRecordApi = (record: IRecord): Promise<boolean> =>
cloudService({
url: '/food-service/food/record',
url: '/food-api/food/record',
method: 'post',
data: record
})
export const updateRecordApi = (id: number, record: IRecord): Promise<boolean> =>
cloudService({
url: `/food-service/food/record/${id}`,
url: `/food-api/food/record/${id}`,
method: 'put',
data: record
})
export const deleteRecordApi = (id: number): Promise<boolean> =>
cloudService({
url: `/food-service/food/record/${id}`,
url: `/food-api/food/record/${id}`,
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[]> =>
cloudService({
url: '/food-service/food/record',
url: '/food-api/food/record',
method: 'get',
params: { startDate, endDate }
})
export const queryCategoryApi = (): Promise<string[]> =>
cloudService({
url: '/food-service/food/category',
url: '/food-api/food/category',
method: 'get'
})
export const queryStatsApi = (): Promise<IStats> =>
cloudService({
url: '/food-service/food/stats',
url: '/food-api/food/stats',
method: 'get'
})
export const queryRecordStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/food-service/food/stats/record',
url: '/food-api/food/stats/record',
method: 'get'
})
export const queryCategoryStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/food-service/food/stats/category',
url: '/food-api/food/stats/category',
method: 'get'
})
export const queryRankStatsApi = (): Promise<IStatsChart[]> =>
cloudService({
url: '/food-service/food/stats/rank',
url: '/food-api/food/stats/rank',
method: 'get'
})

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,8 @@
<template>
<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">
<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">
<el-image :src="serviceFileRootPath + item.imageUrl"
fit="contain" alt="暂无成果">
@@ -21,32 +21,50 @@
<div class="top-container">
<div class="info-list">
<div class="info-item">
<span class="title">{{ props.foodItem.name }}</span>
<span class="title">{{ props.item.name }}</span>
</div>
</div>
<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"/>
<el-button type="primary" :icon="Collection" circle :size="'small'"
@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()" />
</div>
</div>
<div class="bottom-container">
<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>
{{ 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 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">
<i class="fa-regular fa-calendar-check"></i>
{{ formatDate(props.foodItem.recordList[foodCardInfo.currentFoodIndex].date) }}
{{ formatDate(props.item.recordList[foodCardInfo.currentFoodIndex].date) }}
</div>
</div>
@@ -61,7 +79,8 @@
<script setup lang="ts">
import { Edit, Collection, DeleteFilled } from '@element-plus/icons-vue'
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 { IRecipe, IRecord } from '@/types/food.ts'
import { useRouter } from 'vue-router'
@@ -71,6 +90,7 @@ import { serviceFileRootPath } from '@/utils'
const router = useRouter()
const foodStore = useFoodStore()
const userStore = useUserStore()
const foodCardInfo = reactive({
recommendRate: 0,
@@ -78,34 +98,44 @@ const foodCardInfo = reactive({
})
const props = defineProps({
foodItem: {
item: {
required: true,
type: Object as PropType<IRecipe>
}
})
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[]) => {
return recordList.filter((item) => item.imageUrl)
}
const editClick = async () => {
foodStore.recipeApiType = 'UPDATE'
await foodStore.queryRecipe(props.foodItem.id as number)
await foodStore.queryRecipe(props.item.id as number)
await router.push('/record/recipeForm')
}
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 = () => {
foodStore.recipeApiType = 'DELETE'
commonElMessageBox(`请确认是否删除该菜谱: ${props.foodItem?.name}?`).then(() => {
foodStore.handleRecipeApi(props.foodItem as IRecipe)
commonElMessageBox(`请确认是否删除该菜谱: ${props.item?.name}?`).then(() => {
foodStore.handleRecipeApi(props.item as IRecipe)
})
}

View File

@@ -21,9 +21,6 @@ const foodStore = useFoodStore()
const appStore = useAppStore()
onMounted(async () => {
foodStore.queryDateType = 'month'
await foodStore.refreshInfo()
const { month } = foodStore.queryRecord
calendarRef.value.move(new Date(month))
})
@@ -86,7 +83,7 @@ const dateClick = (day) => {
*/
const changeMonthClick = async (value) => {
foodStore.queryRecord.month = value[0].id
await foodStore.refreshInfo()
await foodStore.queryRecordList()
}
</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"/>
</div>
<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" :food-item="item"/>
</div>
</div>
<recipe-list />
<arrive-bottom v-if="foodStore.isScrollEnd"/>
</div>
</template>
<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 { useFoodStore } from '@/store/food.ts'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ICategory } from '@/types'
const foodRecipeRef = ref()
@@ -33,10 +26,7 @@ const foodStore = useFoodStore()
const queryCategoryList = ref([] as ICategory[])
onMounted(async () => {
await foodStore.queryCategory()
initCategoryList()
// foodStore.foodRecipeList = []
await foodStore.refreshInfo()
// foodRecipeRef.value.addEventListener('scroll', handleScroll)
})
@@ -44,6 +34,10 @@ onBeforeUnmount(() => {
// foodRecipeRef.value.removeEventListener('scroll', handleScroll)
})
watch(() => foodStore.isRefresh, () => {
initCategoryList()
})
const initCategoryList = () => {
queryCategoryList.value = []
queryCategoryList.value.push({
@@ -87,13 +81,5 @@ const handleScroll = () => {
.query-item {
padding: 5px 10px;
}
.food-list {
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
}
}
</style>

View File

@@ -1,16 +1,27 @@
<template>
<div class="food-timeline">
<el-date-picker
v-model="currentYear"
type="year" placeholder="请选择年份"
:editable="false"
style="width: 100px;"
@change="changeDateEvent"
/>
<div class="button-container">
<el-button :icon="CaretLeft" circle
type="primary" size="small"
@click="previousYearClick"/>
<el-date-picker
v-model="foodStore.queryRecord.year"
type="year" placeholder="请选择年份"
:editable="false"
style="width: 100px;"
@change="changeDateEvent"
/>
<el-button :icon="CaretRight" circle
type="primary" size="small"
@click="nextYearClick"/>
</div>
<el-timeline v-if="foodStore.recordList.length > 0">
<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">
<span>{{ item.name }}</span>
<el-image :src="serviceFileRootPath + item.imageUrl"
@@ -30,22 +41,28 @@
</template>
<script setup lang="ts">
import { CaretLeft, CaretRight } from '@element-plus/icons-vue'
import { useFoodStore } from '@/store/food.ts'
import { onMounted, ref } from 'vue'
import { formatDate } from 'vue3-common/utils/dateUtil'
import { formatDate, getLastDate, getNextDate } from 'vue3-common/utils/dateUtil'
import { serviceFileRootPath } from '@/utils'
const currentYear = ref(new Date())
const foodStore = useFoodStore()
onMounted(async () => {
foodStore.queryDateType = 'year'
await foodStore.refreshInfo()
})
const changeDateEvent = async (value: Date) => {
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>
@@ -54,13 +71,21 @@ const changeDateEvent = async (value: Date) => {
display: flex;
flex-direction: column;
.el-date-editor {
.button-container {
align-self: center;
display: flex;
align-items: center;
gap: 10px;
}
.el-timeline {
padding: 10px;
.el-timeline-item__timestamp {
font-weight: bold;
color: #000000;
}
.timeline-info {
padding: 10px;
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 class="login-type">
<el-button @click="authStore.changeLoginType('phone')">手机号登录</el-button>
<el-button>手机号登录</el-button>
<el-button>扫码登录</el-button>
</div>
@@ -58,8 +58,7 @@
<div class="type-list">
<i class="fa-brands fa-weixin" style="color: #3CB034"></i>
<i class="fa-brands fa-qq" style="color: #29A4D9"></i>
<i class="fa-solid fa-envelope" style="color: #4971B8"
@click="authStore.changeLoginType('email')"></i>
<i class="fa-solid fa-envelope" style="color: #4971B8"></i>
<i class="fa-brands fa-github" style="color: #555555"></i>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<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">
<span class="username">{{ props.item.username }}</span>
@@ -9,6 +9,7 @@
<div class="moment-image-list">
<el-image v-for="(item, index) in props.item.imageList" :key="index"
:src="serviceFileRootPath + item" fit="cover"
:initial-index="index"
:preview-src-list=previewList />
</div>
@@ -124,9 +125,9 @@ const editClick = () => {
const handleLikeClick = () => {
if (isLike.value) {
foodStore.deleteLike(props.item?.id as number)
foodStore.deleteMomentLike(props.item?.id as number)
} 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 () => {
await foodStore.addComment(props.item?.id as number, momentInfo.comment)
await foodStore.addMomentComment(props.item?.id as number, momentInfo.comment)
momentInfo.comment = ''
momentInfo.isShowComment = false
@@ -150,61 +151,3 @@ const viewProfileClick = async () => {
}
}
</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>
<div class="profile-card">
<div class="profile-card card-item">
<div class="avatar-wrapper">
<el-avatar :src="avatarUrl" alt="用户头像" :size="80"
fit="contain" @click="previewVisible = true"
@@ -75,16 +75,16 @@ const avatarUrl = computed(() => {
<style lang="scss">
.profile-card {
background: linear-gradient(135deg, #6e8efb, #a777e3);
border-radius: 10px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
// background: linear-gradient(135deg, #6e8efb, #a777e3);
// border-radius: 10px;
padding: 10px;
// box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
text-align: center;
color: white;
// color: white;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
gap: 10px;
.avatar-wrapper {
position: relative;

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { getLocalStorage, setLocalStorage } from 'vue3-common/utils/storageUtil'
import { useDark, useToggle } from '@vueuse/core'
import { darken, lighten } from '@/utils/colorUtils'
export const useAppStore = defineStore('ipp', {
state: () => ({
@@ -14,7 +15,7 @@ export const useAppStore = defineStore('ipp', {
isDark: false,
isWebSocket: false,
serverIp: '14.103.235.151',
version: 'v1.0.0.2025062802'
version: 'v1.0.0.2025071801'
}),
actions: {
initApp() {
@@ -71,9 +72,18 @@ export const useAppStore = defineStore('ipp', {
getThemeColor(): string {
return getLocalStorage('theme-color') as string || '#009FA8'
},
setThemeColor(value: string) {
document.documentElement.style.setProperty('--el-color-primary', value)
setLocalStorage('theme-color', value)
setThemeColor(color: string) {
const { style } = document.documentElement
// 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
} from '@/types/food'
import {
addRecipeApi,
addRecordApi,
deleteRecipeApi,
deleteRecordApi,
queryFoodNameListApi,
queryRecipeByIdApi,
queryRecordApi,
queryStatsApi,
updateRecipeApi,
updateRecordApi,
queryCategoryApi,
queryRecordStatsApi,
queryCategoryStatsApi, queryRankStatsApi, queryRecipeApi
addRecipeApi, addRecordApi, deleteRecipeApi, deleteRecordApi,
queryFoodNameListApi, queryRecipeByIdApi, queryRecordApi, queryStatsApi,
updateRecipeApi, updateRecordApi, queryCategoryApi, queryRecordStatsApi,
queryCategoryStatsApi, queryRankStatsApi,
queryRecipeApi, queryRecipeByUserApi, deleteRecipeLikeApi,
addRecipeLikeApi, addRecipeCommentApi, addRecipeFavouriteApi, deleteRecipeFavouriteApi, queryRecipeUserFavouriteApi
} from '@/apis/food.ts'
import type { IDialog, IHandleApi, IStatsChart } from 'vue3-common/types'
import { ElMessage } from 'element-plus'
import { getCurrentMonth, getCurrentYear, getStartEndDate } from 'vue3-common/utils/dateUtil'
import {
addCommentApi,
addLikeApi,
addMomentApi,
deleteLikeApi,
deleteMomentApi,
queryMomentApi,
addMomentLikeApi,
addMomentApi, addMomentCommentApi,
deleteMomentLikeApi,
deleteMomentApi, queryMomentByIdApi,
queryMomentListApi,
updateMomentApi
} from '@/apis/moment'
@@ -40,9 +32,19 @@ export const useFoodStore = defineStore('food', {
category: '',
recommendRate: 0,
remark: '',
isShare: false,
username: '',
userId: 0,
avatar: '',
materialList: [],
stepList: [],
recordList: []
recordList: [],
likeCount: 0,
likeList: [],
favouriteCount: 0,
favouriteList: [],
commentCount: 0,
commentList: []
} as IRecipe,
recordList: [] as IRecord[],
currentRecord: {
@@ -105,7 +107,8 @@ export const useFoodStore = defineStore('food', {
recordStats: [] as IStatsChart[],
categoryStats: [] as IStatsChart[],
rankStats: [] as IStatsChart[],
isRefresh: false
isRefresh: false,
isLoading: false
}),
actions: {
async queryRecipeList() {
@@ -113,16 +116,34 @@ export const useFoodStore = defineStore('food', {
// const result = await queryFoodRecipeByPageApi(currentPage, pageSize, this.queryRecipe)
// this.foodRecipeList.push(...result.records)
// this.pageInfo.totalSize = result.total
this.isLoading = true
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() {
const { year, month } = this.queryRecord
const date = this.queryDateType === 'year' ? year : month
this.queryDateList = getStartEndDate(date, this.queryDateType)
this.isLoading = true
this.recordList = await queryRecordApi(this.queryDateList[0], this.queryDateList[1])
this.isRefresh = !this.isRefresh
this.isLoading = false
},
async queryMomentList() {
this.momentList = await queryMomentApi()
this.isLoading = true
this.momentList = await queryMomentListApi()
this.isLoading = false
},
async queryFoodNameList() {
this.foodNameList = await queryFoodNameListApi()
@@ -130,6 +151,11 @@ export const useFoodStore = defineStore('food', {
async queryRecipe(id: number) {
this.currentRecipe = await queryRecipeByIdApi(id)
},
async queryMoment(id: number) {
this.isLoading = true
this.momentList = await queryMomentByIdApi(id)
this.isLoading = false
},
async queryStats() {
this.stats = await queryStatsApi()
this.recordStats = await queryRecordStatsApi()
@@ -157,7 +183,7 @@ export const useFoodStore = defineStore('food', {
default:
break
}
await this.refreshInfo()
await this.queryRecipeList()
},
async handleRecordApi(record: IRecord) {
switch (this.recordApiType) {
@@ -176,7 +202,7 @@ export const useFoodStore = defineStore('food', {
default:
break
}
await this.refreshInfo()
await this.queryRecordList()
},
async handleMomentApi(moment: IMoment) {
switch (this.momentApiType) {
@@ -195,31 +221,51 @@ export const useFoodStore = defineStore('food', {
default:
break
}
await queryMomentApi()
},
async addComment(id: number, content: string) {
await addCommentApi(id, content)
await this.queryMomentList()
},
async addLike(id: number) {
await addLikeApi(id)
async addMomentComment(id: number, content: string) {
await addMomentCommentApi(id, content)
await this.queryMomentList()
},
async deleteLike(id: number) {
await deleteLikeApi(id)
async addMomentLike(id: number) {
await addMomentLikeApi(id)
await this.queryMomentList()
},
async refreshInfo() {
await this.queryRecipeList()
await this.queryRecordList()
this.isRefresh = !this.isRefresh
async deleteMomentLike(id: number) {
await deleteMomentLikeApi(id)
await this.queryMomentList()
},
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 {
return {
commentCount: 0,
commentList: [],
likeCount: 0,
likeList: [],
category: '',
materialList: [],
name: '',
recommendRate: 0,
isShare: false,
remark: '',
stepList: [],
recordList: []

View File

@@ -79,7 +79,7 @@ span, input {
@font-face {
font-family: 'CustomFont';
src: url('@/assets/font/霞鹜臻楷.ttf') format('truetype');
src: url('@/assets/font/custom.woff2') format('truetype');
font-weight: 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 {

View File

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

View File

@@ -77,7 +77,7 @@ $tabBar-height: 70px;
display: flex;
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;
}
export interface IComment {
id: number;
username: string;
avatar: string;
content: string;
date: string;
}
/**
* 成果信息
*/
@@ -51,6 +59,10 @@ export interface IRecipe {
* 备注
*/
remark: string;
isShare: boolean;
userId?: number;
username?: string;
avatar?: string;
/**
* 食材
*/
@@ -63,6 +75,12 @@ export interface IRecipe {
* 成果
*/
recordList: IRecord[];
likeList?: number[];
likeCount?: number;
favouriteList?: number[];
favouriteCount?: number;
commentList?: IComment[];
commentCount?: number;
}
/**
@@ -78,14 +96,6 @@ export interface IFoodQuery {
category: string;
}
export interface IComment {
id: number;
username: string;
avatar: string;
content: string;
date: string;
}
export interface IMoment {
id?: 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静态资源

View File

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

View File

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

View File

@@ -1,22 +1,48 @@
<template>
<div class="moment-profile-view common-mobile-view">
<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>
</template>
<script lang="ts" setup>
import RecipeList from '@/components/Food/RecipeList.vue'
import ProfileCard from '@/components/Profile/ProfileCard.vue'
import { useUserStore } from '@/store/user.ts'
import { useAppStore } from '@/store/app.ts'
import { useRouter } from 'vue-router'
import { onMounted } from 'vue'
import { useFoodStore } from '@/store/food.ts'
import { onMounted, ref } from 'vue'
import MomentList from '@/components/Moment/MomentList.vue'
import type { TabsPaneContext } from 'element-plus'
const appStore = useAppStore()
const router = useRouter()
const foodStore = useFoodStore()
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>
<style lang="scss">

View File

@@ -2,61 +2,74 @@
<div class="profile-view common-mobile-view">
<profile-card />
<div class="menu-card card-item">
<div class="menu-item" @click="infoClick">
<i class="fas fa-user"></i>
<span>基本资料</span>
<i class="fas fa-chevron-right"></i>
</div>
<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-item" @click="infoClick">
<i class="fas fa-user"></i>
<span>基本资料</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-divider"></div>
<div class="menu-divider"></div>
<div class="menu-item">
<i class="fas fa-cog"></i>
<span>账户设置</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-item">
<i class="fas fa-cog"></i>
<span>账户设置</span>
<i class="fas fa-chevron-right"></i>
</div>
<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-item" @click="passwordClick">
<i class="fas fa-key"></i>
<span>更改密码</span>
<i class="fas fa-chevron-right"></i>
</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>
<div class="menu-item" @click="passwordClick">
<i class="fas fa-key"></i>
<span>更改密码</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-item">
<i class="fa-solid fa-file-arrow-down"></i>
<span>软件下载</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-divider"></div>
<div class="menu-divider"></div>
<div class="menu-item">
<i class="fa-solid fa-file-arrow-down"></i>
<span>软件下载</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-item" @click="refreshClick">
<i class="fa-solid fa-arrows-rotate"></i>
<span>检查更新</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-divider"></div>
<div class="menu-divider"></div>
<div class="menu-item" @click="refreshClick">
<i class="fa-solid fa-arrows-rotate"></i>
<span>检查更新</span>
<i class="fas fa-chevron-right"></i>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="logoutClick">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
<i class="fas fa-chevron-right"></i>
</div>
</div>
<div class="menu-item" @click="logoutClick">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
<i class="fas fa-chevron-right"></i>
</div>
</div>
</el-tab-pane>
</el-tabs>
<div class="version">版本号 {{ appStore.version }}</div>
</div>
@@ -64,18 +77,25 @@
<script lang="ts" setup>
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 { useAppStore } from '@/store/app.ts'
import { useAuthStore } from '@/store/auth'
import { useFoodStore } from '@/store/food.ts'
import { useRouter } from 'vue-router'
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 authStore = useAuthStore()
const appStore = useAppStore()
const foodStore = useFoodStore()
const router = useRouter()
const activeName = ref('basic')
onMounted(() => {
userStore.queryUser(userStore.getUserId() as number)
})
@@ -89,6 +109,19 @@ const passwordClick = () => {
// 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 () => {
await router.push('/')
location.reload()

View File

@@ -12,28 +12,67 @@
<food-others-data/>
</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>
</div>
</template>
<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 FoodMaterialData from '@/components/Food/FoodMaterialData.vue'
import FoodStepData from '@/components/Food/FoodStepData.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 appStore = useAppStore()
const route = useRoute()
const userStore = useUserStore()
onMounted(() => {
appStore.isShowMobileBack = true
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>
<style lang="scss">

View File

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

View File

@@ -1,15 +1,26 @@
<template>
<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"
: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"
placeholder="请输入名称" clearable
show-word-limit maxlength="20">
</el-input>
</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"
placeholder="请选择或输入菜系" allow-create filterable>
<el-option v-for="(item, index) in foodCategoryList"
@@ -17,32 +28,42 @@
</el-select>
</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"
:colors="foodStore.rateColorList"/>
</el-form-item>
<el-form-item label="食材">
<food-material-form />
<el-form-item v-if="currentStepIndex === 0" label="共享">
<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 label="步骤">
<food-step-form />
</el-form-item>
<el-form-item label="备注">
<el-form-item v-if="currentStepIndex === 0" label="备注">
<el-input v-model="foodStore.currentRecipe.remark" autocomplete="off"
placeholder="请输入备注" clearable
type="textarea" :rows="3"
show-word-limit maxlength="50">
</el-input>
</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>
<div class="button-container">
<el-button type="primary" @click="confirmClick">确认</el-button>
<el-button v-if="foodStore.recipeApiType === 'UPDATE'"
type="danger" @click="deleteClick">删除</el-button>
<el-button v-if="currentStepIndex !== 0"
type="primary" @click="currentStepIndex--">上一步</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>
</div>
@@ -60,7 +81,6 @@ import { useFoodStore } from '@/store/food'
import type { FormInstance, FormRules } from 'element-plus'
import { ref, reactive } from 'vue'
import { foodRules } from '@/utils/element/elRules'
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
const rules = reactive<FormRules>(foodRules)
const formRef = ref<FormInstance>()
@@ -68,6 +88,8 @@ const foodStore = useFoodStore()
const foodCategoryList = ['家常菜', '淮扬菜', '甜点', '其他']
const currentStepIndex = ref(0)
/**
* 点击确认按钮
*/
@@ -81,14 +103,6 @@ const confirmClick = () => {
const cancelClick = () => {
history.back()
}
const deleteClick = () => {
commonElMessageBox('是否确认删除该菜谱?').then(() => {
foodStore.recipeApiType = 'DELETE'
foodStore.handleRecipeApi(foodStore.currentRecipe)
history.back()
})
}
</script>
<style lang="scss">

View File

@@ -1,5 +1,9 @@
<template>
<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"
:rules="rules" label-width="80">
<el-form-item label="菜谱名称" prop="name">

View File

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