feat:更新朋友圈功能
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.9.7",
|
||||
"vue": "^3.5.13",
|
||||
"vue3-common": "git+https://gitee.com/Cxx0822/vue3-common",
|
||||
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: ^13.1.0
|
||||
version: 13.4.0(vue@3.5.17(typescript@5.8.3))
|
||||
element-china-area-data:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: ^2.9.7
|
||||
version: 2.10.2(vue@3.5.17(typescript@5.8.3))
|
||||
@@ -833,6 +836,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
china-division@2.7.0:
|
||||
resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -958,6 +964,9 @@ packages:
|
||||
echarts@5.6.0:
|
||||
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||
|
||||
element-china-area-data@6.1.0:
|
||||
resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
|
||||
|
||||
element-plus@2.10.2:
|
||||
resolution: {integrity: sha512-p2KiAa0jEGXrzdlTAfpiS7HQFAhla4gvx6H7RuDf+OO0uC3DGpolxvdHjFR8gt7+vaWyxQNcHa1sAdBkmjqlgA==}
|
||||
peerDependencies:
|
||||
@@ -2772,6 +2781,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
china-division@2.7.0: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -2891,6 +2902,10 @@ snapshots:
|
||||
tslib: 2.3.0
|
||||
zrender: 5.6.1
|
||||
|
||||
element-china-area-data@6.1.0:
|
||||
dependencies:
|
||||
china-division: 2.7.0
|
||||
|
||||
element-plus@2.10.2(vue@3.5.17(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 3.6.1
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { cloudService } from './index'
|
||||
import type { ISession, IUser } from '@/types/auth'
|
||||
|
||||
export const registerApi = (user: IUser): Promise<ISession> =>
|
||||
export const registerApi = (username: string, password: string): Promise<ISession> =>
|
||||
cloudService({
|
||||
url: '/food-service/user',
|
||||
method: 'post',
|
||||
params: { username, password }
|
||||
})
|
||||
|
||||
export const queryUserApi = (id: number): Promise<IUser> =>
|
||||
cloudService({
|
||||
url: `/food-service/user/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
export const updateUserApi = (id: number, user: IUser): Promise<boolean> =>
|
||||
cloudService({
|
||||
url: `/food-service/user/${id}`,
|
||||
method: 'put',
|
||||
data: user
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="button-info">
|
||||
<el-button type="primary" :icon="Edit" circle :size="'small'"
|
||||
@click="editClick"/>
|
||||
<el-button type="primary" :icon="MoreFilled" circle :size="'small'"
|
||||
<el-button type="primary" :icon="Collection" circle :size="'small'"
|
||||
@click="viewClick()" />
|
||||
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
|
||||
@click="deleteClick()" />
|
||||
@@ -59,7 +59,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { Edit, Collection, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { defineProps, onMounted, reactive } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import { FormRules } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { foodMaterialRules } from '@/utils/element/elRules'
|
||||
const rules = reactive<FormRules>(foodMaterialRules)
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-dialog v-model="foodStore.stepDialog.visible"
|
||||
:title="foodStore.stepDialog.title"
|
||||
:title="`${foodStore.stepDialog.title}${foodStore.editStepIndex + 1}`"
|
||||
center @open="openDialogEvent"
|
||||
class="food-step-dialog">
|
||||
<el-form ref="formRef" :model="foodStore.stepDialog.data"
|
||||
:rules="rules" label-width="60px">
|
||||
<el-form-item label="内容" prop="content">
|
||||
<el-input v-model="foodStore.stepDialog.data.content" autocomplete="off"
|
||||
placeholder="请输入内容" clearable
|
||||
:placeholder="`请输入步骤${foodStore.editStepIndex + 1}`" clearable
|
||||
type="textarea" :rows="3"
|
||||
show-word-limit maxlength="50">
|
||||
</el-input>
|
||||
@@ -36,7 +36,7 @@
|
||||
import { UploadImage } from 'vue3-common'
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { ref, reactive, nextTick } from 'vue'
|
||||
import { FormRules } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { foodStepRules } from '@/utils/element/elRules'
|
||||
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
|
||||
|
||||
@@ -60,11 +60,13 @@ const openDialogEvent = () => {
|
||||
const confirmClick = () => {
|
||||
formRef.value.validate()
|
||||
.then(async () => {
|
||||
const { editStepIndex: index } = foodStore
|
||||
if (foodStore.stepApiType === 'UPDATE') {
|
||||
const { editStepIndex: index } = foodStore
|
||||
foodStore.currentRecipe.stepList[index] = foodStore.stepDialog.data
|
||||
} else {
|
||||
foodStore.stepDialog.data.sort = foodStore.editStepIndex
|
||||
foodStore.currentRecipe.stepList.push(foodStore.stepDialog.data)
|
||||
foodStore.editStepIndex++
|
||||
}
|
||||
foodStore.stepDialog.visible = false
|
||||
})
|
||||
|
||||
@@ -91,10 +91,14 @@ const userStore = useUserStore()
|
||||
|
||||
const loginFormRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const storageLoginForm = getLocalStorage('loginForm')
|
||||
if (storageLoginForm) {
|
||||
authStore.loginForm = JSON.parse(storageLoginForm)
|
||||
|
||||
if (!authStore.isLogout) {
|
||||
await doLogin('common')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -109,15 +113,7 @@ const handleLogin = async (type: ILoginType) => {
|
||||
authStore.loginLoading = true
|
||||
|
||||
try {
|
||||
await authStore.handleLogin(type)
|
||||
userStore.handleSession(authStore.session)
|
||||
|
||||
// 2. 存储账户密码到localStorage中
|
||||
if (authStore.loginForm.isRemember) {
|
||||
setLocalStorage('loginForm', JSON.stringify(authStore.loginForm))
|
||||
}
|
||||
|
||||
await router.push('/record')
|
||||
await doLogin(type)
|
||||
} finally {
|
||||
authStore.loginLoading = false
|
||||
}
|
||||
@@ -127,4 +123,16 @@ const handleLogin = async (type: ILoginType) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const doLogin = async (type: ILoginType) => {
|
||||
await authStore.handleLogin(type)
|
||||
userStore.handleSession(authStore.session)
|
||||
|
||||
// 2. 存储账户密码到localStorage中
|
||||
if (authStore.loginForm.isRemember) {
|
||||
setLocalStorage('loginForm', JSON.stringify(authStore.loginForm))
|
||||
}
|
||||
|
||||
await router.push('/record')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="moment-card card-item">
|
||||
<el-avatar :src="serviceFileRootPath + props.item.avatar" fit="contain"/>
|
||||
<el-avatar :src="avatarUrl" fit="contain" @click="viewProfileClick"/>
|
||||
|
||||
<div class="moment-content">
|
||||
<span class="username">{{ props.item.username }}</span>
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="moment-image-list">
|
||||
<el-image v-for="(item, index) in props.item.imageList" :key="index"
|
||||
:src="serviceFileRootPath + item" fit="cover"
|
||||
:preview-src-list="[serviceFileRootPath + item]"/>
|
||||
:preview-src-list=previewList />
|
||||
</div>
|
||||
|
||||
<p>{{ formatTimeAgo(props.item.date) }}</p>
|
||||
@@ -67,17 +67,19 @@
|
||||
import EmojiPicker from 'vue3-emoji-picker'
|
||||
import 'vue3-emoji-picker/css'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import { defineProps, reactive } from 'vue'
|
||||
import { computed, defineProps, reactive } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { IMoment } from '@/types/food'
|
||||
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { formatTimeAgo } from '@/utils/dateUtils'
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -93,10 +95,22 @@ const momentInfo = reactive({
|
||||
comment: ''
|
||||
})
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
if (props.item?.avatar) {
|
||||
return serviceFileRootPath + props.item.avatar
|
||||
} else {
|
||||
return 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
}
|
||||
})
|
||||
|
||||
const previewList = computed(() => {
|
||||
return props.item?.imageList.map((item) => serviceFileRootPath + item)
|
||||
})
|
||||
|
||||
const editClick = () => {
|
||||
foodStore.momentApiType = 'UPDATE'
|
||||
foodStore.currentMoment = props.item as IMoment
|
||||
router.push('/moment/momentForm')
|
||||
router.push('/moment/form')
|
||||
}
|
||||
|
||||
const onSelectEmoji = (emoji) => {
|
||||
@@ -109,6 +123,15 @@ const sendCommentClick = async () => {
|
||||
momentInfo.comment = ''
|
||||
momentInfo.isShowComment = false
|
||||
}
|
||||
|
||||
const viewProfileClick = async () => {
|
||||
if (props.item?.userId !== userStore.userInfo.id) {
|
||||
appStore.isShowMobileBack = true
|
||||
foodStore.currentMomentUserId = props.item?.userId as number
|
||||
await userStore.queryUser(foodStore.currentMomentUserId)
|
||||
await router.push('/moment/profile')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
120
src/components/Profile/ProfileCard.vue
Normal file
120
src/components/Profile/ProfileCard.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="profile-card">
|
||||
<div class="avatar-wrapper">
|
||||
<el-avatar :src="avatarUrl" alt="用户头像" :size="80"
|
||||
fit="contain" @click="previewVisible = true"
|
||||
/>
|
||||
<el-image-viewer
|
||||
v-if="previewVisible"
|
||||
:url-list="[serviceFileRootPath + userStore.userInfo.avatar]"
|
||||
@close="previewVisible = false"
|
||||
/>
|
||||
|
||||
<i v-if="userStore.userInfo.gender"
|
||||
class="fas fa-mars gender-icon" style="color: #409EFF"></i>
|
||||
<i v-else class="fas fa fa-venus gender-icon" style="color: #FFB6C1"></i>
|
||||
</div>
|
||||
|
||||
<span class="user-name">{{ userStore.userInfo.username }}</span>
|
||||
|
||||
<div class="user-info">
|
||||
<div v-if="userStore.userInfo.area" class="info-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>{{ userStore.userInfo.area.join('-') }}</span>
|
||||
</div>
|
||||
<div v-if="userStore.userInfo.birthDate" class="info-item">
|
||||
<i class="fas fa-birthday-cake"></i>
|
||||
<span>{{ userStore.userInfo.birthDate }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="userStore.userInfo.job" class="info-item">
|
||||
<i class="fas fa-user-tie"></i>
|
||||
<span>{{ userStore.userInfo.job }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-info">
|
||||
<div v-if="userStore.userInfo.phoneNumber" class="info-item">
|
||||
<i class="fas fa-mobile-alt"></i>
|
||||
<span>{{ userStore.userInfo.phoneNumber }}</span>
|
||||
</div>
|
||||
<div v-if="userStore.userInfo.email" class="info-item">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>{{ userStore.userInfo.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="userStore.userInfo.tags" class="user-info">
|
||||
<el-tag v-for="(item, index) in userStore.userInfo.tags"
|
||||
:key="index">{{ item }}</el-tag>
|
||||
</div>
|
||||
|
||||
<span v-if="userStore.userInfo.description" class="user-bio">
|
||||
{{ userStore.userInfo.description }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useUserStore } from '@/store/user.ts'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const previewVisible = ref(false)
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
if (userStore.userInfo.avatar) {
|
||||
return serviceFileRootPath + userStore.userInfo.avatar
|
||||
} else {
|
||||
return 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<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);
|
||||
text-align: center;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.gender-icon {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-size: 1.1rem;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,13 +29,11 @@ import TitleBar from '@/layout/TitleBar.vue'
|
||||
import TabBar from '@/layout/TabBar.vue'
|
||||
import DraggableButton from '@/components/DraggableButton.vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAppStore } from '@/store/app'
|
||||
import { useFoodStore } from '@/store/food'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const foodStore = useFoodStore()
|
||||
|
||||
const dialogFormVisible = ref(false)
|
||||
@@ -46,7 +44,7 @@ const checkIsShow = (): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (route.path.includes('moment')) {
|
||||
if (route.path.includes('moment/index')) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -65,7 +63,7 @@ const addClick = () => {
|
||||
if (route.path.includes('moment')) {
|
||||
foodStore.momentApiType = 'ADD'
|
||||
foodStore.currentMoment = foodStore.getEmptyMoment()
|
||||
router.push('/moment/momentForm')
|
||||
router.push('/moment/form')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const useAppStore = defineStore('ipp', {
|
||||
isDark: false,
|
||||
isWebSocket: false,
|
||||
serverIp: '14.103.235.151',
|
||||
version: 'v1.0.0.2025062702'
|
||||
version: 'v1.0.0.2025062802'
|
||||
}),
|
||||
actions: {
|
||||
initApp() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { ILoginType, ISession, IUser } from '@/types/auth'
|
||||
import type { ILoginType, ISession } from '@/types/auth'
|
||||
import { loginApi, logoutApi } from '@/apis/session'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { emailPattern, phonePattern } from '@/utils/element/elRules'
|
||||
@@ -29,6 +29,7 @@ export const useAuthStore = defineStore('auth', {
|
||||
loginLoading: false,
|
||||
isVerified: false,
|
||||
isGettingCode: false,
|
||||
isLogout: false,
|
||||
getCodeCountdown: 60,
|
||||
getCodeTimer: null as NodeJS.Timeout | null,
|
||||
session: {} as ISession
|
||||
@@ -99,17 +100,16 @@ export const useAuthStore = defineStore('auth', {
|
||||
default:
|
||||
break
|
||||
}
|
||||
this.isLogout = false
|
||||
ElMessage.success('登录成功')
|
||||
},
|
||||
async handleRegister() {
|
||||
const user: IUser = {
|
||||
username: this.registerForm.username,
|
||||
password: this.registerForm.password
|
||||
}
|
||||
await registerApi(user)
|
||||
const { username, password } = this.registerForm
|
||||
await registerApi(username, password)
|
||||
},
|
||||
async handleLogout(username: string) {
|
||||
await logoutApi(username)
|
||||
this.isLogout = true
|
||||
ElMessage.success('退出登录成功')
|
||||
},
|
||||
resetLoginForm() {
|
||||
|
||||
@@ -74,6 +74,7 @@ export const useFoodStore = defineStore('food', {
|
||||
editStepIndex: 0,
|
||||
recordApiType: 'ADD' as IHandleApi,
|
||||
editRecordIndex: 0,
|
||||
currentMomentUserId: 0,
|
||||
currentMoment: {
|
||||
content: '',
|
||||
imageList: []
|
||||
|
||||
@@ -2,15 +2,14 @@ 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'
|
||||
import { queryUserApi, updateUserApi } from '@/apis/user'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
userInfo: {
|
||||
id: 0,
|
||||
username: '',
|
||||
password: '',
|
||||
gender: 0,
|
||||
idNumber: '',
|
||||
phoneNumber: '',
|
||||
email: '',
|
||||
birthDate: '',
|
||||
@@ -20,7 +19,8 @@ export const useUserStore = defineStore('user', {
|
||||
address: '',
|
||||
job: '',
|
||||
tags: []
|
||||
} as IUser
|
||||
} as IUser,
|
||||
imagePath: 'food-hub'
|
||||
}),
|
||||
|
||||
actions: {
|
||||
@@ -38,11 +38,11 @@ export const useUserStore = defineStore('user', {
|
||||
this.setUserId(session.userInfo?.id as number)
|
||||
this.userInfo = session.userInfo
|
||||
},
|
||||
async queryUser() {
|
||||
// this.userInfo = await queryUserApi(Number(this.getUserId()))
|
||||
async queryUser(userId: number) {
|
||||
this.userInfo = await queryUserApi(userId)
|
||||
},
|
||||
async updateUser() {
|
||||
// await updateUserApi(this.userInfo.id, this.userInfo)
|
||||
await updateUserApi(this.userInfo.id, this.userInfo)
|
||||
},
|
||||
async updateAccountPassword(oldPassword: string, newPassword: string) {
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 40px 1fr;
|
||||
align-items: center;
|
||||
// align-items: center;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
@@ -100,7 +100,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1vh;
|
||||
min-height: 28vh;
|
||||
// min-height: 28vh;
|
||||
|
||||
.step-container {
|
||||
display: grid;
|
||||
|
||||
@@ -5,9 +5,7 @@ export type IAccountType = 'QQ' | 'WeChat' | 'Phone' | 'EMail'
|
||||
export interface IUser {
|
||||
id?: number;
|
||||
username: string;
|
||||
password: string;
|
||||
gender?: number;
|
||||
idNumber?: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
birthDate?: string;
|
||||
|
||||
@@ -78,3 +78,31 @@ export const foodMomentRules = {
|
||||
{ required: true, message: '请输入内容', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
export const profileInfoRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户姓名', trigger: 'blur' }
|
||||
],
|
||||
phoneNumber: [
|
||||
{ pattern: phonePattern, message: '请输入正确的手机号格式', trigger: ['blur'] }
|
||||
],
|
||||
email: [
|
||||
{ pattern: emailPattern, message: '请输入正确的邮箱格式', trigger: ['blur'] }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
export const profilePasswordRules = {
|
||||
oldPassword: [
|
||||
{ required: true, message: '请输入旧密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '请输入6~20位的密码', trigger: 'change' }
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '请输入6~20位的密码', trigger: 'change' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请输入确认密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '请输入6~20位的密码', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
28
src/views/moment/profile.vue
Normal file
28
src/views/moment/profile.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="moment-profile-view common-mobile-view">
|
||||
<profile-card />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.moment-profile-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +1,150 @@
|
||||
<template>
|
||||
<div class="common-mobile-view profile">
|
||||
个人中心
|
||||
<div>
|
||||
<span>版本号:</span>
|
||||
<span>{{ appStore.version }}</span>
|
||||
<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>
|
||||
|
||||
<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-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">
|
||||
<i class="fas fa-key"></i>
|
||||
<span>更改密码</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</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-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>
|
||||
IOS下载地址:<el-image :src="getAssetsImageFile('ios.png')"/>
|
||||
</div>
|
||||
<div>
|
||||
Android下载地址:
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="viewLatestVersionClick">检查新版本</el-button>
|
||||
<el-button type="primary" @click="logoutClick">退出登录</el-button>
|
||||
</div>
|
||||
<div class="version">版本号 {{ appStore.version }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
<script lang="ts" setup>
|
||||
import ProfileCard from '@/components/Profile/ProfileCard.vue'
|
||||
import { useUserStore } from '@/store/user.ts'
|
||||
import { useAuthStore } from '@/store/auth.ts'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getAssetsImageFile } from '@/utils'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const viewLatestVersionClick = async () => {
|
||||
onMounted(() => {
|
||||
userStore.queryUser(userStore.getUserId() as number)
|
||||
})
|
||||
|
||||
const infoClick = () => {
|
||||
router.push('/profile/info')
|
||||
}
|
||||
|
||||
const passwordClick = () => {
|
||||
// appStore.isShowMobileBack = true
|
||||
// router.push('/mobile-home/profile/password')
|
||||
}
|
||||
|
||||
const refreshClick = async () => {
|
||||
await router.push('/')
|
||||
location.reload()
|
||||
}
|
||||
|
||||
const logoutClick = async () => {
|
||||
await authStore.handleLogout(userStore.userInfo.username)
|
||||
await router.push('/')
|
||||
const logoutClick = () => {
|
||||
commonElMessageBox('请确认是否退出系统?').then(async () => {
|
||||
userStore.removeAll()
|
||||
await authStore.handleLogout(userStore.userInfo.username)
|
||||
await router.push('/')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile {
|
||||
.profile-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.menu-card {
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.menu-item {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr 15px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
|
||||
i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background-color: #E8E8E8;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
align-self: center;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
125
src/views/profile/info.vue
Normal file
125
src/views/profile/info.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="profile-info common-mobile-view">
|
||||
<el-form ref="ruleFormRef"
|
||||
:model="userStore.userInfo"
|
||||
:rules="rules" label-width="auto">
|
||||
<el-form-item label="姓名" prop="username">
|
||||
<el-input v-model="userStore.userInfo.username"
|
||||
:maxlength="10" show-word-limit
|
||||
clearable/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="头像">
|
||||
<upload-image v-model="userStore.userInfo.avatar"
|
||||
:image-limit="1"
|
||||
:service-url="fileServiceUrl"
|
||||
:service-file-root-path="serviceFileRootPath"
|
||||
:image-path="userStore.imagePath"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="userStore.userInfo.gender">
|
||||
<el-radio :value="1">男</el-radio>
|
||||
<el-radio :value="0">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="生日">
|
||||
<el-date-picker v-model="userStore.userInfo.birthDate"
|
||||
type="date" :editable="false" value-format="YYYY-MM-DD"
|
||||
placeholder="请选择日期"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="地区">
|
||||
<el-cascader v-model="userStore.userInfo.area"
|
||||
placeholder="请选择地区" :options="pcTextArr">
|
||||
</el-cascader>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="userStore.userInfo.phoneNumber"
|
||||
:maxlength="11" show-word-limit clearable
|
||||
placeholder="请输入手机号"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="userStore.userInfo.email"
|
||||
:maxlength="20" show-word-limit clearable
|
||||
placeholder="请输入邮箱"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="地址">
|
||||
<el-input v-model="userStore.userInfo.address"
|
||||
:maxlength="20" show-word-limit clearable
|
||||
placeholder="请输入地址"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="职业">
|
||||
<el-input v-model="userStore.userInfo.job"
|
||||
:maxlength="10" show-word-limit clearable
|
||||
placeholder="请输入职业"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="标签">
|
||||
<el-input-tag v-model="userStore.userInfo.tags"
|
||||
:max="3" placeholder="请输入标签(最多3个)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="userStore.userInfo.description"
|
||||
:maxlength="20" show-word-limit clearable
|
||||
placeholder="用一句话描述自己"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="submitForm(ruleFormRef)">保存</el-button>
|
||||
<el-button @click="cancelClick">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadImage } from 'vue3-common'
|
||||
import { pcTextArr } from 'element-china-area-data'
|
||||
import { useUserStore } from '@/store/user.ts'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { profileInfoRules } from '@/utils/element/elRules.ts'
|
||||
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
const rules = reactive<FormRules>(profileInfoRules)
|
||||
|
||||
const cancelClick = () => {
|
||||
history.back()
|
||||
}
|
||||
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid, fields) => {
|
||||
if (valid) {
|
||||
await userStore.updateUser()
|
||||
await userStore.queryUser(userStore.userInfo.id)
|
||||
ElMessage.success('更改信息成功')
|
||||
history.back()
|
||||
} else {
|
||||
ElMessage.error('请先填写信息')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.button-container {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
70
src/views/profile/password.vue
Normal file
70
src/views/profile/password.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="profile-password common-mobile-view">
|
||||
<el-form ref="ruleFormRef"
|
||||
:model="passwordInfo"
|
||||
:rules="rules" label-width="auto">
|
||||
<el-form-item label="原密码" prop="oldPassword">
|
||||
<el-input v-model="passwordInfo.oldPassword" type="password" show-password clearable/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input v-model="passwordInfo.newPassword" type="password" show-password clearable/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input v-model="passwordInfo.confirmPassword" type="password" show-password clearable/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="button-container">
|
||||
<el-button type="primary" @click="submitForm(ruleFormRef)">确认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/store/user.ts'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { profilePasswordRules } from '@/utils/element/elRules.ts'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>(profilePasswordRules)
|
||||
|
||||
const passwordInfo = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid, fields) => {
|
||||
if (valid) {
|
||||
if (passwordInfo.newPassword !== passwordInfo.confirmPassword) {
|
||||
ElMessage.error('确认密码和新密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
await userStore.updateAccountPassword(passwordInfo.oldPassword, passwordInfo.newPassword)
|
||||
ElMessage.success('更改密码成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.profile-password {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.button-container {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user