feat:增加美食表单功能
This commit is contained in:
81
src/apis/food.ts
Normal file
81
src/apis/food.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { cloudService } from './index'
|
||||
import type { IFoodQuery, IFoodRecipe, IFoodRecord, IFoodStats } from '@/types/food'
|
||||
|
||||
export const addFoodRecipeApi = (foodRecipe: IFoodRecipe): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/recipe',
|
||||
method: 'post',
|
||||
data: foodRecipe
|
||||
})
|
||||
|
||||
export const updateFoodRecipeApi = (id: number, foodRecipe: IFoodRecipe): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/food-service/food/recipe/${id}`,
|
||||
method: 'put',
|
||||
data: foodRecipe
|
||||
})
|
||||
|
||||
export const deleteFoodRecipeApi = (id: number): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/food-service/food/recipe/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
export const queryFoodRecipeByIdApi = (id: number): Promise<any> =>
|
||||
cloudService({
|
||||
url: `/food-service/food/recipe/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const queryFoodRecipeApi = (currentPage: number, pageSize: number,
|
||||
query: IFoodQuery): Promise<any> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/recipe/page',
|
||||
method: 'get',
|
||||
params: { currentPage, pageSize, ...query }
|
||||
})
|
||||
|
||||
export const queryFoodNameListApi = (): Promise<string[]> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/recipe/name',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const addFoodRecordApi = (foodRecord: IFoodRecord): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/record',
|
||||
method: 'post',
|
||||
data: foodRecord
|
||||
})
|
||||
|
||||
export const updateFoodRecordApi = (id: number, foodRecord: IFoodRecord): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/food-service/food/record/${id}`,
|
||||
method: 'put',
|
||||
data: foodRecord
|
||||
})
|
||||
|
||||
export const deleteFoodRecordApi = (id: number): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/food-service/food/record/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
export const queryFoodRecordApi = (startDate: string, endDate: string): Promise<IFoodRecord[]> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/record',
|
||||
method: 'get',
|
||||
params: { startDate, endDate }
|
||||
})
|
||||
|
||||
export const queryFoodCategoryApi = (): Promise<string[]> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/category',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const queryFoodStatsApi = (): Promise<IFoodStats> =>
|
||||
cloudService({
|
||||
url: '/food-service/food/stats',
|
||||
method: 'get'
|
||||
})
|
||||
3
src/apis/index.ts
Normal file
3
src/apis/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import createService from 'vue3-common/utils/axiosUtil'
|
||||
|
||||
export const cloudService = createService()
|
||||
9
src/apis/session.ts
Normal file
9
src/apis/session.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { cloudService } from './index'
|
||||
import type { ISession } from '@/types/auth'
|
||||
|
||||
export const loginApi = (name: string, password: string): Promise<ISession> =>
|
||||
cloudService({
|
||||
url: '/food-service/session',
|
||||
method: 'post',
|
||||
params: { name, password }
|
||||
})
|
||||
@@ -6,24 +6,19 @@
|
||||
class="food-calendar card-item"/>
|
||||
|
||||
<food-daily-item :date="dailyInfo.calendarDate"/>
|
||||
|
||||
<draggable-button @click="addClick"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FoodDailyItem from '@/components/Food/FoodDailyItem.vue'
|
||||
import DraggableButton from '@/components/DraggableButton.vue'
|
||||
import { reactive, watch, ref, onMounted } from 'vue'
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const calendarRef = ref()
|
||||
const foodStore = useFoodStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
const { month } = foodStore.queryRecord
|
||||
@@ -39,12 +34,6 @@ const dailyInfo = reactive({
|
||||
attrs: [] as any[]
|
||||
})
|
||||
|
||||
const addClick = () => {
|
||||
foodStore.foodRecordApiType = 'ADD'
|
||||
foodStore.currentFoodRecord = foodStore.getEmptyFoodRecord()
|
||||
router.push({ name: 'MobileHomeFoodRecordForm' })
|
||||
}
|
||||
|
||||
const updateInfo = () => {
|
||||
getCalendarMoveDate()
|
||||
getCalendarAttrDates()
|
||||
|
||||
58
src/components/Food/FoodMaterialForm.vue
Normal file
58
src/components/Food/FoodMaterialForm.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="food-material-form">
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="addClick">新增</el-button>
|
||||
</div>
|
||||
<el-table :data="foodStore.currentFoodRecipe.materialList"
|
||||
border stripe class="material-table">
|
||||
<el-table-column type="index" align="center" width="40" />
|
||||
<el-table-column prop="type" label="类型" align="center" min-width="10%"/>
|
||||
<el-table-column prop="name" label="名称" align="center" min-width="20%"/>
|
||||
<el-table-column prop="amount" label="分量" align="center" min-width="20%"/>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { IFoodMaterial } from '@/types/food'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const addClick = () => {
|
||||
foodStore.foodMaterialApiType = 'ADD'
|
||||
foodStore.foodMaterialDialog = {
|
||||
title: '新增食材',
|
||||
visible: true,
|
||||
data: foodStore.getEmptyFoodMaterial()
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditClick = (index: number, material: IFoodMaterial) => {
|
||||
foodStore.editFoodMaterialIndex = index
|
||||
foodStore.foodMaterialApiType = 'UPDATE'
|
||||
foodStore.foodMaterialDialog = {
|
||||
title: '编辑食材',
|
||||
visible: true,
|
||||
data: deepCopyObject(material)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteClick = (index: number, material: IFoodMaterial) => {
|
||||
foodStore.foodMaterialApiType = 'DELETE'
|
||||
commonElMessageBox(`确认删除该食材: ${material.name}?`).then(() => {
|
||||
foodStore.currentFoodRecipe.materialList.splice(index, 1)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-material-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -17,22 +17,17 @@
|
||||
</div>
|
||||
|
||||
<arrive-bottom v-if="foodStore.isScrollEnd"/>
|
||||
|
||||
<draggable-button @click="addClick"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FoodCard from '@/components/Food/FoodCard.vue'
|
||||
import ArriveBottom from '@/components/ArriveButton.vue'
|
||||
import DraggableButton from '@/components/DraggableButton.vue'
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const foodRecipeRef = ref()
|
||||
const foodStore = useFoodStore()
|
||||
const router = useRouter()
|
||||
|
||||
const foodCategoryList = [{
|
||||
label: '全部',
|
||||
@@ -51,12 +46,6 @@ onBeforeUnmount(() => {
|
||||
foodRecipeRef.value.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
const addClick = () => {
|
||||
foodStore.foodRecordApiType = 'ADD'
|
||||
foodStore.currentFoodRecord = foodStore.getEmptyFoodRecord()
|
||||
router.push({ name: 'MobileHomeFoodRecordForm' })
|
||||
}
|
||||
|
||||
const queryFoodSegmented = async () => {
|
||||
foodStore.pageInfo.currentPage = 1
|
||||
foodStore.foodRecipeList = []
|
||||
|
||||
80
src/components/Food/FoodStepForm.vue
Normal file
80
src/components/Food/FoodStepForm.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="food-step-form">
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="addClick">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="foodStore.currentFoodRecipe.stepList"
|
||||
border class="food-step-table">
|
||||
<el-table-column type="index" align="center" width="40" />
|
||||
<el-table-column prop="content" label="内容" align="center" min-width="20%" />
|
||||
<el-table-column prop="imageUrl" label="图片" align="center" min-width="20%">
|
||||
<template #default="scope">
|
||||
<el-image v-if="scope.row.imageUrl" :src="serviceFileRootPath + scope.row.imageUrl" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { IFoodStep } from '@/types/food'
|
||||
import { deepCopyObject } from 'vue3-common/utils/dataUtil'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const addClick = () => {
|
||||
foodStore.foodStepApiType = 'ADD'
|
||||
// foodStore.foodStepImageList = []
|
||||
foodStore.foodStepDialog = {
|
||||
title: '新增步骤',
|
||||
visible: true,
|
||||
data: foodStore.getEmptyFoodStep()
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditClick = (index: number, step: IFoodStep) => {
|
||||
foodStore.editFoodStepIndex = index
|
||||
foodStore.foodStepApiType = 'UPDATE'
|
||||
foodStore.foodStepDialog = {
|
||||
title: '编辑步骤',
|
||||
visible: true,
|
||||
data: deepCopyObject(step)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteClick = (index: number) => {
|
||||
commonElMessageBox('确认删除该步骤?').then(() => {
|
||||
foodStore.foodStepApiType = 'DELETE'
|
||||
foodStore.currentFoodRecipe.stepList.splice(index, 1)
|
||||
foodStore.currentFoodRecipe.stepList.forEach((item) => {
|
||||
if (item.sort > index) {
|
||||
item.sort -= 1
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-step-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.food-step-table {
|
||||
.el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
max-height: 200px;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
127
src/store/auth.ts
Normal file
127
src/store/auth.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { ILoginType, ISession } from '@/types/auth'
|
||||
import { loginApi } from '@/apis/session'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { emailPattern, phonePattern } from '@/utils/element/elRules'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
loginForm: {
|
||||
username: '132',
|
||||
password: '123',
|
||||
isRemember: false
|
||||
},
|
||||
phoneLoginForm: {
|
||||
phoneNumber: '',
|
||||
code: ''
|
||||
},
|
||||
emailLoginForm: {
|
||||
email: '',
|
||||
code: ''
|
||||
},
|
||||
loginType: 'common' as ILoginType,
|
||||
loginLoading: false,
|
||||
loginAdminLoading: false,
|
||||
isVerified: false,
|
||||
isGettingCode: false,
|
||||
getCodeCountdown: 60,
|
||||
getCodeTimer: null as NodeJS.Timeout | null,
|
||||
homeSession: {} as ISession
|
||||
}),
|
||||
actions: {
|
||||
changeLoginType(type: ILoginType) {
|
||||
this.loginType = type
|
||||
|
||||
if (type === 'common') {
|
||||
this.stopCountdown()
|
||||
}
|
||||
this.resetLoginForm()
|
||||
},
|
||||
getAuthCode(type: ILoginType) {
|
||||
switch (type) {
|
||||
case 'phone':
|
||||
if (phonePattern.test(this.phoneLoginForm.phoneNumber)) {
|
||||
this.isGettingCode = true
|
||||
ElMessage.success('验证码已发送至手机')
|
||||
this.startCountdown()
|
||||
} else {
|
||||
ElMessage.error('请先输入正确的手机号')
|
||||
}
|
||||
break
|
||||
case 'email':
|
||||
if (emailPattern.test(this.emailLoginForm.email)) {
|
||||
this.isGettingCode = true
|
||||
ElMessage.success('验证码已发送至邮箱')
|
||||
this.startCountdown()
|
||||
} else {
|
||||
ElMessage.error('请先输入正确的邮箱地址')
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
startCountdown() {
|
||||
this.getCodeTimer = setInterval(() => {
|
||||
if (this.getCodeCountdown > 0) {
|
||||
this.getCodeCountdown--
|
||||
} else {
|
||||
clearInterval(this.getCodeTimer as NodeJS.Timeout)
|
||||
this.isGettingCode = false
|
||||
this.getCodeCountdown = 60
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
stopCountdown() {
|
||||
if (this.getCodeTimer) {
|
||||
clearInterval(this.getCodeTimer)
|
||||
}
|
||||
},
|
||||
async handleLogin(type: ILoginType) {
|
||||
const { username, password } = this.loginForm
|
||||
const { phoneNumber, code: phoneCode } = this.phoneLoginForm
|
||||
const { email, code: emailCode } = this.emailLoginForm
|
||||
switch (type) {
|
||||
case 'common':
|
||||
console.log(username)
|
||||
console.log(password)
|
||||
this.homeSession = await loginApi(username, password)
|
||||
break
|
||||
case 'phone':
|
||||
this.homeSession = await loginApi(phoneNumber, phoneCode)
|
||||
break
|
||||
case 'email':
|
||||
this.homeSession = await loginApi(email, emailCode)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
ElMessage.success('登录成功')
|
||||
},
|
||||
resetLoginForm() {
|
||||
switch (this.loginType) {
|
||||
case 'common':
|
||||
this.loginForm = {
|
||||
username: '',
|
||||
password: '',
|
||||
isRemember: false
|
||||
}
|
||||
break
|
||||
case 'phone':
|
||||
this.phoneLoginForm = {
|
||||
phoneNumber: '',
|
||||
code: ''
|
||||
}
|
||||
break
|
||||
case 'email':
|
||||
this.emailLoginForm = {
|
||||
email: '',
|
||||
code: ''
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3,11 +3,11 @@ import type {
|
||||
IFoodRecipe, IFoodMaterial, IFoodStep,
|
||||
IFoodStats, IFoodRecord
|
||||
} from '@/types/food'
|
||||
// import {
|
||||
// addFoodRecipeApi, addFoodRecordApi, deleteFoodRecipeApi, deleteFoodRecordApi,
|
||||
// queryFoodNameListApi, queryFoodRecipeByIdApi, queryFoodRecipeApi, queryFoodRecordApi,
|
||||
// queryFoodStatsApi, updateFoodRecipeApi, updateFoodRecordApi, queryFoodCategoryApi
|
||||
// } from '@/apis/food.ts'
|
||||
import {
|
||||
addFoodRecipeApi, addFoodRecordApi, deleteFoodRecipeApi, deleteFoodRecordApi,
|
||||
queryFoodNameListApi, queryFoodRecipeByIdApi, queryFoodRecipeApi, queryFoodRecordApi,
|
||||
queryFoodStatsApi, updateFoodRecipeApi, updateFoodRecordApi, queryFoodCategoryApi
|
||||
} from '@/apis/food.ts'
|
||||
// import { IDateType, ImagePathEnum } from '@/types'
|
||||
import type { IDialog, IHandleApi } from 'vue3-common/types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -47,11 +47,6 @@ export const useFoodStore = defineStore('food', {
|
||||
queryDateType: 'month',
|
||||
queryDateList: [] as string[],
|
||||
foodRecipeApiType: 'ADD' as IHandleApi,
|
||||
foodRecipeDialog: {
|
||||
visible: false,
|
||||
title: '',
|
||||
data: {}
|
||||
} as IDialog<IFoodRecipe>,
|
||||
foodMaterialApiType: 'ADD' as IHandleApi,
|
||||
foodMaterialDialog: {
|
||||
visible: false,
|
||||
@@ -67,11 +62,6 @@ export const useFoodStore = defineStore('food', {
|
||||
} as IDialog<IFoodStep>,
|
||||
editFoodStepIndex: 0,
|
||||
foodRecordApiType: 'ADD' as IHandleApi,
|
||||
foodRecordDialog: {
|
||||
visible: false,
|
||||
title: '',
|
||||
data: {}
|
||||
} as IDialog<IFoodRecord>,
|
||||
editFoodRecordIndex: 0,
|
||||
foodImagePath: 'food',
|
||||
rateColorList: ['#E2DEDC', '#F7BA2A', '#009FA8'],
|
||||
@@ -92,21 +82,9 @@ export const useFoodStore = defineStore('food', {
|
||||
actions: {
|
||||
async queryFoodRecipeList() {
|
||||
const { currentPage, pageSize } = this.pageInfo
|
||||
// const result = await queryFoodRecipeApi(currentPage, pageSize, this.queryRecipe)
|
||||
const foodRecord: IFoodRecipe = {
|
||||
category: '家常菜',
|
||||
materialList: [],
|
||||
name: '红烧鱼',
|
||||
recommendRate: 4,
|
||||
recordList: [],
|
||||
remark: '',
|
||||
stepList: []
|
||||
}
|
||||
this.foodRecipeList = []
|
||||
this.foodRecipeList.push(foodRecord)
|
||||
this.foodRecipeList.push(foodRecord)
|
||||
// this.foodRecipeList.push(...result.records)
|
||||
// this.pageInfo.totalSize = result.total
|
||||
const result = await queryFoodRecipeApi(currentPage, pageSize, this.queryRecipe)
|
||||
this.foodRecipeList.push(...result.records)
|
||||
this.pageInfo.totalSize = result.total
|
||||
},
|
||||
async queryFoodRecordList() {
|
||||
const { year, month } = this.queryRecord
|
||||
@@ -147,19 +125,18 @@ export const useFoodStore = defineStore('food', {
|
||||
async queryFoodCategory() {
|
||||
// this.foodCategoryList = await queryFoodCategoryApi()
|
||||
},
|
||||
async handleFoodRecipeApi(id?: number) {
|
||||
const { data } = this.foodRecipeDialog
|
||||
async handleFoodRecipeApi(foodRecipe: IFoodRecipe) {
|
||||
switch (this.foodRecipeApiType) {
|
||||
case 'ADD':
|
||||
// await addFoodRecipeApi(data)
|
||||
// await addFoodRecipeApi(foodRecipe)
|
||||
ElMessage.success('新增菜谱成功')
|
||||
break
|
||||
case 'UPDATE':
|
||||
// await updateFoodRecipeApi(data.id as number, data)
|
||||
// await updateFoodRecipeApi(foodRecipe.id as number, foodRecipe)
|
||||
ElMessage.success('更新菜谱成功')
|
||||
break
|
||||
case 'DELETE':
|
||||
// await deleteFoodRecipeApi(id as number)
|
||||
// await deleteFoodRecipeApi(foodRecipe.id as number)
|
||||
ElMessage.success('删除菜谱成功')
|
||||
break
|
||||
default:
|
||||
|
||||
75
src/store/user.ts
Normal file
75
src/store/user.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { getLocalStorage, setLocalStorage, removeLocalStorage } from 'vue3-common/utils/storageUtil'
|
||||
import type { ILoginForm } from 'vue3-common'
|
||||
import type { ISession, IUser } from '@/types/auth'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
userInfo: {
|
||||
id: 0,
|
||||
accountId: 0,
|
||||
accountName: '',
|
||||
username: '',
|
||||
gender: 0,
|
||||
idNumber: '',
|
||||
phoneNumber: '',
|
||||
email: '',
|
||||
birthDate: '',
|
||||
avatar: '',
|
||||
description: '',
|
||||
area: ['', ''],
|
||||
address: '',
|
||||
job: '',
|
||||
tags: []
|
||||
} as IUser
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setLoginForm(loginForm: ILoginForm) {
|
||||
setLocalStorage('loginForm', JSON.stringify(loginForm))
|
||||
},
|
||||
getLoginForm() {
|
||||
return getLocalStorage('loginForm')
|
||||
},
|
||||
removeLoginForm() {
|
||||
removeLocalStorage('loginForm')
|
||||
},
|
||||
handleSession(session: ISession) {
|
||||
this.setToken(session.saToken.tokenValue)
|
||||
// this.setUserId(session.userInfo.id)
|
||||
// this.userInfo = session.userInfo
|
||||
},
|
||||
async queryUser() {
|
||||
// this.userInfo = await queryUserApi(Number(this.getUserId()))
|
||||
},
|
||||
async updateUser() {
|
||||
// await updateUserApi(this.userInfo.id, this.userInfo)
|
||||
},
|
||||
async updateAccountPassword(oldPassword: string, newPassword: string) {
|
||||
console.log(this.userInfo.accountId)
|
||||
// await updateAccountPasswordApi(this.userInfo.accountId, oldPassword, newPassword)
|
||||
},
|
||||
setToken(token: string) {
|
||||
setLocalStorage('token', token)
|
||||
},
|
||||
getToken() {
|
||||
return getLocalStorage('token')
|
||||
},
|
||||
setUserId(userId: number) {
|
||||
setLocalStorage('userId', String(userId))
|
||||
},
|
||||
getUserId() {
|
||||
return getLocalStorage('userId')
|
||||
},
|
||||
setAccount(accountName: string) {
|
||||
setLocalStorage('account', accountName)
|
||||
},
|
||||
getAccount() {
|
||||
return getLocalStorage('account')
|
||||
},
|
||||
removeAll() {
|
||||
removeLocalStorage('token')
|
||||
removeLocalStorage('account')
|
||||
}
|
||||
}
|
||||
})
|
||||
48
src/types/auth.ts
Normal file
48
src/types/auth.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export type ILoginType = 'common' | 'phone' | 'email' | 'admin'
|
||||
|
||||
export type IAccountType = 'QQ' | 'WeChat' | 'Phone' | 'EMail'
|
||||
|
||||
export interface IUser {
|
||||
id: number;
|
||||
accountId: number;
|
||||
accountName: string;
|
||||
username: string;
|
||||
gender: number;
|
||||
idNumber: string;
|
||||
phoneNumber: string;
|
||||
email: string;
|
||||
birthDate: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
area: string[];
|
||||
address: string;
|
||||
job: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface ISession {
|
||||
saToken: {
|
||||
tokenName: string;
|
||||
tokenValue: string;
|
||||
isLogin: boolean;
|
||||
loginId: string;
|
||||
loginType: string;
|
||||
tokenTimeout: number;
|
||||
sessionTimeout: number;
|
||||
tokenSessionTimeout: number;
|
||||
tokenActiveTimeout: number;
|
||||
loginDevice: string;
|
||||
tag: string;
|
||||
};
|
||||
userInfo: IUser;
|
||||
}
|
||||
|
||||
export interface IAccount {
|
||||
id: number;
|
||||
accountName: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
username: string;
|
||||
state: number;
|
||||
date: string;
|
||||
}
|
||||
@@ -1,5 +1,35 @@
|
||||
export type IFoodSegment = 'recipe' | 'daily' | 'timeline'
|
||||
|
||||
/**
|
||||
* 食材信息
|
||||
*/
|
||||
export interface IFoodMaterial {
|
||||
type: string;
|
||||
name: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤信息
|
||||
*/
|
||||
export interface IFoodStep {
|
||||
sort: number;
|
||||
content: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成果信息
|
||||
*/
|
||||
export interface IFoodRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
date: string;
|
||||
person: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜谱信息
|
||||
*/
|
||||
@@ -35,36 +65,6 @@ export interface IFoodRecipe {
|
||||
recordList: IFoodRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 食材信息
|
||||
*/
|
||||
export interface IFoodMaterial {
|
||||
type: string;
|
||||
name: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤信息
|
||||
*/
|
||||
export interface IFoodStep {
|
||||
sort: number;
|
||||
content: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成果信息
|
||||
*/
|
||||
export interface IFoodRecord {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
date: string;
|
||||
person: string;
|
||||
imageUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计信息
|
||||
*/
|
||||
|
||||
71
src/utils/element/elRules.ts
Normal file
71
src/utils/element/elRules.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
export const chinesePattern = /^[\u4e00-\u9fa5a-zA-Z0-9]+$/
|
||||
|
||||
export const amountPattern = /^\d+(\.?\d{1,2})?$/
|
||||
|
||||
export const phonePattern = /^1[3-9]\d{9}$/
|
||||
|
||||
export const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
|
||||
export const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||||
{ pattern: /^[0-9a-zA-Z]+$/, message: '请输入正确的格式: 数字或英文字母', trigger: ['blur'] }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ pattern: /^[0-9a-zA-Z]+$/, message: '请输入正确的格式: 数字或英文字母', trigger: ['blur'] }
|
||||
],
|
||||
phoneNumber: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: phonePattern, message: '请输入正确的手机号格式', trigger: ['blur'] }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||
{ pattern: emailPattern, message: '请输入正确的邮箱格式', trigger: ['blur'] }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ pattern: /^\d{6}$/, message: '请输入6位数字验证码', trigger: ['blur'] }
|
||||
]
|
||||
}
|
||||
|
||||
export const foodRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' },
|
||||
{ pattern: new RegExp(chinesePattern), message: '请输入中英文或数字' }
|
||||
],
|
||||
category: [
|
||||
{ required: true, message: '请选择菜系', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
export const foodMaterialRules = {
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: 'change' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: 'blur' }
|
||||
],
|
||||
amount: [
|
||||
{ required: true, message: '请输入分量', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
export const foodStepRules = {
|
||||
content: [
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
export const foodRecordRules = {
|
||||
name: [
|
||||
{ required: true, message: '请选择菜谱名称', trigger: 'change' }
|
||||
],
|
||||
date: [
|
||||
{ required: true, message: '请选择完成时间', trigger: 'change' }
|
||||
],
|
||||
person: [
|
||||
{ required: true, message: '请选择完成人', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
@@ -5,19 +5,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginClick = () => {
|
||||
const loginClick = async () => {
|
||||
if (!isMobile()) {
|
||||
ElMessage.error('暂不支持PC端')
|
||||
return
|
||||
}
|
||||
|
||||
router.push('/record')
|
||||
await authStore.handleLogin('common')
|
||||
userStore.handleSession(authStore.homeSession)
|
||||
await router.push('/record')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<div v-if="foodInfo.recordSegment === 'timeline'" class="food-calendar">
|
||||
<food-timeline />
|
||||
</div>
|
||||
|
||||
<draggable-button v-show="foodInfo.recordSegment !== 'timeline'" @click="addClick"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,12 +37,17 @@
|
||||
import FoodRecipe from '@/components/Food/FoodRecipe.vue'
|
||||
import FoodDaily from '@/components/Food/FoodDaily.vue'
|
||||
import FoodTimeline from '@/components/Food/FoodTimeline.vue'
|
||||
import DraggableButton from '@/components/DraggableButton.vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import type { IFoodSegment } from '@/types/food'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app'
|
||||
|
||||
const foodStore = useFoodStore()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const foodInfo = reactive({
|
||||
recordSegment: 'recipe' as IFoodSegment,
|
||||
@@ -67,6 +74,25 @@ onMounted(() => {
|
||||
const changeRecordSegmentEvent = () => {
|
||||
foodStore.refreshInfo()
|
||||
}
|
||||
|
||||
const addClick = () => {
|
||||
appStore.isShowMobileBack = true
|
||||
|
||||
switch (foodInfo.recordSegment) {
|
||||
case 'recipe':
|
||||
foodStore.foodRecipeApiType = 'ADD'
|
||||
foodStore.currentFoodRecipe = foodStore.getEmptyFoodRecipe()
|
||||
router.push('/record/recipeForm')
|
||||
break
|
||||
case 'daily':
|
||||
foodStore.foodRecordApiType = 'ADD'
|
||||
foodStore.currentFoodRecord = foodStore.getEmptyFoodRecord()
|
||||
router.push('/record/recordForm')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
99
src/views/record/recipeForm.vue
Normal file
99
src/views/record/recipeForm.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="food-recipe-form common-mobile-view">
|
||||
<el-form ref="formRef" :model="foodStore.currentFoodRecipe"
|
||||
:rules="rules" label-width="80px">
|
||||
<el-form-item label="美食名称" prop="name">
|
||||
<el-input v-model="foodStore.currentFoodRecipe.name" autocomplete="off"
|
||||
placeholder="请输入名称" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="美食菜系" prop="category">
|
||||
<el-select v-model="foodStore.currentFoodRecipe.category"
|
||||
placeholder="请选择或输入菜系">
|
||||
<el-option v-for="(item, index) in foodCategoryList"
|
||||
:key="index" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="推荐指数" prop="recommendRate">
|
||||
<el-rate v-model="foodStore.currentFoodRecipe.recommendRate"
|
||||
:colors="foodStore.rateColorList"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="食材">
|
||||
<food-material-form />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="步骤">
|
||||
<food-step-form />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="foodStore.currentFoodRecipe.remark" autocomplete="off"
|
||||
placeholder="请输入备注" clearable
|
||||
show-word-limit maxlength="20">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="confirmClick">确认</el-button>
|
||||
<el-button type="danger" @click="deleteClick">删除</el-button>
|
||||
<el-button type="info" @click="cancelClick">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FoodMaterialForm from '@/components/Food/FoodMaterialForm.vue'
|
||||
import FoodStepForm from '@/components/Food/FoodStepForm.vue'
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { FormInstance, FormRules } from 'element-plus'
|
||||
import { computed, 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>()
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const foodCategoryList = ['家常菜', '淮扬菜', '鲁菜', '川菜', '粤菜', '闽菜', '浙菜', '湘菜', '徽菜', '其他']
|
||||
|
||||
const dialogData = computed(() => foodStore.foodRecipeDialog)
|
||||
|
||||
/**
|
||||
* 点击确认按钮
|
||||
*/
|
||||
const confirmClick = () => {
|
||||
formRef.value.validate().then(() => {
|
||||
foodStore.handleFoodRecipeApi(foodStore.currentFoodRecipe)
|
||||
history.back()
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
history.back()
|
||||
}
|
||||
|
||||
const deleteClick = () => {
|
||||
commonElMessageBox('是否确认删除该菜谱?').then(() => {
|
||||
foodStore.foodRecipeApiType = 'DELETE'
|
||||
foodStore.handleFoodRecipeApi(foodStore.currentFoodRecipe)
|
||||
history.back()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-recipe-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.button-container {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
src/views/record/recordForm.vue
Normal file
101
src/views/record/recordForm.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="food-record-form common-mobile-view">
|
||||
<el-form ref="formRef" :model="foodStore.currentFoodRecord"
|
||||
:rules="rules" label-width="80">
|
||||
<el-form-item label="菜谱名称" prop="name">
|
||||
<el-select
|
||||
v-model="foodStore.currentFoodRecord.name"
|
||||
@focus="foodStore.queryFoodNameList()"
|
||||
allow-create filterable
|
||||
:disabled="foodStore.foodRecordApiType === 'UPDATE'"
|
||||
placeholder="请输入或选择菜谱名称">
|
||||
<el-option
|
||||
v-for="(item, index) in foodStore.foodNameList"
|
||||
:key="index" :label="item" :value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="完成时间" prop="date">
|
||||
<el-date-picker
|
||||
v-model="foodStore.currentFoodRecord.date"
|
||||
type="date" :editable="false" placeholder="请选择完成时间"
|
||||
value-format="YYYY-MM-DD"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="完成人" prop="person">
|
||||
<el-select
|
||||
v-model="foodStore.currentFoodRecord.person"
|
||||
placeholder="请选择完成人">
|
||||
<el-option
|
||||
v-for="(item, index) in ['哥哥', '宝宝']"
|
||||
:key="index" :label="item" :value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片">
|
||||
<upload-image v-model="foodStore.currentFoodRecord.imageUrl"
|
||||
:service-url="fileServiceUrl"
|
||||
:image-limit="1"
|
||||
:service-file-root-path="serviceFileRootPath"
|
||||
:image-path="foodStore.foodImagePath"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="confirmClick">确认</el-button>
|
||||
<el-button type="danger" @click="deleteClick">删除</el-button>
|
||||
<el-button type="info" @click="cancelClick">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UploadImage } from 'vue3-common'
|
||||
import { useFoodStore } from '@/store/food.ts'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { FormRules } from 'element-plus'
|
||||
import { foodRecordRules } from '@/utils/element/elRules'
|
||||
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
|
||||
const rules = reactive<FormRules>(foodRecordRules)
|
||||
|
||||
const formRef = ref()
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
/**
|
||||
* 点击确认按钮
|
||||
*/
|
||||
const confirmClick = () => {
|
||||
formRef.value.validate().then(() => {
|
||||
foodStore.handleFoodRecordApi(foodStore.currentFoodRecord)
|
||||
history.back()
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
history.back()
|
||||
}
|
||||
|
||||
const deleteClick = () => {
|
||||
commonElMessageBox('是否确认删除该记录?').then(() => {
|
||||
foodStore.foodRecordApiType = 'DELETE'
|
||||
foodStore.handleFoodRecordApi(foodStore.currentFoodRecord)
|
||||
history.back()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.food-record-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.button-container {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -20,6 +20,13 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5175
|
||||
port: 5175,
|
||||
proxy: {
|
||||
'/food-service': {
|
||||
target: 'http://127.0.0.1:8090',
|
||||
changeOrigin: true,
|
||||
rewrite: (item) => item.replace(/^\/food-service/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user