个人中心

master
fuxiaochun 2023-08-14 22:19:43 +08:00
parent d4e817ceff
commit cb5f50940c
8 changed files with 611 additions and 57 deletions

View File

@ -0,0 +1,154 @@
@font-face {
font-family: "iconfont"; /* Project id */
src: url('iconfont.ttf?t=1692012217107') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[class^="icon-"],
[class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'iconfont' !important;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: -1px;
}
.icon-bianji:before {
content: "\e62e";
}
.icon-cuowu:before {
content: "\e629";
}
.icon-dingyue:before {
content: "\e62c";
}
.icon-save:before {
content: "\e62d";
}
.icon-fanhui2:before {
content: "\e625";
}
.icon-edit:before {
content: "\e62a";
}
.icon-moban:before {
content: "\e628";
}
.icon-shengcheng:before {
content: "\e62b";
}
.icon-hongguanzhengjing:before {
content: "\e622";
}
.icon-fuzhi:before {
content: "\e627";
}
.icon-shuaxin:before {
content: "\e626";
}
.icon-quxiao:before {
content: "\e624";
}
.icon-fasong:before {
content: "\e621";
}
.icon-duanxin:before {
content: "\e620";
}
.icon-you:before {
content: "\e61d";
}
.icon-dingdan:before {
content: "\e61e";
}
.icon-fanhui:before {
content: "\e61f";
}
.icon-tuijianmoban1:before {
content: "\e623";
}
.icon-shanchu:before {
content: "\e617";
}
.icon-shangchuan:before {
content: "\e61c";
}
.icon-xiangyou:before {
content: "\e61b";
}
.icon-a-mima1:before {
content: "\e61a";
}
.icon-duihua:before {
content: "\e616";
}
.icon-zhengque:before {
content: "\e618";
}
.icon-xiala:before {
content: "\e619";
}
.icon-gonggong-xinghao:before {
content: "\e615";
}
.icon-zanting:before {
content: "\e611";
}
.icon-xiangzuo:before {
content: "\e614";
}
.icon-falvfagui:before {
content: "\e60f";
}
.icon-shangqingyuce:before {
content: "\e610";
}
.icon-hangyedongcha:before {
content: "\e613";
}
.icon-youxiang:before {
content: "\e612";
}

Binary file not shown.

View File

@ -8,6 +8,12 @@ import axios from 'axios';
const http = (url = '', data = {}, type = 'POST', otherConfig = {}) => {
let config = Object.assign({ isLoading: true, timeout: 10000 }, otherConfig);
let _defaultHeaders = {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json; charset=UTF-8',
'isLoading': config.isLoading,
};
let headers = Object.assign(_defaultHeaders, otherConfig.headers);
let promise;
type = type.toUpperCase();
@ -16,11 +22,7 @@ const http = (url = '', data = {}, type = 'POST', otherConfig = {}) => {
promise = axios({
method: type,
url,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json; charset=UTF-8',
'isLoading': config.isLoading,
},
headers: headers,
params: data,
timeout: config.timeout
});
@ -28,11 +30,7 @@ const http = (url = '', data = {}, type = 'POST', otherConfig = {}) => {
promise = axios({
method: type,
url,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json; charset=UTF-8',
'isLoading': config.isLoading,
},
headers: headers,
data: data,
timeout: config.timeout
});

View File

@ -1,5 +1,5 @@
<template>
<RouterView>
<RouterView :key="$route.fullPath">
<template #default="{ Component }">
<transition name="fade-slide" mode="out-in">
<component :is="Component" />

View File

@ -14,13 +14,13 @@
<div class="navIcon" @click="toggleMenu"><van-icon name="wap-nav" size="0.6rem" /></div>
</div>
<ul class="menu">
<li :class="{ active: route.meta.group === 'home' }"></li>
<li :class="{ active: route.meta.group === 'home' }" @click="jump('/')"></li>
<li>AI助理</li>
<li>AI商情</li>
<li>AI传播</li>
<li>AI献策</li>
<li :class="{ active: route.meta.group === 'vip' }">会员服务</li>
<li :class="{ active: route.meta.group === 'ucenter' }">个人中</li>
<li :class="{ active: route.meta.group === 'ucenter' }" @click="jump('/ucenter/userinfo')"></li>
<li @click="onLogout">退</li>
</ul>
</template>
@ -101,6 +101,11 @@ const onLogout = ()=>{
showToast(err.message);
})
};
const jump = (url)=>{
showMenu.value = false;
router.push(url);
}
</script>
<style lang="scss" scoped>

