20260325稳定版

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

After

Width:  |  Height:  |  Size: 23 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+829
View File
@@ -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);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

+13
View File
@@ -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])
});
});
},
});
+78
View File
@@ -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;
+4
View File
@@ -0,0 +1,4 @@
## 1.0.1(2024-08-09)
更新文档
## 1.0.0(2023-09-19)
first
File diff suppressed because one or more lines are too long
+81
View File
@@ -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"
}
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
# Dayjs
Dayjs是一个极简的JavaScript库,可以解析、验证、操作和显示日期和时间。
`uniapp-vue3`推荐使用[rainui](https://ext.dcloud.net.cn/plugin?id=19701),内置dayjs、loadsh、animate.css,方便高效开发
+76
View File
@@ -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>
+81
View File
@@ -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"
}
}
}
}
}
+312
View File
@@ -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"
}
}
+56
View File
@@ -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
+21
View File
@@ -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.
+66
View File
@@ -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>
[![stars](https://img.shields.io/github/stars/umicro/uView2.0?style=flat-square&logo=GitHub)](https://github.com/umicro/uView2.0)
[![forks](https://img.shields.io/github/forks/umicro/uView2.0?style=flat-square&logo=GitHub)](https://github.com/umicro/uView2.0)
[![issues](https://img.shields.io/github/issues/umicro/uView2.0?style=flat-square&logo=GitHub)](https://github.com/umicro/uView2.0/issues)
[![Website](https://img.shields.io/badge/uView-up-blue?style=flat-square)](https://uviewui.com)
[![release](https://img.shields.io/github/v/release/umicro/uView2.0?style=flat-square)](https://gitee.com/umicro/uView2.0/releases)
[![license](https://img.shields.io/github/license/umicro/uView2.0?style=flat-square)](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应用到您的产品中。
+376
View File
@@ -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
}
}
}

Some files were not shown because too many files have changed in this diff Show More