20260325稳定版
@@ -0,0 +1,2 @@
|
||||
/unpackage/
|
||||
/.idea/
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/uni_modules/uview-ui/index.scss";
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import post, {del, get, put} from "@/api/request";
|
||||
|
||||
export const chatListReq = () => get('/gx/gx-chat-list/userList')
|
||||
export const addChatListReq = (data) => post('/gx/gx-chat-list', data)
|
||||
export const updateChatListReq = data => put(`/gx/gx-chat-list`, data)
|
||||
export const deleteChatListReq = (id) => del(`/gx/gx-chat-list/${id}`)
|
||||
export const addChatHistoryReq = (data) => post('/gx/gx-chat-history', data)
|
||||
export const listChatHistoryReq = params => get('/gx/gx-chat-history', {params})
|
||||
export const listChatRoleReq = () => get('/gx/gx-role')
|
||||
export const chatRoleInfoReq = id => get(`/gx/gx-role/${id}`)
|
||||
export const sendChatReq = data => post('/chat/message', data)
|
||||
export const stopChatReq = data => post('/chat/messageStop', data)
|
||||
export const makeVoiceReq = data => post('/voice/make', data)
|
||||
export const makeTransReq = data => post('/voice/makeTrans', data)
|
||||
export const voiceGetResReq = (taskId) => post(`/voice/getRes/${taskId}`)
|
||||
@@ -0,0 +1,6 @@
|
||||
import post, {del, get} from "@/api/request";
|
||||
|
||||
export const coinListReq = () => get('/gx/gx-coin-log')
|
||||
export const coinAddReq = (data) => post('/gx/gx-coin-log', data)
|
||||
export const signLogListReq = () => get('/gx/gx-sign-log')
|
||||
export const signLogAddReq = (data) => post('/gx/gx-sign-log', data)
|
||||
@@ -0,0 +1,43 @@
|
||||
import post, {get} from "@/api/request";
|
||||
|
||||
export const areaListReq = data => post('/area/list-by-level', data)
|
||||
|
||||
export const areaInfoReq = data => post('/area/info', data)
|
||||
|
||||
export const areaInfoByNameReq = data => post('/area/info-by-name', data)
|
||||
|
||||
export const indexListReq = data => post('/com/publish-list/list', data)
|
||||
|
||||
export const swiperListReq = () => get('/sys/sys-swiper')
|
||||
|
||||
export const dictListReq = data => post('/system/dict/info', data)
|
||||
|
||||
export const dictDataInfoReq = id => get(`/system/dict-data/${id}`)
|
||||
|
||||
export const addViewNumReq = data => post('/shop/user-browse/add', data)
|
||||
|
||||
export const sysSettingInfoReq = data => post('/sys', data)
|
||||
|
||||
export const dictDataReq = params => get('/system/dict-data', {params})
|
||||
|
||||
export const listArticleCateReq = params => get('/cms/article-category', {params})
|
||||
|
||||
export const listArticleReq = params => get('/cms/article', {params})
|
||||
|
||||
export const pageArticleReq = params => get('/cms/article/page', {params})
|
||||
|
||||
export const articleInfoReq = id => get(`/cms/article/${id}`)
|
||||
|
||||
export const adListReq = () => get(`/cms/cms-ad`)
|
||||
|
||||
export const addArticleCollectReq = data => post(`/cms/article-collect`, data)
|
||||
|
||||
export const addFeedbackReq = data => post(`/gx/gx-feedback`, data)
|
||||
|
||||
export const listArticleCollectReq = () => get(`/cms/article-collect`)
|
||||
|
||||
export const sysAreaListReq = params => get(`/sys/sys-area`, {params})
|
||||
|
||||
export const articleCommentListReq = params => get(`/cms/article-comment`, {params})
|
||||
|
||||
export const addArticleCommentReq = data => post(`/cms/article-comment`, data)
|
||||
@@ -0,0 +1,3 @@
|
||||
import post, {get} from "@/api/request";
|
||||
|
||||
export const addComplain = data => post('/zhsq/zhsq-property-complaint', data)
|
||||
@@ -0,0 +1,106 @@
|
||||
import Request from '@/js_sdk/luch-request/luch-request/index.js'
|
||||
import {API} from '@/config'
|
||||
import {getUserInfo, clearUserInfo} from "@/util/user";
|
||||
|
||||
const http = new Request();
|
||||
http.config.baseURL = API
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
// uni.showLoading({title: '加载中'})
|
||||
const token = getUserInfo().token
|
||||
if (token && token !== 'undefined') {
|
||||
config.header.Authorization = token
|
||||
}
|
||||
config.header.tenantId = 10543
|
||||
return config
|
||||
}, config => {
|
||||
return Promise.reject(config)
|
||||
})
|
||||
|
||||
http.interceptors.response.use((response) => {
|
||||
const url = response.config.url
|
||||
// uni.hideLoading()
|
||||
switch (parseInt(response.data.code)) {
|
||||
case 401: {
|
||||
if (['user-cart-num', 'new-order', 'user-cart-add', 'user-info'].indexOf(url) === -1) {
|
||||
uni.showToast({
|
||||
title: '请登录',
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
})
|
||||
setTimeout(() => {
|
||||
clearUserInfo()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 500: {
|
||||
if (['/user/change-pay-password', 'new-order-from-goods-list'].indexOf(url) === -1) {
|
||||
console.log(response.data)
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: response.data.content.msg,
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 502: {
|
||||
uni.showToast({
|
||||
icon: 'error',
|
||||
title: '系统维护中',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
}
|
||||
return response
|
||||
}, (error) => {
|
||||
if (error.code === 502) {
|
||||
uni.showToast({
|
||||
icon: 'error',
|
||||
title: '系统维护中',
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
return Promise.reject(error)
|
||||
})
|
||||
|
||||
const post = (url, formData, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(url, formData, options).then(res => {
|
||||
if (res.data.code === 0) resolve(res.data)
|
||||
else if (res.data.code === 401) reject(res.data)
|
||||
else reject(res.data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const get = (url, options) => {
|
||||
// console.log(options)
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, options).then(res => {
|
||||
resolve(res.data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const del = (url, options) => {
|
||||
// console.log(options)
|
||||
return new Promise((resolve, reject) => {
|
||||
http.delete(url, options).then(res => {
|
||||
resolve(res.data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const put = (url, formData, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.put(url, formData, options).then(res => {
|
||||
if (res.data.code === 0) resolve(res.data)
|
||||
else if (res.data.code === 401) reject(res.data)
|
||||
else reject(res.data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default post
|
||||
@@ -0,0 +1,27 @@
|
||||
import post, {get, put} from "@/api/request";
|
||||
|
||||
export const loginReq = data => post('/wx-login/getOpenId', data)
|
||||
|
||||
export const loginByPhoneReq = data => post('/wx-login/loginByMpWxPhone', data)
|
||||
|
||||
export const loginByPhoneForTestReq = data => post('/system/user/loginByPhoneForTest', data)
|
||||
|
||||
export const userInfoReq = () => get('/auth/user')
|
||||
|
||||
export const updateUserReq = data => put('/system/user/self', data)
|
||||
|
||||
export const userInfoUpdateReq = data => post('/system/user/update', data)
|
||||
|
||||
export const userInfoUpdateDataReq = data => post('/system/user/update-data', data)
|
||||
|
||||
export const userSearchReq = data => post('/user/search', data)
|
||||
|
||||
export const userSetCardReq = data => post('/user/set-card', data)
|
||||
|
||||
export const userCardChargeReq = data => post('/user/card-charge', data)
|
||||
|
||||
export const userCardReduceReq = data => post('/user/card-reduce', data)
|
||||
|
||||
export const userCardUseReq = data => post('/user/card-use', data)
|
||||
|
||||
export const addCoinLogReq = data => post('/gx/gx-coin-log', data)
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<u-sticky zIndex="99999">
|
||||
<u-navbar fixed :auto-back="false" :bgColor="bgColor" :status-bar-bg-color="bgColor" placeholder :title="title" safeAreaInsetTop
|
||||
:title-style="{color: 'white'}" auto-back left-icon-color="white" @leftClick="leftClick">
|
||||
<template slot="left">
|
||||
<u-icon :name="currentPages.length > 1 ? 'arrow-left' : 'home'" size="20" color="white" v-if="showBack"/>
|
||||
</template>
|
||||
</u-navbar>
|
||||
</u-sticky>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {LINEAR} from "@/config/color";
|
||||
|
||||
export default {
|
||||
name: "HeaderItem",
|
||||
props: {
|
||||
title: String,
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bgColor: LINEAR,
|
||||
currentPages: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
leftClick() {
|
||||
console.log(this.currentPages)
|
||||
if (this.currentPages.length === 1) uni.switchTab({url: '/pages/index/index'})
|
||||
// else uni.navigateBack()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.currentPages = getCurrentPages()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<u-popup :show="show" close-on-click-overlay @close="refuse" round="10" :safe-area-inset-bottom="false">
|
||||
<view class="p-50 text-black">
|
||||
<template v-if="mode === 'wechat'">
|
||||
<view class="flex flex-col justify-start items-start">
|
||||
<!-- <u-icon name="/static/logo.png" width="100" height="52"/>-->
|
||||
<text class="text-30 font-bold mt-20">为了提供更优质的服务,需要获取</text>
|
||||
</view>
|
||||
<view class="my-25">
|
||||
<u-text text="您的身份信息" size="16" bold margin="60rpx 0"/>
|
||||
</view>
|
||||
<view class="mt-20 text-25">
|
||||
<checkbox-group @change="changeCheck">
|
||||
<view class="flex justify-start items-center">
|
||||
<checkbox value="check" :checked="check"/>
|
||||
<view>
|
||||
<text>我已阅读并同意</text>
|
||||
<text style="color: #00004F" @click="showAgree('USER_PROTOCOL')">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text style="color: #00004F" @click="showAgree('SECRET_PROTOCOL')">
|
||||
《隐私协议》
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
<view class="mt-25 flex justify-between">
|
||||
<view class="w-40p">
|
||||
<u-button type="info" @click="refuse" :custom-style="{backgroundColor: '#F2F2F2'}">拒绝</u-button>
|
||||
</view>
|
||||
<view class="w-40p">
|
||||
<u-button @getphonenumber="auth" open-type="getPhoneNumber"
|
||||
:custom-style="agreeStyle"
|
||||
:disabled="!check || disabled">允许
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<u-form label-width="140rpx">
|
||||
<u-form-item borderBottom label="手机号">
|
||||
<u-input v-model="phone" placeholder="请输入手机号"/>
|
||||
</u-form-item>
|
||||
<u-form-item borderBottom label="密码">
|
||||
<u-input v-model="pwd" type="password" placeholder="请输入密码"/>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="mt-20 text-25">
|
||||
<checkbox-group @change="changeCheck">
|
||||
<view class="flex justify-start items-center">
|
||||
<checkbox value="check" :checked="check"/>
|
||||
<view>
|
||||
<text>我已阅读并同意</text>
|
||||
<text style="color: #00004F" @click="showAgree('USER_PROTOCOL')">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text style="color: #00004F" @click="showAgree('SECRET_PROTOCOL')">
|
||||
《隐私协议》
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</checkbox-group>
|
||||
</view>
|
||||
<view class="mt-25 flex justify-between">
|
||||
<view class="w-40p">
|
||||
<u-button type="info" @click="refuse" :custom-style="{backgroundColor: '#F2F2F2'}">关闭</u-button>
|
||||
</view>
|
||||
<view class="w-40p">
|
||||
<u-button @click="loginByPhone" :custom-style="agreeStyle"
|
||||
:disabled="!check || disabled || !phone || !pwd">登录
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="flex mt-25 justify-center text-25" style="color: #00004F" @click="mode = 'phone'"
|
||||
v-if="mode === 'wechat'">手机号登录
|
||||
</view>
|
||||
<view class="flex mt-25 justify-center text-25" style="color: #00004F" @click="mode = 'wechat'" v-else>一键授权登录
|
||||
</view>
|
||||
</view>
|
||||
<u-popup mode="center" :show="showAgreePopup" closeable @close="showAgreePopup = false" round="10"
|
||||
:custom-style="{height: '80%', width: '80%'}" closeOnClickOverlay>
|
||||
<view class="p-30 overflow-y">
|
||||
<u-parse :content="content"/>
|
||||
</view>
|
||||
</u-popup>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {loginByPhoneForTestReq, loginByPhoneReq, loginReq} from "@/api/user";
|
||||
import {setUserInfo} from "@/util/user";
|
||||
import {articleInfoReq, sysSettingInfoReq} from "@/api/common";
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
props: {
|
||||
isIndex: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isHide: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
check: false,
|
||||
code: null,
|
||||
phoneData: null,
|
||||
agreeStyle: {
|
||||
backgroundColor: this.check ? '#00C655' : '#F2F2F2',
|
||||
border: this.check ? '#00C655' : '#F2F2F2',
|
||||
color: this.check ? '#FFFFFF' : '#828282',
|
||||
},
|
||||
showAgreePopup: false,
|
||||
content: '',
|
||||
phone: '',
|
||||
pwd: '',
|
||||
mode: 'wechat',
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeCheck({detail}) {
|
||||
const {value} = detail
|
||||
this.check = value.length > 0
|
||||
this.agreeStyle = {
|
||||
backgroundColor: this.check ? '#00C655' : '#F2F2F2',
|
||||
border: this.check ? '#00C655' : '#F2F2F2',
|
||||
color: this.check ? '#FFFFFF' : '#828282',
|
||||
}
|
||||
},
|
||||
open() {
|
||||
this.disabled = false
|
||||
this.show = true
|
||||
},
|
||||
hide() {
|
||||
this.show = false
|
||||
},
|
||||
async loginByPhone(){
|
||||
this.disabled = true
|
||||
const {data} = await loginByPhoneForTestReq({phone: this.phone, phoneLoginCode: this.pwd})
|
||||
console.log(data)
|
||||
this.setInfo(data)
|
||||
this.done()
|
||||
this.disabled = false
|
||||
},
|
||||
wxLogin() {
|
||||
return new Promise(resolve => {
|
||||
uni.login({
|
||||
success: (loginRes) => {
|
||||
this.code = loginRes.code
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
auth(phoneE) {
|
||||
//获取手机号
|
||||
this.phoneData = phoneE.detail
|
||||
this.doLogin()
|
||||
},
|
||||
async doLogin() {
|
||||
await this.wxLogin()
|
||||
this.disabled = true
|
||||
const phoneCode = this.phoneData.code
|
||||
const {data} = await loginByPhoneReq({
|
||||
phoneCode,
|
||||
code: this.code,
|
||||
})
|
||||
console.log(data)
|
||||
this.setInfo(data)
|
||||
this.done()
|
||||
this.disabled = false
|
||||
},
|
||||
setInfo(res) {
|
||||
setUserInfo({
|
||||
username: res.user.nickname,
|
||||
header: res.user.avatar,
|
||||
phone: res.phone || res.user.phone,
|
||||
token: res.access_token,
|
||||
uid: res.user.userId,
|
||||
});
|
||||
},
|
||||
done() {
|
||||
this.show = false
|
||||
this.$emit('done')
|
||||
},
|
||||
refuse() {
|
||||
this.show = false
|
||||
if (!this.isHide) uni.switchTab({url: '/pages/index/index'})
|
||||
},
|
||||
async showAgree(enumKey) {
|
||||
const {data} = await articleInfoReq(9852)
|
||||
this.content = data.content
|
||||
this.showAgreePopup = true
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<view class="mb-25 mt-25">
|
||||
<u-icon name="/static/line.png" :label="title" label-pos="right" bold label-color="#4F4F4F" label-size="30rpx"/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TitleItem",
|
||||
props: {
|
||||
title: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<view>
|
||||
<m-tabbar fixed fill :current="current" safeBottom :tabbar="tabbar" native openType="switchTab" @click="tabClick">
|
||||
<!-- <template v-slot:tabbar_index_2>-->
|
||||
<!-- <view class="custom_style">-->
|
||||
<!-- <view class="custom_style_icon">-->
|
||||
<!-- <u-image v-if="!isAuditing" src="https://oss.wsdns.cn/20250627/b6ea18396b074f8db2f649c7db385419.png" width="160rpx"-->
|
||||
<!-- height="160rpx"/>-->
|
||||
<!-- <u-image v-else src="https://img.ggsxiangan.com/62c543af60204ff8868df41a31f79ec0.png" width="160rpx"-->
|
||||
<!-- height="160rpx"/>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </template>-->
|
||||
</m-tabbar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {dictDataInfoReq} from "@/api/common";
|
||||
|
||||
export default {
|
||||
name: "customTabbar",
|
||||
props: {
|
||||
current: {
|
||||
default: 0,
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabbar: {
|
||||
color: "#333",
|
||||
selectedColor: "#98C147",
|
||||
borderStyle: "F0F0F0",
|
||||
backgroundColor: "#FFFFFF",
|
||||
list: [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "static/tabbar/home.png",
|
||||
"selectedIconPath": "static/tabbar/home-active.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "static/tabbar/service.png",
|
||||
"selectedIconPath": "static/tabbar/service-active.png",
|
||||
"text": "服务"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/community/index",
|
||||
"iconPath": "static/tabbar/xiaoqu.png",
|
||||
"selectedIconPath": "static/tabbar/xiaoqu-active.png",
|
||||
"text": "小区"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "static/tabbar/store.png",
|
||||
"selectedIconPath": "static/tabbar/store-active.png",
|
||||
"text": "商城"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/user/index",
|
||||
"iconPath": "static/tabbar/user.png",
|
||||
"selectedIconPath": "static/tabbar/user-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
isAuditing: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabClick(val) {
|
||||
console.log('val', val)
|
||||
if (val === 0 && this.current === 0) this.$jump('/pages/index/index')
|
||||
if (val === 3) {
|
||||
uni.navigateToMiniProgram({
|
||||
appId: 'wxad2376fd45fb7eca',
|
||||
complete() {
|
||||
uni.switchTab({url: '/pages/index/index'})
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
async checkAudit() {
|
||||
const {data} = await dictDataInfoReq(3678)
|
||||
this.isAuditing = data.dictDataCode === '1'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.checkAudit()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.custom_style {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-top: -40rpx;
|
||||
|
||||
&_icon {
|
||||
//background-color: #2ACF8D;
|
||||
font-size: 80rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
export const YELLOW = '#e2d602'
|
||||
export const ORANGE = '#fd7600'
|
||||
export const ORANGE_LIGHT = '#FEEDA8'
|
||||
export const GREEN = '#98C147'
|
||||
export const GREEN_LIGHT = '#96b0a3'
|
||||
export const BLUE = '#3C4DA5'
|
||||
export const BLUE_LIGHT = '#049DFF'
|
||||
export const RED = '#FE6869'
|
||||
export const RED_LIGHT = '#F7B2B2'
|
||||
export const PURPLE = '#7F6CDE'
|
||||
export const PINK = '#FE5397'
|
||||
export const PURPLE_DARK = '#7F6CDE'
|
||||
export const PUBLISH = '#FDCB05'
|
||||
export const GRAY = '#999'
|
||||
export const PURPLE_LIGHT = '#7F6CDE'
|
||||
|
||||
export const MAIN = BLUE
|
||||
export const LINEAR = 'linear-gradient(90deg, #049DFF, #2665c4)'
|
||||
@@ -0,0 +1,23 @@
|
||||
const MAIN_API_FN = () => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// return 'http://127.0.0.1:10051'
|
||||
return 'https://guoxue.gxwebsoft.com'
|
||||
} else {
|
||||
return 'https://guoxue.gxwebsoft.com'
|
||||
}
|
||||
}
|
||||
|
||||
const MAIN_WS_FN = () => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return 'ws://127.0.0.1:10051/chat'
|
||||
// return 'wss://guoxue.gxwebsoft.com/chat'
|
||||
} else {
|
||||
return 'wss://guoxue.gxwebsoft.com/chat'
|
||||
}
|
||||
}
|
||||
|
||||
export const MAIN_API = MAIN_API_FN()
|
||||
|
||||
export const WS_API = MAIN_WS_FN()
|
||||
|
||||
export const API = `${MAIN_API}/api`
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,100 @@
|
||||
import buildURL from '../helpers/buildURL'
|
||||
import buildFullPath from '../core/buildFullPath'
|
||||
import settle from '../core/settle'
|
||||
import {isUndefined} from "../utils"
|
||||
|
||||
/**
|
||||
* 返回可选值存在的配置
|
||||
* @param {Array} keys - 可选值数组
|
||||
* @param {Object} config2 - 配置
|
||||
* @return {{}} - 存在的配置项
|
||||
*/
|
||||
const mergeKeys = (keys, config2) => {
|
||||
let config = {}
|
||||
keys.forEach(prop => {
|
||||
if (!isUndefined(config2[prop])) {
|
||||
config[prop] = config2[prop]
|
||||
}
|
||||
})
|
||||
return config
|
||||
}
|
||||
export default (config) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params, config.paramsSerializer)
|
||||
const _config = {
|
||||
url: fullPath,
|
||||
header: config.header,
|
||||
complete: (response) => {
|
||||
config.fullPath = fullPath
|
||||
response.config = config
|
||||
response.rawData = response.data
|
||||
try {
|
||||
// 对可能字符串不是json 的情况容错
|
||||
if (typeof response.data === 'string') {
|
||||
response.data = JSON.parse(response.data)
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {
|
||||
}
|
||||
settle(resolve, reject, response)
|
||||
}
|
||||
}
|
||||
let requestTask
|
||||
if (config.method === 'UPLOAD') {
|
||||
delete _config.header['content-type']
|
||||
delete _config.header['Content-Type']
|
||||
let otherConfig = {
|
||||
// #ifdef MP-ALIPAY
|
||||
fileType: config.fileType,
|
||||
// #endif
|
||||
filePath: config.filePath,
|
||||
name: config.name
|
||||
}
|
||||
const optionalKeys = [
|
||||
// #ifdef APP-PLUS || H5
|
||||
'files',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'file',
|
||||
// #endif
|
||||
// #ifdef H5 || APP-PLUS
|
||||
'timeout',
|
||||
// #endif
|
||||
'formData'
|
||||
]
|
||||
requestTask = uni.uploadFile({..._config, ...otherConfig, ...mergeKeys(optionalKeys, config)})
|
||||
} else if (config.method === 'DOWNLOAD') {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (!isUndefined(config['timeout'])) {
|
||||
_config['timeout'] = config['timeout']
|
||||
}
|
||||
// #endif
|
||||
requestTask = uni.downloadFile(_config)
|
||||
} else {
|
||||
const optionalKeys = [
|
||||
'data',
|
||||
'method',
|
||||
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
|
||||
'timeout',
|
||||
// #endif
|
||||
'dataType',
|
||||
// #ifndef MP-ALIPAY
|
||||
'responseType',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'sslVerify',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'withCredentials',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'firstIpv4',
|
||||
// #endif
|
||||
]
|
||||
requestTask = uni.request({..._config, ...mergeKeys(optionalKeys, config)})
|
||||
}
|
||||
if (config.getTask) {
|
||||
config.getTask(requestTask, config)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict'
|
||||
|
||||
|
||||
function InterceptorManager() {
|
||||
this.handlers = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new interceptor to the stack
|
||||
*
|
||||
* @param {Function} fulfilled The function to handle `then` for a `Promise`
|
||||
* @param {Function} rejected The function to handle `reject` for a `Promise`
|
||||
*
|
||||
* @return {Number} An ID used to remove interceptor later
|
||||
*/
|
||||
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
|
||||
this.handlers.push({
|
||||
fulfilled: fulfilled,
|
||||
rejected: rejected
|
||||
})
|
||||
return this.handlers.length - 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an interceptor from the stack
|
||||
*
|
||||
* @param {Number} id The ID that was returned by `use`
|
||||
*/
|
||||
InterceptorManager.prototype.eject = function eject(id) {
|
||||
if (this.handlers[id]) {
|
||||
this.handlers[id] = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all the registered interceptors
|
||||
*
|
||||
* This method is particularly useful for skipping over any
|
||||
* interceptors that may have become `null` calling `eject`.
|
||||
*
|
||||
* @param {Function} fn The function to call for each interceptor
|
||||
*/
|
||||
InterceptorManager.prototype.forEach = function forEach(fn) {
|
||||
this.handlers.forEach(h => {
|
||||
if (h !== null) {
|
||||
fn(h)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default InterceptorManager
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* @Class Request
|
||||
* @description luch-request http请求插件
|
||||
* @Author lu-ch
|
||||
* @Email webwork.s@qq.com
|
||||
* 文档: https://www.quanzhan.co/luch-request/
|
||||
* github: https://github.com/lei-mu/luch-request
|
||||
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
|
||||
*/
|
||||
|
||||
|
||||
import dispatchRequest from './dispatchRequest'
|
||||
import InterceptorManager from './InterceptorManager'
|
||||
import mergeConfig from './mergeConfig'
|
||||
import defaults from './defaults'
|
||||
import { isPlainObject } from '../utils'
|
||||
import clone from '../utils/clone'
|
||||
|
||||
export default class Request {
|
||||
/**
|
||||
* @param {Object} arg - 全局配置
|
||||
* @param {String} arg.baseURL - 全局根路径
|
||||
* @param {Object} arg.header - 全局header
|
||||
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
|
||||
* @param {String} arg.dataType = [json] - 全局默认的dataType
|
||||
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType。支付宝小程序不支持
|
||||
* @param {Object} arg.custom - 全局默认的自定义参数
|
||||
* @param {Number} arg.timeout - 全局默认的超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
|
||||
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书。默认true.仅App安卓端支持(HBuilderX 2.3.3+)
|
||||
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+)
|
||||
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4。默认false。仅 App-Android 支持 (HBuilderX 2.8.0+)
|
||||
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器。默认statusCode >= 200 && statusCode < 300
|
||||
*/
|
||||
constructor(arg = {}) {
|
||||
if (!isPlainObject(arg)) {
|
||||
arg = {}
|
||||
console.warn('设置全局参数必须接收一个Object')
|
||||
}
|
||||
this.config = clone({...defaults, ...arg})
|
||||
this.interceptors = {
|
||||
request: new InterceptorManager(),
|
||||
response: new InterceptorManager()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Function
|
||||
* @param {Request~setConfigCallback} f - 设置全局默认配置
|
||||
*/
|
||||
setConfig(f) {
|
||||
this.config = f(this.config)
|
||||
}
|
||||
|
||||
middleware(config) {
|
||||
config = mergeConfig(this.config, config)
|
||||
let chain = [dispatchRequest, undefined]
|
||||
let promise = Promise.resolve(config)
|
||||
|
||||
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
|
||||
chain.unshift(interceptor.fulfilled, interceptor.rejected)
|
||||
})
|
||||
|
||||
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
|
||||
chain.push(interceptor.fulfilled, interceptor.rejected)
|
||||
})
|
||||
|
||||
while (chain.length) {
|
||||
promise = promise.then(chain.shift(), chain.shift())
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
* @Function
|
||||
* @param {Object} config - 请求配置项
|
||||
* @prop {String} options.url - 请求路径
|
||||
* @prop {Object} options.data - 请求参数
|
||||
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
|
||||
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse
|
||||
* @prop {Object} [options.header = config.header] - 请求header
|
||||
* @prop {Object} [options.method = config.method] - 请求方法
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
request(config = {}) {
|
||||
return this.middleware(config)
|
||||
}
|
||||
|
||||
get(url, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
method: 'GET',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
post(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'POST',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #ifndef MP-ALIPAY || MP-KUAISHOU || MP-JD
|
||||
put(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'PUT',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
|
||||
delete(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'DELETE',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP-WEIXIN
|
||||
connect(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'CONNECT',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
|
||||
head(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'HEAD',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
|
||||
options(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'OPTIONS',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP-WEIXIN
|
||||
trace(url, data, options = {}) {
|
||||
return this.middleware({
|
||||
url,
|
||||
data,
|
||||
method: 'TRACE',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
upload(url, config = {}) {
|
||||
config.url = url
|
||||
config.method = 'UPLOAD'
|
||||
return this.middleware(config)
|
||||
}
|
||||
|
||||
download(url, config = {}) {
|
||||
config.url = url
|
||||
config.method = 'DOWNLOAD'
|
||||
return this.middleware(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* setConfig回调
|
||||
* @return {Object} - 返回操作后的config
|
||||
* @callback Request~setConfigCallback
|
||||
* @param {Object} config - 全局默认config
|
||||
*/
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict'
|
||||
|
||||
import isAbsoluteURL from '../helpers/isAbsoluteURL'
|
||||
import combineURLs from '../helpers/combineURLs'
|
||||
|
||||
/**
|
||||
* Creates a new URL by combining the baseURL with the requestedURL,
|
||||
* only when the requestedURL is not already an absolute URL.
|
||||
* If the requestURL is absolute, this function returns the requestedURL untouched.
|
||||
*
|
||||
* @param {string} baseURL The base URL
|
||||
* @param {string} requestedURL Absolute or relative URL to combine
|
||||
* @returns {string} The combined full path
|
||||
*/
|
||||
export default function buildFullPath(baseURL, requestedURL) {
|
||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
||||
return combineURLs(baseURL, requestedURL)
|
||||
}
|
||||
return requestedURL
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 默认的全局配置
|
||||
*/
|
||||
|
||||
|
||||
export default {
|
||||
baseURL: '',
|
||||
header: {},
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
paramsSerializer: null,
|
||||
// #ifndef MP-ALIPAY
|
||||
responseType: 'text',
|
||||
// #endif
|
||||
custom: {},
|
||||
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
|
||||
timeout: 60000,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
sslVerify: true,
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
withCredentials: false,
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
firstIpv4: false,
|
||||
// #endif
|
||||
validateStatus: function validateStatus(status) {
|
||||
return status >= 200 && status < 300
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import adapter from '../adapters/index'
|
||||
|
||||
|
||||
export default (config) => {
|
||||
return adapter(config)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import {deepMerge, isUndefined} from '../utils'
|
||||
|
||||
/**
|
||||
* 合并局部配置优先的配置,如果局部有该配置项则用局部,如果全局有该配置项则用全局
|
||||
* @param {Array} keys - 配置项
|
||||
* @param {Object} globalsConfig - 当前的全局配置
|
||||
* @param {Object} config2 - 局部配置
|
||||
* @return {{}}
|
||||
*/
|
||||
const mergeKeys = (keys, globalsConfig, config2) => {
|
||||
let config = {}
|
||||
keys.forEach(prop => {
|
||||
if (!isUndefined(config2[prop])) {
|
||||
config[prop] = config2[prop]
|
||||
} else if (!isUndefined(globalsConfig[prop])) {
|
||||
config[prop] = globalsConfig[prop]
|
||||
}
|
||||
})
|
||||
return config
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param globalsConfig - 当前实例的全局配置
|
||||
* @param config2 - 当前的局部配置
|
||||
* @return - 合并后的配置
|
||||
*/
|
||||
export default (globalsConfig, config2 = {}) => {
|
||||
const method = config2.method || globalsConfig.method || 'GET'
|
||||
let config = {
|
||||
baseURL: config2.baseURL || globalsConfig.baseURL || '',
|
||||
method: method,
|
||||
url: config2.url || '',
|
||||
params: config2.params || {},
|
||||
custom: {...(globalsConfig.custom || {}), ...(config2.custom || {})},
|
||||
header: deepMerge(globalsConfig.header || {}, config2.header || {})
|
||||
}
|
||||
const defaultToConfig2Keys = ['getTask', 'validateStatus', 'paramsSerializer']
|
||||
config = {...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2)}
|
||||
|
||||
// eslint-disable-next-line no-empty
|
||||
if (method === 'DOWNLOAD') {
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (!isUndefined(config2.timeout)) {
|
||||
config['timeout'] = config2['timeout']
|
||||
} else if (!isUndefined(globalsConfig.timeout)) {
|
||||
config['timeout'] = globalsConfig['timeout']
|
||||
}
|
||||
// #endif
|
||||
} else if (method === 'UPLOAD') {
|
||||
delete config.header['content-type']
|
||||
delete config.header['Content-Type']
|
||||
const uploadKeys = [
|
||||
// #ifdef APP-PLUS || H5
|
||||
'files',
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
'fileType',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'file',
|
||||
// #endif
|
||||
'filePath',
|
||||
'name',
|
||||
// #ifdef H5 || APP-PLUS
|
||||
'timeout',
|
||||
// #endif
|
||||
'formData',
|
||||
]
|
||||
uploadKeys.forEach(prop => {
|
||||
if (!isUndefined(config2[prop])) {
|
||||
config[prop] = config2[prop]
|
||||
}
|
||||
})
|
||||
// #ifdef H5 || APP-PLUS
|
||||
if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) {
|
||||
config['timeout'] = globalsConfig['timeout']
|
||||
}
|
||||
// #endif
|
||||
} else {
|
||||
const defaultsKeys = [
|
||||
'data',
|
||||
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
|
||||
'timeout',
|
||||
// #endif
|
||||
'dataType',
|
||||
// #ifndef MP-ALIPAY
|
||||
'responseType',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'sslVerify',
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
'withCredentials',
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
'firstIpv4',
|
||||
// #endif
|
||||
]
|
||||
config = {...config, ...mergeKeys(defaultsKeys, globalsConfig, config2)}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Resolve or reject a Promise based on response status.
|
||||
*
|
||||
* @param {Function} resolve A function that resolves the promise.
|
||||
* @param {Function} reject A function that rejects the promise.
|
||||
* @param {object} response The response.
|
||||
*/
|
||||
export default function settle(resolve, reject, response) {
|
||||
const validateStatus = response.config.validateStatus
|
||||
const status = response.statusCode
|
||||
if (status && (!validateStatus || validateStatus(status))) {
|
||||
resolve(response)
|
||||
} else {
|
||||
reject(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use strict'
|
||||
|
||||
import * as utils from './../utils'
|
||||
|
||||
function encode(val) {
|
||||
return encodeURIComponent(val).replace(/%40/gi, '@').replace(/%3A/gi, ':').replace(/%24/g, '$').replace(/%2C/gi, ',').replace(/%20/g, '+').replace(/%5B/gi, '[').replace(/%5D/gi, ']')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a URL by appending params to the end
|
||||
*
|
||||
* @param {string} url The base of the url (e.g., http://www.google.com)
|
||||
* @param {object} [params] The params to be appended
|
||||
* @returns {string} The formatted url
|
||||
*/
|
||||
export default function buildURL(url, params, paramsSerializer) {
|
||||
/*eslint no-param-reassign:0*/
|
||||
if (!params) {
|
||||
return url
|
||||
}
|
||||
|
||||
var serializedParams
|
||||
if (paramsSerializer) {
|
||||
serializedParams = paramsSerializer(params)
|
||||
} else if (utils.isURLSearchParams(params)) {
|
||||
serializedParams = params.toString()
|
||||
} else {
|
||||
var parts = []
|
||||
|
||||
utils.forEach(params, function serialize(val, key) {
|
||||
if (val === null || typeof val === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
if (utils.isArray(val)) {
|
||||
key = key + '[]'
|
||||
} else {
|
||||
val = [val]
|
||||
}
|
||||
|
||||
utils.forEach(val, function parseValue(v) {
|
||||
if (utils.isDate(v)) {
|
||||
v = v.toISOString()
|
||||
} else if (utils.isObject(v)) {
|
||||
v = JSON.stringify(v)
|
||||
}
|
||||
parts.push(encode(key) + '=' + encode(v))
|
||||
})
|
||||
})
|
||||
|
||||
serializedParams = parts.join('&')
|
||||
}
|
||||
|
||||
if (serializedParams) {
|
||||
var hashmarkIndex = url.indexOf('#')
|
||||
if (hashmarkIndex !== -1) {
|
||||
url = url.slice(0, hashmarkIndex)
|
||||
}
|
||||
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Creates a new URL by combining the specified URLs
|
||||
*
|
||||
* @param {string} baseURL The base URL
|
||||
* @param {string} relativeURL The relative URL
|
||||
* @returns {string} The combined URL
|
||||
*/
|
||||
export default function combineURLs(baseURL, relativeURL) {
|
||||
return relativeURL
|
||||
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
|
||||
: baseURL
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Determines whether the specified URL is absolute
|
||||
*
|
||||
* @param {string} url The URL to test
|
||||
* @returns {boolean} True if the specified URL is absolute, otherwise false
|
||||
*/
|
||||
export default function isAbsoluteURL(url) {
|
||||
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
|
||||
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
|
||||
// by any combination of letters, digits, plus, period, or hyphen.
|
||||
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
type AnyObject = Record<string | number | symbol, any>
|
||||
type HttpPromise<T> = Promise<HttpResponse<T>>;
|
||||
type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask
|
||||
export interface RequestTask {
|
||||
abort: () => void;
|
||||
offHeadersReceived: () => void;
|
||||
onHeadersReceived: () => void;
|
||||
}
|
||||
export interface HttpRequestConfig<T = Tasks> {
|
||||
/** 请求基地址 */
|
||||
baseURL?: string;
|
||||
/** 请求服务器接口地址 */
|
||||
url?: string;
|
||||
|
||||
/** 请求查询参数,自动拼接为查询字符串 */
|
||||
params?: AnyObject;
|
||||
/** 请求体参数 */
|
||||
data?: AnyObject;
|
||||
|
||||
/** 文件对应的 key */
|
||||
name?: string;
|
||||
/** HTTP 请求中其他额外的 form data */
|
||||
formData?: AnyObject;
|
||||
/** 要上传文件资源的路径。 */
|
||||
filePath?: string;
|
||||
/** 需要上传的文件列表。使用 files 时,filePath 和 name 不生效,App、H5( 2.6.15+) */
|
||||
files?: Array<{
|
||||
name?: string;
|
||||
file?: File;
|
||||
uri: string;
|
||||
}>;
|
||||
/** 要上传的文件对象,仅H5(2.6.15+)支持 */
|
||||
file?: File;
|
||||
|
||||
/** 请求头信息 */
|
||||
header?: AnyObject;
|
||||
/** 请求方式 */
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD";
|
||||
/** 如果设为 json,会尝试对返回的数据做一次 JSON.parse */
|
||||
dataType?: string;
|
||||
/** 设置响应的数据类型,支付宝小程序不支持 */
|
||||
responseType?: "text" | "arraybuffer";
|
||||
/** 自定义参数 */
|
||||
custom?: AnyObject;
|
||||
/** 超时时间,仅微信小程序(2.10.0)、支付宝小程序支持 */
|
||||
timeout?: number;
|
||||
/** DNS解析时优先使用ipv4,仅 App-Android 支持 (HBuilderX 2.8.0+) */
|
||||
firstIpv4?: boolean;
|
||||
/** 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+) */
|
||||
sslVerify?: boolean;
|
||||
/** 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+) */
|
||||
withCredentials?: boolean;
|
||||
|
||||
/** 返回当前请求的task, options。请勿在此处修改options。 */
|
||||
getTask?: (task: T, options: HttpRequestConfig<T>) => void;
|
||||
/** 全局自定义验证器 */
|
||||
validateStatus?: (statusCode: number) => boolean | void;
|
||||
/** params 参数自定义处理 */
|
||||
paramsSerializer?: (params: AnyObject) => string | void;
|
||||
}
|
||||
export interface HttpResponse<T = any> {
|
||||
config: HttpRequestConfig;
|
||||
statusCode: number;
|
||||
cookies: Array<string>;
|
||||
data: T;
|
||||
errMsg: string;
|
||||
header: AnyObject;
|
||||
rawData: T;
|
||||
}
|
||||
export interface HttpUploadResponse<T = any> {
|
||||
config: HttpRequestConfig;
|
||||
statusCode: number;
|
||||
data: T;
|
||||
errMsg: string;
|
||||
rawData: T;
|
||||
}
|
||||
export interface HttpDownloadResponse extends HttpResponse {
|
||||
tempFilePath: string;
|
||||
}
|
||||
export interface HttpError {
|
||||
config: HttpRequestConfig;
|
||||
statusCode?: number;
|
||||
cookies?: Array<string>;
|
||||
data?: any;
|
||||
errMsg: string;
|
||||
header?: AnyObject;
|
||||
}
|
||||
export interface HttpInterceptorManager<V, E = V> {
|
||||
use(
|
||||
onFulfilled?: (config: V) => Promise<V> | V,
|
||||
onRejected?: (config: E) => Promise<E> | E
|
||||
): void;
|
||||
eject(id: number): void;
|
||||
}
|
||||
export abstract class HttpRequestAbstract {
|
||||
constructor(config?: HttpRequestConfig);
|
||||
config: HttpRequestConfig;
|
||||
interceptors: {
|
||||
request: HttpInterceptorManager<HttpRequestConfig, HttpRequestConfig>;
|
||||
response: HttpInterceptorManager<HttpResponse, HttpError>;
|
||||
}
|
||||
middleware<T = any>(config: HttpRequestConfig): HttpPromise<T>;
|
||||
request<T = any>(config: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
get<T = any>(url: string, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
upload<T = any>(url: string, config?: HttpRequestConfig<UniApp.UploadTask>): HttpPromise<T>;
|
||||
delete<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
head<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
post<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
put<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
connect<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
options<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
trace<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
|
||||
|
||||
download(url: string, config?: HttpRequestConfig<UniApp.DownloadTask>): Promise<HttpDownloadResponse>;
|
||||
|
||||
setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void;
|
||||
}
|
||||
|
||||
declare class HttpRequest extends HttpRequestAbstract { }
|
||||
export default HttpRequest;
|
||||
@@ -0,0 +1,2 @@
|
||||
import Request from './core/Request'
|
||||
export default Request
|
||||
@@ -0,0 +1,135 @@
|
||||
'use strict'
|
||||
|
||||
// utils is a library of generic helper functions non-specific to axios
|
||||
|
||||
var toString = Object.prototype.toString
|
||||
|
||||
/**
|
||||
* Determine if a value is an Array
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is an Array, otherwise false
|
||||
*/
|
||||
export function isArray (val) {
|
||||
return toString.call(val) === '[object Array]'
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a value is an Object
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is an Object, otherwise false
|
||||
*/
|
||||
export function isObject (val) {
|
||||
return val !== null && typeof val === 'object'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a value is a Date
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is a Date, otherwise false
|
||||
*/
|
||||
export function isDate (val) {
|
||||
return toString.call(val) === '[object Date]'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a value is a URLSearchParams object
|
||||
*
|
||||
* @param {Object} val The value to test
|
||||
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
|
||||
*/
|
||||
export function isURLSearchParams (val) {
|
||||
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterate over an Array or an Object invoking a function for each item.
|
||||
*
|
||||
* If `obj` is an Array callback will be called passing
|
||||
* the value, index, and complete array for each item.
|
||||
*
|
||||
* If 'obj' is an Object callback will be called passing
|
||||
* the value, key, and complete object for each property.
|
||||
*
|
||||
* @param {Object|Array} obj The object to iterate
|
||||
* @param {Function} fn The callback to invoke for each item
|
||||
*/
|
||||
export function forEach (obj, fn) {
|
||||
// Don't bother if no value provided
|
||||
if (obj === null || typeof obj === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
// Force an array if not already something iterable
|
||||
if (typeof obj !== 'object') {
|
||||
/*eslint no-param-reassign:0*/
|
||||
obj = [obj]
|
||||
}
|
||||
|
||||
if (isArray(obj)) {
|
||||
// Iterate over array values
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
fn.call(null, obj[i], i, obj)
|
||||
}
|
||||
} else {
|
||||
// Iterate over object keys
|
||||
for (var key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
fn.call(null, obj[key], key, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为boolean 值
|
||||
* @param val
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isBoolean(val) {
|
||||
return typeof val === 'boolean'
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为真正的对象{} new Object
|
||||
* @param {any} obj - 检测的对象
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPlainObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]'
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Function equal to merge with the difference being that no reference
|
||||
* to original objects is kept.
|
||||
*
|
||||
* @see merge
|
||||
* @param {Object} obj1 Object to merge
|
||||
* @returns {Object} Result of all merge properties
|
||||
*/
|
||||
export function deepMerge(/* obj1, obj2, obj3, ... */) {
|
||||
let result = {}
|
||||
function assignValue(val, key) {
|
||||
if (typeof result[key] === 'object' && typeof val === 'object') {
|
||||
result[key] = deepMerge(result[key], val)
|
||||
} else if (typeof val === 'object') {
|
||||
result[key] = deepMerge({}, val)
|
||||
} else {
|
||||
result[key] = val
|
||||
}
|
||||
}
|
||||
for (let i = 0, l = arguments.length; i < l; i++) {
|
||||
forEach(arguments[i], assignValue)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function isUndefined (val) {
|
||||
return typeof val === 'undefined'
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/* eslint-disable */
|
||||
var clone = (function() {
|
||||
'use strict';
|
||||
|
||||
function _instanceof(obj, type) {
|
||||
return type != null && obj instanceof type;
|
||||
}
|
||||
|
||||
var nativeMap;
|
||||
try {
|
||||
nativeMap = Map;
|
||||
} catch(_) {
|
||||
// maybe a reference error because no `Map`. Give it a dummy value that no
|
||||
// value will ever be an instanceof.
|
||||
nativeMap = function() {};
|
||||
}
|
||||
|
||||
var nativeSet;
|
||||
try {
|
||||
nativeSet = Set;
|
||||
} catch(_) {
|
||||
nativeSet = function() {};
|
||||
}
|
||||
|
||||
var nativePromise;
|
||||
try {
|
||||
nativePromise = Promise;
|
||||
} catch(_) {
|
||||
nativePromise = function() {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones (copies) an Object using deep copying.
|
||||
*
|
||||
* This function supports circular references by default, but if you are certain
|
||||
* there are no circular references in your object, you can save some CPU time
|
||||
* by calling clone(obj, false).
|
||||
*
|
||||
* Caution: if `circular` is false and `parent` contains circular references,
|
||||
* your program may enter an infinite loop and crash.
|
||||
*
|
||||
* @param `parent` - the object to be cloned
|
||||
* @param `circular` - set to true if the object to be cloned may contain
|
||||
* circular references. (optional - true by default)
|
||||
* @param `depth` - set to a number if the object is only to be cloned to
|
||||
* a particular depth. (optional - defaults to Infinity)
|
||||
* @param `prototype` - sets the prototype to be used when cloning an object.
|
||||
* (optional - defaults to parent prototype).
|
||||
* @param `includeNonEnumerable` - set to true if the non-enumerable properties
|
||||
* should be cloned as well. Non-enumerable properties on the prototype
|
||||
* chain will be ignored. (optional - false by default)
|
||||
*/
|
||||
function clone(parent, circular, depth, prototype, includeNonEnumerable) {
|
||||
if (typeof circular === 'object') {
|
||||
depth = circular.depth;
|
||||
prototype = circular.prototype;
|
||||
includeNonEnumerable = circular.includeNonEnumerable;
|
||||
circular = circular.circular;
|
||||
}
|
||||
// maintain two arrays for circular references, where corresponding parents
|
||||
// and children have the same index
|
||||
var allParents = [];
|
||||
var allChildren = [];
|
||||
|
||||
var useBuffer = typeof Buffer != 'undefined';
|
||||
|
||||
if (typeof circular == 'undefined')
|
||||
circular = true;
|
||||
|
||||
if (typeof depth == 'undefined')
|
||||
depth = Infinity;
|
||||
|
||||
// recurse this function so we don't reset allParents and allChildren
|
||||
function _clone(parent, depth) {
|
||||
// cloning null always returns null
|
||||
if (parent === null)
|
||||
return null;
|
||||
|
||||
if (depth === 0)
|
||||
return parent;
|
||||
|
||||
var child;
|
||||
var proto;
|
||||
if (typeof parent != 'object') {
|
||||
return parent;
|
||||
}
|
||||
|
||||
if (_instanceof(parent, nativeMap)) {
|
||||
child = new nativeMap();
|
||||
} else if (_instanceof(parent, nativeSet)) {
|
||||
child = new nativeSet();
|
||||
} else if (_instanceof(parent, nativePromise)) {
|
||||
child = new nativePromise(function (resolve, reject) {
|
||||
parent.then(function(value) {
|
||||
resolve(_clone(value, depth - 1));
|
||||
}, function(err) {
|
||||
reject(_clone(err, depth - 1));
|
||||
});
|
||||
});
|
||||
} else if (clone.__isArray(parent)) {
|
||||
child = [];
|
||||
} else if (clone.__isRegExp(parent)) {
|
||||
child = new RegExp(parent.source, __getRegExpFlags(parent));
|
||||
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
|
||||
} else if (clone.__isDate(parent)) {
|
||||
child = new Date(parent.getTime());
|
||||
} else if (useBuffer && Buffer.isBuffer(parent)) {
|
||||
if (Buffer.from) {
|
||||
// Node.js >= 5.10.0
|
||||
child = Buffer.from(parent);
|
||||
} else {
|
||||
// Older Node.js versions
|
||||
child = new Buffer(parent.length);
|
||||
parent.copy(child);
|
||||
}
|
||||
return child;
|
||||
} else if (_instanceof(parent, Error)) {
|
||||
child = Object.create(parent);
|
||||
} else {
|
||||
if (typeof prototype == 'undefined') {
|
||||
proto = Object.getPrototypeOf(parent);
|
||||
child = Object.create(proto);
|
||||
}
|
||||
else {
|
||||
child = Object.create(prototype);
|
||||
proto = prototype;
|
||||
}
|
||||
}
|
||||
|
||||
if (circular) {
|
||||
var index = allParents.indexOf(parent);
|
||||
|
||||
if (index != -1) {
|
||||
return allChildren[index];
|
||||
}
|
||||
allParents.push(parent);
|
||||
allChildren.push(child);
|
||||
}
|
||||
|
||||
if (_instanceof(parent, nativeMap)) {
|
||||
parent.forEach(function(value, key) {
|
||||
var keyChild = _clone(key, depth - 1);
|
||||
var valueChild = _clone(value, depth - 1);
|
||||
child.set(keyChild, valueChild);
|
||||
});
|
||||
}
|
||||
if (_instanceof(parent, nativeSet)) {
|
||||
parent.forEach(function(value) {
|
||||
var entryChild = _clone(value, depth - 1);
|
||||
child.add(entryChild);
|
||||
});
|
||||
}
|
||||
|
||||
for (var i in parent) {
|
||||
var attrs = Object.getOwnPropertyDescriptor(parent, i);
|
||||
if (attrs) {
|
||||
child[i] = _clone(parent[i], depth - 1);
|
||||
}
|
||||
|
||||
try {
|
||||
var objProperty = Object.getOwnPropertyDescriptor(parent, i);
|
||||
if (objProperty.set === 'undefined') {
|
||||
// no setter defined. Skip cloning this property
|
||||
continue;
|
||||
}
|
||||
child[i] = _clone(parent[i], depth - 1);
|
||||
} catch(e){
|
||||
if (e instanceof TypeError) {
|
||||
// when in strict mode, TypeError will be thrown if child[i] property only has a getter
|
||||
// we can't do anything about this, other than inform the user that this property cannot be set.
|
||||
continue
|
||||
} else if (e instanceof ReferenceError) {
|
||||
//this may happen in non strict mode
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Object.getOwnPropertySymbols) {
|
||||
var symbols = Object.getOwnPropertySymbols(parent);
|
||||
for (var i = 0; i < symbols.length; i++) {
|
||||
// Don't need to worry about cloning a symbol because it is a primitive,
|
||||
// like a number or string.
|
||||
var symbol = symbols[i];
|
||||
var descriptor = Object.getOwnPropertyDescriptor(parent, symbol);
|
||||
if (descriptor && !descriptor.enumerable && !includeNonEnumerable) {
|
||||
continue;
|
||||
}
|
||||
child[symbol] = _clone(parent[symbol], depth - 1);
|
||||
Object.defineProperty(child, symbol, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
if (includeNonEnumerable) {
|
||||
var allPropertyNames = Object.getOwnPropertyNames(parent);
|
||||
for (var i = 0; i < allPropertyNames.length; i++) {
|
||||
var propertyName = allPropertyNames[i];
|
||||
var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName);
|
||||
if (descriptor && descriptor.enumerable) {
|
||||
continue;
|
||||
}
|
||||
child[propertyName] = _clone(parent[propertyName], depth - 1);
|
||||
Object.defineProperty(child, propertyName, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
return _clone(parent, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flat clone using prototype, accepts only objects, usefull for property
|
||||
* override on FLAT configuration object (no nested props).
|
||||
*
|
||||
* USE WITH CAUTION! This may not behave as you wish if you do not know how this
|
||||
* works.
|
||||
*/
|
||||
clone.clonePrototype = function clonePrototype(parent) {
|
||||
if (parent === null)
|
||||
return null;
|
||||
|
||||
var c = function () {};
|
||||
c.prototype = parent;
|
||||
return new c();
|
||||
};
|
||||
|
||||
// private utility functions
|
||||
|
||||
function __objToStr(o) {
|
||||
return Object.prototype.toString.call(o);
|
||||
}
|
||||
clone.__objToStr = __objToStr;
|
||||
|
||||
function __isDate(o) {
|
||||
return typeof o === 'object' && __objToStr(o) === '[object Date]';
|
||||
}
|
||||
clone.__isDate = __isDate;
|
||||
|
||||
function __isArray(o) {
|
||||
return typeof o === 'object' && __objToStr(o) === '[object Array]';
|
||||
}
|
||||
clone.__isArray = __isArray;
|
||||
|
||||
function __isRegExp(o) {
|
||||
return typeof o === 'object' && __objToStr(o) === '[object RegExp]';
|
||||
}
|
||||
clone.__isRegExp = __isRegExp;
|
||||
|
||||
function __getRegExpFlags(re) {
|
||||
var flags = '';
|
||||
if (re.global) flags += 'g';
|
||||
if (re.ignoreCase) flags += 'i';
|
||||
if (re.multiline) flags += 'm';
|
||||
return flags;
|
||||
}
|
||||
clone.__getRegExpFlags = __getRegExpFlags;
|
||||
|
||||
return clone;
|
||||
})();
|
||||
|
||||
export default clone
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "luch-request",
|
||||
"displayName": "luch-request",
|
||||
"version": "3.0.8",
|
||||
"description": "基于Promise开发的跨平台、项目级别的请求库,它有更小的体积,易用的api,方便简单的自定义能力",
|
||||
"keywords": [
|
||||
"request",
|
||||
"axios",
|
||||
"网络请求",
|
||||
"拦截器",
|
||||
"promise"
|
||||
],
|
||||
"dcloudext": {
|
||||
"category": [
|
||||
"JS SDK",
|
||||
"通用 SDK"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import App from './App'
|
||||
import uView from '@/uni_modules/uview-ui'
|
||||
Vue.use(uView)
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.prototype.$toast = (title, back = false) => {
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title
|
||||
})
|
||||
if (back) {
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
Vue.prototype.$jump = (url) => {
|
||||
uni.navigateTo({url})
|
||||
}
|
||||
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name" : "guoxue",
|
||||
"appid" : "__UNI__DFFC3F0",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx79d17ac6147366fd",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "2"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"pages": [
|
||||
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发现",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "详情",
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true,
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/msg/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消息",
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/ai/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/feedback",
|
||||
"style": {
|
||||
"navigationBarTitleText": "意见反馈",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationBarBackgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/setting",
|
||||
"style": {
|
||||
"navigationBarTitleText": "设置",
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarBackgroundColor": "#667eea",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/collect",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的收藏",
|
||||
"navigationBarBackgroundColor": "#f5f5f5",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的资料",
|
||||
"navigationBarBackgroundColor": "#f5f5f5",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/coin",
|
||||
"style": {
|
||||
"navigationBarTitleText": "金币中心",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#f5f5f5",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我是玄衡",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"navigationBarBackgroundColor": "#f7f7fb",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/select",
|
||||
"style": {
|
||||
"navigationBarTitleText": "选择对话",
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#3C4DA5",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "发现",
|
||||
"iconPath": "static/home.png",
|
||||
"selectedIconPath": "static/home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/msg/index",
|
||||
"text": "消息",
|
||||
"iconPath": "static/msg.png",
|
||||
"selectedIconPath": "static/msg-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/user/index",
|
||||
"text": "我的",
|
||||
"iconPath": "static/user.png",
|
||||
"selectedIconPath": "static/user-active.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 518 B |
|
After Width: | Height: | Size: 422 B |
|
After Width: | Height: | Size: 507 B |
|
After Width: | Height: | Size: 664 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,829 @@
|
||||
$bg: #F8F8F8;
|
||||
$yellow: #e2d602;
|
||||
$orange: #fd7600;
|
||||
$orange-deep: #FF4B00;
|
||||
$orange-light: #FFC5AD;
|
||||
$green: #98C147;
|
||||
$green-light: #a1ded0;
|
||||
$blue: #3C4DA5;
|
||||
$blue-light: #049DFF;
|
||||
$blue-light-50: #ebf3f8;
|
||||
$purple: #c770f6;
|
||||
$purple-light: #E0AFFB;
|
||||
$red: #FE6869;
|
||||
$red-light: #F7B2B2;
|
||||
$gray-weight: #666;
|
||||
$gray: #bbb;
|
||||
$gray-light: #F7F7F7;
|
||||
$gold: #B68320;
|
||||
$gold-light: #FFF0D3;
|
||||
$fdca00: #FDCA00;
|
||||
$u-primary: $blue;
|
||||
|
||||
//p, span {
|
||||
// font-size: 30rpx;
|
||||
// margin: 0;
|
||||
//}
|
||||
|
||||
h6 {
|
||||
font-weight: bolder !important;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.pos-r {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pos-a {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.bg {
|
||||
background-color: $bg;
|
||||
}
|
||||
|
||||
.bg-gray-light {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.bg-main {
|
||||
background-color: $u-primary;
|
||||
}
|
||||
|
||||
.bg-linear {
|
||||
background : linear-gradient(90deg, #049DFF, #2665c4);
|
||||
}
|
||||
|
||||
.bg-red {
|
||||
background-color: $red;
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background-color: $orange;
|
||||
}
|
||||
|
||||
.bg-blue {
|
||||
background-color: $blue;
|
||||
}
|
||||
|
||||
.bg-gold {
|
||||
background-color: $gold;
|
||||
}
|
||||
|
||||
.bg-green {
|
||||
background-color: $green;
|
||||
}
|
||||
|
||||
.bg-main-light {
|
||||
background-color: #E0F2FE;
|
||||
}
|
||||
|
||||
.bg-fff0 {
|
||||
background-color: #F0F0F0FF;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 2px 6px 0 rgba(191, 218, 251, 1);
|
||||
}
|
||||
|
||||
.shadow-top {
|
||||
box-shadow: 0px -2px 13px rgba(191, 218, 251, 1);
|
||||
}
|
||||
|
||||
.shadow-light {
|
||||
box-shadow: 0 4rpx 12rpx 0 rgba(206, 206, 206, 80);
|
||||
}
|
||||
|
||||
.shadow-50 {
|
||||
box-shadow: -2rpx 4rpx 14rpx 4rpx rgba(206, 206, 206, 50);
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20rpx;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.text-main {
|
||||
color: $u-primary;
|
||||
}
|
||||
|
||||
.text-orange {
|
||||
color: #fd7600;
|
||||
}
|
||||
|
||||
.text-blue {
|
||||
color: $blue;
|
||||
}
|
||||
|
||||
.text-blue-light {
|
||||
color: $blue-light;
|
||||
}
|
||||
|
||||
.text-green {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
.text-red {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.text-gray-weight {
|
||||
color: $gray-weight;
|
||||
}
|
||||
|
||||
.text-black {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.color-6C {
|
||||
color: #6C6C6C;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text-line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.color-9A {
|
||||
color: #9A9A9A;
|
||||
}
|
||||
|
||||
.color-333 {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.item-self {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-rev {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.flex-shrink {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flex-none {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1
|
||||
}
|
||||
|
||||
.flex-grow-0 {
|
||||
flex-grow: 0
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.h-100p {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-90p {
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.h-50p {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.h-10p {
|
||||
height: 10%;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.w-100p {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-5p {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.w-10p {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.w-15p {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.w-20p {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.w-25p {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.w-30p {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.w-33p {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.w-40p {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.w-45p {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.w-50p {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.w-60p {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.w-67p {
|
||||
width: 67%;
|
||||
}
|
||||
|
||||
.w-75p {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.w-80p {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.w-85p {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.w-90p {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.p-15 {
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.pb-15 {
|
||||
padding-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.pb-20 {
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.pb-30 {
|
||||
padding-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.p-20 {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.p-25 {
|
||||
padding: 25rpx;
|
||||
}
|
||||
|
||||
.p-30 {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.px-20 {
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.px-40 {
|
||||
padding: 0 40rpx;
|
||||
}
|
||||
|
||||
.p-50 {
|
||||
padding: 50rpx;
|
||||
}
|
||||
|
||||
.py-10 {
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.py-15 {
|
||||
padding: 15rpx 0;
|
||||
}
|
||||
|
||||
.py-20 {
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.py-25 {
|
||||
padding: 25rpx 0;
|
||||
}
|
||||
|
||||
.py-30 {
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.px-15 {
|
||||
padding: 0 15rpx;
|
||||
}
|
||||
|
||||
.px-20 {
|
||||
padding: 0 15rpx;
|
||||
}
|
||||
|
||||
.px-30 {
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.pl-15 {
|
||||
padding-left: 15rpx;
|
||||
}
|
||||
|
||||
.pl-20 {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.pr-15 {
|
||||
padding-right: 15rpx;
|
||||
}
|
||||
|
||||
.pr-20 {
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.pt-10 {
|
||||
padding-top: 10rpx;
|
||||
}
|
||||
|
||||
.pt-20 {
|
||||
padding-top: 20rpx;
|
||||
}
|
||||
|
||||
.pr-25 {
|
||||
padding-right: 25rpx;
|
||||
}
|
||||
|
||||
.m-a {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.m-10 {
|
||||
margin: 10rpx;
|
||||
}
|
||||
|
||||
.m-15 {
|
||||
margin: 15rpx;
|
||||
}
|
||||
|
||||
.m-20 {
|
||||
margin: 20rpx;
|
||||
}
|
||||
|
||||
.m-25 {
|
||||
margin: 25rpx;
|
||||
}
|
||||
|
||||
.m-30 {
|
||||
margin: 30rpx;
|
||||
}
|
||||
|
||||
.m-35 {
|
||||
margin: 35rpx;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.ml-15 {
|
||||
margin-left: 15rpx;
|
||||
}
|
||||
|
||||
.ml-25 {
|
||||
margin-left: 25rpx;
|
||||
}
|
||||
|
||||
.ml-35 {
|
||||
margin-left: 35rpx;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.mt-15 {
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.mt-25 {
|
||||
margin-top: 25rpx;
|
||||
}
|
||||
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.mt-40 {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.mt-50 {
|
||||
margin-top: 50rpx;
|
||||
}
|
||||
|
||||
.mb-25 {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.mb-40 {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.my-10 {
|
||||
margin-top: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.my-15 {
|
||||
margin-top: 15rpx;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.my-20 {
|
||||
margin-top: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.my-25 {
|
||||
margin-top: 25rpx;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.mx-15 {
|
||||
margin-left: 15rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.mx-20 {
|
||||
margin-left: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.mx-25 {
|
||||
margin-left: 25rpx;
|
||||
margin-right: 25rpx;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.mb-15 {
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.mr-15 {
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.ml-20 {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.mr-25 {
|
||||
margin-right: 25rpx;
|
||||
}
|
||||
|
||||
.mr-35 {
|
||||
margin-right: 35rpx;
|
||||
}
|
||||
|
||||
.text-15 {
|
||||
font-size: 15rpx;
|
||||
}
|
||||
|
||||
.text-20 {
|
||||
font-size: 20rpx;
|
||||
line-height: 20rpx;
|
||||
}
|
||||
|
||||
.text-24 {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.text-25 {
|
||||
font-size: 25rpx;
|
||||
line-height: 35rpx;
|
||||
}
|
||||
|
||||
.text-26 {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.text-28 {
|
||||
font-size: 28rpx;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
|
||||
.text-30 {
|
||||
font-size: 30rpx;
|
||||
line-height: 30rpx;
|
||||
}
|
||||
|
||||
.text-35 {
|
||||
font-size: 35rpx;
|
||||
line-height: 35rpx;
|
||||
}
|
||||
|
||||
.text-40 {
|
||||
font-size: 40rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.circle {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.rounded-16 {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.rounded-bottom {
|
||||
border-radius: 0 0 40rpx 40rpx;
|
||||
}
|
||||
|
||||
.rounded-top {
|
||||
border-radius: 40rpx 40rpx 0 0;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid $gray-light;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid $gray-light;
|
||||
}
|
||||
|
||||
|
||||
.border-right {
|
||||
border-right: 1px solid $gray-light;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid $gray;
|
||||
}
|
||||
|
||||
.canvas-hide {
|
||||
//position: fixed;
|
||||
//right: 100vw;
|
||||
//bottom: 100vh;
|
||||
//z-index: -9999;
|
||||
//opacity: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
@extend .rounded;
|
||||
@extend .bg-white;
|
||||
@extend .p-20;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@extend .w-100p;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.public-sub-btn {
|
||||
background: $fdca00;
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.b-20 {
|
||||
bottom: 20rpx;
|
||||
}
|
||||
|
||||
.p-button {
|
||||
background: linear-gradient(#E5CB98, #FBEDBC);
|
||||
color: #B68320;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "iconfont";
|
||||
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAeYAAsAAAAADlAAAAdLAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCEAgqOYItrATYCJAMkCxQABCAFhG0HcBv/CzOjdoNyEiD7nwnxpstuRCLrGmPTaffv/hnWZJHUNtWZeOD/3t03mks73vmC/3jA8SRom0aatimgfv7d9M8lBEJCIHRBKua0E7EEOqedMBEJm8N7js0MWpgq9PlMkGcTsS8NgH3X/AEbFaFGZNFvH+xR2uYofWlu9NO9ypmTgvNlW8CitMbdT7rHpgAXaKxBOfzWBdQCFgyrMV3kKtMLUFGd7GEC0JSkACq79OKAIMbDAW2W48z1QESSiQsUARGRZFyYAZ0AR8Q1ogsAjs7vhy+UBQIgwhPwR83HdB4F7R6gR25M+CrAUEwAobtM6LuBBBRA1qlLZvYO80KFg8isxmImAH1xHwzc0UPpQ/ph9cNLD+8+cn/9mnAgGQwUGkVj0B1kUhL4EP94IgQIVeCoDc62pg7uyLCRwUMCauARlsQ0ElWAq5EoAV9CogB8FzYieLq7qQM0rM5DYAriAik/Li6hNpHBj2knBBuokIJWymUizhppEknSNKNTIHnPfnR5WRIjFnmB0MgyTh9CFr9W5WgCNNfq00hdfi0KEVVL7I0kQvMYjy8OUbDAQIFwzDiGGzMKX+/PVu1sih2FbLk8PneQ0e84zT9a9GckbXu06x/h1C0hw45Twmlb0BLgUtCw7qqMfsqCGhV6bZl8tzTOhwXIwI1ZQCJLIIvccUoHE88h4XC2eGu4M/1XNB3fedRypMHlpyhbEwA5zn0qQ+4JIFxpmvQRIqHR6kOYxT8yRF6DufwjDl1JEG+8Wqk8ej0Z33StarLSdvb0fhAL+06dIRXWc4EDCLccDJ7nRTRGNUgi5S3sUk8hG4BAIuuqdJzCcXaWtWpZK0O7I0Mqs9SeBk6HJs+pMZ3cqWSo18Up8nh2vcql1drvYnRZfFaWawwlbAjnNuUeD0fz47f2GNsa/fF7o5EiX96JqiMhfWMOZ3c32aoWarUuu1GgZb3ZijxTszPBoebYYVU51WtjNSTXad3ekZzB2YUmmynf7Frv87CjOrjBApsDIHqPD//DhUKGSMSAEKRwBgqH+XDUGI0YIlaOKiYk2EnSqUEWF7XAageI3OVCf/PhsDEaNSKnhrSzKBTiQlVdDbJsPEl0ONSUS4ssTnLO4dOJ8O/y7lwLIsG9w6MxwIZTFYojgSR8c7B6ksJqZ8UOB6W0ubb0SifPuQb42mUpNFyJuQI0OLvELTPRSN5ODs3CBUKIBoDa1xrz1K/SNy3fVPVDMKK2XqdM6a9QLFycnZWRVUJ89QUvJOjZoAOfr7zKhGDrb79CEfP5M8/DTEvPngRh8AWBr9OZ66+89rX3tVtZ8tVf0r2fhur6+fMn0ALooJU7KIuS79+fpFNWX018UNVONCAU6vv7QGWHX5rbxpdvCW5JI9sFt9aUVW1pjXfr1K+5X6dOeOsts8vm/p1Iq+glCpUiObh4EVm2JbClPL5V603DrqjoTQomnlFsolUURBISwDpcroJ7b0oA02NQonc6VOv1iZVzBiYiduCcysTgHXF6OAN6GftRd0b3kZV9q0+AxEQ+IRESEvhEqIl262o2d+ZSUQfnmQcNY2dMXbp06rKxDTkJDJeW6vN3QmnfdO+etSJLmp0tSazIHjphQaZo7r6Dq2MZ3eom36nkLlp5St+X1jcrBkv4xK/79qbGfer3tXOWL0+NzJUNjuev1m2r8TwarGszs8uaVXMJRufavYvjGRWXNvLMw4a6hqnTp+OWddN3rFg+ixl4/IR5XXolag7ZzhxpDtvDmd17wTeo3HzocG+79mhzQ6GYS6SY3AUFNu1aUC5jsE6YcVIdLZk+YIqhI161tOWSVwtt2jUJey9Kv5VI+tWYJCCe8U3j1Nhp6t/ry8zF48oidd2GdLkxpPOAeiG9UXgmkpyh9YWSltIkJvUm3/2XckNk/emn1TPGhuRv5ddp5qpQ5XLRDWCzQqyzQ4tWrMxPsIRfxsInjYr7g9evq5O7w5KQAvAPYk1YW0p4kkSoGpozaS3WThNOfixfE8B+wzrpbqcdWAzzu1akq+jGw+IgeIpNPUYoW30gqPdHzI3c6qeif0AMzVuoE4HK3keDt+KE/PXzUOM3K4anQW5D40VTGYGmWAAeCflDN7ulJpW9TyBKHUAklQgJIYOROQVQQamAKkJbaMrX9fKWFqYDBmUZgJkVDBDGXhBRCAKGcdEJJzg3AKfxAiSYgAHJgFrckJIVVqPJSTBWzCD5AKVcG5mmE7ftHW1TxhL2O+o/lEBlsJjO43U3rFG6OCU87VLVgBGu4Moeh2XJ4IVzTHXqVP1xNjNZp05TrlqTVkfBWC3zDJKPPaVcm611kvj5O9qmjKVgyI/9H0qg7cNiOq/AuKG60pBT6RyedqmUacDoLVzBlTKxFNUMPnu/HFOdugaV/jijqUxVNB2eqs41gtM3LCcta4sQhsQIRxIkRQSSIRLEim8daOMprvsqcXCDRdX2cFgMy8TbcsJh6vBNMY++jmv7bXgQHHmqWy0A') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 22px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-undo:before {
|
||||
content: "\e607";
|
||||
}
|
||||
|
||||
.icon-redo:before {
|
||||
content: "\e606";
|
||||
}
|
||||
|
||||
.icon-img:before {
|
||||
content: "\e6e2";
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: "\e798";
|
||||
}
|
||||
|
||||
.icon-link:before {
|
||||
content: "\e60d";
|
||||
}
|
||||
|
||||
.icon-text:before {
|
||||
content: "\e6ce";
|
||||
}
|
||||
|
||||
.icon-clear:before {
|
||||
content: "\e637";
|
||||
}
|
||||
|
||||
.icon-save:before {
|
||||
content: "\e501";
|
||||
}
|
||||
|
||||
.required {
|
||||
@extend .pos-a;
|
||||
left: -9px;
|
||||
color: #f56c6c;
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.overflow-y {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.overflow-x {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.album-item {
|
||||
width: 190rpx;
|
||||
height: 190rpx;
|
||||
@extend .m-10;
|
||||
@extend .flex;
|
||||
@extend .justify-center;
|
||||
@extend .items-center;
|
||||
}
|
||||
|
||||
.del-wrap {
|
||||
@extend .pos-a;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border-radius: 0 0 0 5px;
|
||||
padding: 10rpx;
|
||||
background-color: $red;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.min-height {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.border-main{
|
||||
border-color: $u-primary !important;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
bottom: -80rpx;
|
||||
left: calc(50% - 20rpx);
|
||||
}
|
||||
|
After Width: | Height: | Size: 254 B |
|
After Width: | Height: | Size: 592 B |
@@ -0,0 +1,13 @@
|
||||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
@import '@/uni_modules/uview-ui/theme.scss';
|
||||
@import "@/static/style/common.scss";
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
@@ -0,0 +1,4 @@
|
||||
## 1.0.1(2024-08-09)
|
||||
更新文档
|
||||
## 1.0.0(2023-09-19)
|
||||
first
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "iRainna-dayjs",
|
||||
"displayName": "dayjs",
|
||||
"version": "1.0.1",
|
||||
"description": "可直接在uniapp中使用dayjs的内置方法",
|
||||
"keywords": [
|
||||
"dayjs"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "sdk-js",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "插件不采集任何数据",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "y",
|
||||
"百度": "y",
|
||||
"字节跳动": "y",
|
||||
"QQ": "y",
|
||||
"钉钉": "y",
|
||||
"快手": "y",
|
||||
"飞书": "y",
|
||||
"京东": "y"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "y",
|
||||
"联盟": "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Dayjs
|
||||
|
||||
Dayjs是一个极简的JavaScript库,可以解析、验证、操作和显示日期和时间。
|
||||
|
||||
`uniapp-vue3`推荐使用[rainui](https://ext.dcloud.net.cn/plugin?id=19701),内置dayjs、loadsh、animate.css,方便高效开发
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
## 2.0.0(2023-11-14)
|
||||
1、新增切换拦截返回当前点击index
|
||||
2、新增自动读取主页面配置功能
|
||||
3、优化部分代码结构实现
|
||||
注意:代码重构优化,2.0.0版本不支持老版本兼容
|
||||
## 1.3.3(2023-02-27)
|
||||
修复openType类型为navigateTo时,页面返回,重新点击不响应问题
|
||||
## 1.3.2(2023-02-27)
|
||||
修改占位空间点击事件遮挡问题
|
||||
## 1.3.1(2023-02-27)
|
||||
1、新增页面打开方式,可以自定义每个tabbarItem的点击打开方式
|
||||
2、新增tabbarHeight参数,方便你直接使用参数控制tabbar高度
|
||||
## 1.3.0(2023-02-21)
|
||||
新增ref方法若干,优化使用说明文档
|
||||
## 1.2.9(2023-02-21)
|
||||
修改safe计算,使用原生css,抛弃原始计算耗时能力
|
||||
## 1.2.8(2023-02-20)
|
||||
修正文档使用说明
|
||||
## 1.2.7(2023-02-20)
|
||||
1、新增角标显示
|
||||
2、新增setTabBarBadge,reLoad方法
|
||||
## 1.2.6(2023-02-13)
|
||||
修复判断safebottom底部安全距离数值写死问题
|
||||
## 1.2.5(2023-01-12)
|
||||
1、修复默认初始高度闪烁问题,不在使用计算,直接默认
|
||||
2、修复上个版本遗留current选中问题
|
||||
3、修复safeBottom底部判断错误问题
|
||||
4、更新使用说明文档
|
||||
## 1.2.4(2023-01-09)
|
||||
新增文档说明,增加示例项目页面
|
||||
## 1.2.3(2022-12-14)
|
||||
修复外部不跳转页面修改current,组件不响应bug
|
||||
## 1.2.2(2022-12-14)
|
||||
新增click事件,click事件会优先于任何事件
|
||||
## 1.2.1(2022-12-09)
|
||||
修改文档使用说明,路由拦截有平台兼容性,暂时无法解决,平台原生不支持
|
||||
## 1.2.0(2022-12-09)
|
||||
新增插槽,可以自定义凸起导航,优化文档使用说明
|
||||
## 1.1.9(2022-12-06)
|
||||
修复iconPath图片路径为网络路径判断错误问题
|
||||
## 1.1.7(2022-12-01)
|
||||
优化部分机器计算错误问题
|
||||
## 1.1.6(2022-12-01)
|
||||
修改高度计算错误问题
|
||||
## 1.1.5(2022-12-01)
|
||||
修复各别设备上高度计算有误问题
|
||||
## 1.1.4(2022-11-25)
|
||||
修复vue2版本在微信小程序上不支持:style传入参数bug,优化参数错误逻辑
|
||||
## 1.1.3(2022-11-24)
|
||||
修复native模式borderStyle样式丢失问题
|
||||
## 1.1.2(2022-11-24)
|
||||
修改描述文档说明,修改插件描述信息
|
||||
## 1.1.1(2022-11-24)
|
||||
优化使用说明文档
|
||||
## 1.1.0(2022-11-24)
|
||||
极度简化native模式,页面只需要引入组件即可,任何操作都不需要
|
||||
## 1.0.9(2022-11-24)
|
||||
修复native模式下,fill忘记计算高度.
|
||||
## 1.0.8(2022-11-24)
|
||||
优化native模式,简化参数数量,使用更简单
|
||||
## 1.0.7(2022-11-24)
|
||||
新增native配置,可以兼容原生tabbar,新增beforeChange,可自行根据要求自己兼容路由守卫
|
||||
## 1.0.6(2022-11-23)
|
||||
修改文档描述错误
|
||||
## 1.0.5(2022-11-23)
|
||||
修复fill高度遗漏安全距离问题,文档使用说明优化更新
|
||||
## 1.0.4(2022-11-23)
|
||||
优化配置选项,提取当前选中项,新增fixed配置
|
||||
## 1.0.3(2022-11-14)
|
||||
添加上阴影效果,修复由于去除了上线条,造成如果内容如果是白色,tabbar会和内容高度重合的问题
|
||||
## 1.0.2(2022-11-14)
|
||||
修改说明文档,更加详细备注说明
|
||||
## 1.0.1(2022-11-14)
|
||||
新增当前选中项class名,方便用户直接样式覆盖
|
||||
## 1.0.0(2022-11-14)
|
||||
第一个自定义tabbar版本
|
||||
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<view class="m-tabbar-box" :style="tabbarBoxStyle" v-if="isShowTabBar">
|
||||
<view class="m-tabbar__fill" v-if="fill || native" :class="{'m-tabbar__safe': (safeBottom || native)}"
|
||||
:style="tabbarFillStyle" />
|
||||
<view id="m-tabbar" class="m-tabbar"
|
||||
:class="{'fixed': (fixed || native), 'm-tabbar__safe': (safeBottom || native)}" :style="tabbarStyle">
|
||||
<view class="m-tabbar__border" v-if="borderStyle === 'black'" />
|
||||
<!-- tabbar border -->
|
||||
<view class="m-tabbar__flex">
|
||||
<view class="m-tabbar__item" @click="_tabChange(index)" v-for="(item, index) in tabbarList" :key="index"
|
||||
:class="{
|
||||
'm-tabbar__item__active': index === currentIndex,
|
||||
}">
|
||||
<slot :name="`tabbar_index_${index}`">
|
||||
<view class="m-tabbar__icon">
|
||||
<view class="m-tabbar__badge" v-if="item.dot">{{item.dot}}</view>
|
||||
<image :src="currentIndex === index ? item.selectedIconPath : item.iconPath"
|
||||
class="m-tabbar__icon_img" />
|
||||
</view>
|
||||
<view class="m-tabbar__label"
|
||||
:style="{'color': index === currentIndex ? tabbarConfig.selectedColor : tabbarConfig.color }">
|
||||
{{ item.text }}
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
<!-- tabbar item -->
|
||||
</view>
|
||||
<!-- tabbar item box -->
|
||||
</view>
|
||||
<!-- tabbar block -->
|
||||
</view>
|
||||
<!-- tabbar box -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// style转为string
|
||||
const obj2strStyle = (obj) => {
|
||||
let style = ''
|
||||
for (let key in obj) {
|
||||
style += `${key}:${obj[key]};`
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
// 自动填充路径
|
||||
const padFirstSymbol = (str, smb) => {
|
||||
if (str.startsWith(smb) || str.startsWith('http')) {
|
||||
return str
|
||||
}
|
||||
return `/${str}`
|
||||
}
|
||||
|
||||
// 替换原生路径问题
|
||||
const replaceTabbarList = (list) => {
|
||||
if (!list.length > 0) {
|
||||
return []
|
||||
}
|
||||
return list.map(item => {
|
||||
if (item.iconPath) {
|
||||
item.iconPath = padFirstSymbol(item.iconPath, '/')
|
||||
}
|
||||
if (item.pagePath) {
|
||||
item.pagePath = padFirstSymbol(item.pagePath, '/')
|
||||
}
|
||||
if (item.selectedIconPath) {
|
||||
item.selectedIconPath = padFirstSymbol(item.selectedIconPath, '/')
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// 设置缓存,方便重复调用方法
|
||||
const setTabbarStorage = (key, value, name) => {
|
||||
try {
|
||||
uni.setStorageSync(`mTab_${key}_${name}`, value)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 读取缓存
|
||||
const getTabbarStorage = (key, name) => {
|
||||
try {
|
||||
const value = uni.getStorageSync(`mTab_${key}_${name}`)
|
||||
if (value) {
|
||||
return value
|
||||
}
|
||||
return ''
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import PageConfig from '@/pages.json'
|
||||
|
||||
export default {
|
||||
emits: ['change', 'click'],
|
||||
props: {
|
||||
// 使用原生配置自动生成
|
||||
native: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 当前选择下标,默认0开始
|
||||
current: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
},
|
||||
// tabbar配置项
|
||||
tabbar: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
// 是否固定在底部
|
||||
fixed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否自动填充底部
|
||||
fill: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 显示层级
|
||||
zIndex: {
|
||||
type: [Number, String],
|
||||
default: 9999
|
||||
},
|
||||
// 是否自动规避底部安全距离
|
||||
safeBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 切换时拦截事件(有兼容性,部分平台不支持)
|
||||
beforeChange: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// 自定义tabbar高度,单位rpx
|
||||
tabbarHeight: {
|
||||
type: [Number, String],
|
||||
default: 100
|
||||
},
|
||||
// 自定义tabbar的name,方便读取默认tabbar
|
||||
name: {
|
||||
type: String,
|
||||
default: 'custom'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShowTabBar: false, //是否显示导航
|
||||
currentIndex: 0, //当前值
|
||||
beforeData: {}, //切换前值
|
||||
initTabbarConfig: {},
|
||||
isReload: false //uni不支持computed的get set,使用reload无感重置刷新
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
current(val) {
|
||||
this.currentIndex = val * 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tabbarConfig() {
|
||||
const {
|
||||
isReload,
|
||||
native,
|
||||
initTabbarConfig: tabbar
|
||||
} = this
|
||||
if(isReload){}
|
||||
// 如果是native模式,直接读取执行
|
||||
if (native) {
|
||||
if (!tabbar) {
|
||||
console.error('Native mode, Pages.json no tabbar config')
|
||||
return {
|
||||
borderStyle: 'black',
|
||||
list: []
|
||||
}
|
||||
}
|
||||
return tabbar
|
||||
}
|
||||
// 如果没有配置tabbar,会尝试在缓存读取
|
||||
if (!tabbar.color) {
|
||||
return this._getDefaultTabbar()
|
||||
}
|
||||
return tabbar
|
||||
},
|
||||
tabbarList() {
|
||||
const {
|
||||
isReload,
|
||||
tabbarConfig: {
|
||||
list
|
||||
}
|
||||
} = this
|
||||
if(isReload){}
|
||||
if (!list) {
|
||||
console.error('No tabbar config')
|
||||
return []
|
||||
}
|
||||
return replaceTabbarList(list)
|
||||
},
|
||||
borderStyle() {
|
||||
const {
|
||||
isReload,
|
||||
borderStyle
|
||||
} = this.tabbarConfig
|
||||
if(isReload){}
|
||||
return borderStyle
|
||||
},
|
||||
tabbarBoxStyle() {
|
||||
const {
|
||||
isReload,
|
||||
zIndex
|
||||
} = this
|
||||
if(isReload){}
|
||||
return obj2strStyle({
|
||||
'z-index': zIndex,
|
||||
})
|
||||
},
|
||||
tabbarFillStyle() {
|
||||
const {
|
||||
isReload,
|
||||
tabbarHeight
|
||||
} = this
|
||||
if(isReload){}
|
||||
return obj2strStyle({
|
||||
'height': `${tabbarHeight}rpx`
|
||||
})
|
||||
},
|
||||
tabbarStyle() {
|
||||
const {
|
||||
isReload,
|
||||
tabbarHeight,
|
||||
tabbarConfig: {
|
||||
backgroundColor
|
||||
}
|
||||
} = this
|
||||
if(isReload){}
|
||||
return obj2strStyle({
|
||||
'height': `${tabbarHeight}rpx`,
|
||||
'background-color': backgroundColor || '#fff',
|
||||
})
|
||||
},
|
||||
tabbarItemStyle() {
|
||||
const {
|
||||
isReload,
|
||||
currentIndex,
|
||||
tabbarConfig: {
|
||||
color,
|
||||
selectedColor
|
||||
}
|
||||
} = this
|
||||
if(isReload){}
|
||||
return obj2strStyle({
|
||||
'color': currentIndex ? selectedColor : color
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const {
|
||||
tabbar,
|
||||
native
|
||||
} = this
|
||||
const pageTabbar = PageConfig.tabBar
|
||||
this.initTabbarConfig = native ? pageTabbar : tabbar
|
||||
console.log(this.name)
|
||||
this._initTabbar()
|
||||
},
|
||||
methods: {
|
||||
_initTabbar() {
|
||||
const {
|
||||
current,
|
||||
fill,
|
||||
native,
|
||||
tabbarList
|
||||
} = this
|
||||
this.currentIndex = current * 1
|
||||
if (native && tabbarList.length > 0) {
|
||||
const currentPage = `/${getCurrentPages()[0].route}`
|
||||
const currentIndex = tabbarList.findIndex(item => item.pagePath === currentPage)
|
||||
this.currentIndex = currentIndex
|
||||
uni.hideTabBar()
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.isShowTabBar = true
|
||||
})
|
||||
this._setTabbarStorage('tabbarConfig')
|
||||
},
|
||||
_setTabbarStorage(key){
|
||||
setTabbarStorage(key, this.tabbarConfig, this.name)
|
||||
this._setReload()
|
||||
},
|
||||
_getDefaultTabbar() {
|
||||
return getTabbarStorage('tabbarConfig', this.name)
|
||||
},
|
||||
_checkMaxIndex(index) {
|
||||
const {
|
||||
tabbarConfig: {
|
||||
list
|
||||
}
|
||||
} = this
|
||||
if (list.length < 1) {
|
||||
return false
|
||||
}
|
||||
if (!list[index]) {
|
||||
console.error('Max tabbar index')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
_setReload(){
|
||||
this.isReload = true
|
||||
setTimeout(()=>{
|
||||
this.isReload = false
|
||||
})
|
||||
},
|
||||
setTabBarBadge(obj) {
|
||||
const {
|
||||
index,
|
||||
text
|
||||
} = obj
|
||||
if (this._checkMaxIndex(index)) {
|
||||
this.tabbarConfig.list[index].dot = text
|
||||
this._setTabbarStorage('tabbarConfig')
|
||||
}
|
||||
},
|
||||
setTabBarItem(obj) {
|
||||
const {
|
||||
index,
|
||||
text,
|
||||
pagePath: newPagePath,
|
||||
iconPath,
|
||||
selectedIconPath
|
||||
} = obj
|
||||
const {
|
||||
pagePath: oldPagePath
|
||||
} = this.tabbarConfig.list[index]
|
||||
if (this._checkMaxIndex(index)) {
|
||||
this.tabbarConfig.list[index] = {
|
||||
pagePath: newPagePath ? newPagePath : oldPagePath,
|
||||
text,
|
||||
iconPath,
|
||||
selectedIconPath
|
||||
}
|
||||
this._setTabbarStorage('tabbarConfig')
|
||||
}
|
||||
},
|
||||
showTabBar() {
|
||||
this.isShowTabBar = true
|
||||
},
|
||||
hideTabBar() {
|
||||
this.isShowTabBar = false
|
||||
},
|
||||
reload(){
|
||||
this.initTabbarConfig = this._getDefaultTabbar()
|
||||
},
|
||||
_tabChange(index) {
|
||||
const {
|
||||
currentIndex
|
||||
} = this
|
||||
this.$emit('click', index)
|
||||
if (index === currentIndex) {
|
||||
return
|
||||
}
|
||||
this.beforeData = {
|
||||
newIndex: index,
|
||||
oldIndex: currentIndex,
|
||||
next: this.jumpPage
|
||||
}
|
||||
if (this.beforeChange) {
|
||||
this.beforeChange(this._jumpPage, index)
|
||||
} else {
|
||||
this._jumpPage()
|
||||
}
|
||||
},
|
||||
_jumpPage() {
|
||||
const {
|
||||
native,
|
||||
beforeData: {
|
||||
newIndex: index
|
||||
},
|
||||
tabbarList: list
|
||||
} = this
|
||||
const {
|
||||
pagePath: url,
|
||||
openType
|
||||
} = list[index]
|
||||
if (url) {
|
||||
if (native) {
|
||||
uni.switchTab({
|
||||
url
|
||||
})
|
||||
} else {
|
||||
if (openType !== 'navigate') {
|
||||
this.currentIndex = index
|
||||
}
|
||||
switch (openType) {
|
||||
case 'navigate':
|
||||
uni.navigateTo({
|
||||
url
|
||||
})
|
||||
break;
|
||||
case 'redirect':
|
||||
uni.redirectTo({
|
||||
url
|
||||
})
|
||||
break;
|
||||
case 'reLaunch':
|
||||
uni.reLaunch({
|
||||
url
|
||||
})
|
||||
break;
|
||||
case 'switchTab':
|
||||
uni.switchTab({
|
||||
url
|
||||
})
|
||||
break;
|
||||
case 'navigateBack':
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
break;
|
||||
default:
|
||||
uni.reLaunch({
|
||||
url
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$emit('change', index)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.m-tabbar-box {
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.m-tabbar {
|
||||
position: relative;
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
&__safe {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
.m-tabbar__fill {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.m-tabbar__flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.m-tabbar__border {
|
||||
background-color: rgba(0, 0, 0, 0.33);
|
||||
width: 100%;
|
||||
height: 1rpx;
|
||||
transform: scaleY(0.5);
|
||||
}
|
||||
|
||||
.m-tabbar__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: 4px 0 2px;
|
||||
}
|
||||
|
||||
.m-tabbar__icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 6rpx;
|
||||
position: relative;
|
||||
|
||||
&_img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.m-tabbar__badge {
|
||||
color: #fff;
|
||||
background-color: #f00;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
position: absolute;
|
||||
right: -25rpx;
|
||||
left: 40rpx;
|
||||
padding: 2rpx 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.m-tabbar__label {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "m-tabbar",
|
||||
"displayName": "自定义tabbar、tabbar路由守卫、零配置tabbar、凸起导航",
|
||||
"version": "2.0.0",
|
||||
"description": "自定义tabbar,超高还原原生配置模式,一行代码自定义导航,自带tabbar路由守卫功能",
|
||||
"keywords": [
|
||||
"自定义tabbar,tabbar,自定义标签栏,tabbar路由守卫,零配置tabbar,路由守卫、凸起导航"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.5.4"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "y",
|
||||
"百度": "y",
|
||||
"字节跳动": "y",
|
||||
"QQ": "y",
|
||||
"钉钉": "y",
|
||||
"快手": "y",
|
||||
"飞书": "y",
|
||||
"京东": "y"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "y",
|
||||
"联盟": "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
# m-tabbar自定义
|
||||
|
||||
|
||||
## 使用说明,注意事项(必看)
|
||||
|
||||
# 我配套上传了一个案例包,强烈建议下载阅读体验使用
|
||||
|
||||
---
|
||||
|
||||
> 1、自定义tabbar的情况下,不建议在一个页面内通过几个组件,用v-if切换去模拟各个页面,会存在各种不可控bug
|
||||
|
||||
> 闪烁的话,可以使用一个页面,多个组件,如果是`if切换组件`的话,就是一个页面控制多个组件显示隐藏来实现。 如果组件封装的有问题,会出现组件之间的协调问题,请看情况使用。 还有一些原生的交互没有办法达到预期,会影响到原生体验。 比如下拉刷新,滚动加载更多,切换tabbar后滚动位置不能固定等
|
||||
|
||||
>2、在pages.json中正常定义tabbar配置和字段,使用`native`模式,组件会自动加载pages.json配置项,并自动判断当前选中项,并自动隐藏原生的tabbar, 但是有个闪烁问题,暂时无解,如果你有好的方案,欢迎指正
|
||||
|
||||
> 3、如果出现tabbar不显示,但是控制台无任何报错信息,应该就是样式布局影响了,请自行排查
|
||||
|
||||
> 4、因为是自定义导航,所以原生方法是不支持的,只能通过 ref 可以获取到 tabbar 实例并调用插件的实例方法,详细请看页面最下方
|
||||
|
||||
|
||||
---
|
||||
# 快速使用
|
||||
|
||||
## 方式一、Native模式使用
|
||||
|
||||
```
|
||||
// native模式,无需配置其他项
|
||||
<m-tabbar native></m-tabbar>
|
||||
```
|
||||
在各个tabbar页面引入tabbar组件,传入属性`native`,`native`模式下无需任何配置
|
||||
|
||||
组件会默认自动通过`uni.hideTabBar()`隐藏系统tabbar
|
||||
|
||||
|
||||
## 方式二、页面使用(current默认从0开始)(强烈推荐)
|
||||
|
||||
```
|
||||
// 普通页面模式
|
||||
<m-tabbar fixed fill current="1" :tabbar="tabbar"></m-tabbar>
|
||||
```
|
||||
|
||||
配置选项和`uniapp`的配置完全相同,直接复制过来, 默认传入`pagePath`后,直接使用`reLaunch`跳转
|
||||
插件支持扩展`openType`参数,用户可根据自己情况自行扩展页面打开方式,详细看下方配置
|
||||
|
||||
> 闪烁的话,可以使用一个页面,多个组件,如果是`if切换组件`的话,就是一个页面控制多个组件显示隐藏来实现。 如果组件封装的有问题,会出现组件之间的协调问题,请看情况使用。 还有一些原生的交互没有办法达到预期,会影响到原生体验。 比如下拉刷新,滚动加载更多,切换tabbar后滚动位置不能固定等
|
||||
|
||||
### 1、提取tabbar配置(2.0.0版本支持自动读取配置,主页面配置,内页可以不配置)
|
||||
|
||||
新建文件config/tabbar.js(默认你有config目录,根据自己情况而定)
|
||||
```
|
||||
export default {
|
||||
color: "#161616",
|
||||
selectedColor: "#161616",
|
||||
borderStyle: "black",
|
||||
backgroundColor: "#ffffff",
|
||||
list: [{
|
||||
pagePath: "/pages/index/index",
|
||||
iconPath: "/static/tabbar/index.png",
|
||||
selectedIconPath: "/static/tabbar/index_active.png",
|
||||
text: "首页",
|
||||
openType: 'navigate', //新版本新增,页面被打开方式,默认为reLaunch
|
||||
dot: 1 //新版本新增参数,详细看页面最下方使用说明
|
||||
}, {
|
||||
pagePath: "/pages/shop/index",
|
||||
iconPath: "/static/tabbar/shop.png",
|
||||
selectedIconPath: "/static/tabbar/shop_active.png",
|
||||
text: "门店"
|
||||
}, {
|
||||
pagePath: "/pages/my/index",
|
||||
iconPath: "/static/tabbar/my.png",
|
||||
selectedIconPath: "/static/tabbar/my_active.png",
|
||||
text: "我的"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### 2、引入tabbar (2.0.0版本支持自动读取配置,主页面配置,内页可以不引入)
|
||||
#### VUE2引入
|
||||
```
|
||||
import TabbarConfig from '@/config/tabbar.js'
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
tabbar: TabbarConfig
|
||||
}
|
||||
},
|
||||
onLoad(){
|
||||
// 没有开启native模式下,使用reLaunch跳转,会存在首页标志,需要隐藏
|
||||
#ifdef MP-JD || MP-WEIXIN
|
||||
uni.hideHomeButton()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### VUE3 setup引入
|
||||
```
|
||||
import TabbarConfig from '@/config/tabbar.js'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
// 没有开启native模式下,使用reLaunch跳转,会存在首页标志,需要隐藏
|
||||
#ifdef MP-JD || MP-WEIXIN
|
||||
uni.hideHomeButton()
|
||||
#endif
|
||||
|
||||
const tabbar = reactive(TabbarConfig)
|
||||
```
|
||||
|
||||
### 3、页面使用
|
||||
```
|
||||
<m-tabbar fixed fill current="1" :tabbar="tabbar"></m-tabbar>
|
||||
```
|
||||
|
||||
## 高级用法(beforeChange)(路由守卫)
|
||||
|
||||
有些特殊需求,我们在点击一个tabbar其他一项的时候,可能需要判断权限是否可以进入,那么我们在切换前做一下路由拦截`beforeChange`,如果达到自己的预期,就进行跳转
|
||||
|
||||
> uniapp 微信小程序不支持$listeners,只能使用prop方式传入, 部分平台不支持prop传入方法,有平台限制,详细请看(问题解答)[https://ask.dcloud.net.cn/question/70659]
|
||||
|
||||
### 页面使用传入beforeChange
|
||||
|
||||
```
|
||||
// native模式,无需传入 fixed fill
|
||||
<m-tabbar native :beforeChange="onBeforeChange"></m-tabbar>
|
||||
|
||||
// 普通页面模式
|
||||
<m-tabbar fixed fill current="1" :tabbar="tabbar" :beforeChange="onBeforeChange"></m-tabbar>
|
||||
```
|
||||
|
||||
### 进行事件判断监听
|
||||
|
||||
函数必选参数 next,当判断逻辑执行完毕后,满足条件的情况下执行 `next()`
|
||||
|
||||
```
|
||||
methods: {
|
||||
onBeforeChange(next, index){
|
||||
//index为当前点击的是第几项,0开始
|
||||
console.log('before page2 switch')
|
||||
setTimeout(()=>{
|
||||
console.log('switch page2 end')
|
||||
next()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义凸起导航(插槽使用)
|
||||
|
||||
```
|
||||
<m-tabbar native>
|
||||
<template v-slot:tabbar_index_1> //插槽详细看文档,样式你自己写
|
||||
<view class="custom_style">
|
||||
<view class="custom_style_icon">+</view>
|
||||
</view>
|
||||
</template>
|
||||
</m-tabbar>
|
||||
<style lang="scss">
|
||||
.custom_style{
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
&_icon{
|
||||
background-color: #15357A;
|
||||
font-size: 80rpx;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: -40rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### 属性说明(Native模式)
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| ------------ | ------------- | ------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| zIndex | Number,String | 999 | 否 | 当前处于z-index层级 |
|
||||
| native | Boolean | false | 否 | native模式,当前页面是系统原生tabbar页面(pages.json里面配置了tabBar) |
|
||||
| beforeChange | Function | null | 否 | 导航切换前事件hooks,用于判断点击tabbar的时候,可以先执行自己定义的事件,达到预期后在跳转(类似router的路由守卫),方法需要调用next参数回调,部分平台不支持,存在兼容性 |
|
||||
| tabbarHeight | Number,String | 100 | 否 | 默认tabbar高度,有些时候你可能想控制tabbar高度(没啥用,如果非要更改,布局有影响,请使用样式覆盖) |
|
||||
| name | string | custom | 否 | 用于内部调用页面时,自动读取前一个页面的配置,无需二次配置 |
|
||||
|
||||
### 属性说明(普通模式)
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 必填 | 说明 |
|
||||
| ------------ | ------------- | ------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| current | Number,String | 0 | 是 | 默认选中第几项,0开始 |
|
||||
| tabbar | Object | {} | 是 | tabbar配置项(新增dot参数,详细看下方使用说明) |
|
||||
| fixed | Boolean | false | 否 | 是否定位在底部 |
|
||||
| fill | Boolean | false | 否 | 是否填充底部高度(如果开启fixed后,会出现tabbar遮盖内容问题,开启此属性,会自动填充高度,可单独使用) |
|
||||
| safeBottom | Boolean | true | 否 | 是否自动规避iphoneX\XR等底部安全距离 |
|
||||
| zIndex | Number,String | 999 | 否 | 当前处于z-index层级 |
|
||||
| beforeChange | Function | null | 否 | 导航切换前事件hooks,用于判断点击tabbar的时候,可以先执行自己定义的事件,达到预期后在跳转(类似router的路由守卫),方法需要调用next参数回调,部分平台不支持,存在兼容性 |
|
||||
| tabbarHeight | Number,String | 100 | 否 | 默认tabbar高度,有些时候你可能想控制tabbar高度(没啥用,如果非要更改,布局有影响,请使用样式覆盖) |
|
||||
| name | string | custom | 否 | 用于内部调用页面时,自动读取前一个页面的配置,无需二次配置 |
|
||||
|
||||
### openType对应值(默认reLaunch跳转)
|
||||
|
||||
| 方法名 | 返回值说明 |
|
||||
| ------- | ------- |
|
||||
| navigate | [对应navigateTo](https://uniapp.dcloud.io/api/router?id=navigateto) |
|
||||
| redirect | [对应redirectTo](https://uniapp.dcloud.io/api/router?id=redirectto) |
|
||||
| reLaunch | [对应reLaunch](https://uniapp.dcloud.io/api/router?id=relaunch) |
|
||||
| switchTab | [对应switchTab](https://uniapp.dcloud.io/api/router?id=switchtab) 注意此方法需要你有原生tabbar,比如内页使用了自定义导航,想回tabbar的指定页面 |
|
||||
| navigateBack | [对应navigateBack](https://uniapp.dcloud.io/api/router?id=navigateback) 只能后退一步 |
|
||||
|
||||
### 方法说明
|
||||
|
||||
| 方法名 | 返回值说明 |
|
||||
| ------- | ------- |
|
||||
| click | 当前选中index,无论什么情况下都会先触发click事件,方便自由定制更多方法 |
|
||||
| change | 当前选中index(beforeChange会在change之前执行,只有执行next才会返回) |
|
||||
|
||||
### 插槽 (注意Vue3存在跨断不兼容问题)
|
||||
|
||||
| 插槽名 | 返回值说明 |
|
||||
| ------- | ------- |
|
||||
| tabbar_index_{index} | 插槽名字为tabbar_index_你要变化的index, 可以做到任意控制自己的导航,比如中心凸起,比如你想让第一个变化,index就是0,比如你tabbarList里面有5个item,你想让中间的凸起,那么index就是2,取下标 |
|
||||
|
||||
### 方法 (通过 ref 可以获取到 tabbar 实例并调用实例方法)
|
||||
|
||||
> 注意,由于是使用了自定义,所以原生的方法是不能使用的,只能通过 ref 可以获取到 tabbar 实例并调用实例方法,目前只有下面4个方法,而且方法是没有`success,fail,complete`回掉的,考虑到跨平台型,如果原生方法有平台差异,插件也是不考虑支持的
|
||||
|
||||
| 事件名 | 参数 | 参数说明|
|
||||
| ------- | ------- | ------- |
|
||||
| setTabBarBadge | object | [为 tabBar 某一项的右上角添加文本](https://uniapp.dcloud.io/api/ui/tabbar?id=settabbarbadge) |
|
||||
| setTabBarItem | object | [动态设置 tabBar 某一项的内容](https://uniapp.dcloud.io/api/ui/tabbar?id=settabbaritem) |
|
||||
| reload | 无 | 有特殊情况下,你可能需要调用重新载入tabbar配置,比如在内页进行了配置更新,返回前一个页面,自动读取前一个页面的配置 |
|
||||
| showTabBar | 无 | 显示tabbar,默认显示,无动画效果 |
|
||||
| hideTabBar | 无 | 隐藏tabbar,无动画效果 |
|
||||
|
||||
|
||||
```
|
||||
//页面调用组件添加ref
|
||||
<m-tabbar ref="tabbar" native></m-tabbar>
|
||||
// setTabBarBadge ,为 tabBar 某一项的右上角添加文本
|
||||
this.$refs.tabbar.setTabBarBadge({
|
||||
index: 0,
|
||||
text: '10'
|
||||
})
|
||||
|
||||
//setTabBarItem 动态设置 tabBar 某一项的内容
|
||||
this.$refs.tabbar.setTabBarBadge({
|
||||
index: 0,
|
||||
text: 'text',
|
||||
pagePath: 'newPagePath',
|
||||
//插件新增pagePath,注意native模式下,如果更改了pagePath,可能存在选中项自动选中失败问题
|
||||
iconPath: '/path/to/iconPath',
|
||||
selectedIconPath: '/path/to/selectedIconPath'
|
||||
})
|
||||
|
||||
//如果直接在onload或者onshow等组件还在加载中的特殊情况下,由于加载比较慢,
|
||||
// 方法可能会失效,建议放在nextTick函数里面
|
||||
this.$nextTick(()=>{
|
||||
this.$refs.tabbar.setTabBarBadge({
|
||||
index: 0,
|
||||
text: '10'
|
||||
})
|
||||
})
|
||||
//reload重载整个组件
|
||||
this.$nextTick(()=>{
|
||||
this.$refs.tabbar.reload()
|
||||
})
|
||||
```
|
||||
|
||||
tabbarConfig参数新增dot配置项,可以单独配置每一项的右上角角标,可传入任意类型,不显示为空即可或者不填写, 默认为红色,如果想更改样式,请使用样式覆盖`m-tabbar__badge`
|
||||
```
|
||||
list: [{
|
||||
pagePath: "/pages/index/index",
|
||||
iconPath: "/static/tabbar/index.png",
|
||||
selectedIconPath: "/static/tabbar/index_active.png",
|
||||
text: "首页",
|
||||
dot: 1
|
||||
}, {
|
||||
pagePath: "/pages/shop/index",
|
||||
iconPath: "/static/tabbar/shop.png",
|
||||
selectedIconPath: "/static/tabbar/shop_active.png",
|
||||
text: "门店",
|
||||
dot: ''
|
||||
}, {
|
||||
pagePath: "/pages/my/index",
|
||||
iconPath: "/static/tabbar/my.png",
|
||||
selectedIconPath: "/static/tabbar/my_active.png",
|
||||
text: "我的",
|
||||
dot: ''
|
||||
}]
|
||||
```
|
||||
|
||||
|
||||
### 目前已知问题
|
||||
|
||||
- 1、在全局加样式 filter: grayscale(1) 后,tabbar组件的fixed样式失效,排版在页面最底部(无法修复)
|
||||
- 2、微信小程序native模式首次进入的时候,底部tabbar会闪动一下(无法修复) 建议全局页面做一个延迟加载效果,等全部加载完成后,在显示
|
||||
|
||||
|
||||
### 插件写的时候,没办法照顾到所有平台,欢迎点评指正,如有问题欢迎给我留言
|
||||
|
||||
#### 例如:
|
||||
```
|
||||
设备:iphone13
|
||||
系统: ios13
|
||||
使用环境平台: 微信小程序、app
|
||||
使用vue版本 :vue3
|
||||
问题描述: 提示什么什么错误
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
## 1.0.1(2023-09-19)
|
||||
改为uni_modules规范
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<view class="progress-bar" :style="'min-height:' + height + 'rpx'">
|
||||
<view v-if="!isCircular" class="progress-bar-bg"
|
||||
:style="{ backgroundColor: backgroundColor, height: height + 'rpx' }"></view>
|
||||
<view v-if="!isCircular" class="progress-bar-inner"
|
||||
:style="{ width: progress + '%', backgroundColor: progressBackgroundColor, height: height + 'rpx' }">
|
||||
<view v-if="showText" class="progress-bar-text" :style="{ color: textColor, fontSize: textSize + 'rpx' }">
|
||||
{{ progress + '%' }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="isCircular" class="progress-bar-circular"
|
||||
:style="{ width: diameter + 'rpx', height: diameter + 'rpx' }">
|
||||
<canvas :canvas-id="canvasId" :style="{ width: diameter + 'rpx', height: diameter + 'rpx' }"></canvas>
|
||||
<view v-if="showText" class="progress-bar-text" :style="{ color: textColor, fontSize: textSize + 'rpx' }">
|
||||
{{ progress + '%' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
screenWidth: 0, // 屏幕宽度
|
||||
}
|
||||
},
|
||||
props: {
|
||||
progress: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator(value) {
|
||||
// 自定义验证函数,限制范围为 0 到 100
|
||||
return value >= 0 && value <= 100;
|
||||
}
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: '#EFEFF4'
|
||||
},
|
||||
progressBackgroundColor: {
|
||||
type: String,
|
||||
default: '#07C160'
|
||||
},
|
||||
showText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
},
|
||||
textSize: {
|
||||
type: Number,
|
||||
default: 28
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
isCircular: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
diameter: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
canvasId: {
|
||||
type: String,
|
||||
default: 'canvasId'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 获取设备信息
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
this.screenWidth = res.screenWidth;
|
||||
if (this.isCircular) {
|
||||
this.drawCircularProgress();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
progress: function(val) {
|
||||
console.log(val)
|
||||
if (this.isCircular) {
|
||||
this.drawCircularProgress();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
drawCircularProgress() {
|
||||
const canvas = uni.createCanvasContext(this.canvasId, this);
|
||||
const radius = (this.rpxToPx(this.diameter) - this.rpxToPx(this.height)) /
|
||||
2; // Subtracting 10 to leave space for the border
|
||||
|
||||
const startAngle = -Math.PI / 2;
|
||||
const endAngle = (2 * Math.PI * this.progress) / 100 + startAngle;
|
||||
// Draw background circle
|
||||
canvas.setLineWidth(this.rpxToPx(this.height));
|
||||
canvas.setStrokeStyle(this.backgroundColor);
|
||||
canvas.beginPath();
|
||||
canvas.arc(this.rpxToPx(this.diameter) / 2, this.rpxToPx(this.diameter) / 2, radius, 0, 2 * Math.PI);
|
||||
canvas.stroke();
|
||||
|
||||
// Draw progress arc
|
||||
canvas.setLineWidth(this.rpxToPx(this.height));
|
||||
canvas.setStrokeStyle(this.progressBackgroundColor);
|
||||
canvas.beginPath();
|
||||
canvas.arc(this.rpxToPx(this.diameter) / 2, this.rpxToPx(this.diameter) / 2, radius, startAngle, endAngle,
|
||||
false);
|
||||
canvas.stroke();
|
||||
canvas.draw();
|
||||
},
|
||||
rpxToPx(rpx) {
|
||||
// 计算像素值
|
||||
const px = (rpx / 750) * this.screenWidth
|
||||
return px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.progress-bar-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 10rpx;
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.progress-bar-circular {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "piaoyi-progress-bar",
|
||||
"name": "piaoyi进度条",
|
||||
"displayName": "piaoyi进度条",
|
||||
"version": "1.0.1",
|
||||
"description": "进度条 可设置环状 开箱即用",
|
||||
"keywords": [
|
||||
"进度条",
|
||||
"可设置环状",
|
||||
"开箱即用"
|
||||
],
|
||||
"dcloudext": {
|
||||
"type": "component-vue"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
### piaoyi-progress-bar 进度条
|
||||
|
||||
**使用方法:**
|
||||
|
||||
正常进度条
|
||||
```
|
||||
<piaoyiProgressBar :progress="50" backgroundColor="#EFEFF4" progressBackgroundColor="#07C160" :showText="true"
|
||||
textColor="#fff" :textSize="24" :height="20"></piaoyiProgressBar>
|
||||
<view class="bg">
|
||||
```
|
||||
|
||||
环状进度条
|
||||
```
|
||||
<piaoyiProgressBar canvasId="progressCanvas4" :progress="75" backgroundColor="#EFEFF4"
|
||||
progressBackgroundColor="#07C160" :showText="true" textColor="#000000" :textSize="24" :height="20"
|
||||
:isCircular="true" :diameter="200"></piaoyiProgressBar>
|
||||
<view class="bg">
|
||||
```
|
||||
|
||||
```
|
||||
import piaoyiProgressBar from '@/uni_modules/piaoyi-progress-bar/components/piaoyi-progress-bar/piaoyi-progress-bar.vue';
|
||||
export default {
|
||||
components: {
|
||||
piaoyiProgressBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 事件说明
|
||||
|
||||
无
|
||||
|
||||
#### Prop
|
||||
|
||||
| 参数名称 | 描述 | 默认值 |
|
||||
| -------- | ------------------------------ | ------------------------------ |
|
||||
| progress | 进度值 | 空(范围:0-100) |
|
||||
| backgroundColor | 背景色 | #EFEFF4 |
|
||||
| progressBackgroundColor | 进度背景色 | #07C160 |
|
||||
| showText | 是否显示文本 | true |
|
||||
| textColor | 文本颜色 | #000000 |
|
||||
| textSize | 文本大小 | 24 |
|
||||
| height | 进度条线条宽度 | 20 |
|
||||
| diameter | 进度条整体大小 | 200 |
|
||||
| isCircular | 是否显示环状 | false |
|
||||
| canvasId | isCircular为true时必传,一个页面使用多个需要定义不同的canvasId | canvasId |
|
||||
|
||||
### 可接定制化组件开发
|
||||
### 右侧有本人代表作小程序二维码,可以扫码体验
|
||||
### 如使用过程中有问题或有一些好的建议,欢迎加QQ群互相学习交流:120594820
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 www.uviewui.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,66 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
|
||||
</p>
|
||||
<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView 2.0</h3>
|
||||
<h3 align="center">多平台快速开发的UI框架</h3>
|
||||
|
||||
[](https://github.com/umicro/uView2.0)
|
||||
[](https://github.com/umicro/uView2.0)
|
||||
[](https://github.com/umicro/uView2.0/issues)
|
||||
[](https://uviewui.com)
|
||||
[](https://gitee.com/umicro/uView2.0/releases)
|
||||
[](https://en.wikipedia.org/wiki/MIT_License)
|
||||
|
||||
## 说明
|
||||
|
||||
uView UI,是[uni-app](https://uniapp.dcloud.io/)全面兼容nvue的uni-app生态框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
|
||||
|
||||
## [官方文档:https://uviewui.com](https://uviewui.com)
|
||||
|
||||
|
||||
## 预览
|
||||
|
||||
您可以通过**微信**扫码,查看最佳的演示效果。
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
|
||||
|
||||
|
||||
## 链接
|
||||
|
||||
- [官方文档](https://www.uviewui.com/)
|
||||
- [更新日志](https://www.uviewui.com/components/changelog.html)
|
||||
- [升级指南](https://www.uviewui.com/components/changeGuide.html)
|
||||
- [关于我们](https://www.uviewui.com/cooperation/about.html)
|
||||
|
||||
## 交流反馈
|
||||
|
||||
欢迎加入我们的QQ群交流反馈:[点此跳转](https://www.uviewui.com/components/addQQGroup.html)
|
||||
|
||||
## 关于PR
|
||||
|
||||
> 我们非常乐意接受各位的优质PR,但在此之前我希望您了解uView2.0是一个需要兼容多个平台的(小程序、h5、ios app、android app)包括nvue页面、vue页面。
|
||||
> 所以希望在您修复bug并提交之前尽可能的去这些平台测试一下兼容性。最好能携带测试截图以方便审核。非常感谢!
|
||||
|
||||
## 安装
|
||||
|
||||
#### **uni-app插件市场链接** —— [https://ext.dcloud.net.cn/plugin?id=1593](https://ext.dcloud.net.cn/plugin?id=1593)
|
||||
|
||||
请通过[官网安装文档](https://www.uviewui.com/components/install.html)了解更详细的内容
|
||||
|
||||
## 快速上手
|
||||
|
||||
请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容
|
||||
|
||||
## 使用方法
|
||||
配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
|
||||
|
||||
```html
|
||||
<template>
|
||||
<u-button text="按钮"></u-button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 版权信息
|
||||
uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
## 2.0.38(2024-06-12)
|
||||
插件市场处理
|
||||
## 2.0.37(2024-03-17)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复表单校验`trigger`触发器参数无效问题
|
||||
2. 修复`u-input`组件的`password`属性在动态切换为`false`时失效的问题
|
||||
3. 添加微信小程序用户同意隐私协议事件回调
|
||||
4. 修复支付宝小程序picker样式问题
|
||||
5. `u-modal`添加`duration`字段控制动画过度时间
|
||||
6. 修复`picker` `lastIndex`异常导致的`column`异常问题
|
||||
7. `tabs`增加长按事件支持
|
||||
8. 修复`u-avatar` `square`属性在小程序`open-data`下无效问题
|
||||
9. 其他一些修复
|
||||
## 2.0.36(2023-03-27)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 重构`deepClone` & `deepMerge`方法
|
||||
2. 其他优化
|
||||
## 2.0.34(2022-09-24)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. `u-input`、`u-textarea`增加`ignoreCompositionEvent`属性
|
||||
2. 修复`route`方法调用可能报错的问题
|
||||
3. 修复`u-no-network`组件`z-index`无效的问题
|
||||
4. 修复`textarea`组件在h5上confirmType=""报错的问题
|
||||
5. `u-rate`适配`nvue`
|
||||
6. 优化验证手机号码的正则表达式(根据工信部发布的《电信网编号计划(2017年版)》进行修改。)
|
||||
7. `form-item`添加`labelPosition`属性
|
||||
8. `u-calendar`修复`maxDate`设置为当前日期,并且当前时间大于08:00时无法显示日期列表的问题 (#724)
|
||||
9. `u-radio`增加一个默认插槽用于自定义修改label内容 (#680)
|
||||
10. 修复`timeFormat`函数在safari重的兼容性问题 (#664)
|
||||
## 2.0.33(2022-06-17)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复`loadmore`组件`lineColor`类型错误问题
|
||||
2. 修复`u-parse`组件`imgtap`、`linktap`不生效问题
|
||||
## 2.0.32(2022-06-16)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
1. `u-loadmore`新增自定义颜色、虚/实线
|
||||
2. 修复`u-swiper-action`组件部分平台不能上下滑动的问题
|
||||
3. 修复`u-list`回弹问题
|
||||
4. 修复`notice-bar`组件动画在低端安卓机可能会抖动的问题
|
||||
5. `u-loading-page`添加控制图标大小的属性`iconSize`
|
||||
6. 修复`u-tooltip`组件`color`参数不生效的问题
|
||||
7. 修复`u--input`组件使用`blur`事件输出为`undefined`的bug
|
||||
8. `u-code-input`组件新增键盘弹起时,是否自动上推页面参数`adjustPosition`
|
||||
9. 修复`image`组件`load`事件无回调对象问题
|
||||
10. 修复`button`组件`loadingSize`设置无效问题
|
||||
10. 其他修复
|
||||
## 2.0.31(2022-04-19)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复`upload`在`vue`页面上传成功后没有成功标志的问题
|
||||
2. 解决演示项目中微信小程序模拟上传图片一直出于上传中问题
|
||||
3. 修复`u-code-input`组件在`nvue`页面编译到`app`平台上光标异常问题(`app`去除此功能)
|
||||
4. 修复`actionSheet`组件标题关闭按钮点击事件名称错误的问题
|
||||
5. 其他修复
|
||||
## 2.0.30(2022-04-04)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. `u-rate`增加`readonly`属性
|
||||
2. `tabs`滑块支持设置背景图片
|
||||
3. 修复`u-subsection` `mode`为`subsection`时,滑块样式不正确的问题
|
||||
4. `u-code-input`添加光标效果动画
|
||||
5. 修复`popup`的`open`事件不触发
|
||||
6. 修复`u-flex-column`无效的问题
|
||||
7. 修复`u-datetime-picker`索引在特定场合异常问题
|
||||
8. 修复`u-datetime-picker`最小时间字符串模板错误问题
|
||||
9. `u-swiper`添加`m3u8`验证
|
||||
10. `u-swiper`修改判断image和video逻辑
|
||||
11. 修复`swiper`无法使用本地图片问题,增加`type`参数
|
||||
12. 修复`u-row-notice`格式错误问题
|
||||
13. 修复`u-switch`组件当`unit`为`rpx`时,`nodeStyle`消失的问题
|
||||
14. 修复`datetime-picker`组件`showToolbar`与`visibleItemCount`属性无效的问题
|
||||
15. 修复`upload`组件条件编译位置判断错误,导致`previewImage`属性设置为`false`时,整个组件都会被隐藏的问题
|
||||
16. 修复`u-checkbox-group`设置`shape`属性无效的问题
|
||||
17. 修复`u-upload`的`capture`传入字符串的时候不生效的问题
|
||||
18. 修复`u-action-sheet`组件,关闭事件逻辑错误的问题
|
||||
19. 修复`u-list`触顶事件的触发错误的问题
|
||||
20. 修复`u-text`只有手机号可拨打的问题
|
||||
21. 修复`u-textarea`不能换行的问题
|
||||
22. 其他修复
|
||||
## 2.0.29(2022-03-13)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复`u--text`组件设置`decoration`属性未生效的问题
|
||||
2. 修复`u-datetime-picker`使用`formatter`后返回值不正确
|
||||
3. 修复`u-datetime-picker` `intercept` 可能为undefined
|
||||
4. 修复已设置单位 uni..config.unit = 'rpx'时,线型指示器 `transform` 的位置翻倍,导致指示器超出宽度
|
||||
5. 修复mixin中bem方法生成的类名在支付宝和字节小程序中失效
|
||||
6. 修复默认值传值为空的时候,打开`u-datetime-picker`报错,不能选中第一列时间的bug
|
||||
7. 修复`u-datetime-picker`使用`formatter`后返回值不正确
|
||||
8. 修复`u-image`组件`loading`无效果的问题
|
||||
9. 修复`config.unit`属性设为`rpx`时,导航栏占用高度不足导致塌陷的问题
|
||||
10. 修复`u-datetime-picker`组件`itemHeight`无效问题
|
||||
11. 其他修复
|
||||
## 2.0.28(2022-02-22)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. search组件新增searchIconSize属性
|
||||
2. 兼容Safari/Webkit中传入时间格式如2022-02-17 12:00:56
|
||||
3. 修复text value.js 判断日期出format错误问题
|
||||
4. priceFormat格式化金额出现精度错误
|
||||
5. priceFormat在部分情况下出现精度损失问题
|
||||
6. 优化表单rules提示
|
||||
7. 修复avatar组件src为空时,展示状态不对
|
||||
8. 其他修复
|
||||
## 2.0.27(2022-01-28)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1.样式修复
|
||||
## 2.0.26(2022-01-28)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1.样式修复
|
||||
## 2.0.25(2022-01-27)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复text组件mode=price时,可能会导致精度错误的问题
|
||||
2. 添加$u.setConfig()方法,可设置uView内置的config, props, zIndex, color属性,详见:[修改uView内置配置方案](https://uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE)
|
||||
3. 优化form组件在errorType=toast时,如果输入错误页面会有抖动的问题
|
||||
4. 修复$u.addUnit()对配置默认单位可能无效的问题
|
||||
## 2.0.24(2022-01-25)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复swiper在current指定非0时缩放有误
|
||||
2. 修复u-icon添加stop属性的时候报错
|
||||
3. 优化遗留的通过正则判断rpx单位的问题
|
||||
4. 优化Layout布局 vue使用gutter时,会超出固定区域
|
||||
5. 优化search组件高度单位问题(rpx -> px)
|
||||
6. 修复u-image slot 加载和错误的图片失去了高度
|
||||
7. 修复u-index-list中footer插槽与header插槽存在性判断错误
|
||||
8. 修复部分机型下u-popup关闭时会闪烁
|
||||
9. 修复u-image在nvue-app下失去宽高
|
||||
10. 修复u-popup运行报错
|
||||
11. 修复u-tooltip报错
|
||||
12. 修复box-sizing在app下的警告
|
||||
13. 修复u-navbar在小程序中报运行时错误
|
||||
14. 其他修复
|
||||
## 2.0.23(2022-01-24)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复image组件在hx3.3.9的nvue下可能会显示异常的问题
|
||||
2. 修复col组件gutter参数带rpx单位处理不正确的问题
|
||||
3. 修复text组件单行时无法显示省略号的问题
|
||||
4. navbar添加titleStyle参数
|
||||
5. 升级到hx3.3.9可消除nvue下控制台样式警告的问题
|
||||
## 2.0.22(2022-01-19)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. $u.page()方法优化,避免在特殊场景可能报错的问题
|
||||
2. picker组件添加immediateChange参数
|
||||
3. 新增$u.pages()方法
|
||||
## 2.0.21(2022-01-19)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 优化:form组件在用户设置rules的时候提示用户model必传
|
||||
2. 优化遗留的通过正则判断rpx单位的问题
|
||||
3. 修复微信小程序环境中tabbar组件开启safeAreaInsetBottom属性后,placeholder高度填充不正确
|
||||
4. 修复swiper在current指定非0时缩放有误
|
||||
5. 修复u-icon添加stop属性的时候报错
|
||||
6. 修复upload组件在accept=all的时候没有作用
|
||||
7. 修复在text组件mode为phone时call属性无效的问题
|
||||
8. 处理u-form clearValidate方法
|
||||
9. 其他修复
|
||||
## 2.0.20(2022-01-14)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复calendar默认会选择一个日期,如果直接点确定的话,无法取到值的问题
|
||||
2. 修复Slider缺少disabled props 还有注释
|
||||
3. 修复u-notice-bar点击事件无法拿到index索引值的问题
|
||||
4. 修复u-collapse-item在vue文件下,app端自定义插槽不生效的问题
|
||||
5. 优化头像为空时显示默认头像
|
||||
6. 修复图片地址赋值后判断加载状态为完成问题
|
||||
7. 修复日历滚动到默认日期月份区域
|
||||
8. search组件暴露点击左边icon事件
|
||||
9. 修复u-form clearValidate方法不生效
|
||||
10. upload h5端增加返回文件参数(文件的name参数)
|
||||
11. 处理upload选择文件后url为blob类型无法预览的问题
|
||||
12. u-code-input 修复输入框没有往左移出一半屏幕
|
||||
13. 修复Upload上传 disabled为true时,控制台报hoverClass类型错误
|
||||
14. 临时处理ios app下grid点击坍塌问题
|
||||
15. 其他修复
|
||||
## 2.0.19(2021-12-29)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 优化微信小程序包体积可在微信中预览,请升级HbuilderX3.3.4,同时在“运行->运行到小程序模拟器”中勾选“运行时是否压缩代码”
|
||||
2. 优化微信小程序setData性能,处理某些方法如$u.route()无法在模板中使用的问题
|
||||
3. navbar添加autoBack参数
|
||||
4. 允许avatar组件的事件冒泡
|
||||
5. 修复cell组件报错问题
|
||||
6. 其他修复
|
||||
## 2.0.18(2021-12-28)
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复app端编译报错问题
|
||||
2. 重新处理微信小程序端setData过大的性能问题
|
||||
3. 修复边框问题
|
||||
4. 修复最大最小月份不大于0则没有数据出现的问题
|
||||
5. 修复SwipeAction微信小程序端无法上下滑动问题
|
||||
6. 修复input的placeholder在小程序端默认显示为true问题
|
||||
7. 修复divider组件click事件无效问题
|
||||
8. 修复u-code-input maxlength 属性值为 String 类型时显示异常
|
||||
9. 修复当 grid只有 1到2时 在小程序端algin设置无效的问题
|
||||
10. 处理form-item的label为top时,取消错误提示的左边距
|
||||
11. 其他修复
|
||||
## 2.0.17(2021-12-26)
|
||||
## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 解决HBuilderX3.3.3.20211225版本导致的样式问题
|
||||
2. calendar日历添加monthNum参数
|
||||
3. navbar添加center slot
|
||||
## 2.0.16(2021-12-25)
|
||||
## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 解决微信小程序setData性能问题
|
||||
2. 修复count-down组件change事件不触发问题
|
||||
## 2.0.15(2021-12-21)
|
||||
## uView正在参与开源中国的“年度最佳项目”评选,之前投过票的现在也可以投票,恳请同学们投一票,[点此帮助uView](https://www.oschina.net/project/top_cn_2021/?id=583)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复Cell单元格titleWidth无效
|
||||
2. 修复cheakbox组件ischecked不更新
|
||||
3. 修复keyboard是否显示"."按键默认值问题
|
||||
4. 修复number-keyboard是否显示键盘的"."符号问题
|
||||
5. 修复Input输入框 readonly无效
|
||||
6. 修复u-avatar 导致打包app、H5时候报错问题
|
||||
7. 修复Upload上传deletable无效
|
||||
8. 修复upload当设置maxSize时无效的问题
|
||||
9. 修复tabs lineWidth传入带单位的字符串的时候偏移量计算错误问题
|
||||
10. 修复rate组件在有padding的view内,显示的星星位置和可触摸区域不匹配,无法正常选中星星
|
||||
## 2.0.13(2021-12-14)
|
||||
## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复配置默认单位为rpx可能会导致自定义导航栏高度异常的问题
|
||||
## 2.0.12(2021-12-14)
|
||||
## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复tabs组件在vue环境下划线消失的问题
|
||||
2. 修复upload组件在安卓小程序无法选择视频的问题
|
||||
3. 添加uni.$u.config.unit配置,用于配置参数默认单位,详见:[默认单位配置](https://www.uviewui.com/components/setting.html#%E9%BB%98%E8%AE%A4%E5%8D%95%E4%BD%8D%E9%85%8D%E7%BD%AE)
|
||||
4. 修复textarea组件在没绑定v-model时,字符统计不生效问题
|
||||
5. 修复nvue下控制是否出现滚动条失效问题
|
||||
## 2.0.11(2021-12-13)
|
||||
## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. text组件align参数无效的问题
|
||||
2. subsection组件添加keyName参数
|
||||
3. upload组件无法判断[Object file]类型的问题
|
||||
4. 处理notify层级过低问题
|
||||
5. codeInput组件添加disabledDot参数
|
||||
6. 处理actionSheet组件round参数无效的问题
|
||||
7. calendar组件添加round参数用于控制圆角值
|
||||
8. 处理swipeAction组件在vue环境下默认被打开的问题
|
||||
9. button组件的throttleTime节流参数无效的问题
|
||||
10. 解决u-notify手动关闭方法close()无效的问题
|
||||
11. input组件readonly不生效问题
|
||||
12. tag组件type参数为info不生效问题
|
||||
## 2.0.10(2021-12-08)
|
||||
## [点击加群交流反馈:364463526](https://jq.qq.com/?_chanwv=1027&k=mCxS3TGY)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复button sendMessagePath属性不生效
|
||||
2. 修复DatetimePicker选择器title无效
|
||||
3. 修复u-toast设置loading=true不生效
|
||||
4. 修复u-text金额模式传0报错
|
||||
5. 修复u-toast组件的icon属性配置不生效
|
||||
6. button的icon在特殊场景下的颜色优化
|
||||
7. IndexList优化,增加#
|
||||
## 2.0.9(2021-12-01)
|
||||
## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 优化swiper的height支持100%值(仅vue有效),修复嵌入视频时click事件无法触发的问题
|
||||
2. 优化tabs组件对list值为空的判断,或者动态变化list时重新计算相关尺寸的问题
|
||||
3. 优化datetime-picker组件逻辑,让其后续打开的默认值为上一次的选中值,需要通过v-model绑定值才有效
|
||||
4. 修复upload内嵌在其他组件中,选择图片可能不会换行的问题
|
||||
## 2.0.8(2021-12-01)
|
||||
## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复toast的position参数无效问题
|
||||
2. 处理input在ios nvue上无法获得焦点的问题
|
||||
3. avatar-group组件添加extraValue参数,让剩余展示数量可手动控制
|
||||
4. tabs组件添加keyName参数用于配置从对象中读取的键名
|
||||
5. 处理text组件名字脱敏默认配置无效的问题
|
||||
6. 处理picker组件item文本太长换行问题
|
||||
## 2.0.7(2021-11-30)
|
||||
## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 修复radio和checkbox动态改变v-model无效的问题。
|
||||
2. 优化form规则validator在微信小程序用法
|
||||
3. 修复backtop组件mode参数在微信小程序无效的问题
|
||||
4. 处理Album的previewFullImage属性无效的问题
|
||||
5. 处理u-datetime-picker组件mode='time'在选择改变时间时,控制台报错的问题
|
||||
## 2.0.6(2021-11-27)
|
||||
## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. 处理tag组件在vue下边框无效的问题。
|
||||
2. 处理popup组件圆角参数可能无效的问题。
|
||||
3. 处理tabs组件lineColor参数可能无效的问题。
|
||||
4. propgress组件在值很小时,显示异常的问题。
|
||||
## 2.0.5(2021-11-25)
|
||||
## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. calendar在vue下显示异常问题。
|
||||
2. form组件labelPosition和errorType参数无效的问题
|
||||
3. input组件inputAlign无效的问题
|
||||
4. 其他一些修复
|
||||
## 2.0.4(2021-11-23)
|
||||
## [点击加群交流反馈:232041042](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
0. input组件缺失@confirm事件,以及subfix和prefix无效问题
|
||||
1. component.scss文件样式在vue下干扰全局布局问题
|
||||
2. 修复subsection在vue环境下表现异常的问题
|
||||
3. tag组件的bgColor等参数无效的问题
|
||||
4. upload组件不换行的问题
|
||||
5. 其他的一些修复处理
|
||||
## 2.0.3(2021-11-16)
|
||||
## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. uView2.0已实现全面兼容nvue
|
||||
2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升
|
||||
3. 目前uView2.0为公测阶段,相关细节可能会有变动
|
||||
4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
|
||||
5. 处理modal的confirm回调事件拼写错误问题
|
||||
6. 处理input组件@input事件参数错误问题
|
||||
7. 其他一些修复
|
||||
## 2.0.2(2021-11-16)
|
||||
## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. uView2.0已实现全面兼容nvue
|
||||
2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升
|
||||
3. 目前uView2.0为公测阶段,相关细节可能会有变动
|
||||
4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
|
||||
5. 修复input组件formatter参数缺失问题
|
||||
6. 优化loading-icon组件的scss写法问题,防止不兼容新版本scss
|
||||
## 2.0.0(2020-11-15)
|
||||
## [点击加群交流反馈:1129077272](https://jq.qq.com/?_wv=1027&k=KnbeceDU)
|
||||
|
||||
# uView2.0重磅发布,利剑出鞘,一统江湖
|
||||
|
||||
1. uView2.0已实现全面兼容nvue
|
||||
2. uView2.0对1.x进行了架构重构,细节和性能都有极大提升
|
||||
3. 目前uView2.0为公测阶段,相关细节可能会有变动
|
||||
4. 我们写了一份与1.x的对比指南,详见[对比1.x](https://www.uviewui.com/components/diff1.x.html)
|
||||
5. 修复input组件formatter参数缺失问题
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<uvForm
|
||||
ref="uForm"
|
||||
:model="model"
|
||||
:rules="rules"
|
||||
:errorType="errorType"
|
||||
:borderBottom="borderBottom"
|
||||
:labelPosition="labelPosition"
|
||||
:labelWidth="labelWidth"
|
||||
:labelAlign="labelAlign"
|
||||
:labelStyle="labelStyle"
|
||||
:customStyle="customStyle"
|
||||
>
|
||||
<slot />
|
||||
</uvForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-form被uni-app官方占用了,u-form在nvue中相当于form组件
|
||||
* 所以在nvue下,取名为u--form,内部其实还是u-form.vue,只不过做一层中转
|
||||
*/
|
||||
import uvForm from '../u-form/u-form.vue';
|
||||
import props from '../u-form/props.js'
|
||||
export default {
|
||||
// #ifdef MP-WEIXIN
|
||||
name: 'u-form',
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
name: 'u--form',
|
||||
// #endif
|
||||
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
|
||||
components: {
|
||||
uvForm
|
||||
},
|
||||
created() {
|
||||
this.children = []
|
||||
},
|
||||
methods: {
|
||||
// 手动设置校验的规则,如果规则中有函数的话,微信小程序中会过滤掉,所以只能手动调用设置规则
|
||||
setRules(rules) {
|
||||
this.$refs.uForm.setRules(rules)
|
||||
},
|
||||
validate() {
|
||||
/**
|
||||
* 在微信小程序中,通过this.$parent拿到的父组件是u--form,而不是其内嵌的u-form
|
||||
* 导致在u-form组件中,拿不到对应的children数组,从而校验无效,所以这里每次调用u-form组件中的
|
||||
* 对应方法的时候,在小程序中都先将u--form的children赋值给u-form中的children
|
||||
*/
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.validate()
|
||||
},
|
||||
validateField(value, callback, event) {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.validateField(value, callback, event)
|
||||
},
|
||||
resetFields() {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.resetFields()
|
||||
},
|
||||
clearValidate(props) {
|
||||
// #ifdef MP-WEIXIN
|
||||
this.setMpData()
|
||||
// #endif
|
||||
return this.$refs.uForm.clearValidate(props)
|
||||
},
|
||||
setMpData() {
|
||||
this.$refs.uForm.children = this.children
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<uvImage
|
||||
:src="src"
|
||||
:mode="mode"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:shape="shape"
|
||||
:radius="radius"
|
||||
:lazyLoad="lazyLoad"
|
||||
:showMenuByLongpress="showMenuByLongpress"
|
||||
:loadingIcon="loadingIcon"
|
||||
:errorIcon="errorIcon"
|
||||
:showLoading="showLoading"
|
||||
:showError="showError"
|
||||
:fade="fade"
|
||||
:webp="webp"
|
||||
:duration="duration"
|
||||
:bgColor="bgColor"
|
||||
:customStyle="customStyle"
|
||||
@click="$emit('click')"
|
||||
@error="$emit('error')"
|
||||
@load="$emit('load')"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<slot name="loading"></slot>
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<slot name="error"></slot>
|
||||
</template>
|
||||
</uvImage>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-image被uni-app官方占用了,u-image在nvue中相当于image组件
|
||||
* 所以在nvue下,取名为u--image,内部其实还是u-iamge.vue,只不过做一层中转
|
||||
*/
|
||||
import uvImage from '../u-image/u-image.vue';
|
||||
import props from '../u-image/props.js';
|
||||
export default {
|
||||
name: 'u--image',
|
||||
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
|
||||
components: {
|
||||
uvImage
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<uvInput
|
||||
:value="value"
|
||||
:type="type"
|
||||
:fixed="fixed"
|
||||
:disabled="disabled"
|
||||
:disabledColor="disabledColor"
|
||||
:clearable="clearable"
|
||||
:password="password"
|
||||
:maxlength="maxlength"
|
||||
:placeholder="placeholder"
|
||||
:placeholderClass="placeholderClass"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
:showWordLimit="showWordLimit"
|
||||
:confirmType="confirmType"
|
||||
:confirmHold="confirmHold"
|
||||
:holdKeyboard="holdKeyboard"
|
||||
:focus="focus"
|
||||
:autoBlur="autoBlur"
|
||||
:disableDefaultPadding="disableDefaultPadding"
|
||||
:cursor="cursor"
|
||||
:cursorSpacing="cursorSpacing"
|
||||
:selectionStart="selectionStart"
|
||||
:selectionEnd="selectionEnd"
|
||||
:adjustPosition="adjustPosition"
|
||||
:inputAlign="inputAlign"
|
||||
:fontSize="fontSize"
|
||||
:color="color"
|
||||
:prefixIcon="prefixIcon"
|
||||
:suffixIcon="suffixIcon"
|
||||
:suffixIconStyle="suffixIconStyle"
|
||||
:prefixIconStyle="prefixIconStyle"
|
||||
:border="border"
|
||||
:readonly="readonly"
|
||||
:shape="shape"
|
||||
:customStyle="customStyle"
|
||||
:formatter="formatter"
|
||||
:ignoreCompositionEvent="ignoreCompositionEvent"
|
||||
@focus="$emit('focus')"
|
||||
@blur="e => $emit('blur', e)"
|
||||
@keyboardheightchange="$emit('keyboardheightchange')"
|
||||
@change="e => $emit('change', e)"
|
||||
@input="e => $emit('input', e)"
|
||||
@confirm="e => $emit('confirm', e)"
|
||||
@clear="$emit('clear')"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<!-- #ifdef MP -->
|
||||
<slot name="prefix"></slot>
|
||||
<slot name="suffix"></slot>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP -->
|
||||
<slot name="prefix" slot="prefix"></slot>
|
||||
<slot name="suffix" slot="suffix"></slot>
|
||||
<!-- #endif -->
|
||||
</uvInput>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-input被uni-app官方占用了,u-input在nvue中相当于input组件
|
||||
* 所以在nvue下,取名为u--input,内部其实还是u-input.vue,只不过做一层中转
|
||||
*/
|
||||
import uvInput from '../u-input/u-input.vue';
|
||||
import props from '../u-input/props.js'
|
||||
export default {
|
||||
name: 'u--input',
|
||||
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
|
||||
components: {
|
||||
uvInput
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<uvText
|
||||
:type="type"
|
||||
:show="show"
|
||||
:text="text"
|
||||
:prefixIcon="prefixIcon"
|
||||
:suffixIcon="suffixIcon"
|
||||
:mode="mode"
|
||||
:href="href"
|
||||
:format="format"
|
||||
:call="call"
|
||||
:openType="openType"
|
||||
:bold="bold"
|
||||
:block="block"
|
||||
:lines="lines"
|
||||
:color="color"
|
||||
:decoration="decoration"
|
||||
:size="size"
|
||||
:iconStyle="iconStyle"
|
||||
:margin="margin"
|
||||
:lineHeight="lineHeight"
|
||||
:align="align"
|
||||
:wordWrap="wordWrap"
|
||||
:customStyle="customStyle"
|
||||
@click="$emit('click')"
|
||||
></uvText>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u-text被uni-app官方占用了,u-text在nvue中相当于input组件
|
||||
* 所以在nvue下,取名为u--input,内部其实还是u-text.vue,只不过做一层中转
|
||||
* 不使用v-bind="$attrs",而是分开独立写传参,是因为微信小程序不支持此写法
|
||||
*/
|
||||
import uvText from "../u-text/u-text.vue";
|
||||
import props from "../u-text/props.js";
|
||||
export default {
|
||||
name: "u--text",
|
||||
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
|
||||
components: {
|
||||
uvText,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<uvTextarea
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
:height="height"
|
||||
:confirmType="confirmType"
|
||||
:disabled="disabled"
|
||||
:count="count"
|
||||
:focus="focus"
|
||||
:autoHeight="autoHeight"
|
||||
:fixed="fixed"
|
||||
:cursorSpacing="cursorSpacing"
|
||||
:cursor="cursor"
|
||||
:showConfirmBar="showConfirmBar"
|
||||
:selectionStart="selectionStart"
|
||||
:selectionEnd="selectionEnd"
|
||||
:adjustPosition="adjustPosition"
|
||||
:disableDefaultPadding="disableDefaultPadding"
|
||||
:holdKeyboard="holdKeyboard"
|
||||
:maxlength="maxlength"
|
||||
:border="border"
|
||||
:customStyle="customStyle"
|
||||
:formatter="formatter"
|
||||
:ignoreCompositionEvent="ignoreCompositionEvent"
|
||||
@focus="e => $emit('focus')"
|
||||
@blur="e => $emit('blur')"
|
||||
@linechange="e => $emit('linechange', e)"
|
||||
@confirm="e => $emit('confirm')"
|
||||
@input="e => $emit('input', e)"
|
||||
@keyboardheightchange="e => $emit('keyboardheightchange')"
|
||||
></uvTextarea>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 此组件存在的理由是,在nvue下,u--textarea被uni-app官方占用了,u-textarea在nvue中相当于textarea组件
|
||||
* 所以在nvue下,取名为u--textarea,内部其实还是u-textarea.vue,只不过做一层中转
|
||||
*/
|
||||
import uvTextarea from '../u-textarea/u-textarea.vue';
|
||||
import props from '../u-textarea/props.js'
|
||||
export default {
|
||||
name: 'u--textarea',
|
||||
mixins: [uni.$u.mpMixin, props, uni.$u.mixin],
|
||||
components: {
|
||||
uvTextarea
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
export default {
|
||||
props: {
|
||||
// 操作菜单是否展示 (默认false)
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.actionSheet.show
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: uni.$u.props.actionSheet.title
|
||||
},
|
||||
// 选项上方的描述信息
|
||||
description: {
|
||||
type: String,
|
||||
default: uni.$u.props.actionSheet.description
|
||||
},
|
||||
// 数据
|
||||
actions: {
|
||||
type: Array,
|
||||
default: uni.$u.props.actionSheet.actions
|
||||
},
|
||||
// 取消按钮的文字,不为空时显示按钮
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: uni.$u.props.actionSheet.cancelText
|
||||
},
|
||||
// 点击某个菜单项时是否关闭弹窗
|
||||
closeOnClickAction: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.actionSheet.closeOnClickAction
|
||||
},
|
||||
// 处理底部安全区(默认true)
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.actionSheet.safeAreaInsetBottom
|
||||
},
|
||||
// 小程序的打开方式
|
||||
openType: {
|
||||
type: String,
|
||||
default: uni.$u.props.actionSheet.openType
|
||||
},
|
||||
// 点击遮罩是否允许关闭 (默认true)
|
||||
closeOnClickOverlay: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.actionSheet.closeOnClickOverlay
|
||||
},
|
||||
// 圆角值
|
||||
round: {
|
||||
type: [Boolean, String, Number],
|
||||
default: uni.$u.props.actionSheet.round
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
|
||||
<template>
|
||||
<u-popup
|
||||
:show="show"
|
||||
mode="bottom"
|
||||
@close="closeHandler"
|
||||
:safeAreaInsetBottom="safeAreaInsetBottom"
|
||||
:round="round"
|
||||
>
|
||||
<view class="u-action-sheet">
|
||||
<view
|
||||
class="u-action-sheet__header"
|
||||
v-if="title"
|
||||
>
|
||||
<text class="u-action-sheet__header__title u-line-1">{{title}}</text>
|
||||
<view
|
||||
class="u-action-sheet__header__icon-wrap"
|
||||
@tap.stop="cancel"
|
||||
>
|
||||
<u-icon
|
||||
name="close"
|
||||
size="17"
|
||||
color="#c8c9cc"
|
||||
bold
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<text
|
||||
class="u-action-sheet__description"
|
||||
:style="[{
|
||||
marginTop: `${title && description ? 0 : '18px'}`
|
||||
}]"
|
||||
v-if="description"
|
||||
>{{description}}</text>
|
||||
<slot>
|
||||
<u-line v-if="description"></u-line>
|
||||
<view class="u-action-sheet__item-wrap">
|
||||
<template v-for="(item, index) in actions">
|
||||
<!-- #ifdef MP -->
|
||||
<button
|
||||
:key="index"
|
||||
class="u-reset-button"
|
||||
:openType="item.openType"
|
||||
@getuserinfo="onGetUserInfo"
|
||||
@contact="onContact"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
@error="onError"
|
||||
@launchapp="onLaunchApp"
|
||||
@opensetting="onOpenSetting"
|
||||
:lang="lang"
|
||||
:session-from="sessionFrom"
|
||||
:send-message-title="sendMessageTitle"
|
||||
:send-message-path="sendMessagePath"
|
||||
:send-message-img="sendMessageImg"
|
||||
:show-message-card="showMessageCard"
|
||||
:app-parameter="appParameter"
|
||||
@tap="selectHandler(index)"
|
||||
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
|
||||
>
|
||||
<!-- #endif -->
|
||||
<view
|
||||
class="u-action-sheet__item-wrap__item"
|
||||
@tap.stop="selectHandler(index)"
|
||||
:hover-class="!item.disabled && !item.loading ? 'u-action-sheet--hover' : ''"
|
||||
:hover-stay-time="150"
|
||||
>
|
||||
<template v-if="!item.loading">
|
||||
<text
|
||||
class="u-action-sheet__item-wrap__item__name"
|
||||
:style="[itemStyle(index)]"
|
||||
>{{ item.name }}</text>
|
||||
<text
|
||||
v-if="item.subname"
|
||||
class="u-action-sheet__item-wrap__item__subname"
|
||||
>{{ item.subname }}</text>
|
||||
</template>
|
||||
<u-loading-icon
|
||||
v-else
|
||||
custom-class="van-action-sheet__loading"
|
||||
size="18"
|
||||
mode="circle"
|
||||
/>
|
||||
</view>
|
||||
<!-- #ifdef MP -->
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<u-line v-if="index !== actions.length - 1"></u-line>
|
||||
</template>
|
||||
</view>
|
||||
</slot>
|
||||
<u-gap
|
||||
bgColor="#eaeaec"
|
||||
height="6"
|
||||
v-if="cancelText"
|
||||
></u-gap>
|
||||
<view hover-class="u-action-sheet--hover">
|
||||
<text
|
||||
@touchmove.stop.prevent
|
||||
:hover-stay-time="150"
|
||||
v-if="cancelText"
|
||||
class="u-action-sheet__cancel-text"
|
||||
@tap="cancel"
|
||||
>{{cancelText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import openType from '../../libs/mixin/openType'
|
||||
import button from '../../libs/mixin/button'
|
||||
import props from './props.js';
|
||||
/**
|
||||
* ActionSheet 操作菜单
|
||||
* @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
|
||||
* @tutorial https://www.uviewui.com/components/actionSheet.html
|
||||
*
|
||||
* @property {Boolean} show 操作菜单是否展示 (默认 false )
|
||||
* @property {String} title 操作菜单标题
|
||||
* @property {String} description 选项上方的描述信息
|
||||
* @property {Array<Object>} actions 按钮的文字数组,见官方文档示例
|
||||
* @property {String} cancelText 取消按钮的提示文字,不为空时显示按钮
|
||||
* @property {Boolean} closeOnClickAction 点击某个菜单项时是否关闭弹窗 (默认 true )
|
||||
* @property {Boolean} safeAreaInsetBottom 处理底部安全区 (默认 true )
|
||||
* @property {String} openType 小程序的打开方式 (contact | launchApp | getUserInfo | openSetting |getPhoneNumber |error )
|
||||
* @property {Boolean} closeOnClickOverlay 点击遮罩是否允许关闭 (默认 true )
|
||||
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
|
||||
* @property {String} lang 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文
|
||||
* @property {String} sessionFrom 会话来源,openType="contact"时有效
|
||||
* @property {String} sendMessageTitle 会话内消息卡片标题,openType="contact"时有效
|
||||
* @property {String} sendMessagePath 会话内消息卡片点击跳转小程序路径,openType="contact"时有效
|
||||
* @property {String} sendMessageImg 会话内消息卡片图片,openType="contact"时有效
|
||||
* @property {Boolean} showMessageCard 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,openType="contact"时有效 (默认 false )
|
||||
* @property {String} appParameter 打开 APP 时,向 APP 传递的参数,openType=launchApp 时有效
|
||||
*
|
||||
* @event {Function} select 点击ActionSheet列表项时触发
|
||||
* @event {Function} close 点击取消按钮时触发
|
||||
* @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与 wx.getUserInfo 返回的一致,openType="getUserInfo"时有效
|
||||
* @event {Function} contact 客服消息回调,openType="contact"时有效
|
||||
* @event {Function} getphonenumber 获取用户手机号回调,openType="getPhoneNumber"时有效
|
||||
* @event {Function} error 当使用开放能力时,发生错误的回调,openType="error"时有效
|
||||
* @event {Function} launchapp 打开 APP 成功的回调,openType="launchApp"时有效
|
||||
* @event {Function} opensetting 在打开授权设置页后回调,openType="openSetting"时有效
|
||||
* @example <u-action-sheet :actions="list" :title="title" :show="show"></u-action-sheet>
|
||||
*/
|
||||
export default {
|
||||
name: "u-action-sheet",
|
||||
// 一些props参数和methods方法,通过mixin混入,因为其他文件也会用到
|
||||
mixins: [openType, button, uni.$u.mixin, props],
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 操作项目的样式
|
||||
itemStyle() {
|
||||
return (index) => {
|
||||
let style = {};
|
||||
if (this.actions[index].color) style.color = this.actions[index].color
|
||||
if (this.actions[index].fontSize) style.fontSize = uni.$u.addUnit(this.actions[index].fontSize)
|
||||
// 选项被禁用的样式
|
||||
if (this.actions[index].disabled) style.color = '#c0c4cc'
|
||||
return style;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeHandler() {
|
||||
// 允许点击遮罩关闭时,才发出close事件
|
||||
if(this.closeOnClickOverlay) {
|
||||
this.$emit('close')
|
||||
}
|
||||
},
|
||||
// 点击取消按钮
|
||||
cancel() {
|
||||
this.$emit('close')
|
||||
},
|
||||
selectHandler(index) {
|
||||
const item = this.actions[index]
|
||||
if (item && !item.disabled && !item.loading) {
|
||||
this.$emit('select', item)
|
||||
if (this.closeOnClickAction) {
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/components.scss";
|
||||
$u-action-sheet-reset-button-width:100% !default;
|
||||
$u-action-sheet-title-font-size: 16px !default;
|
||||
$u-action-sheet-title-padding: 12px 30px !default;
|
||||
$u-action-sheet-title-color: $u-main-color !default;
|
||||
$u-action-sheet-header-icon-wrap-right:15px !default;
|
||||
$u-action-sheet-header-icon-wrap-top:15px !default;
|
||||
$u-action-sheet-description-font-size:13px !default;
|
||||
$u-action-sheet-description-color:14px !default;
|
||||
$u-action-sheet-description-margin: 18px 15px !default;
|
||||
$u-action-sheet-item-wrap-item-padding:15px !default;
|
||||
$u-action-sheet-item-wrap-name-font-size:16px !default;
|
||||
$u-action-sheet-item-wrap-subname-font-size:13px !default;
|
||||
$u-action-sheet-item-wrap-subname-color: #c0c4cc !default;
|
||||
$u-action-sheet-item-wrap-subname-margin-top:10px !default;
|
||||
$u-action-sheet-cancel-text-font-size:16px !default;
|
||||
$u-action-sheet-cancel-text-color:$u-content-color !default;
|
||||
$u-action-sheet-cancel-text-font-size:15px !default;
|
||||
$u-action-sheet-cancel-text-hover-background-color:rgb(242, 243, 245) !default;
|
||||
|
||||
.u-reset-button {
|
||||
width: $u-action-sheet-reset-button-width;
|
||||
}
|
||||
|
||||
.u-action-sheet {
|
||||
text-align: center;
|
||||
&__header {
|
||||
position: relative;
|
||||
padding: $u-action-sheet-title-padding;
|
||||
&__title {
|
||||
font-size: $u-action-sheet-title-font-size;
|
||||
color: $u-action-sheet-title-color;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
position: absolute;
|
||||
right: $u-action-sheet-header-icon-wrap-right;
|
||||
top: $u-action-sheet-header-icon-wrap-top;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $u-action-sheet-description-font-size;
|
||||
color: $u-tips-color;
|
||||
margin: $u-action-sheet-description-margin;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__item-wrap {
|
||||
|
||||
&__item {
|
||||
padding: $u-action-sheet-item-wrap-item-padding;
|
||||
@include flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
&__name {
|
||||
font-size: $u-action-sheet-item-wrap-name-font-size;
|
||||
color: $u-main-color;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__subname {
|
||||
font-size: $u-action-sheet-item-wrap-subname-font-size;
|
||||
color: $u-action-sheet-item-wrap-subname-color;
|
||||
margin-top: $u-action-sheet-item-wrap-subname-margin-top;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel-text {
|
||||
font-size: $u-action-sheet-cancel-text-font-size;
|
||||
color: $u-action-sheet-cancel-text-color;
|
||||
text-align: center;
|
||||
padding: $u-action-sheet-cancel-text-font-size;
|
||||
}
|
||||
|
||||
&--hover {
|
||||
background-color: $u-action-sheet-cancel-text-hover-background-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
export default {
|
||||
props: {
|
||||
// 图片地址,Array<String>|Array<Object>形式
|
||||
urls: {
|
||||
type: Array,
|
||||
default: uni.$u.props.album.urls
|
||||
},
|
||||
// 指定从数组的对象元素中读取哪个属性作为图片地址
|
||||
keyName: {
|
||||
type: String,
|
||||
default: uni.$u.props.album.keyName
|
||||
},
|
||||
// 单图时,图片长边的长度
|
||||
singleSize: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.album.singleSize
|
||||
},
|
||||
// 多图时,图片边长
|
||||
multipleSize: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.album.multipleSize
|
||||
},
|
||||
// 多图时,图片水平和垂直之间的间隔
|
||||
space: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.album.space
|
||||
},
|
||||
// 单图时,图片缩放裁剪的模式
|
||||
singleMode: {
|
||||
type: String,
|
||||
default: uni.$u.props.album.singleMode
|
||||
},
|
||||
// 多图时,图片缩放裁剪的模式
|
||||
multipleMode: {
|
||||
type: String,
|
||||
default: uni.$u.props.album.multipleMode
|
||||
},
|
||||
// 最多展示的图片数量,超出时最后一个位置将会显示剩余图片数量
|
||||
maxCount: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.album.maxCount
|
||||
},
|
||||
// 是否可以预览图片
|
||||
previewFullImage: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.album.previewFullImage
|
||||
},
|
||||
// 每行展示图片数量,如设置,singleSize和multipleSize将会无效
|
||||
rowCount: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.album.rowCount
|
||||
},
|
||||
// 超出maxCount时是否显示查看更多的提示
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.album.showMore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<view class="u-album">
|
||||
<view
|
||||
class="u-album__row"
|
||||
ref="u-album__row"
|
||||
v-for="(arr, index) in showUrls"
|
||||
:forComputedUse="albumWidth"
|
||||
:key="index"
|
||||
>
|
||||
<view
|
||||
class="u-album__row__wrapper"
|
||||
v-for="(item, index1) in arr"
|
||||
:key="index1"
|
||||
:style="[imageStyle(index + 1, index1 + 1)]"
|
||||
@tap="previewFullImage ? onPreviewTap(getSrc(item)) : ''"
|
||||
>
|
||||
<image
|
||||
:src="getSrc(item)"
|
||||
:mode="
|
||||
urls.length === 1
|
||||
? imageHeight > 0
|
||||
? singleMode
|
||||
: 'widthFix'
|
||||
: multipleMode
|
||||
"
|
||||
:style="[
|
||||
{
|
||||
width: imageWidth,
|
||||
height: imageHeight
|
||||
}
|
||||
]"
|
||||
></image>
|
||||
<view
|
||||
v-if="
|
||||
showMore &&
|
||||
urls.length > rowCount * showUrls.length &&
|
||||
index === showUrls.length - 1 &&
|
||||
index1 === showUrls[showUrls.length - 1].length - 1
|
||||
"
|
||||
class="u-album__row__wrapper__text"
|
||||
>
|
||||
<u--text
|
||||
:text="`+${urls.length - maxCount}`"
|
||||
color="#fff"
|
||||
:size="multipleSize * 0.3"
|
||||
align="center"
|
||||
customStyle="justify-content: center"
|
||||
></u--text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import props from './props.js'
|
||||
// #ifdef APP-NVUE
|
||||
// 由于weex为阿里的KPI业绩考核的产物,所以不支持百分比单位,这里需要通过dom查询组件的宽度
|
||||
const dom = uni.requireNativePlugin('dom')
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* Album 相册
|
||||
* @description 本组件提供一个类似相册的功能,让开发者开发起来更加得心应手。减少重复的模板代码
|
||||
* @tutorial https://www.uviewui.com/components/album.html
|
||||
*
|
||||
* @property {Array} urls 图片地址列表 Array<String>|Array<Object>形式
|
||||
* @property {String} keyName 指定从数组的对象元素中读取哪个属性作为图片地址
|
||||
* @property {String | Number} singleSize 单图时,图片长边的长度 (默认 180 )
|
||||
* @property {String | Number} multipleSize 多图时,图片边长 (默认 70 )
|
||||
* @property {String | Number} space 多图时,图片水平和垂直之间的间隔 (默认 6 )
|
||||
* @property {String} singleMode 单图时,图片缩放裁剪的模式 (默认 'scaleToFill' )
|
||||
* @property {String} multipleMode 多图时,图片缩放裁剪的模式 (默认 'aspectFill' )
|
||||
* @property {String | Number} maxCount 取消按钮的提示文字 (默认 9 )
|
||||
* @property {Boolean} previewFullImage 是否可以预览图片 (默认 true )
|
||||
* @property {String | Number} rowCount 每行展示图片数量,如设置,singleSize和multipleSize将会无效 (默认 3 )
|
||||
* @property {Boolean} showMore 超出maxCount时是否显示查看更多的提示 (默认 true )
|
||||
*
|
||||
* @event {Function} albumWidth 某些特殊的情况下,需要让文字与相册的宽度相等,这里事件的形式对外发送 (回调参数 width )
|
||||
* @example <u-album :urls="urls2" @albumWidth="width => albumWidth = width" multipleSize="68" ></u-album>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-album',
|
||||
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
|
||||
data() {
|
||||
return {
|
||||
// 单图的宽度
|
||||
singleWidth: 0,
|
||||
// 单图的高度
|
||||
singleHeight: 0,
|
||||
// 单图时,如果无法获取图片的尺寸信息,让图片宽度默认为容器的一定百分比
|
||||
singlePercent: 0.6
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
urls: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal.length === 1) {
|
||||
this.getImageRect()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageStyle() {
|
||||
return (index1, index2) => {
|
||||
const { space, rowCount, multipleSize, urls } = this,
|
||||
{ addUnit, addStyle } = uni.$u,
|
||||
rowLen = this.showUrls.length,
|
||||
allLen = this.urls.length
|
||||
const style = {
|
||||
marginRight: addUnit(space),
|
||||
marginBottom: addUnit(space)
|
||||
}
|
||||
// 如果为最后一行,则每个图片都无需下边框
|
||||
if (index1 === rowLen) style.marginBottom = 0
|
||||
// 每行的最右边一张和总长度的最后一张无需右边框
|
||||
if (
|
||||
index2 === rowCount ||
|
||||
(index1 === rowLen &&
|
||||
index2 === this.showUrls[index1 - 1].length)
|
||||
)
|
||||
style.marginRight = 0
|
||||
return style
|
||||
}
|
||||
},
|
||||
// 将数组划分为二维数组
|
||||
showUrls() {
|
||||
const arr = []
|
||||
this.urls.map((item, index) => {
|
||||
// 限制最大展示数量
|
||||
if (index + 1 <= this.maxCount) {
|
||||
// 计算该元素为第几个素组内
|
||||
const itemIndex = Math.floor(index / this.rowCount)
|
||||
// 判断对应的索引是否存在
|
||||
if (!arr[itemIndex]) {
|
||||
arr[itemIndex] = []
|
||||
}
|
||||
arr[itemIndex].push(item)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
},
|
||||
imageWidth() {
|
||||
return uni.$u.addUnit(
|
||||
this.urls.length === 1 ? this.singleWidth : this.multipleSize
|
||||
)
|
||||
},
|
||||
imageHeight() {
|
||||
return uni.$u.addUnit(
|
||||
this.urls.length === 1 ? this.singleHeight : this.multipleSize
|
||||
)
|
||||
},
|
||||
// 此变量无实际用途,仅仅是为了利用computed特性,让其在urls长度等变化时,重新计算图片的宽度
|
||||
// 因为用户在某些特殊的情况下,需要让文字与相册的宽度相等,所以这里事件的形式对外发送
|
||||
albumWidth() {
|
||||
let width = 0
|
||||
if (this.urls.length === 1) {
|
||||
width = this.singleWidth
|
||||
} else {
|
||||
width =
|
||||
this.showUrls[0].length * this.multipleSize +
|
||||
this.space * (this.showUrls[0].length - 1)
|
||||
}
|
||||
this.$emit('albumWidth', width)
|
||||
return width
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 预览图片
|
||||
onPreviewTap(url) {
|
||||
const urls = this.urls.map((item) => {
|
||||
return this.getSrc(item)
|
||||
})
|
||||
uni.previewImage({
|
||||
current: url,
|
||||
urls
|
||||
})
|
||||
},
|
||||
// 获取图片的路径
|
||||
getSrc(item) {
|
||||
return uni.$u.test.object(item)
|
||||
? (this.keyName && item[this.keyName]) || item.src
|
||||
: item
|
||||
},
|
||||
// 单图时,获取图片的尺寸
|
||||
// 在小程序中,需要将网络图片的的域名添加到小程序的download域名才可能获取尺寸
|
||||
// 在没有添加的情况下,让单图宽度默认为盒子的一定宽度(singlePercent)
|
||||
getImageRect() {
|
||||
const src = this.getSrc(this.urls[0])
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: (res) => {
|
||||
// 判断图片横向还是竖向展示方式
|
||||
const isHorizotal = res.width >= res.height
|
||||
this.singleWidth = isHorizotal
|
||||
? this.singleSize
|
||||
: (res.width / res.height) * this.singleSize
|
||||
this.singleHeight = !isHorizotal
|
||||
? this.singleSize
|
||||
: (res.height / res.width) * this.singleWidth
|
||||
},
|
||||
fail: () => {
|
||||
this.getComponentWidth()
|
||||
}
|
||||
})
|
||||
},
|
||||
// 获取组件的宽度
|
||||
async getComponentWidth() {
|
||||
// 延时一定时间,以获取dom尺寸
|
||||
await uni.$u.sleep(30)
|
||||
// #ifndef APP-NVUE
|
||||
this.$uGetRect('.u-album__row').then((size) => {
|
||||
this.singleWidth = size.width * this.singlePercent
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
// 这里ref="u-album__row"所在的标签为通过for循环出来,导致this.$refs['u-album__row']是一个数组
|
||||
const ref = this.$refs['u-album__row'][0]
|
||||
ref &&
|
||||
dom.getComponentRect(ref, (res) => {
|
||||
this.singleWidth = res.size.width * this.singlePercent
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../libs/css/components.scss';
|
||||
|
||||
.u-album {
|
||||
@include flex(column);
|
||||
|
||||
&__row {
|
||||
@include flex(row);
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
|
||||
&__text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
@include flex(row);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
export default {
|
||||
props: {
|
||||
// 显示文字
|
||||
title: {
|
||||
type: String,
|
||||
default: uni.$u.props.alert.title
|
||||
},
|
||||
// 主题,success/warning/info/error
|
||||
type: {
|
||||
type: String,
|
||||
default: uni.$u.props.alert.type
|
||||
},
|
||||
// 辅助性文字
|
||||
description: {
|
||||
type: String,
|
||||
default: uni.$u.props.alert.description
|
||||
},
|
||||
// 是否可关闭
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.alert.closable
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.alert.showIcon
|
||||
},
|
||||
// 浅或深色调,light-浅色,dark-深色
|
||||
effect: {
|
||||
type: String,
|
||||
default: uni.$u.props.alert.effect
|
||||
},
|
||||
// 文字是否居中
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.alert.center
|
||||
},
|
||||
// 字体大小
|
||||
fontSize: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.alert.fontSize
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<u-transition
|
||||
mode="fade"
|
||||
:show="show"
|
||||
>
|
||||
<view
|
||||
class="u-alert"
|
||||
:class="[`u-alert--${type}--${effect}`]"
|
||||
@tap.stop="clickHandler"
|
||||
:style="[$u.addStyle(customStyle)]"
|
||||
>
|
||||
<view
|
||||
class="u-alert__icon"
|
||||
v-if="showIcon"
|
||||
>
|
||||
<u-icon
|
||||
:name="iconName"
|
||||
size="18"
|
||||
:color="iconColor"
|
||||
></u-icon>
|
||||
</view>
|
||||
<view
|
||||
class="u-alert__content"
|
||||
:style="[{
|
||||
paddingRight: closable ? '20px' : 0
|
||||
}]"
|
||||
>
|
||||
<text
|
||||
class="u-alert__content__title"
|
||||
v-if="title"
|
||||
:style="[{
|
||||
fontSize: $u.addUnit(fontSize),
|
||||
textAlign: center ? 'center' : 'left'
|
||||
}]"
|
||||
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
|
||||
>{{ title }}</text>
|
||||
<text
|
||||
class="u-alert__content__desc"
|
||||
v-if="description"
|
||||
:style="[{
|
||||
fontSize: $u.addUnit(fontSize),
|
||||
textAlign: center ? 'center' : 'left'
|
||||
}]"
|
||||
:class="[effect === 'dark' ? 'u-alert__text--dark' : `u-alert__text--${type}--light`]"
|
||||
>{{ description }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="u-alert__close"
|
||||
v-if="closable"
|
||||
@tap.stop="closeHandler"
|
||||
>
|
||||
<u-icon
|
||||
name="close"
|
||||
:color="iconColor"
|
||||
size="15"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</u-transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import props from './props.js';
|
||||
/**
|
||||
* Alert 警告提示
|
||||
* @description 警告提示,展现需要关注的信息。
|
||||
* @tutorial https://www.uviewui.com/components/alertTips.html
|
||||
*
|
||||
* @property {String} title 显示的文字
|
||||
* @property {String} type 使用预设的颜色 (默认 'warning' )
|
||||
* @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
|
||||
* @property {Boolean} closable 关闭按钮(默认为叉号icon图标) (默认 false )
|
||||
* @property {Boolean} showIcon 是否显示左边的辅助图标 ( 默认 false )
|
||||
* @property {String} effect 多图时,图片缩放裁剪的模式 (默认 'light' )
|
||||
* @property {Boolean} center 文字是否居中 (默认 false )
|
||||
* @property {String | Number} fontSize 字体大小 (默认 14 )
|
||||
* @property {Object} customStyle 定义需要用到的外部样式
|
||||
* @event {Function} click 点击组件时触发
|
||||
* @example <u-alert :title="title" type = "warning" :closable="closable" :description = "description"></u-alert>
|
||||
*/
|
||||
export default {
|
||||
name: 'u-alert',
|
||||
mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
|
||||
data() {
|
||||
return {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconColor() {
|
||||
return this.effect === 'light' ? this.type : '#fff'
|
||||
},
|
||||
// 不同主题对应不同的图标
|
||||
iconName() {
|
||||
switch (this.type) {
|
||||
case 'success':
|
||||
return 'checkmark-circle-fill';
|
||||
break;
|
||||
case 'error':
|
||||
return 'close-circle-fill';
|
||||
break;
|
||||
case 'warning':
|
||||
return 'error-circle-fill';
|
||||
break;
|
||||
case 'info':
|
||||
return 'info-circle-fill';
|
||||
break;
|
||||
case 'primary':
|
||||
return 'more-circle-fill';
|
||||
break;
|
||||
default:
|
||||
return 'error-circle-fill';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 点击内容
|
||||
clickHandler() {
|
||||
this.$emit('click')
|
||||
},
|
||||
// 点击关闭按钮
|
||||
closeHandler() {
|
||||
this.show = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../libs/css/components.scss";
|
||||
|
||||
.u-alert {
|
||||
position: relative;
|
||||
background-color: $u-primary;
|
||||
padding: 8px 10px;
|
||||
@include flex(row);
|
||||
align-items: center;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
|
||||
&--primary--dark {
|
||||
background-color: $u-primary;
|
||||
}
|
||||
|
||||
&--primary--light {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
&--error--dark {
|
||||
background-color: $u-error;
|
||||
}
|
||||
|
||||
&--error--light {
|
||||
background-color: #FEF0F0;
|
||||
}
|
||||
|
||||
&--success--dark {
|
||||
background-color: $u-success;
|
||||
}
|
||||
|
||||
&--success--light {
|
||||
background-color: #f5fff0;
|
||||
}
|
||||
|
||||
&--warning--dark {
|
||||
background-color: $u-warning;
|
||||
}
|
||||
|
||||
&--warning--light {
|
||||
background-color: #FDF6EC;
|
||||
}
|
||||
|
||||
&--info--dark {
|
||||
background-color: $u-info;
|
||||
}
|
||||
|
||||
&--info--light {
|
||||
background-color: #f4f4f5;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@include flex(column);
|
||||
flex: 1;
|
||||
|
||||
&__title {
|
||||
color: $u-main-color;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
color: $u-main-color;
|
||||
font-size: 14px;
|
||||
flex-wrap: wrap;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__title--dark,
|
||||
&__desc--dark {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__text--primary--light,
|
||||
&__text--primary--light {
|
||||
color: $u-primary;
|
||||
}
|
||||
|
||||
&__text--success--light,
|
||||
&__text--success--light {
|
||||
color: $u-success;
|
||||
}
|
||||
|
||||
&__text--warning--light,
|
||||
&__text--warning--light {
|
||||
color: $u-warning;
|
||||
}
|
||||
|
||||
&__text--error--light,
|
||||
&__text--error--light {
|
||||
color: $u-error;
|
||||
}
|
||||
|
||||
&__text--info--light,
|
||||
&__text--info--light {
|
||||
color: $u-info;
|
||||
}
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
export default {
|
||||
props: {
|
||||
// 头像图片组
|
||||
urls: {
|
||||
type: Array,
|
||||
default: uni.$u.props.avatarGroup.urls
|
||||
},
|
||||
// 最多展示的头像数量
|
||||
maxCount: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.avatarGroup.maxCount
|
||||
},
|
||||
// 头像形状
|
||||
shape: {
|
||||
type: String,
|
||||
default: uni.$u.props.avatarGroup.shape
|
||||
},
|
||||
// 图片裁剪模式
|
||||
mode: {
|
||||
type: String,
|
||||
default: uni.$u.props.avatarGroup.mode
|
||||
},
|
||||
// 超出maxCount时是否显示查看更多的提示
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
default: uni.$u.props.avatarGroup.showMore
|
||||
},
|
||||
// 头像大小
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: uni.$u.props.avatarGroup.size
|
||||
},
|
||||
// 指定从数组的对象元素中读取哪个属性作为图片地址
|
||||
keyName: {
|
||||
type: String,
|
||||
default: uni.$u.props.avatarGroup.keyName
|
||||
},
|
||||
// 头像之间的遮挡比例
|
||||
gap: {
|
||||
type: [String, Number],
|
||||
validator(value) {
|
||||
return value >= 0 && value <= 1
|
||||
},
|
||||
default: uni.$u.props.avatarGroup.gap
|
||||
},
|
||||
// 需额外显示的值
|
||||
extraValue: {
|
||||
type: [Number, String],
|
||||
default: uni.$u.props.avatarGroup.extraValue
|
||||
}
|
||||
}
|
||||
}
|
||||