View File

@ -1,7 +1,8 @@
import "./assets/css/main.css";
import 'virtual:svg-icons-register';
import 'virtual:windi.css'
// import './assets/js/rem';
import "@/assets/css/iconfont.css";
import { createApp } from "vue";
import { createPinia } from "pinia";

View File

@ -2,13 +2,9 @@
<div class="ucenterContainer">
<div class="navBox">
<div class="nameCard">
<div class="avatar">
<div class="avatar" @click="chooseFile">
<img :src="userInfo.userData.avatar || defaultAvatar">
<div class="uploadBtn">
<a-upload name="file" accept=".jpg,.jpeg,.png" :action="uploadAPI" @success="avatarUploadSuccess">
修改
</a-upload>
</div>
<input type="file" class="fileInput" name="avatar" id="avatar" accept="image/png, image/jpeg" @change="onAvatarChange">
</div>
<div class="name">{{ userInfo.userData.name || userInfo.userData.phone || userInfo.userData.email }}</div>
</div>
@ -36,7 +32,7 @@ import { useRouter, useRoute } from 'vue-router';
import hostAPI from '@/config/host.config.js';
import { showToast } from 'vant';
import { localCache } from '@/io/cache';
import defaultAvatar from '@/assets/img/avatar@2x.png';
import defaultAvatar from '@/assets/images/avatar@2x.png';
import { useUserInfo } from '@/stores/userInfo';
import http from '@/io/http';
@ -44,7 +40,31 @@ const router = useRouter();
const route = useRoute();
const userInfo = useUserInfo();
const uploadAPI = hostAPI + '/api/web/upload';
const chooseFile = ()=>{
document.getElementById('avatar').click();
};
const onAvatarChange = (e)=>{
let files = e.target.files;
if(files.length == 0){
return false;
}
let file = files[0];
let formData = new FormData();
formData.append('file', file);
http('/api/web/upload', formData, 'post', {
headers: { 'Content-Type': 'multipart/form-data; charset=UTF-8', }
}).then(res=>{
let _avatar = res.data.file;
showToast('头像修改成功!');
let temp = { ...userInfo.userData };
temp.avatar = _avatar;
userInfo.updateUserInfo(temp);
localCache.set('userInfo', temp);
}).catch(err=>{
showToast(err.message);
});
};
const jump = (path) => {
@ -56,23 +76,20 @@ const jump = (path) => {
<style lang="scss" scoped>
.ucenterContainer {
padding: 35px 0;
max-width: 1280px;
margin: 0 auto;
display: flex;
color: #FFF;
.navBox {
width: 248px;
padding: 47px;
display: flex;
justify-content: space-between;
align-items: flex-start;
.nameCard {
width: 150px;
display: flex;
height: 83px;
flex-direction: column;
align-items: center;
.avatar {
width: 83px;
height: 83px;
width: 110px;
height: 110px;
border-radius: 50%;
overflow: hidden;
position: relative;
@ -81,44 +98,42 @@ const jump = (path) => {
width: 100%;
object-fit: contain;
}
.uploadBtn {
width: 100%;
height: 20px;
background: rgba($color: #000000, $alpha: 0.5);
line-height: 20px;
text-align: center;
position: absolute;
bottom: 0;
span {
color: #FFF;
font-size: 12px;
}
}
}
.fileInput{
display: none;
}
.name {
font-size: 18px;
margin-left: 10px;
width: 100%;
height: 40px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 28px;
margin-top: 20px;
text-align: center;
}
}
.nav {
padding-top: 26px;
padding-left: 20px;
height: 180px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
li {
width: 248px;
height: 38px;
width: 466px;
height: 72px;
border: 2px solid #FFFFFF;
opacity: 0.4;
border-radius: 4px;
font-size: 14px;
font-size: 27px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
margin-bottom: 22px;
strong {
padding: 0 30px;

View File

@ -1,13 +1,394 @@
<template>
<div>
个人中心
<div class="container">
<section>
<div class="blockTitle">个人信息</div>
<div class="formBox">
<div class="item">
<div class="label"><span>*</span>姓名</div>
<div class="val">
<van-field :clearable="true" :center="true" class="txt" v-model="name" />
</div>
</div>
<div class="item">
<div class="label"><span>*</span>地区</div>
<div class="val">
<van-field
v-model="fieldValue"
class="areaInput"
is-link
readonly
placeholder="请选择所在地区"
@click="showAreaPopup = true"
/>
<van-popup v-model:show="showAreaPopup" round position="bottom">
<van-cascader
:field-names="{text: 'name', value: 'id', children: 'children'}"
v-model="cascaderValue"
title="请选择所在地区"
:options="options"
@close="showAreaPopup = false"
@finish="onFinish"
/>
</van-popup>
<!-- <a-select class="areaInput" v-model="province" @change="provinceChoosed">
<a-select-option :value="item.id" :key="item.id"
v-for="item in provinces">{{ item.name }}</a-select-option>
</a-select>
<a-select class="areaInput" v-model="city" @change="cityChoosed">
<a-select-option :value="item.id" :key="item.id"
v-for="item in cities">{{ item.name }}</a-select-option>
</a-select>
<a-select class="areaInput" v-model="area">
<a-select-option :value="item.id" :key="item.id"
v-for="item in areas">{{ item.name }}</a-select-option>
</a-select> -->
</div>
</div>
<div class="item">
<div class="label"><span>*</span>行业</div>
<div class="val">
<van-field :clearable="true" :center="true" class="txt" v-model="industry" />
</div>
</div>
<div class="item">
<div class="label"><span>*</span>职位</div>
<div class="val">
<van-field :clearable="true" :center="true" class="txt" v-model="job" />
</div>
</div>
<div class="item">
<div class="label"><span></span>联系手机</div>
<div class="val">
<div class="txtBox">
<span>{{ userInfo.userData.phone }}</span>
<van-button type="primary" @click="updateType = 'tel'" ><i class="icon-edit"></i>修改</van-button>
</div>
</div>
</div>
<div class="item">
<div class="label"><span></span>联系邮箱</div>
<div class="val">
<div class="txtBox">
<span>{{ userInfo.userData.email || '--' }}</span>
<van-button type="primary" @click="updateType = 'email'" ><i class="icon-edit"></i>修改</van-button>
</div>
</div>
</div>
</div>
<div class="saveBtn">
<van-button type="primary" @click="onSave"></van-button>
</div>
</section>
<section>
<div class="blockTitle">会员信息</div>
<div class="formBox">
<div class="item">
<div class="label"><span></span>会员级别</div>
<div class="val">
<div class="txtBox">{{ userInfo.userData.equity ? userInfo.userData.equity.product_type_name : '' }}
</div>
</div>
</div>
<div class="item">
<div class="label"><span></span>有效期</div>
<div class="val">
<div class="txtBox">
<span>{{ userInfo.userData.equity ? userInfo.userData.equity.end_at : '' }}</span>
<van-button type="primary" @click="jump('/vip')"><i class="icon-dingyue"></i>订阅</van-button>
</div>
</div>
</div>
</div>
</section>
<!-- <UpdateContactModal v-if="!!updateType" :type="updateType" :onOk="onUpdateContactOk"
:onClose="onUpdateContactClose" /> -->
</div>
</template>
<script setup>
import { h, ref, onBeforeMount, onMounted } from 'vue';
import http from '@/io/http';
import { localCache } from '@/io/cache';
import { showToast } from 'vant';
import { useUserInfo } from '@/stores/userInfo';
// import UpdateContactModal from './components/updateContactModal.vue';
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const userInfo = useUserInfo();
const updateType = ref('');
const name = ref();
const industry = ref(); //
const job = ref(); //
const provinces = ref([]);
const cities = ref([]);
const areas = ref([]);
const province = ref();
const city = ref();
const area = ref();
const showAreaPopup = ref(false);
const options = ref([]);
const fieldValue = ref([]);
const cascaderValue = ref([]);
const onFinish = (v)=>{
console.log(v);
}
onBeforeMount(() => {
getAreas();
});
onMounted(() => {
initData();
if (province.value) {
getAreaByPid(province.value, res => {
cities.value = res.data;
});
}
if (city.value) {
getAreaByPid(city.value, res => {
areas.value = res.data;
});
}
});
const initData = () => {
const data = userInfo.userData;
name.value = data.name;
industry.value = data.industry;
job.value = data.job;
province.value = data.province_id;
city.value = data.city_id;
area.value = data.district_id;
};
const getAreas = () => {
http('/api/region/tree', {}, 'get').then(res => {
options.value = res.data;
}).catch(err => {
showToast(err.message);
});
}
const getAreaByPid = (parent_key, cb) => {
http('/api/region', { parent_key }, 'get').then(res => {
cb(res);
}).catch(err => {
showToast(err.message);
});
}
const provinceChoosed = (pid) => {
city.value = null;
area.value = null;
getAreaByPid(pid, res => {
cities.value = res.data;
});
};
const cityChoosed = (pid) => {
area.value = null;
getAreaByPid(pid, res => {
areas.value = res.data;
});
};
const onUpdateContactOk = (newVal) => {
let temp = { ...userInfo.userData };
if (updateType.value == 'tel') {
temp.phone = newVal;
}
if (updateType.value == 'email') {
temp.email = newVal;
}
userInfo.updateUserInfo(temp);
localCache.set('userInfo', temp);
updateType.value = '';
};
const onUpdateContactClose = () => {
updateType.value = '';
};
const validate = () => {
if (!name.value) {
showToast('请输入姓名!');
return false;
}
if (!(province.value && city.value && area.value)) {
showToast('请选择地区!');
return false;
}
if (!industry.value) {
showToast('请输入所在行业!');
return false;
}
if (!job.value) {
showToast('请输入您的职位!');
return false;
}
return true;
}
const onSave = () => {
if (!validate()) {
return false;
}
let formData = {
name: name.value,
province_id: province.value,
city_id: city.value,
district_id: area.value,
industry: industry.value,
job: job.value
};
http('/api/user/profile', formData).then(res => {
message.success('保存成功!');
let temp = { ...userInfo.userData };
temp.name = name.value;
temp.province_id = province.value;
temp.city_id = city.value;
temp.district_id = area.value;
temp.industry = industry.value;
temp.job = job.value;
userInfo.updateUserInfo(temp);
localCache.set('userInfo', temp);
}).catch(err => {
showToast(err.message);
});
};
const jump = (path) => {
router.push(path);
};
</script>
<style lang="scss" scoped>
.container {
padding: 47px;
}
.blockTitle {
display: inline-block;
height: 50px;
font-size: 27px;
color: #FFF;
font-weight: bold;
border-bottom: 4px solid #3662FE;
margin-bottom: 20px;
}
.formBox {
.item {
margin-bottom: 10px;
.label {
height: 66px;
line-height: 66px;
font-size: 27px;
span {
color: #f00;
padding-right: 5px;
}
}
.val {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
}
.valTxt {
flex: 1;
height: 66px;
color: #ccc;
line-height: 66px;
padding-left: 10px;
}
.areaInput,
.txt {
flex: 1;
height: 72px;
border: 2px solid rgba($color: #414548, $alpha: 0.4);
border-radius: 4px;
background: none;
color: #FFF;
padding: 10px 20px;
box-sizing: border-box;
:deep(input) {
background: none;
color: #FFF;
font-size: 27px;
line-height: 52px;
&::placeholder {
color: #999;
}
}
}
}
}
.saveBtn {
height: 72px;
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 50px;
button {
width: 100%;
height: 72px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
}
}
.txtBox {
width: 100%;
height: 72px;
border: 2px solid rgba($color: #414548, $alpha: 0.4);
border-radius: 4px;
background: none;
color: #FFF;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0 10px 20px;
font-size: 27px;
button {
width: 151px;
height: 72px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
font-size: 27px;
i{
font-size: 30px;
margin-right: 10px;
}
}
}
</style>