20260325稳定版

This commit is contained in:
2026-03-25 15:59:24 +08:00
commit 0decd78604
470 changed files with 53100 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
<template>
<view class="min-height bg-main-light">
<HeaderItem title="AI智慧助手"/>
<view style="line-height: 35rpx"
class="flex flex-col justify-start items-start text-25 p-30">
<view class="text-25 text-gray text-center my-15 w-100p">该内容由人工智能生成</view>
<view v-for="(item, index) in list" :key="index" class="mb-25 w-100p">
<view v-if="!item.isMine" class="flex justify-start items-start">
<u-icon name="/static/robot.png" size="60rpx"/>
<view class="shadow bg-white ml-15" style="border-radius: 0 15rpx 15rpx 15rpx; max-width: 80%">
<view v-if="item.loading" class="p-15">
<u-loading-icon mode="semicircle" :color="MAIN()"></u-loading-icon>
</view>
</view>
</view>
<view v-else class="flex justify-end items-start w-100p">
<view class="shadow p-15 text-white"
style="border-radius: 15rpx 0 15rpx 15rpx; max-width: 80%; background-color: #006DFF">{{
item.content
}}
</view>
</view>
<!-- <view v-if="index === 0" class="mt-25">-->
<!-- <view class="font-bold text-28 mb-25 text-center">猜你想问</view>-->
<!-- <view class="font-bold text-28 mb-25 text-center" @click="send(quest.dictDataCode)"-->
<!-- style="color: #006DFF"-->
<!-- v-for="(quest, questIndex) in questionList" :key="questIndex">-->
<!-- {{ quest.dictDataCode }}-->
<!-- </view>-->
<!-- </view>-->
</view>
<u-gap height="140rpx"></u-gap>
</view>
<view class="stop" v-if="answering" @click="stopAI">
<u-icon name="/static/stop.png" width="80rpx" height="80rpx"/>
</view>
<view class="footer">
<view class="p-20 bg-white shadow-top flex justify-center items-center">
<view class="flex-1 mr-15">
<u-textarea maxlength="-1" auto-height v-model="content" border="none" placeholder="点此开始提问"/>
</view>
<view class="mr-25">
<u-icon name="/static/send.png" size="60rpx" @click="send()"/>
</view>
</view>
</view>
<Login ref="Login" @done="getUserData"/>
</view>
</template>
<script>
import HeaderItem from "@/components/HeaderItem.vue";
import {WS_API} from "@/config";
import {sendChatReq, stopChatReq} from "@/api/chat";
import {getUserInfo} from "@/util/user";
import {MAIN} from "@/config/color";
import {dictDataReq} from "@/api/common";
import Login from "@/components/Login.vue";
import {userInfoReq} from "@/api/user";
export default {
name: "index",
components: {Login, HeaderItem},
data() {
return {
list: [],
questionList: [],
content: '',
taskId: null,
roomList: null,
userData: null,
answering: false,
isLogin: false,
billList: [],
socket: null,
timer: null
}
},
methods: {
MAIN() {
return MAIN
},
async getUserData() {
const {data} = await userInfoReq()
this.userData = data
this.isLogin = true
this.ws()
},
async getWelcomeWord() {
const {data} = await dictDataReq({dictId: '1435'})
this.list.push({
content: data[0].dictDataCode,
isMine: false,
loading: false,
done: true,
type: 'text'
})
console.log(this.list)
},
async send(content = null) {
if (!this.isLogin) return this.$refs.Login.open()
const contentItem = content || this.content
// console.log(contentItem, this.content)
if (!contentItem.trim()) return this.$toast('请输入提问内容')
if (!this.list[this.list.length - 1].done) return this.$toast('请等待AI回答完毕再继续提问')
this.list.push({
content: contentItem,
isMine: true,
loading: false,
done: true,
type: 'text'
})
this.scrollToBottom()
this.answering = true
this.list.push({
content: '',
isMine: false,
loading: true,
done: false,
})
this.taskId = null
sendChatReq({
query: contentItem,
user: this.userData.userId,
}).then(() => {
this.list[this.list.length - 1].done = true
this.answering = false
})
this.content = ''
},
scrollToBottom() {
setTimeout(() => {
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 99999,
duration: 0
})
})
}, 100)
},
async stopAI() {
await stopChatReq({
taskId: this.taskId
})
this.answering = false
this.list.splice(this.list.length - 1, 1)
// uni.closeSocket()
},
ws() {
const _this = this
this.socket = uni.connectSocket({
url: `${WS_API}/${this.userData.userId}`,
success: () => {
_this.ping()
},
})
console.log(this.socket)
uni.onSocketMessage(async res => {
// console.log(res.data)
if (res.data.includes('连接成功')) return
const aiData = JSON.parse(res.data)
this.list[this.list.length - 1].loading = false
if (aiData.answer !== '__END__' && !this.list[this.list.length - 1].isMine) {
this.list[this.list.length - 1].type = 'text'
this.list[this.list.length - 1].content += aiData.answer
} else {
this.list[this.list.length - 1].done = true
const lastContent = this.list[this.list.length - 1].content
console.log('check url', lastContent, lastContent.includes(`<openUrl url="/pages/user/property-bill">`))
if (lastContent.includes(`pages/user/property-bill`)) {
await this.getRoomList()
this.list.push({
content: '',
isMine: false,
loading: false,
done: true,
type: 'bill'
})
} else if (lastContent.includes(`servicePages/pages/suggest`) || lastContent.includes(`servicePages/pages/suuggest`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/suggest'
})
} else if (lastContent.includes(`servicePages/pages/survey`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/survey'
})
} else if (lastContent.includes(`servicePages/pages/fix`)) {
this.list.push({
content: '点击跳转',
isMine: false,
loading: false,
done: true,
type: 'link',
url: '/servicePages/pages/fix'
})
}
}
this.taskId = aiData.taskId
this.scrollToBottom()
// console.log(this.list)
})
},
ping() {
this.timer = setInterval(() => {
if (this.socket) {
this.socket.send({
data: 'ping'
})
}
}, 10000)
}
},
onLoad() {
this.getWelcomeWord()
},
onShow() {
if (getUserInfo().token) this.getUserData()
},
onUnload() {
if (this.timer) clearInterval(this.timer)
uni.closeSocket()
this.stopAI()
},
onHide() {
if (this.timer) clearInterval(this.timer)
uni.closeSocket()
this.stopAI()
},
}
</script>
<style scoped>
page {
background-color: #E0F2FE;
}
.bg {
background: url("https://sifa-api.wsdns.cn/api/file/20250420/bg.jpg") no-repeat 100% 100%;
}
.stop {
position: fixed;
right: 10rpx;
bottom: 160rpx;
width: 100rpx;
height: 100rpx;
border-radius: 100rpx;
background: rgba(255, 255, 255, 0.4);
box-shadow: 1px 1px 5px #3c9cff;
display: flex;
align-items: center;
justify-content: center;
}
</style>
+1012
View File
File diff suppressed because it is too large Load Diff
+579
View File
@@ -0,0 +1,579 @@
<template>
<view class="select-page">
<view class="select-header" :style="{ paddingTop: statusBarPadding }">
<u-icon name="arrow-left" size="32" color="#1f2048" @click="goBack"></u-icon>
<text class="header-title">我是玄衡</text>
</view>
<view class="intro-card">
<image class="intro-avatar" src="/static/logo.jpg"
mode="aspectFill"></image>
<view class="flex flex-col justify-start items-start">
<text class="intro-name">我是玄衡</text>
<text class="intro-desc">知五行 破内卷 助你关键决策</text>
</view>
</view>
<view class="phrase-list">
<view class="phrase" :style="{color: selectedPhrase === item ? '#1b2a75' : '#9a9fc3'}"
v-for="(item, index) in phrases" :key="index" @click="selectPhrase(item)">{{
item
}}
</view>
</view>
<!-- <view class="mentor-choose">-->
<!-- <view class="mentor-item" :class="{active: currentMentor === mentor.roleName}" v-for="mentor in mentors"-->
<!-- :key="mentor.id" @click="currentMentor = mentor.roleName">-->
<!-- <image class="mentor-img" :src="mentor.avatar" mode="aspectFit"></image>-->
<!-- <text class="mentor-text">{{ mentor.roleName }}</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- <button class="go-btn" @click="goChat">去聊天</button>-->
<Login ref="Login" @done="getMessageList"/>
<view class="chat-input" :style="{bottom: `${inputBottom}px`}">
<view class="m-40 flex justify-between items-center w-100p">
<view class="ml-15">
<u-icon :name="voiceMode ? '/static/icon/键盘.png' : '/static/icon/话筒.png'" size="60rpx"
@click="toggleVoiceMode"/>
</view>
<view class="input-wrap" v-if="!voiceMode">
<textarea
:adjust-position="false"
class="textarea"
v-model="content"
placeholder="迷茫困惑的话和我聊聊吧"
maxlength="-1"
placeholder-style="text-align:center;"
auto-height
fixed
:show-confirm-bar="false"
/>
</view>
<view class="record-wrap" v-else>
<view
class="record-btn"
@longpress="startRecord"
@touchend="stopRecord"
@touchcancel="stopRecord"
>
{{ recording ? '录音中...' : recordBtnText }}
</view>
</view>
<button class="send-btn" @click="goChat">发送</button>
</view>
</view>
</view>
</template>
<script>
import {addChatListReq, chatListReq, listChatRoleReq, makeTransReq, voiceGetResReq} from "@/api/chat";
import Login from "@/components/Login.vue";
import {getUserInfo} from "@/util/user";
import {dictDataReq} from "@/api/common";
import {MAIN_API} from "@/config";
import {coinAddReq} from "@/api/coin";
export default {
components: {Login},
data() {
return {
statusBarPadding: '40rpx',
phrases: ['你好', '今天天气怎么样', '聊聊山海经', '聊聊论语'],
mentors: [],
currentMentor: '',
selectedPhrase: '你好',
messageList: [],
content: '',
inputBottom: 0,
toChat: false,
voiceMode: false,
recording: false,
recordBtnText: '按住说话',
recorder: null,
voicePollingTimer: null,
maxVoicePolling: 15,
voicePlayingIndex: null,
audioCtx: null,
voiceType: 'default',
}
},
onLoad() {
// this.inputBottom = uni.getSystemInfoSync().safeAreaInsets.bottom
const info = uni.getSystemInfoSync()
this.statusBarPadding = `${(info.statusBarHeight || 20) + 10}px`
this.getWelcomeWords()
// if (!getUserInfo().token) this.$refs.Login.open()
// else this.getMessageList()
if (getUserInfo().token) this.getMessageList()
uni.removeStorageSync('forceNoJump')
uni.onKeyboardHeightChange(res => {
setTimeout(() => {
console.log('onKeyboardHeightChange', res.height)
this.inputBottom = res.height
}, 100)
})
},
methods: {
onKeyboardChange(e) {
this.inputBottom = e.detail.height
},
async getWelcomeWords() {
const {data} = await dictDataReq({
dictId: 167
})
this.phrases = data.map(item => item.dictDataCode)
this.selectedPhrase = this.phrases[0]
this.content = this.phrases[0]
},
goBack() {
uni.setStorageSync('forceNoJump', 1)
uni.switchTab({url: '/pages/index/index'})
},
async getRoleList() {
const {data} = await listChatRoleReq()
this.mentors = data
this.currentMentor = data[0].roleName
},
toggleVoiceMode() {
uni.authorize({
scope: 'scope.record',
success() {
console.log('授权成功')
},
fail() {
console.log('授权失败')
},
complete(e) {
console.log('授权完成', e)
}
})
if (this.voiceMode) {
if (this.recorder && this.recording) {
this.recorder.stop()
}
this.recording = false
this.voiceMode = false
this.recordBtnText = '按住说话'
return
}
this.voiceMode = true
this.recordBtnText = '按住说话'
typeof uni.hideKeyboard === 'function' && uni.hideKeyboard()
this.initRecorder()
},
async goChat() {
if (!getUserInfo().token) return this.$refs.Login.open()
const mentor = this.mentors.find(item => item.name === this.currentMentor)
let url = `/pages/user/setting?word=${this.content}`
if (mentor) {
url += `&roleName=${mentor.name}&roleDesc=${mentor.desc}&avatar=${mentor.avatar}`
const {data} = await addChatListReq({
roleName: mentor.name,
roleDesc: mentor.desc,
roleId: mentor.id
})
url += `&chatId=${data.id}`
}
uni.navigateTo({url})
},
async getMessageList() {
const res = await chatListReq()
this.messageList = res.data
if (this.messageList.length > 0)
await this.getRoleList()
},
selectPhrase(item) {
this.selectedPhrase = item
this.content = item
},
initRecorder() {
if (this.recorder) return
this.recorder = uni.getRecorderManager()
this.recorder.onStart(() => {
this.recording = true
this.recordBtnText = '录音中...'
})
this.recorder.onStop((res) => {
this.recording = false
const path = res && res.tempFilePath
if (path) this.processVoiceFile(path)
else this.recordBtnText = '按住说话'
})
this.recorder.onError(() => {
this.recording = false
this.recordBtnText = '按住说话'
uni.showToast({title: '录音失败', icon: 'none'})
})
},
startRecord() {
if (!this.voiceMode) return
this.initRecorder()
if (this.recording) return
this.recording = true
this.recordBtnText = '录音中...'
try {
this.recorder.start({
duration: 60000,
format: 'mp3',
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000
})
} catch (e) {
this.recording = false
this.recordBtnText = '按住说话'
uni.showToast({title: '无法开始录音', icon: 'none'})
}
},
stopRecord() {
if (!this.voiceMode || !this.recording || !this.recorder) return
this.recording = false
this.recorder.stop()
},
async processVoiceFile(filePath) {
this.recordBtnText = '处理中...'
uni.showLoading({title: '识别中...'})
try {
const {url} = await this.uploadVoiceFile(filePath)
if (!url) throw new Error('no file')
// const file = 'https://guoxue.gxwebsoft.com/api/file/20251202/97e5e082becb44e88ece0e9cbd050dca.mp3'
// const {data: taskId} = await makeTransReq({file})
const {data: taskId} = await makeTransReq({file: url})
if (taskId) {
this.startVoicePolling(taskId)
} else {
throw new Error('no task')
}
} catch (err) {
uni.hideLoading()
uni.showToast({title: '语音识别失败', icon: 'none'})
this.recordBtnText = '按住说话'
}
},
uploadVoiceFile(filePath) {
const {token} = getUserInfo()
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${MAIN_API}/api/file/upload`,
name: 'file',
filePath,
header: {
Authorization: token,
TenantId: 10543
},
success: (res) => {
try {
const result = JSON.parse(res.data)
const url = result?.data?.url || result?.data
resolve({url})
} catch (e) {
reject(e)
}
},
fail: reject
})
})
},
startVoicePolling(taskId) {
this.clearVoicePolling()
let attempt = 0
this.voicePollingTimer = setInterval(async () => {
attempt += 1
try {
const {data} = await voiceGetResReq(taskId)
const result = typeof data === 'string' ? data : (data?.text || '')
if (result && result !== '__NULL__') {
this.clearVoicePolling()
uni.hideLoading()
this.recordBtnText = '按住说话'
const wordResult = JSON.parse(data)
// console.log('wordResult', wordResult)
if (wordResult.Sentences && wordResult.Sentences.length)
// this.send(wordResult.Sentences[0].Text)
this.content = wordResult.Sentences[0].Text
else uni.showToast({title: '未识别到内容', icon: 'none'})
} else if (result === '__NULL__') {
this.clearVoicePolling()
uni.hideLoading()
this.recordBtnText = '按住说话'
uni.showToast({title: '未识别到内容', icon: 'none'})
} else if (attempt >= this.maxVoicePolling) {
this.clearVoicePolling()
uni.hideLoading()
this.recordBtnText = '按住说话'
uni.showToast({title: '识别超时', icon: 'none'})
}
} catch (err) {
this.clearVoicePolling()
uni.hideLoading()
this.recordBtnText = '按住说话'
uni.showToast({title: '识别失败', icon: 'none'})
}
}, 2000)
},
clearVoicePolling() {
if (this.voicePollingTimer) {
clearInterval(this.voicePollingTimer)
this.voicePollingTimer = null
}
},
},
onUnload() {
this.clearVoicePolling()
this.stopAudio()
this.recordBtnText = '按住说话'
uni.hideLoading()
uni.offKeyboardHeightChange()
},
onHide() {
this.clearVoicePolling()
this.stopAudio()
this.recordBtnText = '按住说话'
uni.hideLoading()
uni.offKeyboardHeightChange()
},
onShareTimeline() {
if (getUserInfo().token) {
coinAddReq({
type: '+',
desc: '分享获得',
num: 1
})
}
return {
title: '简幸',
path: '/pages/index/index'
}
},
onShareAppMessage() {
if (getUserInfo().token) {
coinAddReq({
type: '+',
desc: '分享获得',
num: 1
})
}
return {
title: '简幸',
path: '/pages/index/index'
}
},
}
</script>
<style lang="scss" scoped>
.select-page {
min-height: 100vh;
//background: linear-gradient(180deg, #f5f7ff 0%, #ffffff 100%);
padding: 40rpx 30rpx 80rpx;
box-sizing: border-box;
}
.select-header {
display: flex;
align-items: center;
justify-content: flex-start;
}
.header-title {
font-size: 32rpx;
color: #14164e;
}
.pill {
background: rgba(255, 255, 255, 0.7);
padding: 10rpx 28rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.intro-card {
margin-top: 200rpx;
display: flex;
align-items: center;
background: #fff;
border-radius: 30rpx;
padding: 20rpx;
//box-shadow: 0 10rpx 20rpx rgba(123, 137, 255, 0.15);
}
.intro-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
margin-right: 20rpx;
}
.intro-name {
font-size: 32rpx;
color: #14164e;
font-weight: 600;
}
.intro-desc {
font-size: 26rpx;
color: #9a9fc3;
margin-top: 10rpx;
}
.phrase-list {
margin-top: 30rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.phrase {
background: rgba(160, 175, 255, 0.2);
border-radius: 40rpx;
padding: 20rpx 30rpx;
color: #26306d;
font-size: 28rpx;
display: inline-flex;
width: auto;
align-self: flex-start;
}
.mentor-choose {
margin-top: 40rpx;
display: flex;
justify-content: space-between;
}
.mentor-item {
width: 48%;
background: rgba(255, 255, 255, 0.9);
border-radius: 30rpx;
text-align: center;
padding: 20rpx;
box-shadow: 0 12rpx 20rpx rgba(120, 134, 255, 0.15);
}
.mentor-item.active {
border: 2rpx solid #f0c730;
}
.mentor-img {
width: 160rpx;
height: 160rpx;
}
.mentor-text {
margin-top: 12rpx;
font-size: 28rpx;
color: #424577;
}
.go-btn {
margin-top: 60rpx;
height: 90rpx;
line-height: 90rpx;
border-radius: 45rpx;
background: #1b2a75;
color: #fff;
font-size: 30rpx;
}
.chat-input {
width: 100%;
padding: 40rpx 0;
//padding: 20rpx 30rpx 40rpx;
align-items: center;
gap: 20rpx;
background: #eee;
display: flex;
justify-content: center;
position: fixed;
left: 0;
bottom: 0;
}
.chat-input .input-wrap {
flex: 1;
border-radius: 10rpx;
background: #fff;
color: #000;
padding: 15rpx 30rpx;
margin: 0 10rpx;
display: flex;
align-items: center;
justify-content: center;
//height: 100rpx;
min-height: 40rpx;
}
.chat-input .textarea {
width: 100%;
//height: 100%;
background: none !important;
color: #000 !important;
text-align: left;
line-height: 40rpx;
padding: 0;
min-height: 35rpx;
}
.send-btn {
width: 120rpx;
height: 70rpx;
border-radius: 20rpx;
//background: linear-gradient(120deg, #fbc889, #f37a5b);
background-color: white;
border: 1px solid white;
color: #000;
display: flex;
align-items: center;
justify-content: center;
margin: 0 15rpx 0 0;
font-size: 25rpx;
}
.send-btn:after {
border: none;
}
.record-wrap {
flex: 1;
border-radius: 10rpx;
//background: linear-gradient(120deg, #fbc889, #f37a5b);
background: #fff;
color: #000;
padding: 15rpx 30rpx;
margin: 0 10rpx;
display: flex;
align-items: center;
justify-content: center;
min-height: 40rpx;
}
.record-btn {
width: 100%;
text-align: center;
color: #000;
}
.voice-toggle {
width: 120rpx;
height: 100rpx;
border-radius: 999rpx;
background: #dfe1f2;
color: #1f2654;
display: flex;
align-items: center;
justify-content: center;
margin: 0 10rpx;
border: none;
}
.voice-toggle.active {
background: #21284f;
color: #fff;
}
</style>
+645
View File
@@ -0,0 +1,645 @@
<template>
<view class="detail-page" :style="{paddingTop: paddingTop}">
<view class="detail-bg"></view>
<view class="detail-header">
<view class="header-left" @click="goBack">
<u-icon name="arrow-left" size="32" color="#1b1b1b"></u-icon>
</view>
<text class="header-title">详情</text>
<!-- <view class="header-right">-->
<!-- <u-icon name="more-dot-fill" size="26" color="#1b1b1b"></u-icon>-->
<!-- <u-icon name="calendar-fill" size="26" color="#1b1b1b" class="header-icon-gap"></u-icon>-->
<!-- </view>-->
</view>
<scroll-view scroll-y class="detail-scroll">
<view class="detail-info">
<view class="detail-label">{{ detailInfo.category }}</view>
<view class="detail-title">{{ detailInfo.title }}</view>
<view class="detail-subtitle">{{ detailInfo.source }}</view>
<view class="detail-author">
<!-- <view class="author-meta">-->
<!-- <text class="author-name">{{ detailInfo.author }}</text>-->
<!-- <text class="author-desc">{{ detailInfo.authorDesc }}</text>-->
<!-- </view>-->
<text class="detail-date">{{ dayjs(detailInfo.createTime).format('YYYY-MM-DD') }}</text>
</view>
</view>
<!-- <image class="detail-cover" :src="detailInfo.image" mode="aspectFill"></image>-->
<view class="detail-article">
<u-parse :content="detailInfo.content"/>
</view>
<u-gap height="200rpx"/>
</scroll-view>
<view class="detail-action">
<view class="flex-1 flex justify-center items-center">
<view class="action-item" @click="openCommentPopup" v-if="!authing">
<u-icon name="chat" size="28" color="#1b1b1b"></u-icon>
<text class="action-text">评论</text>
</view>
<view class="action-item" @click="toggleFavorite">
<u-icon :name="isFavorite ? 'star-fill' : 'star'" size="28" color="#f7b500"></u-icon>
<text class="action-text">收藏</text>
</view>
</view>
<button open-type="share" class="action-button" @click="handleShare">
<u-icon name="share" size="28" color="#fff"></u-icon>
<text class="action-btn-text">分享</text>
</button>
</view>
<u-popup
:show="showCommentPopup"
mode="bottom"
round="20"
close-on-click-overlay
:safe-area-inset-bottom="false"
@close="showCommentPopup = false"
>
<view class="comment-popup">
<view class="comment-popup-header">
<text class="comment-popup-title">评论 ({{ commentList.length }})</text>
<u-icon name="close" size="28" color="#9ca3af" @click="showCommentPopup = false"></u-icon>
</view>
<scroll-view scroll-y class="comment-list">
<view v-if="commentLoading" class="comment-placeholder">评论加载中...</view>
<view v-else-if="!commentList.length" class="comment-placeholder">暂时还没有评论</view>
<view v-else>
<view
class="comment-item"
v-for="(item, index) in commentList"
:key="item.commentId || item.id || index"
>
<view class="comment-item-header">
<text class="comment-user">{{ item.userName || item.nickname || '匿名用户' }}</text>
<text class="comment-time">{{ formatCommentTime(item.createTime || item.createdAt) }}</text>
</view>
<view class="comment-content">{{ item.content }}</view>
</view>
</view>
</scroll-view>
<view class="comment-footer">
<view class="comment-publish-btn" @click="openPublishPopup">发表评论</view>
</view>
</view>
</u-popup>
<u-popup
:show="showPublishPopup"
mode="center"
round="20"
close-on-click-overlay
:safe-area-inset-bottom="false"
@close="closePublishPopup"
>
<view class="publish-popup">
<view class="publish-header">写评论</view>
<textarea
class="publish-textarea"
v-model="newCommentContent"
maxlength="200"
auto-height
placeholder="请输入评论内容"
placeholder-style="color:#9ca3af"
></textarea>
<view class="publish-actions">
<view class="publish-cancel" @click="closePublishPopup">取消</view>
<view
class="publish-submit"
:class="{'is-disabled': commentSubmitting}"
@click="submitComment"
>
发表
</view>
</view>
</view>
</u-popup>
<Login ref="loginPopup" @done="onLoginDone"/>
</view>
</template>
<script>
import {
addArticleCollectReq,
articleInfoReq,
articleCommentListReq,
addArticleCommentReq, dictDataReq
} from "@/api/common";
import {dayjs} from '@/uni_modules/iRainna-dayjs/js_sdk/dayjs.min.js'
import Login from "@/components/Login.vue";
import {getUserInfo} from "@/util/user";
export default {
components: {Login},
data() {
return {
detailInfo: {
category: '',
title: '',
subtitle: '',
author: '',
authorDesc: '',
date: '',
image: '',
content: []
},
isFavorite: false,
articleImages: [
'/static/icon/article_1.png',
'/static/icon/article_2.png',
'/static/icon/article_3.png',
'/static/icon/article_4.png',
'/static/icon/article_5.png',
'/static/icon/article_6.png'
],
mockArticles: [
{
id: 5,
category: '',
title: '',
subtitle: '',
author: '',
authorDesc: '8年情感心理师 · 自由写作者',
date: '',
image: '',
content: ''
}
],
paddingTop: '40px',
showCommentPopup: false,
commentList: [],
commentLoading: false,
showPublishPopup: false,
newCommentContent: '',
commentSubmitting: false,
authing: true,
}
},
onLoad(options) {
const {id} = options || {}
this.getInfo(id)
this.paddingTop = uni.getSystemInfoSync().statusBarHeight + 'px'
},
onShow(){
this.getDictList()
},
methods: {
dayjs,
async getDictList() {
const {data} = await dictDataReq({dictId: '171'})
this.authing = parseInt(data[0].dictDataCode) === 1
},
async getInfo(id) {
const {data} = await articleInfoReq(id)
this.detailInfo = data
this.isFavorite = data.hasCollect
},
goBack() {
if (getCurrentPages().length === 1) uni.switchTab({url: '/pages/index/index'})
else uni.navigateBack()
},
toggleFavorite() {
this.isFavorite = !this.isFavorite
uni.showToast({
title: this.isFavorite ? '已收藏' : '已取消',
icon: 'none'
})
addArticleCollectReq({
articleId: this.detailInfo.articleId
})
},
handleShare() {
uni.showToast({
title: '已复制分享链接',
icon: 'none'
})
},
openCommentPopup() {
if (!this.detailInfo.articleId) return
this.showCommentPopup = true
this.fetchComments()
},
async fetchComments() {
if (!this.detailInfo.articleId) return
this.commentLoading = true
try {
const {data} = await articleCommentListReq({articleId: this.detailInfo.articleId})
this.commentList = this.normalizeCommentList(data)
} catch (error) {
console.log(error)
uni.showToast({
title: '评论加载失败',
icon: 'none'
})
} finally {
this.commentLoading = false
}
},
normalizeCommentList(data) {
if (Array.isArray(data)) return data
if (data && Array.isArray(data.records)) return data.records
if (data && Array.isArray(data.list)) return data.list
if (data && Array.isArray(data.rows)) return data.rows
return []
},
openPublishPopup() {
if (!this.ensureLogin()) return
this.showPublishPopup = true
this.newCommentContent = ''
},
closePublishPopup() {
this.showPublishPopup = false
},
async submitComment() {
if (this.commentSubmitting) return
if (!this.ensureLogin()) return
const content = (this.newCommentContent || '').trim()
if (!content) {
return uni.showToast({
title: '请输入评论内容',
icon: 'none'
})
}
this.commentSubmitting = true
try {
await addArticleCommentReq({
content,
articleId: this.detailInfo.articleId
})
uni.showToast({
title: '发表成功',
icon: 'success'
})
this.newCommentContent = ''
this.showPublishPopup = false
this.fetchComments()
} catch (error) {
console.log(error)
const message = error && (error.msg || error.message)
uni.showToast({
title: message || '发表评论失败',
icon: 'none'
})
} finally {
this.commentSubmitting = false
}
},
ensureLogin() {
const {token} = getUserInfo()
if (!token) {
this.$refs.loginPopup && this.$refs.loginPopup.open()
return false
}
return true
},
onLoginDone() {
if (this.showCommentPopup) {
this.fetchComments()
}
},
formatCommentTime(time) {
if (!time) return '--'
const parsed = dayjs(time)
return typeof parsed.isValid === 'function' && parsed.isValid() ? parsed.format('YYYY-MM-DD HH:mm') : time
}
},
onShareTimeline() {
return {
title: this.detailInfo.title,
path: '/pages/index/detail?id=' + this.detailInfo.articleId
}
},
onShareAppMessage() {
return {
title: this.detailInfo.title,
path: '/pages/index/detail?id=' + this.detailInfo.articleId
}
},
}
</script>
<style scoped lang="scss">
.detail-page {
position: relative;
min-height: 100vh;
background: linear-gradient(180deg, #e5edff 0%, #f6f4ff 45%, #fef7ff 100%);
//padding: 60rpx 32rpx 140rpx;
box-sizing: border-box;
}
.detail-bg {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 300rpx;
background: rgba(255, 255, 255, 0.72);
filter: blur(35rpx);
z-index: 0;
}
.detail-header {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 30rpx;
}
.header-left,
.header-right {
display: flex;
align-items: center;
}
.header-title {
font-size: 34rpx;
font-weight: 600;
color: #1b1b1b;
}
.header-icon-gap {
margin-left: 18rpx;
}
.detail-scroll {
position: relative;
z-index: 2;
max-height: calc(100vh - 260rpx);
padding-bottom: 220rpx;
}
.detail-info {
padding: 20rpx 16rpx;
}
.detail-label {
display: inline-flex;
padding: 8rpx 22rpx;
border-radius: 40rpx;
font-size: 24rpx;
color: #4a5568;
background: rgba(255, 255, 255, 0.8);
margin-bottom: 16rpx;
}
.detail-title {
font-size: 40rpx;
font-weight: 600;
color: #16171d;
line-height: 1.4;
}
.detail-subtitle {
font-size: 26rpx;
color: #8b8f99;
margin-top: 16rpx;
line-height: 1.5;
}
.detail-author {
margin-top: 22rpx;
display: flex;
justify-content: space-between;
align-items: flex-end;
border-bottom: 1px solid rgba(255, 255, 255, 0.6);
padding-bottom: 20rpx;
}
.author-meta {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.author-name {
font-size: 28rpx;
color: #2c2c2c;
font-weight: 600;
}
.author-desc {
font-size: 24rpx;
color: #a0a3b0;
}
.detail-date {
font-size: 24rpx;
color: #babec6;
}
.detail-cover {
//width: 100%;
height: 360rpx;
border-radius: 30rpx;
margin: 30rpx;
box-shadow: 0 20rpx 40rpx rgba(128, 139, 255, 0.2);
}
.detail-article {
margin: 30rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 32rpx;
padding: 30rpx;
box-shadow: 0 20rpx 40rpx rgba(128, 139, 255, 0.15);
}
.detail-paragraph {
font-size: 26rpx;
color: #555c6b;
line-height: 1.8;
margin-bottom: 26rpx;
}
.detail-paragraph:last-child {
margin-bottom: 0;
}
.detail-action {
z-index: 999;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 26rpx 40rpx calc(26rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-top: 1px solid rgba(148, 163, 184, 0.2);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -12rpx 40rpx rgba(99, 102, 241, 0.12);
}
.action-item {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #1b1b1b;
font-size: 24rpx;
}
.action-text {
color: #1f2a37;
}
.action-button {
flex: 1;
margin-left: 40rpx;
height: 90rpx;
border-radius: 50rpx;
background: linear-gradient(120deg, #0b1c56, #1a2e8e);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: 28rpx;
font-weight: 500;
}
.action-btn-text {
color: #fff;
}
.comment-popup {
padding: 30rpx;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.comment-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.comment-popup-title {
font-size: 32rpx;
font-weight: 600;
color: #1f2933;
}
.comment-list {
flex: 1;
max-height: 60vh;
}
.comment-item {
padding: 20rpx 0;
border-bottom: 1px solid #eef2ff;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-item-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.comment-user {
font-size: 28rpx;
color: #111827;
font-weight: 600;
}
.comment-time {
font-size: 22rpx;
color: #9ca3af;
}
.comment-content {
margin-top: 12rpx;
font-size: 26rpx;
line-height: 1.6;
color: #4b5563;
white-space: pre-wrap;
word-break: break-all;
}
.comment-placeholder {
text-align: center;
color: #9ca3af;
padding: 80rpx 0;
font-size: 26rpx;
}
.comment-footer {
padding-top: 24rpx;
}
.comment-publish-btn {
width: 100%;
text-align: center;
padding: 22rpx 0;
border-radius: 50rpx;
color: #fff;
font-size: 28rpx;
font-weight: 500;
background: linear-gradient(120deg, #0b1c56, #1a2e8e);
}
.publish-popup {
width: 640rpx;
padding: 40rpx 32rpx 32rpx;
box-sizing: border-box;
}
.publish-header {
font-size: 32rpx;
font-weight: 600;
color: #111827;
margin-bottom: 24rpx;
}
.publish-textarea {
width: 100%;
min-height: 200rpx;
background: #f3f4f6;
border-radius: 24rpx;
padding: 24rpx;
font-size: 26rpx;
color: #1f2937;
box-sizing: border-box;
}
.publish-actions {
display: flex;
justify-content: flex-end;
gap: 24rpx;
margin-top: 24rpx;
}
.publish-cancel {
padding: 16rpx 32rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: #6b7280;
background: #f5f5f5;
}
.publish-submit {
padding: 16rpx 40rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: #fff;
background: linear-gradient(120deg, #0b1c56, #1a2e8e);
}
.publish-submit.is-disabled {
opacity: 0.6;
}
</style>
+326
View File
@@ -0,0 +1,326 @@
<template>
<view class="discover-page" :style="{paddingTop: paddingTop}">
<view class="blur-bg"></view>
<view class="header-area">
<view class="status-bar">
<text class="header-title">发现</text>
</view>
<view class="search-section">
<view class="search-input">
<u-icon name="search" size="28" color="#9EA3B0"></u-icon>
<input
class="search-field"
v-model="searchKeyword"
placeholder="请输入关键词搜索"
placeholder-class="placeholder"
confirm-type="search"
@confirm="handleSearch"
/>
</view>
<view class="search-btn" @click="handleSearch">搜索</view>
</view>
<scroll-view class="category-scroll" scroll-x="true" enable-flex>
<view
class="category-item"
v-for="item in categories"
:key="item"
:class="{ active: currentCategory === item.categoryId }"
@click="selectCategory(item)"
>
{{ item.title }}
</view>
</scroll-view>
</view>
<view class="card-grid">
<view
class="discover-card"
v-for="card in cardList"
:key="card.articleId"
@click="goToDetail(card)"
>
<image class="card-cover" :src="`${card.image}`" mode="aspectFill"></image>
<view class="card-title">{{ card.title }}</view>
<view class="card-desc">{{ card.source }}</view>
</view>
</view>
<view class="indicator">
<view
v-for="dotIndex in indicatorDots"
:key="dotIndex"
class="dot"
:class="{ active: dotIndex - 1 === activeIndicator }"
></view>
</view>
</view>
</template>
<script>
import {dictDataReq, listArticleCateReq, listArticleReq} from "@/api/common";
import {getUserInfo} from "@/util/user";
import {chatListReq} from "@/api/chat";
import {coinAddReq} from "@/api/coin";
export default {
data() {
return {
searchKeyword: '',
categories: [],
currentCategory: null,
indicatorDots: 2,
activeIndicator: 0,
paddingTop: '100px',
cardList: []
}
},
methods: {
async getCategories() {
const {data} = await listArticleCateReq()
this.categories = [{categoryId: null, title: '全部'}, ...data]
await this.getArticleList()
},
selectCategory(category) {
this.currentCategory = category.categoryId
this.getArticleList()
},
async getArticleList() {
const {data} = await listArticleReq({
categoryId: this.currentCategory,
title: this.searchKeyword ?? null
})
this.cardList = data
// console.log(this.cardList)
},
goToDetail(card) {
const targetId = card && card.articleId ? card.articleId : ''
uni.navigateTo({
url: `/pages/index/detail?id=${targetId}`
})
},
handleSearch() {
const keyword = this.searchKeyword.trim()
this.getCategories()
},
async getMessageList() {
const res = await chatListReq()
this.messageList = res.data
if (!this.messageList.length) this.toSelect()
},
toSelect(){
if (!uni.getStorageSync('forceNoJump'))
uni.navigateTo({url: '/pages/chat/select'})
}
},
onShow() {
this.paddingTop = uni.getSystemInfoSync().statusBarHeight + 40 + 'px'
this.getCategories()
if (!getUserInfo().token) {
this.toSelect()
} else {
this.getMessageList()
}
},
onShareTimeline() {
coinAddReq({
type: '+',
desc: '分享获得',
num: 1
})
return {
title: '简幸',
path: '/pages/index/index'
}
},
onShareAppMessage() {
coinAddReq({
type: '+',
desc: '分享获得',
num: 1
})
return {
title: '简幸',
path: '/pages/index/index'
}
},
}
</script>
<style lang="scss" scoped>
.discover-page {
position: relative;
min-height: 100vh;
background: linear-gradient(180deg, #e4f0ff 0%, #f4f3ff 45%, #fef9ff 100%);
padding: 40rpx 30rpx 80rpx;
box-sizing: border-box;
}
.blur-bg {
position: absolute;
left: 0;
top: 0;
right: 0;
height: 220rpx;
background: rgba(255, 255, 255, 0.7);
filter: blur(30rpx);
z-index: 0;
}
.header-area {
position: relative;
z-index: 1;
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
}
.header-title {
font-size: 38rpx;
font-weight: 600;
color: #151515;
}
.header-icons {
display: flex;
align-items: center;
}
.header-icon-gap {
margin-left: 15rpx;
}
.search-section {
display: flex;
align-items: center;
margin-top: 10rpx;
gap: 20rpx;
}
.search-input {
flex: 1;
height: 76rpx;
border-radius: 48rpx;
background-color: #fff;
display: flex;
align-items: center;
padding: 0 28rpx;
box-shadow: 0 8rpx 30rpx rgba(124, 157, 255, 0.2);
}
.search-field {
flex: 1;
font-size: 28rpx;
color: #333;
margin-left: 12rpx;
}
.placeholder {
color: #9EA3B0;
}
.search-btn {
width: 140rpx;
height: 76rpx;
border-radius: 50rpx;
background: linear-gradient(120deg, #5fb3f0, #8b7df0);
color: #fff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
}
.category-scroll {
margin-top: 30rpx;
white-space: nowrap;
}
.category-item {
display: inline-flex;
align-items: center;
justify-content: center;
height: 60rpx;
padding: 0 32rpx;
border-radius: 40rpx;
font-size: 26rpx;
color: #7a7d85;
background-color: rgba(255, 255, 255, 0.6);
margin-right: 20rpx;
}
.category-item.active {
color: #111;
font-weight: 600;
background: #fff;
box-shadow: 0 8rpx 20rpx rgba(130, 152, 255, 0.25);
}
.card-grid {
position: relative;
z-index: 1;
margin-top: 40rpx;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.discover-card {
width: calc(50% - 12rpx);
margin-bottom: 30rpx;
border-radius: 26rpx;
background-color: rgba(255, 255, 255, 0.92);
box-shadow: 0 10rpx 30rpx rgba(133, 149, 255, 0.2);
padding: 16rpx 18rpx 26rpx;
box-sizing: border-box;
}
.card-cover {
width: 100%;
height: 260rpx;
border-radius: 20rpx;
margin-bottom: 18rpx;
}
.card-title {
font-size: 28rpx;
color: #1f1f1f;
font-weight: 600;
line-height: 1.4;
margin-bottom: 12rpx;
}
.card-desc {
font-size: 24rpx;
color: #6b6f7a;
line-height: 1.4;
}
.indicator {
margin-top: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.15);
margin: 0 8rpx;
}
.dot.active {
width: 28rpx;
border-radius: 8rpx;
background-color: #151515;
}
</style>
+285
View File
@@ -0,0 +1,285 @@
<template>
<view class="msg-page" :style="{ paddingTop: pagePadding }">
<view class="background-blur"></view>
<view class="msg-header">
<text class="header-title">消息</text>
<u-icon name="plus-circle" @click="$jump('/pages/user/setting')" size="60rpx"/>
</view>
<view class="msg-list">
<view
v-for="item in messageList"
:key="item.id"
class="msg-item"
@click="openMessage(item)"
@longpress="handleLongPress(item)"
>
<image class="avatar" :src="item.role.avatar" mode="aspectFit"></image>
<view class="msg-info">
<view class="msg-name u-line-1">{{ item.title ? `${item.title}·` : '' }}{{ item.role.roleName }}</view>
<view class="msg-preview u-line-2">{{ item.lastMessage.reply }}</view>
</view>
<text class="msg-time">{{ item.createTime.replace('T', ' ') }}</text>
</view>
</view>
<Login ref="Login" @done="getMessageList"/>
</view>
</template>
<script>
import {chatListReq, deleteChatListReq, updateChatListReq} from "@/api/chat";
import {getUserInfo} from "@/util/user";
import Login from "@/components/Login.vue";
export default {
components: {Login},
data() {
return {
pagePadding: '120rpx',
messageList: [
// {
// avatar: '/static/icon/图片 2 Copy 1@1x.png',
// name: '五师兄',
// preview: '你好',
// time: '2025-11-20 12:00:00',
// }
]
}
},
async onShow() {
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 20
this.pagePadding = `${statusBarHeight + 44}px`
if (!getUserInfo().token) {
return this.$refs.Login.open()
}
await this.getMessageList()
},
methods: {
handleBack() {
uni.navigateBack().catch(() => {
uni.switchTab({url: '/pages/index/index'})
})
},
async getMessageList() {
const res = await chatListReq()
this.messageList = res.data
if (!this.messageList.length) {
uni.navigateTo({url: '/pages/chat/select'})
}
},
openMessage(item) {
// if (!this.messageList.length) {
// uni.navigateTo({ url: '/pages/chat/select' })
// return
// }
let url = `/pages/chat/index?chatId=${item.id}&conversationId=${item.conversationId}`
url += `&roleId=${item.roleId}`
uni.navigateTo({url})
},
handleLongPress(item) {
uni.showActionSheet({
itemList: ['修改标题', '删除'],
success: ({tapIndex}) => {
if (tapIndex === 0) {
this.promptRename(item)
} else if (tapIndex === 1) {
this.confirmDelete(item)
}
}
})
},
promptRename(item) {
const currentName = (item && item.title) || ''
uni.showModal({
title: '修改标题',
editable: true,
placeholderText: '请输入新的标题',
content: currentName,
success: (res) => {
if (!res.confirm) return
if (typeof res.content === 'undefined') {
uni.showToast({title: '当前环境暂不支持此操作', icon: 'none'})
return
}
const newTitle = res.content.trim()
if (!newTitle) {
uni.showToast({title: '标题不能为空', icon: 'none'})
return
}
if (newTitle === currentName) {
uni.showToast({title: '标题无改动', icon: 'none'})
return
}
this.updateChatTitle(item, newTitle)
}
})
},
async updateChatTitle(item, title) {
try {
uni.showLoading({title: '保存中...', mask: true})
await updateChatListReq({
id: item.id,
title,
})
this.messageList = this.messageList.map(msg => {
if (msg.id === item.id) {
return {
...msg,
title,
role: msg.role
}
}
return msg
})
uni.showToast({title: '修改成功', icon: 'success'})
} catch (err) {
const msg = (err && err.msg) || '修改失败'
uni.showToast({title: msg, icon: 'none'})
} finally {
uni.hideLoading()
}
},
confirmDelete(item) {
uni.showModal({
title: '删除会话',
content: '删除后不可恢复,是否继续?',
confirmText: '删除',
confirmColor: '#fa3534',
success: (res) => {
if (res.confirm) {
this.deleteChat(item)
}
}
})
},
async deleteChat(item) {
try {
uni.showLoading({title: '删除中...', mask: true})
await deleteChatListReq(item.id)
this.messageList = this.messageList.filter(msg => msg.id !== item.id)
uni.showToast({title: '已删除', icon: 'success'})
if (!this.messageList.length) {
uni.navigateTo({url: '/pages/chat/select'})
}
} catch (err) {
const msg = (err && err.msg) || '删除失败'
uni.showToast({title: msg, icon: 'none'})
} finally {
uni.hideLoading()
}
}
}
}
</script>
<style scoped lang="scss">
.msg-page {
position: relative;
min-height: 100vh;
padding: 60rpx 30rpx 60rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #e4f4ff 0%, #f3edff 50%, #fbf7ff 100%);
}
.background-blur {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.6);
filter: blur(40rpx);
z-index: 0;
}
.msg-header {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40rpx;
}
.header-left {
width: 80rpx;
display: flex;
align-items: center;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #111;
}
.header-right {
width: 140rpx;
display: flex;
justify-content: flex-end;
}
.right-pill {
min-width: 120rpx;
height: 60rpx;
border-radius: 40rpx;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
gap: 14rpx;
box-shadow: 0 8rpx 16rpx rgba(116, 136, 255, 0.18);
}
.pill-icon-gap {
margin-left: 6rpx;
}
.msg-list {
position: relative;
z-index: 1;
}
.msg-item {
display: flex;
align-items: center;
padding: 30rpx;
//border-bottom: 1px solid rgba(226, 232, 240, 0.8);
margin: 20rpx 0;
background: rgba(255, 255, 255, 0.8);
border-radius: 30rpx;
box-shadow: 0 16rpx 30rpx rgba(146, 160, 255, 0.2);
}
.msg-item:last-child {
border-bottom: none;
}
.avatar {
width: 90rpx;
height: 90rpx;
border-radius: 18rpx;
margin-right: 24rpx;
}
.msg-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.msg-name {
font-size: 32rpx;
color: #111;
font-weight: 600;
}
.msg-preview {
font-size: 26rpx;
color: #8f8fb0;
}
.msg-time {
font-size: 24rpx;
color: #a0aec0;
}
</style>
+542
View File
@@ -0,0 +1,542 @@
<template>
<view class="coin-page" :style="{ paddingTop }">
<view class="coin-bg"></view>
<view class="coin-header">
<view class="header-left" @click="goBack">
<u-icon name="arrow-left" size="28" color="#1b1b1b"></u-icon>
</view>
<text class="header-title">金币中心</text>
<view class="header-right"></view>
</view>
<view class="purchase-card">
<view class="card-row between">
<text class="card-title">金币购买</text>
<view class="sign-info" @click="openSignPopup">
<u-icon name="calendar" size="24" color="#f37a5b"></u-icon>
<text class="sign-text">{{ todaySigned ? '今日已签到' : '签到领金币' }}</text>
</view>
</view>
<view class="amount-row">
<text class="amount-label">金币数量</text>
<u-number-box
v-model="coinAmount"
:step="1"
:min="1"
:integer="true"
></u-number-box>
</view>
<text class="tips">最少购买 1 枚金币,步长 1,可自由增减。</text>
<u-button
type="primary"
shape="circle"
:loading="submitting"
:disabled="submitting"
@click="handlePurchase"
>
立即购买
</u-button>
</view>
<view class="record-section">
<view class="record-header">
<text class="record-title">金币记录</text>
<text class="record-count">共 {{ recordList.length }} 条</text>
</view>
<view v-if="loading" class="record-empty">正在加载记录...</view>
<view v-else-if="!recordList.length" class="record-empty">暂无金币记录</view>
<view v-else class="record-list">
<view
v-for="item in recordList"
:key="item.id || item.coinLogId || item.createTime"
class="record-item"
>
<view class="record-left">
<view class="flex flex-col justify-start items-start">
<text class="record-name">{{ item.desc || item.title || '金币变动' }}</text>
<template v-if="item.type === '+' && item.desc === '金币购买'">
<u-tag type="error" v-if="item.status === 0" shape="circle" size="mini" text="未支付"></u-tag>
<u-tag type="success" v-else shape="circle" size="mini" text="已支付"></u-tag>
</template>
</view>
<text class="record-time">
{{ formatTime(item.createTime || item.createdAt || item.updateTime) }}
</text>
</view>
<text
class="record-coin"
:class="{ positive: item.type === '+' }"
>
{{ formatCoinChange(item) }}
</text>
</view>
</view>
</view>
<Login ref="loginPopup" :isHide="true" @done="handleLoginSuccess"/>
<u-popup :show="signPopupVisible" mode="bottom" round="20" @close="signPopupVisible = false">
<view class="sign-popup">
<view class="sign-header">
<text class="sign-title">连续签到</text>
<view class="sign-status" :class="{active: !todaySigned}">
{{ todaySigned ? '今日已签到' : '未签到' }}
</view>
</view>
<view class="sign-list">
<view
class="sign-day"
v-for="day in signCalendar"
:key="day.date"
:class="[{signed: day.signed}, {today: day.isToday}]"
>
<text class="day-text">{{ day.display }}</text>
<text class="status-text">{{ day.signed ? '已签' : '未签' }}</text>
</view>
</view>
<u-button
type="primary"
shape="circle"
:disabled="todaySigned || signing"
:loading="signing"
class="sign-btn"
@click="handleSign"
>
{{ todaySigned ? '今日已签到' : '立即签到' }}
</u-button>
</view>
</u-popup>
</view>
</template>
<script>
import {coinAddReq, coinListReq, signLogAddReq, signLogListReq} from '@/api/coin'
import {dayjs} from '@/uni_modules/iRainna-dayjs/js_sdk/dayjs.min.js'
import {getUserInfo} from '@/util/user'
import Login from '@/components/Login.vue'
export default {
components: {Login},
data() {
return {
coinAmount: 1,
recordList: [],
loading: false,
submitting: false,
paddingTop: '60px',
signPopupVisible: false,
signLogs: [],
todaySigned: false,
signing: false,
signCalendar: []
}
},
onShow() {
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 20
this.paddingTop = `${statusBarHeight + 40}px`
this.ensureLoginAndLoad()
},
methods: {
ensureLoginAndLoad() {
if (!this.ensureLogin()) {
this.recordList = []
return false
}
this.fetchRecords()
this.fetchSignLogs()
return true
},
ensureLogin() {
const {token} = getUserInfo()
if (!token) {
this.$nextTick(() => {
if (this.$refs.loginPopup) this.$refs.loginPopup.open()
})
return false
}
return true
},
async fetchRecords() {
this.loading = true
try {
const {data} = await coinListReq()
if (Array.isArray(data)) {
this.recordList = data
} else if (Array.isArray(data?.records)) {
this.recordList = data.records
} else {
this.recordList = []
}
} catch (error) {
uni.showToast({title: '加载失败,请稍后重试', icon: 'none'})
} finally {
this.loading = false
}
},
async handlePurchase() {
if (this.submitting) return
if (!this.ensureLogin()) return
if (!this.coinAmount || this.coinAmount < 1) {
uni.showToast({title: '最少购买 1 枚金币', icon: 'none'})
this.coinAmount = 1
return
}
this.submitting = true
try {
const {data} = await coinAddReq({
num: this.coinAmount,
type: '+',
desc: '金币购买'
})
const _this = this
uni.requestPayment({
provider: data.provider,
timeStamp: data.timeStamp, // 时间戳
nonceStr: data.nonceStr, // 随机字符串
package: data.package,
signType: data.signType, // 签名算法
paySign: data.paySign, // 签名
success: function (res) {
uni.showToast({
title: '支付成功',
icon: 'success'
});
_this.fetchRecords()
},
fail: function (err) {
console.log('支付失败', err);
}
})
} catch (error) {
const msg = error?.msg || error?.message || '购买失败,请稍后再试'
uni.showToast({title: msg, icon: 'none'})
} finally {
this.submitting = false
}
},
formatTime(value) {
if (!value) return '--'
const time = dayjs(value)
if (typeof time.isValid === 'function' && !time.isValid()) return value
return time.format('YYYY-MM-DD HH:mm')
},
getCoinValue(item) {
const fields = ['coin', 'coinNum', 'number', 'amount', 'value', 'change']
for (const field of fields) {
if (item[field] !== undefined && item[field] !== null) {
const num = Number(item[field])
if (!Number.isNaN(num)) return num
}
}
return 0
},
formatCoinChange(item) {
return `${item.type}${item.num}`
},
handleLoginSuccess() {
this.fetchRecords()
this.fetchSignLogs()
},
async fetchSignLogs() {
if (!this.ensureLogin()) return
try {
const {data} = await signLogListReq()
const list = Array.isArray(data) ? data : Array.isArray(data?.records) ? data.records : []
this.signLogs = list
this.todaySigned = list.some(item => dayjs(item.date).isSame(dayjs(), 'day'))
this.buildSignCalendar()
} catch (error) {
this.signLogs = []
this.todaySigned = false
}
},
buildSignCalendar() {
const today = dayjs()
const calendar = []
for (let i = 6; i >= 0; i--) {
const date = today.subtract(i, 'day')
const formatted = date.format('YYYY-MM-DD')
const signed = this.signLogs.some(item => dayjs(item.date).isSame(date, 'day'))
calendar.push({
date: formatted,
signed,
isToday: date.isSame(today, 'day'),
display: date.format('MM/DD')
})
}
this.signCalendar = calendar
},
openSignPopup() {
if (!this.ensureLogin()) return
this.signPopupVisible = true
if (!this.signCalendar.length) {
this.buildSignCalendar()
}
},
async handleSign() {
if (this.signing || this.todaySigned) return
if (!this.ensureLogin()) return
this.signing = true
const today = dayjs().format('YYYY-MM-DD')
try {
await signLogAddReq({date: today})
uni.showToast({title: '签到成功', icon: 'success'})
this.todaySigned = true
this.signLogs.push({date: today})
this.buildSignCalendar()
setTimeout(() => {
this.signPopupVisible = false
this.fetchRecords()
}, 1000)
} catch (error) {
const msg = error?.msg || error?.message || '签到失败'
uni.showToast({title: msg, icon: 'none'})
} finally {
this.signing = false
}
},
goBack() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.switchTab({url: '/pages/user/index'})
}
}
}
}
</script>
<style scoped lang="scss">
.coin-page {
position: relative;
min-height: 100vh;
padding: 40rpx 30rpx 80rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #e1ecff 0%, #f5f0ff 45%, #fef8ff 100%);
}
.coin-bg {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.7);
filter: blur(40rpx);
z-index: 0;
}
.coin-header {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.header-left,
.header-right {
width: 80rpx;
display: flex;
align-items: center;
justify-content: flex-start;
}
.header-right {
justify-content: flex-end;
}
.header-title {
font-size: 34rpx;
font-weight: 600;
color: #1b1b1b;
}
.purchase-card {
position: relative;
z-index: 2;
background: rgba(255, 255, 255, 0.94);
border-radius: 32rpx;
padding: 40rpx 32rpx;
box-shadow: 0 20rpx 50rpx rgba(117, 148, 255, 0.25);
margin-bottom: 30rpx;
}
.card-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
justify-content: center;
}
.card-row.between {
justify-content: space-between;
}
.sign-info {
display: flex;
align-items: center;
gap: 10rpx;
padding: 10rpx 20rpx;
border-radius: 999rpx;
background: rgba(243, 122, 91, 0.12);
color: #f37a5b;
font-size: 24rpx;
}
.sign-popup {
padding: 40rpx 30rpx 60rpx;
}
.sign-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.sign-title {
font-size: 30rpx;
font-weight: 600;
color: #1b1b1b;
}
.sign-status {
padding: 12rpx 30rpx;
border-radius: 999rpx;
font-size: 24rpx;
background: #f5f5f5;
color: #7a7a7a;
}
.sign-status.active {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.sign-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
margin-bottom: 40rpx;
}
.sign-day {
background: #f7f8ff;
border-radius: 18rpx;
padding: 20rpx 10rpx;
text-align: center;
font-size: 24rpx;
color: #5e5f7a;
}
.sign-day.today {
border: 2rpx solid #f37a5b;
}
.sign-day.signed {
background: rgba(52, 199, 89, 0.15);
color: #1f7338;
}
.sign-btn {
width: 100%;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #1b1b1b;
}
.amount-row {
margin: 30rpx 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.amount-label {
font-size: 28rpx;
color: #5b5f7b;
}
.tips {
font-size: 24rpx;
color: #8f90aa;
margin-bottom: 30rpx;
}
.record-section {
position: relative;
z-index: 2;
background: rgba(255, 255, 255, 0.9);
border-radius: 32rpx;
padding: 30rpx;
box-shadow: 0 20rpx 40rpx rgba(150, 164, 255, 0.2);
}
.record-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.record-title {
font-size: 30rpx;
font-weight: 600;
color: #1f2251;
}
.record-count {
font-size: 24rpx;
color: #a0a2c2;
}
.record-empty {
text-align: center;
color: #a0a2c2;
font-size: 26rpx;
padding: 40rpx 0;
}
.record-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 26rpx 0;
border-bottom: 1px solid rgba(226, 232, 255, 0.7);
}
.record-item:last-child {
border-bottom: none;
}
.record-left {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.record-name {
font-size: 28rpx;
color: #1f2251;
}
.record-time {
font-size: 24rpx;
color: #a0a2c2;
}
.record-coin {
font-size: 30rpx;
font-weight: 600;
color: #f26c5b;
}
.record-coin.positive {
color: #00c78c;
}
</style>
+136
View File
@@ -0,0 +1,136 @@
<template>
<view class="collect-page">
<view v-if="loading" class="state-wrap">
<u-loading-icon mode="circle"></u-loading-icon>
</view>
<view v-else-if="articles.length" class="list-wrapper">
<view class="article-card" v-for="item in articles" :key="item.id" @click="goToDetail(item)">
<image class="cover" :src="item.article.image" mode="aspectFill"></image>
<view class="content">
<text class="title">{{ item.article.title }}</text>
<text class="desc">{{ item.article.source }}</text>
<!-- <view class="meta">-->
<!-- <text class="time">{{ item.time }}</text>-->
<!-- </view>-->
</view>
</view>
</view>
<view v-else class="state-wrap">
<u-empty mode="favor" text="暂无收藏"></u-empty>
</view>
</view>
</template>
<script>
import {listArticleCollectReq} from "@/api/common";
export default {
data() {
return {
articles: [],
loading: false,
defaultCover: '/static/icon/article_1.png'
}
},
onShow() {
this.fetchCollects()
},
onPullDownRefresh() {
this.fetchCollects()
},
methods: {
async fetchCollects() {
this.loading = true
try {
const {data} = await listArticleCollectReq()
const list = Array.isArray(data) ? data : []
this.articles = list
} catch (e) {
uni.showToast({title: '加载失败,请稍后重试', icon: 'none'})
} finally {
this.loading = false
uni.stopPullDownRefresh()
}
},
goToDetail(item) {
if (!item.articleId) {
uni.showToast({title: '文章信息缺失', icon: 'none'})
return
}
uni.navigateTo({
url: `/pages/index/detail?id=${item.articleId}`
})
}
}
}
</script>
<style lang="scss" scoped>
.collect-page {
min-height: 100vh;
padding: 30rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #eef1ff 0%, #fdfdff 45%, #ffffff 100%);
}
.state-wrap {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
}
.list-wrapper {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.article-card {
display: flex;
background: #ffffff;
border-radius: 30rpx;
padding: 24rpx;
box-shadow: 0 16rpx 40rpx rgba(79, 92, 190, 0.12);
}
.cover {
width: 200rpx;
height: 160rpx;
border-radius: 24rpx;
margin-right: 24rpx;
background: #f0f2ff;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
}
.title {
font-size: 30rpx;
font-weight: 600;
color: #1f2140;
line-height: 1.4;
}
.desc {
margin-top: 10rpx;
font-size: 24rpx;
color: #7377a5;
line-height: 1.4;
max-height: 70rpx;
overflow: hidden;
}
.meta {
margin-top: auto;
font-size: 22rpx;
color: #a0a4c3;
display: flex;
justify-content: space-between;
}
</style>
+259
View File
@@ -0,0 +1,259 @@
<template>
<view class="feedback-page">
<view class="top-header" :style="{ paddingTop: statusBarPadding }">
<u-icon name="arrow-left" size="32" color="#1e244f" @click="goBack"></u-icon>
<text class="header-title">意见反馈</text>
<view style="width:32rpx;height:32rpx;"></view>
</view>
<view class="feedback-card">
<view class="card-header">
<text class="card-title">意见反馈</text>
<text class="card-sub">多选</text>
</view>
<view class="section" v-for="group in feedbackOptions" :key="group.title">
<view class="section-title">{{ group.title }}</view>
<view class="option" v-for="option in group.options" :key="option.value" @tap="toggleOption(option)">
<view class="check" :class="{ active: option.selected }">
<view class="dot" v-if="option.selected"></view>
</view>
<text class="option-text">{{ option.label }}</text>
</view>
</view>
</view>
<view class="say-card">
<view class="say-header">
<text class="say-title">我还想说</text>
<text class="say-optional">选填</text>
</view>
<textarea
class="say-textarea"
placeholder="有了您的宝贵意见,我们才能变得更好哦~"
placeholder-style="color:#b4bad0"
v-model="extraMessage"
/>
</view>
<button class="submit-btn" @click="submitFeedback">提交反馈</button>
</view>
</template>
<script>
import {addFeedbackReq} from "@/api/common";
export default {
data() {
return {
statusBarPadding: '40rpx',
extraMessage: '',
feedbackOptions: [
{
title: '咨询体验',
options: [
{ label: '咨询过程不够流畅', value: 'flow' },
{ label: '咨询师答非所问', value: 'answer' },
{ label: '咨询结果不满意', value: 'result' },
{ label: '咨询耗费时间', value: 'time' },
{ label: '我对咨询体验过程无意见', value: 'noExperience' }
]
},
{
title: '功能体验',
options: [
{ label: '产品界面不够美观', value: 'ui' },
{ label: '不喜欢输入方式', value: 'input' },
{ label: '我对功能无意见', value: 'noFunction' }
]
}
]
}
},
onLoad() {
const info = uni.getSystemInfoSync()
this.statusBarPadding = `${(info.statusBarHeight || 20) + 10}px`
},
methods: {
goBack() {
uni.navigateBack().catch(() => {
uni.switchTab({ url: '/pages/user/index' })
})
},
toggleOption(option) {
this.$set(option, 'selected', !option.selected)
},
async submitFeedback() {
const advisory = []
const func = []
this.feedbackOptions.forEach(group => {
group.options.forEach(opt => {
if (opt.selected) {
if (group.title === '咨询体验') advisory.push(opt.label)
else if (group.title === '功能体验') func.push(opt.label)
}
})
})
if (!advisory.length && !func.length && !this.extraMessage.trim()) {
uni.showToast({ title: '请选择或填写反馈', icon: 'none' })
return
}
try {
await addFeedbackReq({
advisoryExp: advisory.join('、'),
funcExp: func.join('、'),
comments: this.extraMessage.trim()
})
uni.showToast({ title: '感谢反馈', icon: 'success' })
this.feedbackOptions.forEach(group => {
group.options.forEach(opt => (opt.selected = false))
})
this.extraMessage = ''
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
const msg = error?.msg || error?.message || '提交失败,请稍后再试'
uni.showToast({title: msg, icon: 'none'})
}
}
}
}
</script>
<style lang="scss" scoped>
.feedback-page {
min-height: 100vh;
padding: 40rpx 30rpx 120rpx;
background: linear-gradient(180deg, #e8f2ff 0%, #f4f5ff 100%);
box-sizing: border-box;
}
.top-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 30rpx;
}
.header-title {
font-size: 32rpx;
color: #1e244f;
font-weight: 600;
}
.feedback-card {
background: rgba(255, 255, 255, 0.92);
border-radius: 30rpx;
padding: 30rpx;
box-shadow: 0 20rpx 50rpx rgba(149, 170, 255, 0.2);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.card-title {
font-size: 34rpx;
font-weight: 600;
color: #3b437a;
}
.card-sub {
font-size: 26rpx;
color: #b3b8d3;
}
.section {
margin-top: 20rpx;
}
.section-title {
font-size: 28rpx;
color: #9ea4c5;
margin-bottom: 12rpx;
}
.option {
display: flex;
align-items: center;
padding: 18rpx 0;
}
.check {
width: 32rpx;
height: 32rpx;
border-radius: 16rpx;
border: 2rpx solid #ccd0e6;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.check.active {
border-color: #8d9cff;
background-color: #eef0ff;
}
.dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #7c8cff;
}
.option-text {
font-size: 28rpx;
color: #5a5f85;
}
.say-card {
margin-top: 30rpx;
padding: 30rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 30rpx;
box-shadow: 0 20rpx 50rpx rgba(149, 170, 255, 0.15);
}
.say-header {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.say-title {
font-size: 32rpx;
color: #3b437a;
}
.say-optional {
font-size: 26rpx;
color: #b3b8d3;
}
.say-textarea {
width: 100%;
min-height: 200rpx;
background: #f5f6fb;
border-radius: 20rpx;
padding: 20rpx;
box-sizing: border-box;
font-size: 28rpx;
color: #4c5277;
}
.submit-btn {
margin-top: 40rpx;
height: 96rpx;
line-height: 96rpx;
border-radius: 48rpx;
background: linear-gradient(120deg, #7c8cff, #9da6ff);
color: #fff;
font-size: 30rpx;
letter-spacing: 4rpx;
}
</style>
+490
View File
@@ -0,0 +1,490 @@
<template>
<view class="profile-page">
<image class="profile-bg" src="/static/icon/轮播图 Copy kTtd4FG@1x.png" mode="aspectFill"></image>
<view class="profile-header">
<text class="header-title">我的</text>
</view>
<view class="profile-card">
<view class="profile-info" @click="$jump('/pages/user/profile')">
<image class="avatar" :src="user.avatar" mode="aspectFill"></image>
<view class="info-text">
<view class="user-name">{{ user.nickname }}</view>
<!-- <view class="badge">-->
<!-- <u-icon name="camera-fill" size="20" color="#ffc44d"></u-icon>-->
<!-- <text>小X会员</text>-->
<!-- </view>-->
</view>
</view>
<!-- <text class="profile-desc">为您提供更精准的推荐</text>-->
<view class="profile-stats">
<view class="stat" v-for="stat in stats" :key="stat.label" @click="handleMenu(stat)">
<view class="stat-value">{{ stat.value }}</view>
<view class="stat-label">{{ stat.label }}</view>
</view>
</view>
</view>
<view class="vip-card" v-if="!authing">
<view class="flex justify-start items-center">
<view class="vip-amount">剩余金币:
<text class="highlight">{{ user.coin }}</text>
</view>
<view class="vip-amount">,赠送金币:
<text class="highlight">{{ user.sendCoin }}</text>
</view>
</view>
<view class="vip-btn" @click="openRecharge">
<text>立即续费</text>
</view>
</view>
<view class="menu-section">
<view
class="menu-item"
v-for="item in menuList"
:key="item.label"
@click="handleMenu(item)"
>
<view class="menu-left">
<u-icon :name="item.icon" size="28" color="#8c8fad"></u-icon>
<text class="menu-text">{{ item.label }}</text>
</view>
<u-icon name="arrow-right" color="#a3a7c2" size="18"></u-icon>
<view class="menu-overlay" @click.stop="handleMenu(item)"></view>
</view>
</view>
<view class="logout-card" @click="logout" v-if="user">
<text class="logout-text">退出</text>
<u-icon name="arrow-right" color="#5e5f79" size="18"></u-icon>
</view>
<u-popup :show="showRecharge" mode="center" round="20" @close="showRecharge = false">
<view class="recharge-modal">
<view class="balance-card">
<text class="balance-label">账户余额</text>
<text class="balance-value">¥{{ user.balance.toFixed(2) }}</text>
</view>
<view class="modal-block">
<text class="modal-title">充值金额</text>
<input
class="amount-input"
type="digit"
v-model="rechargeAmount"
placeholder="可输入自定义充值金额"
/>
</view>
<view class="modal-block">
<text class="modal-title">支付方式</text>
<view class="pay-option" @click="paymentMethod = 'wechat'">
<view class="checkbox" :class="{ active: paymentMethod === 'wechat' }"></view>
<text class="pay-text">微信支付</text>
</view>
<text class="agreement">购买协议</text>
</view>
<button class="charge-btn" @click="confirmRecharge">立即充值</button>
<view class="charge-desc">
<text>充值说明</text>
<text>1. 账户充值仅限微信在线方式支付,充值金额实时到账;</text>
<text>2. 账户充值套餐赠送的金额即时到账;</text>
<text>3. 账户余额有效期:自充值日起至用完即止;</text>
<text>4. 若有其它疑问,可拨打客服电话400-000-1234。</text>
</view>
</view>
</u-popup>
<u-popup :show="showPopup" close-on-click-overlay round="10" mode="bottom" :safe-area-inset-bottom="false">
<view class="p-10 flex flex-col justify-center items-center">
<view class="my-10 flex justify-center items-center">
<u-image :src="qr" height="600rpx" width="509rpx" show-menu-by-longpress/>
</view>
<u-button shape="circle" type="primary" @click="closePopup">关闭</u-button>
</view>
</u-popup>
</view>
</template>
<script>
import {userInfoReq} from "@/api/user";
import {clearUserInfo, getUserInfo} from "@/util/user";
import {dictDataReq} from "@/api/common";
export default {
data() {
return {
user: {
nickname: '',
avatar: '',
coin: 0,
sendCoin: 0,
},
stats: [
{label: '收藏', value: 0, path: '/pages/user/collect'},
// {label: '赞过', value: 20},
{label: '评论过', value: 0}
],
menuList: [
{label: '客服入口', icon: 'server-fill', path: ''},
{label: '签到中心', icon: 'calendar', path: '/pages/user/coin'},
// {label: '金币中心', icon: 'red-packet-fill', path: '/pages/user/coin'},
{label: '意见反馈', icon: 'edit-pen', path: '/pages/user/feedback'},
{label: '设置(您还可以选择其他人物进行聊天)', icon: 'setting', path: '/pages/user/setting?noChat=true'},
// {label: '新的聊天', icon: 'chat', path: '/pages/chat/select'}
],
showRecharge: false,
rechargeAmount: '',
paymentMethod: 'wechat',
showPopup: false,
qr: '/static/service.png',
authing: true
}
},
methods: {
backHome() {
uni.switchTab({url: '/pages/index/index'})
},
async getDictList() {
const {data} = await dictDataReq({dictId: '171'})
this.authing = parseInt(data[0].dictDataCode) === 1
},
openRecharge() {
// this.showRecharge = true
uni.navigateTo({url: '/pages/user/coin'})
},
handleMenu(item) {
console.log(item)
if (item.path) {
uni.navigateTo({url: item.path})
} else {
console.log(item.label)
if (item.label === '客服入口')
this.showPopup = true
}
},
logout() {
clearUserInfo()
this.user = null
uni.showToast({title: '已退出', icon: 'success'})
this.$refs.Login.open()
},
confirmRecharge() {
if (!this.rechargeAmount) {
uni.showToast({title: '请输入金额', icon: 'none'})
return
}
uni.showToast({title: '充值成功', icon: 'success'})
this.showRecharge = false
this.rechargeAmount = ''
},
async getUserData(){
const {data} = await userInfoReq()
this.user = data
},
closePopup(){
this.showPopup = false
}
},
onShow() {
this.getDictList()
if (getUserInfo().token)
this.getUserData()
},
}
</script>
<style lang="scss" scoped>
.profile-page {
position: relative;
min-height: 100vh;
padding: 120rpx 30rpx 40rpx;
background: linear-gradient(180deg, #c6d5ff 0%, #eef0ff 45%, #fcfbff 100%);
box-sizing: border-box;
}
.profile-bg {
position: absolute;
inset: 0;
width: 100%;
height: 60%;
z-index: 0;
opacity: 0.35;
filter: blur(6rpx);
}
.profile-header {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.header-title {
font-size: 34rpx;
color: #fff;
font-weight: 600;
}
.profile-card {
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.18);
border-radius: 34rpx;
padding: 30rpx;
color: #353750;
text-align: center;
box-shadow: inset 0 0 0 1rpx rgba(255, 255, 255, 0.25);
}
.profile-info {
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
}
.avatar {
width: 140rpx;
height: 140rpx;
border-radius: 70rpx;
border: 4rpx solid rgba(255, 255, 255, 0.9);
}
.info-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12rpx;
}
.user-name {
font-size: 34rpx;
font-weight: 600;
}
.badge {
display: inline-flex;
align-items: center;
gap: 8rpx;
background: rgba(255, 255, 255, 0.25);
border-radius: 30rpx;
padding: 6rpx 20rpx;
font-size: 24rpx;
}
.profile-desc {
margin-top: 20rpx;
color: rgba(255, 255, 255, 0.85);
font-size: 26rpx;
}
.profile-stats {
display: flex;
justify-content: space-around;
margin-top: 30rpx;
}
.stat-value {
font-size: 34rpx;
font-weight: 600;
}
.stat-label {
font-size: 24rpx;
margin-top: 8rpx;
opacity: 0.9;
}
.vip-card {
position: relative;
margin-top: 30rpx;
background: rgba(255, 255, 255, 0.8);
border-radius: 24rpx;
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 20rpx 40rpx rgba(86, 107, 255, 0.15);
}
.vip-title {
font-size: 42rpx;
color: #f5a623;
font-weight: 600;
}
.vip-amount {
margin-top: 10rpx;
color: #606585;
font-size: 26rpx;
}
.highlight {
color: #ff4d4f;
font-weight: 600;
}
.vip-btn {
width: 180rpx;
height: 72rpx;
border-radius: 40rpx;
background: linear-gradient(120deg, #0f1f59, #1e31a6);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 500;
}
.menu-section {
margin-top: 30rpx;
background: rgba(255, 255, 255, 0.9);
border-radius: 24rpx;
box-shadow: 0 20rpx 40rpx rgba(118, 135, 255, 0.14);
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid rgba(215, 217, 245, 0.8);
position: relative;
}
.menu-overlay {
position: absolute;
inset: 0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.menu-text {
font-size: 28rpx;
color: #353750;
}
.logout-card {
margin-top: 30rpx;
background: rgba(233, 234, 255, 0.9);
border-radius: 24rpx;
padding: 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #353750;
}
.logout-text {
font-size: 28rpx;
}
.recharge-modal {
width: 600rpx;
background: #fff;
border-radius: 30rpx;
padding: 30rpx;
box-sizing: border-box;
}
.balance-card {
background: linear-gradient(120deg, #a979ff, #7160ff);
border-radius: 26rpx;
padding: 30rpx;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.balance-label {
font-size: 30rpx;
}
.balance-value {
font-size: 40rpx;
font-weight: 600;
}
.modal-block {
margin-bottom: 20rpx;
}
.modal-title {
font-size: 28rpx;
color: #4d4f6c;
margin-bottom: 10rpx;
display: block;
}
.amount-input {
width: 100%;
height: 80rpx;
border-radius: 20rpx;
background: #f4f5f9;
padding: 0 24rpx;
box-sizing: border-box;
font-size: 28rpx;
}
.pay-option {
display: flex;
align-items: center;
gap: 12rpx;
margin-top: 10rpx;
}
.checkbox {
width: 32rpx;
height: 32rpx;
border-radius: 10rpx;
border: 2rpx solid #b8bbd3;
}
.checkbox.active {
background: #32c26a;
border-color: #32c26a;
}
.pay-text {
font-size: 28rpx;
color: #3a3f62;
}
.agreement {
margin-top: 12rpx;
color: #3752b4;
font-size: 26rpx;
}
.charge-btn {
width: 100%;
height: 90rpx;
border-radius: 45rpx;
background: linear-gradient(120deg, #a979ff, #7160ff);
color: #fff;
font-size: 30rpx;
margin: 20rpx 0;
}
.charge-desc {
display: flex;
flex-direction: column;
gap: 6rpx;
font-size: 24rpx;
color: #8a8fae;
}
</style>
+119
View File
@@ -0,0 +1,119 @@
<template>
<view>
<view class="flex items-center justify-center" v-if="userInfo" style="padding: 130rpx 0">
<button open-type="chooseAvatar" @chooseavatar="chooseAvatar" class="btn">
<u-avatar :src="`${userInfo.avatar ? userInfo.avatar : ''}`" size="80"/>
</button>
</view>
<view class="p-20">
<u-cell-group :border="false">
<u-cell title="昵称">
<input type="nickname" class="text-right" v-model="userInfo.nickname"
slot="right-icon" @change="changeUsername"/>
</u-cell>
</u-cell-group>
</view>
<view class="p-20">
<u-button @click="save" type="primary" shape="circle">保存修改</u-button>
</view>
<u-action-sheet :actions="genderList" :show="showActionSheet" :closeOnClickAction="true"
round="10" cancelText="取消" @select="selectGender"
@close="showActionSheet = false"/>
</view>
</template>
<script>
import {userInfoReq, userInfoUpdateReq} from "@/api/user";
import {setUserInfo} from "@/util/user";
import {uploadImg} from "@/util";
import {MAIN_API} from "@/config";
export default {
name: "profile",
data() {
return {
cardTypeList: ['充值卡', '月卡', '次卡'],
uri: MAIN_API,
userInfo: {
avatar: null,
nickname: null,
phone: null,
gender: null,
},
genderList: [
{
name: '未知',
value: 0
},
{
name: '男',
value: 1
},
{
name: '女',
value: 2
}
],
showModal: false,
showActionSheet: false,
}
},
methods: {
async getUserInfo() {
const {data} = await userInfoReq()
this.userInfo = data
},
async chooseAvatar({detail}) {
const url = detail.avatarUrl
const {path} = await uploadImg(url)
console.log(path)
this.userInfo.avatar = path
},
selectGender(item) {
this.userInfo.gender = item.value
this.showActionSheet = false
},
changeUsername({detail}) {
const {value} = detail
this.userInfo.nickname = value
},
async save() {
uni.showLoading({
title: '保存中'
})
await userInfoUpdateReq({
avatar: this.userInfo.avatar,
nickname: this.userInfo.nickname,
})
uni.hideLoading()
uni.showToast({
icon: 'success',
title: '已保存',
complete: () => {
setTimeout(() => {
setUserInfo({
header: this.userInfo.avatar,
nickname: this.userInfo.nickname,
})
uni.navigateBack()
}, 1500)
}
})
}
},
async onShow() {
await this.getUserInfo()
}
}
</script>
<style scoped lang="scss">
.btn {
background: none;
}
.btn::after {
border: none;
}
</style>
+679
View File
@@ -0,0 +1,679 @@
<template>
<view class="setting-page">
<view class="top-bar" :style="{ paddingTop: statusBarPadding }">
<u-icon name="arrow-left" size="32" color="#fff" @click="goBack"></u-icon>
</view>
<view class="form-section">
<text class="form-title">请完善您的信息</text>
<view class="form-item" v-for="item in formItems" :key="item.key">
<template v-if="item.key !== 'time' || (item.key === 'time' && !authing) ">
<text class="label">{{ item.label }}</text>
<view class="input" v-if="item.type === 'input'">
<input class="w-100p" :placeholder="item.placeholder" v-model="form[item.key]"/>
</view>
<view v-else class="picker" @click="openPicker(item)">
<text class="picker-value">{{ displayValue(item.key, item.placeholder) }}</text>
<u-icon class="picker-arrow" name="arrow-down" size="20" color="#777"></u-icon>
</view>
</template>
</view>
</view>
<u-picker
:show="pickerVisible.gender"
:columns="genderColumns"
@confirm="onGenderConfirm"
@cancel="closePicker('gender')"
@close="closePicker('gender')"
></u-picker>
<u-datetime-picker
ref="birthRef"
mode="date"
:show="pickerVisible.birth"
v-model="birthPickerValue"
:minDate="minDate"
:maxDate="maxDate"
@change="onBirthChange"
@confirm="onBirthConfirm"
@cancel="closePicker('birth')"
@close="closePicker('birth')"
></u-datetime-picker>
<u-picker
:show="pickerVisible.time"
:columns="timeColumns"
@confirm="onTimeConfirm"
@cancel="closePicker('time')"
@close="closePicker('time')"
></u-picker>
<u-picker
ref="birthplaceRef"
:show="pickerVisible.birthplace"
:columns="areaColumns"
:defaultIndex="areaDefaultIndex"
@change="onBirthplaceChange"
@confirm="onBirthplaceConfirm"
@cancel="closePicker('birthplace')"
@close="closePicker('birthplace')"
></u-picker>
<view class="tips">我们会根据你的选择,提供更精准的推荐</view>
<view class="tips">您还可以选择其他人物进行聊天</view>
<view class="mentor-section">
<view class="mentor-card" v-for="mentor in mentors" :key="mentor.id"
:class="{ active: currentMentor === mentor.roleName }" @click="selectMentor(mentor)">
<view class="mentor-name">{{ mentor.roleName }}</view>
<image class="mentor-avatar" :src="mentor.avatar" mode="aspectFit"></image>
</view>
</view>
<view class="intro-card" v-if="currentMentorItem">
<text class="intro-text">
简介:{{ currentMentorItem.roleDesc }}
</text>
</view>
<!-- <u-gap height="200rpx"/>-->
<!-- <view class="chat-input" :style="{bottom: `${inputBottom}px`}">-->
<!-- <view class="m-40 flex justify-between items-center w-100p">-->
<!-- <view class="input-wrap">-->
<!-- <textarea-->
<!-- class="textarea"-->
<!-- v-model="content"-->
<!-- placeholder="迷茫困惑的话和我聊聊吧"-->
<!-- maxlength="-1"-->
<!-- placeholder-style="text-align:center;"-->
<!-- auto-height-->
<!-- fixed-->
<!-- :show-confirm-bar="false"-->
<!-- />-->
<!-- </view>-->
<!-- <button class="send-btn" @click="save">发送</button>-->
<!-- </view>-->
<!-- </view>-->
<button class="primary-btn" @click="save">保存</button>
<!-- <text class="skip" @click="skip" v-if="!noChat">跳过设置</text>-->
<Login ref="Login" @done="getUserData"/>
</view>
</template>
<script>
import {addChatListReq, chatListReq, listChatRoleReq} from "@/api/chat";
import {updateUserReq, userInfoReq} from "@/api/user";
import {getUserInfo} from "@/util/user";
import {dayjs} from '@/uni_modules/iRainna-dayjs/js_sdk/dayjs.min.js'
import Login from "@/components/Login.vue";
import {dictDataReq, sysAreaListReq} from "@/api/common";
const TIME_OPTIONS = [
'子时23~1点',
'丑时1~3点',
'寅时3~5点',
'卯时5~7点',
'辰时7~9点',
'巳时9~11点',
'午时11~13点',
'未时13~15点',
'申时15~17点',
'酉时17~19点',
'戌时19~21点',
'亥时21~23点'
]
export default {
components: {Login},
data() {
return {
statusBarPadding: '40rpx',
form: {
name: '',
gender: '',
birth: '',
time: '子时23~1点',
birthplace: '',
},
pickerVisible: {
gender: false,
birth: false,
time: false,
birthplace: false
},
genderColumns: [['男', '女']],
timeColumns: [TIME_OPTIONS],
birthPickerValue: Date.now(),
chatId: null,
roleName: null,
roleDesc: null,
avatar: null,
word: null,
formItems: [
{key: 'name', label: '*姓名', type: 'input', placeholder: '请输入'},
{key: 'gender', label: '*性别', type: 'picker', placeholder: '请选择', mode: 'selector', range: ['男', '女']},
{key: 'birth', label: '*出生年月日(阳历)', type: 'picker', placeholder: '请选择', mode: 'date', range: []},
{key: 'time', label: '时辰', type: 'picker', placeholder: '请选择', mode: 'selector', range: TIME_OPTIONS},
{key: 'birthplace', label: '出生地', type: 'picker', placeholder: '请选择', mode: 'selector'}
],
mentors: [],
currentMentor: '五师妹',
currentMentorItem: null,
minDate: dayjs('1900-01-01').valueOf(),
maxDate: dayjs().valueOf(),
noChat: false,
messageList: [],
defaultIndex: [],
areaColumns: [[], []],
areaDefaultIndex: [0, 0],
provinceList: [],
cityList: [],
selectedProvinceIndex: 0,
selectedCityIndex: 0,
inputBottom: 0,
content: '',
authing: true
}
},
onLoad(option) {
this.getDictList()
const info = uni.getSystemInfoSync()
this.inputBottom = uni.getSystemInfoSync().safeAreaInsets.bottom
this.statusBarPadding = `${(info.statusBarHeight || 20) + 10}px`
if (option.chatId) this.chatId = option.chatId
if (option.roleName) this.roleName = option.roleName
if (option.roleDesc) this.roleDesc = option.roleDesc
if (option.avatar) this.avatar = option.avatar
if (option.word) {
this.word = option.word
this.content = option.word
}
if (option.noChat) this.noChat = true
if (!getUserInfo().token) this.$refs.Login.open()
else {
this.getUserData()
}
},
methods: {
async getMessageList() {
const res = await chatListReq()
this.messageList = res.data
this.getRoleList()
},
async getDictList() {
const {data} = await dictDataReq({dictId: '171'})
this.authing = parseInt(data[0].dictDataCode) === 1
},
async getRoleList() {
const {data} = await listChatRoleReq()
const roleIdList = this.messageList.map(item => item.roleId)
// this.mentors = data.filter(item => {
// return !roleIdList.includes(item.id)
// })
this.mentors = data.map(item => {
const hasChat = roleIdList.includes(item.id)
let chatId = null
if (hasChat) {
chatId = this.messageList.find(msg => msg.roleId === item.id).id
}
return {
...item,
hasChat,
chatId
}
})
console.log(this.mentors)
this.currentMentor = ''
this.currentMentorItem = null
if (this.mentors.length) {
this.currentMentor = this.mentors[0].roleName
this.currentMentorItem = this.mentors[0]
}
// this.currentMentor = data[0].roleName
// this.currentMentorItem = data[0]
},
goBack() {
uni.navigateBack().catch(() => {
uni.switchTab({url: '/pages/user/index'})
})
},
openPicker(item) {
if (item.key === 'gender') {
this.pickerVisible.gender = true
return
}
if (item.key === 'birth') {
this.syncBirthPickerValue()
this.pickerVisible.birth = true
return
}
if (item.key === 'time') {
this.pickerVisible.time = true
return
}
if (item.key === 'birthplace') {
this.openAreaPicker()
}
},
closePicker(type) {
if (Object.prototype.hasOwnProperty.call(this.pickerVisible, type)) {
this.pickerVisible[type] = false
}
},
async openAreaPicker() {
await this.loadProvinceList()
if (!this.provinceList.length) {
uni.showToast({title: '暂无地区数据', icon: 'none'})
return
}
this.syncProvinceFromForm()
await this.loadCityList(this.selectedProvinceIndex)
this.syncCityFromForm()
this.updateAreaColumns()
this.areaDefaultIndex = [this.selectedProvinceIndex, this.selectedCityIndex]
this.pickerVisible.birthplace = true
},
parseBirthplace() {
const birthplace = (this.form.birthplace || '').trim()
if (!birthplace) return {provinceName: '', cityName: ''}
const parts = birthplace.split(/\s+/).filter(Boolean)
return {
provinceName: parts[0] || '',
cityName: parts[1] || ''
}
},
syncProvinceFromForm() {
const {provinceName} = this.parseBirthplace()
if (!provinceName) return
const idx = this.provinceList.findIndex(item => this.getAreaName(item) === provinceName)
if (idx !== -1) {
this.selectedProvinceIndex = idx
}
},
syncCityFromForm() {
const {cityName} = this.parseBirthplace()
if (!cityName || !this.cityList.length) return
const idx = this.cityList.findIndex(item => this.getAreaName(item) === cityName)
if (idx !== -1) {
this.selectedCityIndex = idx
}
},
async loadProvinceList() {
if (this.provinceList.length) {
this.updateAreaColumns(-1)
return
}
try {
const {data} = await sysAreaListReq({fid: 0})
this.provinceList = data || []
this.updateAreaColumns(0)
} catch (e) {
uni.showToast({title: '获取省份失败', icon: 'none'})
}
},
async loadCityList(provinceIndex = 0) {
const province = this.provinceList[provinceIndex]
if (!province) {
this.cityList = []
this.updateAreaColumns(1)
return
}
const fid = province.id || province.fid || province.value
if (fid === undefined || fid === null) {
this.cityList = []
this.updateAreaColumns(2)
return
}
try {
const {data} = await sysAreaListReq({fid})
this.cityList = data || []
this.updateAreaColumns(3)
} catch (e) {
this.cityList = []
this.updateAreaColumns(4)
uni.showToast({title: '获取城市失败', icon: 'none'})
}
},
updateAreaColumns(flag = null) {
console.log('this.selectedCityIndex', this.selectedCityIndex, flag)
if (this.selectedProvinceIndex >= this.provinceList.length) {
this.selectedProvinceIndex = 0
}
const provinceNames = this.provinceList.map(item => this.getAreaName(item))
this.$set(this.areaColumns, 0, provinceNames)
if (this.cityList.length) {
if (this.selectedCityIndex >= this.cityList.length) {
this.selectedCityIndex = 0
}
} else {
this.selectedCityIndex = 0
}
const cityNames = (this.cityList.length ? this.cityList : []).map(item => this.getAreaName(item))
this.$set(this.areaColumns, 1, cityNames.length ? cityNames : [''])
this.areaDefaultIndex = [this.selectedProvinceIndex, this.selectedCityIndex]
console.log('this.areaDefaultIndex', this.areaDefaultIndex)
},
onBirthplaceChange({columnIndex, index}) {
if (columnIndex === 0) {
this.selectedProvinceIndex = index
this.selectedCityIndex = 0
this.loadCityList(index)
} else if (columnIndex === 1) {
this.selectedCityIndex = index
}
},
onBirthplaceConfirm({value, indexs}) {
const provinceIndex = indexs && indexs.length ? indexs[0] : this.selectedProvinceIndex
const cityIndex = indexs && indexs.length > 1 ? indexs[1] : this.selectedCityIndex
this.selectedProvinceIndex = provinceIndex
this.selectedCityIndex = cityIndex
this.areaDefaultIndex = [this.selectedProvinceIndex, this.selectedCityIndex]
const province = this.provinceList[provinceIndex]
const city = this.cityList[cityIndex]
const provinceName = this.getAreaName(province)
const cityName = this.getAreaName(city)
this.form.birthplace = cityName ? `${provinceName} ${cityName}` : provinceName
this.closePicker('birthplace')
},
getAreaName(area) {
if (!area) return ''
return area.areaName || area.name || area.label || area.title || ''
},
syncBirthPickerValue() {
if (this.form.birth) {
this.birthPickerValue = dayjs(this.form.birth).valueOf()
this.$refs.birthRef.innerValue = this.birthPickerValue
} else {
this.birthPickerValue = Date.now()
this.birthPickerValue = Number(this.birthPickerValue)
}
console.log('this.birthPickerValue', this.birthPickerValue)
},
onGenderConfirm({value}) {
this.form.gender = value && value.length ? value[0] : ''
this.closePicker('gender')
},
onBirthConfirm({value}) {
this.form.birth = this.formatDate(value)
this.birthPickerValue = value
this.closePicker('birth')
},
onBirthChange({value}) {
this.form.birth = this.formatDate(value)
this.birthPickerValue = value
},
onTimeConfirm({value}) {
this.form.time = value && value.length ? value[0] : ''
this.closePicker('time')
},
formatDate(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
return `${year}-${month}-${day}`
},
displayValue(key, placeholder) {
return this.form[key] || placeholder
},
selectMentor(item) {
this.currentMentor = item.roleName
this.currentMentorItem = item
},
async save() {
// if (!this.content) return uni.showToast({title: '请输入内容', icon: 'none'})
await updateUserReq({
userId: getUserInfo().uid,
birthday: this.form.birth,
sex: this.form.gender === '男' ? 0 : 1,
birthAddress: this.form.birthplace,
birthTime: this.form.time,
realName: this.form.name,
})
uni.showToast({title: '已保存', icon: 'success'})
await this.toChat()
},
skip() {
uni.showToast({title: '已跳过设置', icon: 'none'})
this.toChat()
},
async toChat() {
if (!this.currentMentor) return
let url = '/pages/chat/index'
const mentor = this.mentors.find(item => item.roleName === this.currentMentor)
url += `?roleName=${mentor.roleName}&roleDesc=${mentor.roleDesc}&avatar=${mentor.avatar}`
let chatId = null
// if (!mentor.hasChat) {
const {data} = await addChatListReq({
roleName: mentor.roleName,
roleDesc: mentor.roleDesc,
roleId: mentor.id
})
chatId = data.id
// } else {
// chatId = mentor.chatId
// }
url += `&chatId=${chatId}&roleId=${mentor.id}`
if (this.content) url += `&word=${this.content}`
uni.navigateTo({url})
},
async getUserData() {
const {data} = await userInfoReq()
if (data.birthday) {
this.form.birth = dayjs(data.birthday).format('YYYY-MM-DD')
this.birthPickerValue = dayjs(data.birthday).toDate()
}
if (data.sex !== null && data.sex !== undefined)
this.form.gender = parseInt(data.sex) === 0 ? '男' : '女'
if (data.birthTime)
this.form.time = data.birthTime
if (data.birthAddress)
this.form.birthplace = data.birthAddress
if (data.realName)
this.form.name = data.realName
console.log(data.gender)
console.log(this.form)
this.getMessageList()
}
}
}
</script>
<style lang="scss" scoped>
.setting-page {
min-height: 100vh;
padding: 40rpx 30rpx 60rpx;
background: linear-gradient(180deg, #8f99dd 0%, #e3e8ff 60%, #ffffff 100%);
color: #fff;
box-sizing: border-box;
}
.top-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
}
.top-title {
font-size: 32rpx;
}
.top-right .pill {
display: flex;
align-items: center;
gap: 14rpx;
background: rgba(255, 255, 255, 0.8);
padding: 12rpx 30rpx;
border-radius: 40rpx;
}
.form-section {
margin-top: 40rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 30rpx;
padding: 30rpx;
}
.form-title {
font-size: 30rpx;
font-weight: 600;
color: #f2f2ff;
}
.form-item {
margin-top: 30rpx;
}
.label {
font-size: 26rpx;
color: #f3f5ff;
margin-bottom: 12rpx;
display: block;
}
.input,
.picker {
width: 100%;
height: 76rpx;
border-radius: 40rpx;
background: rgba(255, 255, 255, 0.45);
color: #333;
padding: 0 30rpx;
box-sizing: border-box;
display: flex;
align-items: center;
}
.picker {
justify-content: space-between;
}
.picker-value {
color: #51526b;
font-size: 28rpx;
flex: 1;
}
.picker-arrow {
margin-left: 20rpx;
}
.tips {
margin: 30rpx 0;
text-align: center;
color: #5f5f81;
font-size: 24rpx;
}
.mentor-section {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
}
.mentor-card {
width: 45%;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.6);
padding: 20rpx;
text-align: center;
color: #4d4f7f;
}
.mentor-card.active {
background: linear-gradient(160deg, #7f8aff, #b0bdff);
color: #fff;
}
.mentor-name {
font-size: 30rpx;
margin-bottom: 20rpx;
}
.mentor-avatar {
width: 140rpx;
height: 140rpx;
border-radius: 70rpx;
margin: 0 auto;
}
.intro-card {
background: rgba(255, 255, 255, 0.7);
border-radius: 30rpx;
padding: 30rpx;
color: #3a406d;
font-size: 26rpx;
line-height: 1.6;
margin-bottom: 30rpx;
}
.primary-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
border-radius: 48rpx;
background: linear-gradient(120deg, #1a2663, #293892);
color: #fff;
font-size: 30rpx;
letter-spacing: 4rpx;
}
.skip {
display: block;
margin-top: 20rpx;
text-align: center;
color: #454669;
}
.chat-input {
width: 100%;
padding: 40rpx 0;
//padding: 20rpx 30rpx 40rpx;
align-items: center;
gap: 20rpx;
//background: #f7f7fb;
display: flex;
justify-content: center;
position: fixed;
left: 0;
bottom: 0;
}
.chat-input .input-wrap {
flex: 1;
border-radius: 30rpx;
background: #21284f;
color: #fff;
padding: 20rpx 30rpx;
margin-left: 30rpx;
display: flex;
align-items: center;
justify-content: center;
//height: 100rpx;
min-height: 60rpx;
}
.chat-input .textarea {
width: 100%;
//height: 100%;
background: none !important;
color: #ffffff !important;
text-align: left;
line-height: 40rpx;
padding: 0;
min-height: 50rpx;
}
.send-btn {
width: 140rpx;
height: 100rpx;
border-radius: 999rpx;
background: linear-gradient(120deg, #fbc889, #f37a5b);
color: #000;
display: flex;
align-items: center;
justify-content: center;
margin: 0 30rpx;
}
</style>