20260325稳定版
This commit is contained in:
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user