wangfukang 1 week ago
parent
commit
00f49fd566
  1. 771
      package2/IdleTrad/chat.vue
  2. 1068
      package2/IdleTrad/detail.vue
  3. 236
      package2/IdleTrad/sessionList.vue
  4. 211
      package2/components/ie-auth-dialog/ie-auth-dialog.vue
  5. 131
      package2/components/sp-editor/changelog.md
  6. 825
      package2/components/sp-editor/components/sp-editor/color-picker.vue
  7. 140
      package2/components/sp-editor/components/sp-editor/fab-tool.vue
  8. 152
      package2/components/sp-editor/components/sp-editor/link-edit.vue
  9. 828
      package2/components/sp-editor/components/sp-editor/sp-editor.vue
  10. 24
      package2/components/sp-editor/icons/custom-icon.css
  11. 238
      package2/components/sp-editor/icons/editor-icon.css
  12. 83
      package2/components/sp-editor/package.json
  13. 19
      package2/components/sp-editor/readme.md
  14. 1
      package2/components/sp-editor/static/image-resize.min.js
  15. 8
      package2/components/sp-editor/static/quill.min.js
  16. 132
      package2/components/sp-editor/utils/index.js
  17. 45
      package2/group/groupBuyList.vue
  18. 62
      package2/group/groupBuySingle.vue
  19. 306
      package2/group/openKitchenLive.vue
  20. 16
      package2/myCenter/addGoods.vue
  21. 385
      package2/myCenter/myPost.vue

771
package2/IdleTrad/chat.vue

@ -1,218 +1,279 @@
<template>
<view class="page1">
<!-- 固定顶部导航栏悬浮在背景图之上 -->
<view class="nav-bar" :style="{'padding-top': menuButtonInfo.top +'px'}">
<view style="display:flex;align-items: center;">
<view class="back-btn" @tap="back">
<uni-icons type="left" size="28"></uni-icons>
</view>
<view class="title-search">
<view class="title1">
<view style="width: 50rpx;height: 50rpx;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png"
alt="" style="width: 100%;height:100%;background-size: 100%;border-radius: 50rpx;">
</view>
<view class="c-2">
AAA二手数码
</view>
<view class="c-3">
10分钟前来过
</view>
</view>
</view>
</view>
<view class="buy1">
<view class="buy2">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/62c9aa038b2541ad9c400a76ddae7ef8.png"
alt="" />
</view>
<view class="buy3">
<view class="buy4">
给钱就出二手电竞桌电脑桌快速发货
</view>
<view class="buy5">
<view style="flex: 1;">33.6</view>
<view class="buy6">
立即购买
</view>
</view>
</view>
<view class="fish-chat-page">
<view class="nav" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="back" @tap="back"><uni-icons type="left" size="24" color="#15362f"></uni-icons></view>
<view class="nav-main">
<view class="nav-title">{{peerName}}</view>
<view class="nav-sub">校园二手咨询 · 独立聊天</view>
</view>
</view>
<!-- 背景头图随页面滚动 -->
<view class="title">
<view class="hero-spark spark-a"></view>
<view class="hero-spark spark-b"></view>
<view class="hero-spark spark-c"></view>
</view>
<view class="content" :style="{'height':(windowHeight - 280) + 'px'}">
<view class="c-1" style="flex-flow: row-reverse;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png" alt="" />
<view class="ct-1">
你好我的奖品什么时候发货呀
</view>
</view>
<view class="c-1" style="margin-left:20rpx;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png" alt="" />
<view class="ct-1" style="margin-left:20rpx;">
您好您的奖品已经发货请注意查收
</view>
</view>
<view class="c-1" style="flex-flow: row-reverse;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png" alt="" />
<view class="ct-1">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/76670c20e8e444d388f2027794ed1178.png"
alt="" />
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/76670c20e8e444d388f2027794ed1178.png"
alt="" />
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/76670c20e8e444d388f2027794ed1178.png"
alt="" />
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/76670c20e8e444d388f2027794ed1178.png"
alt="" />
</view>
</view>
<view style="margin-top: 40rpx;font-size: 22rpx;font-weight: 700;text-align: center;height: 60rpx;line-height: 60rpx;color: rgba(0, 45, 51, 0.5);">
3月30日 下午3:16
</view>
<view class="c-1" style="flex-flow: row-reverse;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png" alt="" />
<view class="ct-1">
<uni-icons type="phone" size="12"></uni-icons>
通话时长 04:02
</view>
<view class="goods-card" v-if="goods" @tap="goGoods">
<image class="goods-cover" :src="goods.coverImage || imageList[0] || defaultAvatar" mode="aspectFill"></image>
<view class="goods-main">
<view class="goods-title">{{goods.title}}</view>
<view class="goods-price">¥{{goods.price}}</view>
</view>
<view class="c-1" style="margin-left:20rpx;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png" alt="" />
<view class="ct-1" style="margin-left:20rpx;">
<uni-icons type="mic" size="12"></uni-icons>
20``
</view>
</view>
<view class="c-1" style="margin-left:20rpx;">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png" alt="" />
<view class="ct-1" style="margin-left:20rpx;">
<uni-icons type="phone" size="12"></uni-icons>
忙线未接听
</view>
</view>
<view class="goods-link">查看 </view>
</view>
<view class="liao-bottom" :style="{'background':(isIcon||isPhoto)?'#fff':''}">
<view style="display: flex;">
<view class="liao1" @tap="isOpen('input')">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/63a50caf793046db8f6a148dc8f6d158.png"
alt="" />
</view>
<view class="liao2" v-if="!isInput">
<input type="text" placeholder="请输入消息...">
</view>
<view class="liao2" v-else style="text-align: center;line-height: 76rpx;color: #777;">
长按发送语音消息
</view>
<view class="liao1" @tap="isOpen('icon')">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/f69a4fec720848158cba542e2bd25453.png"
alt="" />
</view>
<view class="liao1" @tap="isOpen('photo')">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/6167c3f6fb0e47688417cf8d75de3a36.png"
alt="" />
</view>
</view>
<view class="liao3" v-if="isIcon">
<view class="liao3tu" v-for="(item,index) in iconList" :key="index">
<img :src="item" alt="" />
<scroll-view
class="message-scroll"
scroll-y
:scroll-into-view="scrollIntoView"
:scroll-with-animation="true"
@scrolltoupper="loadOlder"
>
<view class="load-tip" v-if="loadingOlder">加载中...</view>
<view class="safe-tip">请勿发送涉黄涉暴辱骂诈骗等内容文字和图片会进行内容审核</view>
<view
class="message-row"
:class="{ mine: item.mine }"
v-for="item in messages"
:key="item.id"
:id="'msg-' + item.id"
>
<image class="avatar" :src="item.mine ? myAvatar : peerAvatar" mode="aspectFill"></image>
<view class="message-main">
<view class="bubble text-bubble" :class="{ failed: item.localState == 'failed' }" v-if="item.messageType == 1">{{item.content}}</view>
<view class="bubble image-bubble" :class="{ failed: item.localState == 'failed' }" v-else>
<image :src="item.content" mode="aspectFill" @tap="previewImage(item.content)"></image>
</view>
<view class="message-status" v-if="item.mine && item.localState">{{statusText(item)}}</view>
</view>
<view style="width: 100%;height: 80rpx;"></view>
</view>
<view class="liao4" v-if="isPhoto">
<view class="liao5">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/9b7289f1572c409691697a01c35253cd.png"
alt="" />
<view>相册</view>
</view>
<view class="liao5">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/323d6cb335f64ed0873e26337cea1c02.png"
alt="" />
<view>拍摄</view>
</view>
<view class="liao5">
<img src="https://jewel-shop.oss-cn-beijing.aliyuncs.com/927e82199f8542f98287dd6c44979194.png"
alt="" />
<view>语音通话</view>
</view>
<view id="chat-bottom" class="bottom-anchor"></view>
</scroll-view>
<view class="input-bar">
<view class="image-btn" @tap="chooseImage">
<uni-icons type="image" size="24" color="#61736e"></uni-icons>
</view>
<input
v-model.trim="draft"
confirm-type="send"
placeholder="请输入消息..."
@confirm="sendText"
/>
<view class="send-btn" @tap="sendText">发送</view>
</view>
</view>
</template>
<script>
import fishMarketSocket from '@/common/fishMarketSocket.js'
import {
getFishGoodsDetail,
getFishChatSession,
pageFishChatMessages,
uploadFishImage
} from '@/common/fishMarketApi.js'
export default {
data() {
return {
isIcon: false,
isPhoto: false,
isInput: false,
windowHeight: 0,
menuButtonInfo: {},
iconList: [
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/76670c20e8e444d388f2027794ed1178.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/e8297cd515fd4c6f85cc8599e6df9c4f.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/7aa2dcaa230947cca202fe05fe343f3a.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/3252eed51bbc4dc5b6afcd1a1323ee92.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/69e54d7ea20a4eaeaf8e8e2701d1bd11.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/5fc9da807f1643e08ae9a6a0246abe0f.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/2a315a8fd67e4e64a0ee341944c602af.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/2ead554b58bb49089f60ffd6b79856e6.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/ca9a0d17ad664c5ebe77b83174bf23f4.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/00b4d8eaac6f46caade8e470cd8dd1aa.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/1ab15345b6b64124b43404ce2278a4fe.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/230d135b45294d65a244f4b36f6754fb.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/e77f0fcd5aa9427b8e5a0f67a672e7c5.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/778411200f6c443fada53cd167a75337.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/8950d199c11a45c28f18ff6c21a37bf9.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/b6d8f633a37f4f8fa149568f2175ea9d.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/6be32b344f2a4dd493d6e59be737a34b.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/0ea75fe4997045818f659540ca8d62f6.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/5ec0b6185b7348b29dd54bf364d0121a.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/c1c0a93e71f041eaa92eb049015ebedb.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/486e5c5281ed4b819f0f511c70263bb2.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/694cc1e1f2f043758de3dca2cd01914a.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/220e8d566f684819bfc8ba8a26fb95ac.png',
'https://jewel-shop.oss-cn-beijing.aliyuncs.com/5343b3d275ef4bafa9a7dc7cd89d6ffd.png',
]
statusBarTop: 24,
sessionId: '',
goodsId: '',
session: null,
goods: null,
messages: [],
pageNum: 1,
pageSize: 20,
totalPages: 1,
loadingOlder: false,
draft: '',
scrollIntoView: 'chat-bottom',
selfId: String(uni.getStorageSync('id') || ''),
myAvatar: uni.getStorageSync('avatar') || 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png',
defaultAvatar: 'https://jewel-shop.oss-cn-beijing.aliyuncs.com/801c569079da4540990c1cc634186fdd.png',
socketReady: false
}
},
onLoad(option) {
computed: {
imageList() {
if (!this.goods || !this.goods.imageList) return []
return this.goods.imageList
},
peerName() {
if (!this.goods || !this.session) return '校园同学'
return this.selfId == String(this.session.sellerId) ? '买家同学' : (this.goods.userName || '卖家同学')
},
peerAvatar() {
return (this.goods && this.goods.userAvatar) || this.defaultAvatar
}
},
onShow() {
this.menuButtonInfo = uni.getMenuButtonBoundingClientRect();
const windowInfo = uni.getWindowInfo();
this.windowHeight = windowInfo.windowHeight;
onLoad(options) {
this.sessionId = options && options.sessionId ? options.sessionId : ''
this.goodsId = options && options.goodsId ? options.goodsId : ''
if (uni.getMenuButtonBoundingClientRect) {
this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop
}
this.load()
},
onUnload() {
if (this.sessionId) {
fishMarketSocket.unsubscribeSession(this.sessionId)
}
fishMarketSocket.resetHandlers()
fishMarketSocket.close()
},
methods: {
isOpen(type) {
if (type == 'photo') {
this.isIcon = false
this.isPhoto = !this.isPhoto
} else if (type == 'icon') {
this.isPhoto = false
this.isIcon = !this.isIcon
} else {
this.isInput = !this.isInput
back() {
uni.navigateBack()
},
load() {
if (!this.sessionId) return
getFishChatSession(this.sessionId).then(session => {
this.session = session
const goodsId = this.goodsId || (session && session.goodsId)
if (goodsId) {
this.goodsId = goodsId
getFishGoodsDetail(goodsId).then(goods => {
this.goods = goods
})
}
})
this.loadMessages(true).then(() => {
this.initSocket()
})
},
loadMessages(reset) {
if (reset) {
this.pageNum = 1
this.totalPages = 1
}
return pageFishChatMessages(this.sessionId, this.pageNum, this.pageSize).then(page => {
const records = page && (page.records || page.content) ? (page.records || page.content) : []
const normalized = records.reverse().map(this.normalizeMessage)
this.totalPages = Number(page && (page.pages || page.totalPages)) || 1
this.messages = reset ? normalized : normalized.concat(this.messages)
this.scrollToBottom()
})
},
back() {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url: '/pages/index/index'
});
loadOlder() {
if (this.loadingOlder || this.pageNum >= this.totalPages) return
this.loadingOlder = true
this.pageNum += 1
this.loadMessages(false).finally(() => {
this.loadingOlder = false
})
},
normalizeMessage(item) {
return {
...item,
mine: String(item.senderId) == this.selfId,
localState: item.localState || ''
}
},
initSocket() {
if (!this.sessionId) return
fishMarketSocket.resetHandlers()
fishMarketSocket.onMessage((message) => {
if (!message || String(message.sessionId) !== String(this.sessionId)) return
this.handleSocketMessage(message)
})
fishMarketSocket.connect().then(() => {
this.socketReady = fishMarketSocket.subscribeSession(this.sessionId)
}).catch(() => {
this.socketReady = false
})
},
handleSocketMessage(message) {
if (message.type == 'audit_failed' || Number(message.auditStatus) == 2) {
this.markMessageFailed(message.clientMsgId, message.auditReason || '内容审核未通过')
uni.showToast({ title: message.auditReason || '内容审核未通过,请修改后重试', icon: 'none', duration: 3000 })
return
}
this.appendMessage(message)
},
appendMessage(message) {
if (!message || !message.id) return
if (message.clientMsgId) {
const local = this.messages.find(item => item.clientMsgId && item.clientMsgId == message.clientMsgId)
if (local) {
Object.assign(local, this.normalizeMessage(message), { localState: '' })
this.scrollToBottom()
return
}
}
const exists = this.messages.some(item => String(item.id) == String(message.id))
if (exists) return
this.messages.push(this.normalizeMessage(message))
this.scrollToBottom()
},
statusText(item) {
if (item.localState == 'failed') return item.auditReason || '内容审核未通过'
if (item.localState == 'sending') return '发送中'
return ''
},
pushLocalMessage(messageType, content, clientMsgId) {
this.messages.push({
id: clientMsgId,
clientMsgId,
sessionId: this.sessionId,
senderId: this.selfId,
messageType,
content,
mine: true,
localState: 'sending'
})
this.scrollToBottom()
},
markMessageFailed(clientMsgId, reason) {
const local = this.messages.find(item => item.clientMsgId && item.clientMsgId == clientMsgId)
if (!local) return
local.localState = 'failed'
local.auditStatus = 2
local.auditReason = reason || '内容审核未通过'
},
sendText() {
if (!this.draft) return
const content = this.draft
this.draft = ''
this.sendMessage(1, content)
},
sendMessage(messageType, content) {
const clientMsgId = 'fish-' + Date.now() + '-' + Math.random().toString(16).slice(2)
this.pushLocalMessage(messageType, content, clientMsgId)
const payload = {
sessionId: this.sessionId,
messageType,
content,
clientMsgId
}
if (fishMarketSocket.sendMessage(payload)) return
this.markMessageFailed(clientMsgId, '聊天连接未就绪,请稍后重试')
uni.showToast({ title: '聊天连接未就绪,请稍后重试', icon: 'none' })
},
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: async (res) => {
const path = res.tempFilePaths && res.tempFilePaths[0]
if (!path) return
const url = await uploadFishImage(path)
if (url) this.sendMessage(2, url)
}
})
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollIntoView = ''
setTimeout(() => {
this.scrollIntoView = 'chat-bottom'
}, 30)
})
},
previewImage(url) {
if (!url) return
uni.previewImage({ urls: [url], current: url })
},
goGoods() {
if (!this.goodsId) return
uni.navigateTo({ url: '/package2/IdleTrad/detail?id=' + this.goodsId })
}
}
}
@ -222,259 +283,217 @@
page {
width: 100%;
height: 100%;
font-size: 24rpx;
background: #F5F8F5;
color: #00231C;
background: #f7f9f7;
}
.page1 {
.fish-chat-page {
width: 100%;
height: 100%;
font-size: 24rpx;
position: relative;
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #e8fbf3 0%, #f7f9f7 360rpx);
color: #15362f;
}
.title {
background: url('https://jewel-shop.oss-cn-beijing.aliyuncs.com/25cad6ddbfb44230abf0c4d1a6259e16.png') no-repeat;
width: 100%;
height: 400rpx;
background-size: 100%;
position: relative;
overflow: hidden;
.nav {
padding-left: 24rpx;
padding-right: 24rpx;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 8rpx 20rpx rgba(20, 68, 52, 0.06);
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
.back {
width: 68rpx;
height: 68rpx;
display: flex;
align-items: center;
z-index: 100;
padding: 16rpx 24rpx;
box-sizing: border-box;
}
.radio-check {
background: linear-gradient(90deg, #e3ff96, #a6ffea);
width: 42rpx;
height: 42rpx;
border-radius: 42rpx;
text-align: center;
line-height: 42rpx;
margin: 0 18rpx 0 0;
box-shadow: 0 6rpx 16rpx rgba(49, 224, 183, 0.22);
.nav-main {
flex: 1;
}
.radio-no-check {
width: 42rpx;
height: 42rpx;
border-radius: 42rpx;
border: 2rpx solid #e8eeee;
margin: 0 18rpx 0 0;
background: #fff;
.nav-title {
font-size: 30rpx;
font-weight: 900;
}
.pack-options {
display: flex;
align-items: center;
margin-top: 22rpx;
.nav-sub {
margin-top: 4rpx;
color: #7d8b86;
font-size: 20rpx;
}
.pack-option {
.goods-card {
margin: 18rpx 24rpx 0;
padding: 16rpx;
display: flex;
align-items: center;
color: #00231C;
font-size: 28rpx;
font-weight: 800;
margin-right: 40rpx;
border-radius: 24rpx;
background: #fff;
box-shadow: 0 8rpx 22rpx rgba(20, 60, 48, 0.06);
}
.title-search {
display: flex;
flex: 1;
height: 54rpx;
margin-left: 20rpx;
.goods-cover {
width: 104rpx;
height: 104rpx;
border-radius: 18rpx;
background: #edf4f1;
}
.content {}
.back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
.goods-main {
flex: 1;
min-width: 0;
margin-left: 16rpx;
}
.title1 {
display: flex;
width: 100%;
height: 50rpx;
line-height: 50rpx;
.goods-title {
overflow: hidden;
font-size: 26rpx;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.c-1 {
height: 76rpx;
display: flex;
line-height: 76rpx;
font-weight: 700;
margin: 40rpx 20rpx 0 0;
img {
width: 76rpx;
height: 76rpx;
background-size: 100%;
border-radius: 76rpx;
}
.goods-price {
margin-top: 8rpx;
color: #ef4c3f;
font-size: 28rpx;
font-weight: 900;
}
.c-2 {
margin: 0 16rpx;
font-size: 30rpx;
font-weight: 700;
.goods-link {
color: #7f8d88;
font-size: 22rpx;
}
.c-3 {
.message-scroll {
flex: 1;
color: rgba(0, 35, 28, 0.7);
min-height: 0;
padding: 20rpx 24rpx;
box-sizing: border-box;
}
.buy1 {
.safe-tip,
.load-tip {
margin: 10rpx auto 24rpx;
color: #8a9994;
font-size: 21rpx;
text-align: center;
}
.message-row {
margin-bottom: 26rpx;
display: flex;
margin-top: 34rpx;
padding-bottom: 20rpx;
border-bottom: 1px solid #eee;
align-items: flex-start;
}
.buy2 {
width: 168rpx;
height: 168rpx;
.message-row.mine {
flex-direction: row-reverse;
}
img {
width: 100%;
height: 100%;
background-size: 100%;
border-radius: 20rpx;
}
.avatar {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
background: #edf4f1;
}
.buy3 {
flex: 1;
.message-main {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.buy4 {
flex: 1;
font-size: 30rpx;
font-weight: 700;
padding: 10rpx 20rpx;
.mine .message-main {
align-items: flex-end;
}
.buy5 {
height: 70rpx;
line-height: 74rpx;
font-size: 40rpx;
padding: 0 20rpx;
color: red;
font-weight: 700;
display: flex;
.bubble {
max-width: 520rpx;
margin: 0 16rpx;
border-radius: 24rpx;
font-size: 27rpx;
line-height: 42rpx;
word-break: break-all;
}
.liao-bottom {
min-height: 160rpx;
width: 100%;
position: fixed;
bottom: 0;
padding-top: 20rpx;
.text-bubble {
padding: 18rpx 22rpx;
background: #fff;
color: #253b35;
}
.liao1 {
width: 48rpx;
height: 48rpx;
margin: 14rpx;
.mine .text-bubble {
background: linear-gradient(135deg, #37c990, #19af7e);
color: #fff;
}
img {
width: 100%;
height: 100%;
background-size: 100%;
border-radius: 48rpx;
}
.bubble.failed {
opacity: .58;
}
.liao2 {
.image-bubble {
overflow: hidden;
background: #fff;
flex: 1;
height: 76rpx;
border-radius: 76rpx;
}
input {
width: 100%;
height: 100%;
padding: 26rpx 20rpx;
}
.image-bubble image {
width: 260rpx;
height: 260rpx;
display: block;
}
.liao3 {
background: #fff;
display: flex;
flex-wrap: wrap;
.message-status {
max-width: 520rpx;
margin: 8rpx 18rpx 0;
color: #ef4c3f;
font-size: 21rpx;
line-height: 30rpx;
}
.liao3tu {
width: 64rpx;
height: 64rpx;
margin: 30rpx 0 0 38rpx;
.bottom-anchor {
height: 20rpx;
}
img {
width: 100%;
height: 100%;
background-size: 100%;
}
.input-bar {
padding: 16rpx 22rpx calc(16rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -8rpx 22rpx rgba(20, 60, 48, 0.06);
}
.liao4 {
.image-btn {
width: 70rpx;
height: 70rpx;
display: flex;
height: 260rpx;
align-items: center;
justify-content: center;
}
.liao5 {
background: #fff;
text-align: center;
.input-bar input {
flex: 1;
padding-top: 60rpx;
img {
width: 100rpx;
height: 100rpx;
background-size: 100%;
}
height: 72rpx;
padding: 0 22rpx;
border-radius: 38rpx;
background: #f2f7f4;
color: #213832;
font-size: 26rpx;
}
.buy6 {
text-align: center;
width: 180rpx;
height: 70rpx;
background: #00231c;
line-height: 70rpx;
border-radius: 70rpx;
margin: 0 10rpx;
color: #a6ffea;
.send-btn {
width: 104rpx;
height: 72rpx;
margin-left: 14rpx;
border-radius: 38rpx;
background: linear-gradient(135deg, #37c990, #19af7e);
color: #fff;
font-size: 26rpx;
font-weight: 900;
text-align: center;
line-height: 72rpx;
}
.ct-1{
min-width: 160rpx;
background: linear-gradient(90deg, rgba(121, 255, 223, 1), rgba(227, 255, 150, 1));
padding: 0 20rpx;
border-bottom-left-radius: 76rpx;
border-top-left-radius: 76rpx;
border-top-right-radius: 76rpx;
margin-right: 20rpx;
img{
width: 44rpx;
height: 44rpx;
margin-top: 16rpx;
}
}
</style>
</style>

1068
package2/IdleTrad/detail.vue

File diff suppressed because it is too large

236
package2/IdleTrad/sessionList.vue

@ -0,0 +1,236 @@
<template>
<view class="session-page">
<view class="header" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="back" @tap="back"><uni-icons type="left" size="22" color="#15362f"></uni-icons></view>
<view class="title">我的咨询</view>
<view class="subtitle">我咨询的和咨询我的都在这里</view>
</view>
<scroll-view scroll-y class="session-list" @scrolltolower="loadMore">
<view class="session-card" v-for="item in list" :key="item.id" @tap="openChat(item)">
<image class="cover" :src="item.goodsCover || defaultCover" mode="aspectFill"></image>
<view class="main">
<view class="top-row">
<view class="goods-title">{{item.goodsTitle || '校园二手商品'}}</view>
<view class="role-tag" :class="{ seller: item.role == 'seller' }">{{item.roleText}}</view>
</view>
<view class="goods-meta">
<text v-if="item.goodsPrice !== ''">¥{{item.goodsPrice}}</text>
<text>{{item.lastChatTime || item.firstConsultTime || item.createTime || ''}}</text>
</view>
<view class="session-desc">{{item.desc}}</view>
</view>
</view>
<view class="empty" v-if="!list.length && !loading">暂无咨询记录</view>
<view class="load-more" v-if="loading">加载中...</view>
<view class="load-more" v-else-if="list.length && pageNum >= totalPages">没有更多了</view>
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script>
import {
pageMyFishSessions,
getFishGoodsDetail
} from '@/common/fishMarketApi.js'
export default {
data() {
return {
statusBarTop: 24,
list: [],
pageNum: 1,
pageSize: 10,
totalPages: 1,
loading: false,
selfId: String(uni.getStorageSync('id') || ''),
defaultCover: '/static/images/img/songshu.png'
}
},
onLoad() {
if (uni.getMenuButtonBoundingClientRect) {
this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop
}
this.load()
},
methods: {
back() {
uni.navigateBack()
},
loadMore() {
if (this.loading || this.pageNum >= this.totalPages) return
this.pageNum += 1
this.load()
},
load() {
if (this.loading) return
this.loading = true
pageMyFishSessions({
pageNum: this.pageNum,
pageSize: this.pageSize
}).then(async page => {
const records = page && (page.records || page.content) ? (page.records || page.content) : []
this.totalPages = Number(page && (page.pages || page.totalPages)) || 1
const normalized = await this.normalizeSessions(records)
this.list = this.pageNum == 1 ? normalized : this.list.concat(normalized)
}).finally(() => {
this.loading = false
})
},
async normalizeSessions(records) {
const result = []
for (let i = 0; i < records.length; i += 1) {
const session = records[i]
let goods = null
if (session.goodsId) {
goods = await getFishGoodsDetail(session.goodsId).catch(() => null)
}
const role = String(session.sellerId) == this.selfId ? 'seller' : 'buyer'
const imageList = goods && goods.imageList ? goods.imageList : []
result.push({
...session,
role,
roleText: role == 'seller' ? '咨询我的' : '我咨询的',
goodsTitle: goods ? goods.title : '',
goodsCover: goods ? (goods.coverImage || imageList[0] || '') : '',
goodsPrice: goods && goods.price !== undefined ? goods.price : '',
desc: role == 'seller' ? '买家正在咨询这件商品' : '你正在咨询这件商品'
})
}
return result
},
openChat(item) {
const sessionId = item.sessionId || item.id
if (!sessionId) return
uni.navigateTo({
url: `/package2/IdleTrad/chat?sessionId=${sessionId}&goodsId=${item.goodsId || ''}`
})
}
}
}
</script>
<style lang="scss">
page {
background: #f7f9f7;
}
.session-page {
min-height: 100vh;
background: linear-gradient(180deg, #e8fbf3 0%, #f7f9f7 300rpx);
color: #15362f;
}
.header {
padding: 0 28rpx 24rpx;
}
.back {
width: 68rpx;
height: 68rpx;
display: flex;
align-items: center;
}
.title {
margin-top: 8rpx;
font-size: 42rpx;
font-weight: 900;
}
.subtitle {
margin-top: 8rpx;
color: #71857f;
font-size: 23rpx;
}
.session-list {
height: calc(100vh - 190rpx);
}
.session-card {
margin: 18rpx 24rpx 0;
padding: 18rpx;
display: flex;
align-items: center;
border-radius: 26rpx;
background: #fff;
box-shadow: 0 10rpx 26rpx rgba(20, 60, 48, 0.06);
}
.cover {
width: 132rpx;
height: 132rpx;
border-radius: 20rpx;
background: #edf4f1;
flex-shrink: 0;
}
.main {
flex: 1;
min-width: 0;
margin-left: 18rpx;
}
.top-row {
display: flex;
align-items: center;
}
.goods-title {
flex: 1;
overflow: hidden;
font-size: 28rpx;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-tag {
margin-left: 12rpx;
padding: 6rpx 14rpx;
border-radius: 18rpx;
background: #e8fbf3;
color: #18a878;
font-size: 20rpx;
font-weight: 900;
}
.role-tag.seller {
background: #fff2df;
color: #df8618;
}
.goods-meta {
margin-top: 10rpx;
display: flex;
gap: 18rpx;
color: #8a9994;
font-size: 21rpx;
}
.goods-meta text:first-child {
color: #ef4c3f;
font-weight: 900;
}
.session-desc {
margin-top: 10rpx;
color: #61736e;
font-size: 23rpx;
}
.empty,
.load-more {
padding: 70rpx 0;
color: #9aa8a4;
text-align: center;
font-size: 24rpx;
}
.bottom-space {
height: 80rpx;
}
</style>

211
package2/components/ie-auth-dialog/ie-auth-dialog.vue

@ -0,0 +1,211 @@
<template>
<view v-if="visible" class="auth-mask" @touchmove.stop.prevent="noop" @tap.stop="cancel">
<view class="auth-card" @tap.stop>
<view class="auth-glow auth-glow-a"></view>
<view class="auth-glow auth-glow-b"></view>
<view class="auth-badge">{{ badge }}</view>
<view class="auth-title">{{ title }}</view>
<view class="auth-content">{{ content }}</view>
<view class="auth-steps" v-if="steps && steps.length">
<view class="auth-step" v-for="(step, index) in steps" :key="step">
<view class="step-dot">{{ index + 1 }}</view>
<view>{{ step }}</view>
</view>
</view>
<view class="auth-actions">
<view class="auth-btn ghost" @tap="cancel">{{ cancelText }}</view>
<view class="auth-btn primary" @tap="confirm">{{ confirmText }}</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'IeAuthDialog',
data() {
return {
visible: false,
badge: 'i/e 安全认证',
title: '',
content: '',
confirmText: '继续',
cancelText: '暂不继续',
steps: [],
resolver: null
}
},
created() {
uni.$on('ie-auth-dialog:show', this.show)
},
beforeDestroy() {
uni.$off('ie-auth-dialog:show', this.show)
},
methods: {
show(options = {}) {
if (typeof options.handled === 'function') {
options.handled()
}
this.badge = options.badge || 'i/e 安全认证'
this.title = options.title || '进阶实名认证'
this.content = options.content || ''
this.confirmText = options.confirmText || '继续'
this.cancelText = options.cancelText || '暂不继续'
this.steps = options.steps || []
this.resolver = options.resolve || null
this.visible = true
},
confirm() {
this.close(true)
},
cancel() {
this.close(false)
},
close(value) {
this.visible = false
if (this.resolver) {
this.resolver(value)
}
this.resolver = null
},
noop() {}
}
}
</script>
<style lang="scss" scoped>
.auth-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 99998;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
background: rgba(13, 18, 34, 0.48);
backdrop-filter: blur(18rpx);
}
.auth-card {
position: relative;
width: 100%;
overflow: hidden;
padding: 46rpx 38rpx 34rpx;
border-radius: 46rpx;
background: linear-gradient(180deg, rgba(255,255,255,.98), rgba(245,249,255,.96));
border: 1rpx solid rgba(255,255,255,.88);
box-shadow: 0 34rpx 90rpx rgba(28, 35, 74, .28);
}
.auth-glow {
position: absolute;
border-radius: 999rpx;
filter: blur(4rpx);
opacity: .72;
}
.auth-glow-a {
right: -80rpx;
top: -90rpx;
width: 230rpx;
height: 230rpx;
background: rgba(169,255,231,.78);
}
.auth-glow-b {
left: -70rpx;
bottom: 120rpx;
width: 190rpx;
height: 190rpx;
background: rgba(255,126,179,.22);
}
.auth-badge {
position: relative;
display: inline-flex;
padding: 10rpx 18rpx;
border-radius: 999rpx;
color: #6c55d8;
background: rgba(122,77,255,.09);
font-size: 22rpx;
font-weight: 800;
}
.auth-title {
position: relative;
margin-top: 22rpx;
color: #11162a;
font-size: 42rpx;
line-height: 54rpx;
font-weight: 900;
}
.auth-content {
position: relative;
margin-top: 16rpx;
color: rgba(17,22,42,.62);
font-size: 26rpx;
line-height: 42rpx;
}
.auth-steps {
position: relative;
margin-top: 26rpx;
padding: 22rpx;
border-radius: 28rpx;
background: rgba(241,247,255,.82);
}
.auth-step {
display: flex;
align-items: center;
gap: 16rpx;
min-height: 48rpx;
color: rgba(17,22,42,.72);
font-size: 24rpx;
font-weight: 700;
}
.step-dot {
width: 34rpx;
height: 34rpx;
line-height: 34rpx;
text-align: center;
border-radius: 50%;
color: #11162a;
background: #a9ffe7;
font-size: 20rpx;
font-weight: 900;
}
.auth-actions {
position: relative;
display: flex;
gap: 18rpx;
margin-top: 34rpx;
}
.auth-btn {
flex: 1;
height: 84rpx;
line-height: 84rpx;
text-align: center;
border-radius: 999rpx;
font-size: 27rpx;
font-weight: 900;
}
.auth-btn.ghost {
color: rgba(17,22,42,.56);
background: rgba(17,22,42,.06);
}
.auth-btn.primary {
color: #11162a;
background: linear-gradient(135deg, #a9ffe7, #ffcf7b);
box-shadow: 0 16rpx 32rpx rgba(76, 209, 184, .22);
}
</style>

131
package2/components/sp-editor/changelog.md

@ -0,0 +1,131 @@
## 1.5.0(2024-11-01)
1. 更新弹窗中使用的示例
## 1.4.9(2024-08-30)
1. 修复在app端和小程序端插入超链接可能会回显出部分特殊标识的bug
## 1.4.8(2024-08-16)
1. 有群友反馈,通过wangEditor添加的含有图片的富文本的宽高是在style内联样式中的,但是uni-editor不能解析img标签的内联宽高,因此我封装一个工具方法convertImgStylesToAttributes,处理一下富文本字符串即可,详见示例一
## 1.4.7(2024-08-12)
1. 新增标题、字体、字体大小、字间距段前后距等工具子级悬浮工具栏
2. 对于字体样式:可在组件的data中的fabTools.fontFamily中自行引入添加你本地的自定义字体(app中对于部分css自带的字体例如微软雅黑等不支持,因为app中不自带这些字体,你可能需要自行引入字体文件)
## 1.4.6(2024-08-08)
1. 修复字体工具栏无法隐藏的bug
## 1.4.5(2024-07-21)
1. 解决video图标冲突问题
## 1.4.4(2024-07-20)
1. 新增视频插入功能
2. 更新示例一(关于视频插入参考请见示例一)
## 1.4.3(2024-07-16)
1. 更新示例工程
## 1.4.2(2024-07-16)
1. 更新示例工程
## 1.4.1(2024-06-14)
1. 更新示例工程
## 1.4.0(2024-05-31)
1. 更新示例工程
## 1.3.9(2024-05-31)
1. 修复调色板无法正常选色的问题
## 1.3.8(2024-05-13)
1. 更新示例三(微信小程序上使用setContents造成聚焦滚动的处理)
## 1.3.7(2024-05-10)
1. 修复添加超链接后,不触发input更新当前最新内容的bug
## 1.3.6(2024-05-09)
1. 文档迁移
## 1.3.5(2024-05-09)
1. 所有事件携带编辑器id参数,以便循环时能区分处理
2. 更新示例工程
## 1.3.4(2024-05-08)
1. 更新示例工程
2. 新增editorId参数
## 1.3.3(2024-03-22)
1. 修复微信小程序长按无法粘贴的问题
## 1.3.2(2024-03-14)
1. 更新了toolbar样式与配置,见文档
2. 更新示例工程,媒体查询响应式写法
3. 优化了只读模式效果,开启只读模式后,文章内容的超链接可正常点击并跳转
## 1.3.1(2024-03-14)
1. 优化了只读功能,开启只读后自动隐藏工具栏
2. 更新示例工程
## 1.3.0(2024-03-07)
1. 新增addLink的emit事件
## 1.2.9(2024-02-23)
1. 更新文档
## 1.2.8(2024-02-23)
1. 新增了添加超链接的工具,toolbar中link字段,默认开启
2. 优化了部分逻辑
3. 更新文档、更新示例工程
## 1.2.7(2024-02-23)
1. 更新文档,更新示例工程
2. 添加toolbar中图标字体大小可配置项
## 1.2.6(2024-02-22)
1. 添加导出工具按钮,可将当前已编辑的html导出至页面解析
2. 超链接工具按钮正在尝试开发中(貌似目前官方不支持)
## 1.2.5(2024-02-19)
1. 更新示例工程(吸顶写法)
2. 完善调色板功能
## 1.2.4(2024-02-18)
1. 修复工具栏颜色按钮底色动态切换问题
## 1.2.3(2024-02-18)
1. 更新示例工程
## 1.2.2(2024-02-18)
1. 删除log调试打印
## 1.2.1(2024-02-18)
1. 修复了颜色图标不会动态切换的问题
## 1.2.0(2024-02-18)
1. 修复选择颜色时会将所选文字删除的bug
## 1.1.9(2024-02-04)
1. 更新示例工程
## 1.1.8(2024-02-04)
1. 文档修改
## 1.1.7(2024-02-04)
1. 新增toolbar配置项,可自由配置工具栏工具列表
2. 移除组件内原templates属性,默认初始化编辑器内容请看文档使用方式示例
3. 更新文档
## 1.1.6(2024-01-31)
1. 更好的兼容vue2了,修复在vue2下高度可能超出的问题
2. 示例工程兼容vue2
## 1.1.5(2024-01-30)
1. 修复工具栏字体按钮无效的问题
## 1.1.4(2024-01-30)
1. 解决默认初始化内容时前缀空格或缩进无效的问题
2. 解决点击工具栏高亮状态后输入内容时便失去高亮的bug
3. 更新示例工程
## 1.1.3(2024-01-23)
1. 重写高度动态计算逻辑,现在对不同屏幕尺寸的适应性更强了
## 1.1.2(2024-01-17)
1. 修复分割线会生成多条的问题
## 1.1.1(2024-01-15)
1. 更新文档
## 1.1.0(2024-01-15)
1. insertText方法在插入内容的时候会移动光标聚焦,导致焦点回滚到视口处
2. 更新示例工程
## 1.0.9(2024-01-04)
1. 更新文档
## 1.0.8(2024-01-04)
1. 修复h5端官方cdn请求失败的问题,详见问答贴:https://ask.dcloud.net.cn/article/40900
## 1.0.7(2024-01-03)
1. 移除v-bind="$attrs",该写法在微信小程序不支持
## 1.0.6(2023-12-29)
1. 更新文档
## 1.0.5(2023-12-29)
1. 更新了init方法,可以使用返回的editor实例尽情自定义
2. 组件在<editor>上添加v-bind="$attrs"属性穿透,可以使用原editor参数,官方文档:https://uniapp.dcloud.net.cn/component/editor.html
## 1.0.4(2023-12-29)
1. 优化了切换文字和背景颜色是,可能会导致切换后不生效的问题
2. 修复在部分设备上的微信小程序中可能会存在颜色版无法正常滑动的问题
3. 更友好的交互体验:添加图标悬停字样描述、添加格式化文本弹窗确认
4. 有插入视频的需求,暂时可能无法实现,官方给予的回复是:目前各端的eidtor组件都不能直接插入视频,编辑时可以采用视频封面占位,并在图片中保存视频信息,在预览时再还原为视频。
## 1.0.3(2023-10-13)
1. 更新readme文档
2. 更新调整组件示例项目,添加插件代码中部分注释
## 1.0.2(2023-10-13)
1. 更新uni_modules规范,可一键导入组件
2. 更新组件示例项目(包括使用uniCloud.uploadFile多选上传图片示例方法)
## 1.0.1(2023-10-12)
1. 修复小程序中自动聚焦滚动到富文本组件区域的bug
2. 略微调整了富文本上方toolbar工具栏中按钮的大小尺寸
## 1.0.0(2023-9-19)
1. 新增字体与背景颜色板
2. 可自定义预设内容模板
3. 解决官方样例在小程序和app部分报错不兼容的问题
4. 可配合云存储上传富文本中插入的图片 本质上是基于官方内置富文本editor组件改版封装,所以官方有的功能都有,官方能兼容的也都兼容

825
package2/components/sp-editor/components/sp-editor/color-picker.vue

@ -0,0 +1,825 @@
<template>
<view v-if="show" class="t-wrapper" @touchmove.stop.prevent="moveHandle">
<view class="t-mask" :class="{ active: active }" @click.stop="close"></view>
<view class="t-box" :class="{ active: active }">
<view class="t-header">
<view class="t-header-button" @click="close">取消</view>
<view class="t-header-button" @click="confirm">确认</view>
</view>
<view class="t-color__box" :style="{ background: 'rgb(' + bgcolor.r + ',' + bgcolor.g + ',' + bgcolor.b + ')' }">
<view
class="t-background boxs"
@touchstart="touchstart($event, 0)"
@touchmove="touchmove($event, 0)"
@touchend="touchend($event, 0)"
>
<view class="t-color-mask"></view>
<view class="t-pointer" :style="{ top: site[0].top - 8 + 'px', left: site[0].left - 8 + 'px' }"></view>
</view>
</view>
<view class="t-control__box">
<view class="t-control__color">
<view
class="t-control__color-content"
:style="{ background: 'rgba(' + rgba.r + ',' + rgba.g + ',' + rgba.b + ',' + rgba.a + ')' }"
></view>
</view>
<view class="t-control-box__item">
<view
class="t-controller boxs"
@touchstart="touchstart($event, 1)"
@touchmove="touchmove($event, 1)"
@touchend="touchend($event, 1)"
>
<view class="t-hue">
<view class="t-circle" :style="{ left: site[1].left - 12 + 'px' }"></view>
</view>
</view>
<view
class="t-controller boxs"
@touchstart="touchstart($event, 2)"
@touchmove="touchmove($event, 2)"
@touchend="touchend($event, 2)"
>
<view class="t-transparency">
<view class="t-circle" :style="{ left: site[2].left - 12 + 'px' }"></view>
</view>
</view>
</view>
</view>
<view class="t-result__box">
<view v-if="mode" class="t-result__item">
<view class="t-result__box-input">{{ hex }}</view>
<view class="t-result__box-text">HEX</view>
</view>
<template v-else>
<view class="t-result__item">
<view class="t-result__box-input">{{ rgba.r }}</view>
<view class="t-result__box-text">R</view>
</view>
<view class="t-result__item">
<view class="t-result__box-input">{{ rgba.g }}</view>
<view class="t-result__box-text">G</view>
</view>
<view class="t-result__item">
<view class="t-result__box-input">{{ rgba.b }}</view>
<view class="t-result__box-text">B</view>
</view>
<view class="t-result__item">
<view class="t-result__box-input">{{ rgba.a }}</view>
<view class="t-result__box-text">A</view>
</view>
</template>
<view class="t-result__item t-select" @click="select">
<view class="t-result__box-input">
<view>切换</view>
<view>模式</view>
</view>
</view>
</view>
<view class="t-alternative">
<view class="t-alternative__item" v-for="(item, index) in colorList" :key="index">
<view
class="t-alternative__item-content"
:style="{ background: 'rgba(' + item.r + ',' + item.g + ',' + item.b + ',' + item.a + ')' }"
@click="selectColor(item)"
></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
color: {
type: Object,
default: () => {
return {
r: 0,
g: 0,
b: 0,
a: 0
}
}
},
spareColor: {
type: Array,
default() {
return []
}
}
},
data() {
return {
show: false,
active: false,
// rgba
rgba: {
r: 0,
g: 0,
b: 0,
a: 1
},
// hsb
hsb: {
h: 0,
s: 0,
b: 0
},
site: [
{
top: 0,
left: 0
},
{
left: 0
},
{
left: 0
}
],
index: 0,
bgcolor: {
r: 255,
g: 0,
b: 0,
a: 1
},
hex: '#000000',
mode: true,
colorList: [
{
r: 244,
g: 67,
b: 54,
a: 1
},
{
r: 233,
g: 30,
b: 99,
a: 1
},
{
r: 156,
g: 39,
b: 176,
a: 1
},
{
r: 103,
g: 58,
b: 183,
a: 1
},
{
r: 63,
g: 81,
b: 181,
a: 1
},
{
r: 33,
g: 150,
b: 243,
a: 1
},
{
r: 3,
g: 169,
b: 244,
a: 1
},
{
r: 0,
g: 188,
b: 212,
a: 1
},
{
r: 0,
g: 150,
b: 136,
a: 1
},
{
r: 76,
g: 175,
b: 80,
a: 1
},
{
r: 139,
g: 195,
b: 74,
a: 1
},
{
r: 205,
g: 220,
b: 57,
a: 1
},
{
r: 255,
g: 235,
b: 59,
a: 1
},
{
r: 255,
g: 193,
b: 7,
a: 1
},
{
r: 255,
g: 152,
b: 0,
a: 1
},
{
r: 255,
g: 87,
b: 34,
a: 1
},
{
r: 121,
g: 85,
b: 72,
a: 1
},
{
r: 158,
g: 158,
b: 158,
a: 1
},
{
r: 0,
g: 0,
b: 0,
a: 0.5
},
{
r: 0,
g: 0,
b: 0,
a: 0
}
]
}
},
created() {
this.ready()
},
methods: {
ready() {
this.rgba = this.color
if (this.spareColor.length !== 0) {
this.colorList = this.spareColor
}
},
/**
* 初始化
*/
init() {
// hsb
this.hsb = this.rgbToHex(this.rgba)
// this.setColor();
this.setValue(this.rgba)
},
moveHandle() {},
open() {
this.show = true
this.$nextTick(() => {
this.init()
setTimeout(() => {
this.active = true
setTimeout(() => {
this.getSelectorQuery()
}, 350)
}, 50)
})
},
close() {
this.active = false
this.$nextTick(() => {
setTimeout(() => {
this.show = false
}, 500)
})
},
confirm() {
this.close()
this.$emit('confirm', {
rgba: this.rgba,
hex: this.hex
})
},
//
select() {
this.mode = !this.mode
},
//
selectColor(item) {
this.setColorBySelect(item)
},
touchstart(e, index) {
const { pageX, pageY, clientX, clientY } = e.touches[0]
// pageXclientX
this.moveX = clientX || pageX
this.moveY = clientY || pageY
this.setPosition(this.moveX, this.moveY, index)
},
touchmove(e, index) {
const { pageX, pageY, clientX, clientY } = e.touches[0]
this.moveX = clientX || pageX
this.moveY = clientY || pageY
this.setPosition(this.moveX, this.moveY, index)
},
touchend(e, index) {},
/**
* 设置位置
*/
setPosition(x, y, index) {
this.index = index
const { top, left, width, height } = this.position[index]
//
this.site[index].left = Math.max(0, Math.min(parseInt(x - left), width))
if (index === 0) {
this.site[index].top = Math.max(0, Math.min(parseInt(y - top), height))
//
this.hsb.s = parseInt((100 * this.site[index].left) / width)
this.hsb.b = parseInt(100 - (100 * this.site[index].top) / height)
this.setColor()
this.setValue(this.rgba)
} else {
this.setControl(index, this.site[index].left)
}
},
/**
* 设置 rgb 颜色
*/
setColor() {
const rgb = this.HSBToRGB(this.hsb)
this.rgba.r = rgb.r
this.rgba.g = rgb.g
this.rgba.b = rgb.b
},
/**
* 设置二进制颜色
* @param {Object} rgb
*/
setValue(rgb) {
this.hex = '#' + this.rgbToHex(rgb)
},
setControl(index, x) {
const { top, left, width, height } = this.position[index]
if (index === 1) {
this.hsb.h = parseInt((360 * x) / width)
this.bgcolor = this.HSBToRGB({
h: this.hsb.h,
s: 100,
b: 100
})
this.setColor()
} else {
this.rgba.a = (x / width).toFixed(1)
}
this.setValue(this.rgba)
},
/**
* rgb 二进制 hex
* @param {Object} rgb
*/
rgbToHex(rgb) {
let hex = [rgb.r.toString(16), rgb.g.toString(16), rgb.b.toString(16)]
hex.map(function (str, i) {
if (str.length == 1) {
hex[i] = '0' + str
}
})
return hex.join('')
},
setColorBySelect(getrgb) {
const { r, g, b, a } = getrgb
let rgb = {}
rgb = {
r: r ? parseInt(r) : 0,
g: g ? parseInt(g) : 0,
b: b ? parseInt(b) : 0,
a: a ? a : 0
}
this.rgba = rgb
this.hsb = this.rgbToHsb(rgb)
this.changeViewByHsb()
},
changeViewByHsb() {
const [a, b, c] = this.position
this.site[0].left = parseInt((this.hsb.s * a.width) / 100)
this.site[0].top = parseInt(((100 - this.hsb.b) * a.height) / 100)
this.setColor(this.hsb.h)
this.setValue(this.rgba)
this.bgcolor = this.HSBToRGB({
h: this.hsb.h,
s: 100,
b: 100
})
this.site[1].left = (this.hsb.h / 360) * b.width
this.site[2].left = this.rgba.a * c.width
},
/**
* hsb rgb
* @param {Object} 颜色模式 H(hues)表示色相S(saturation)表示饱和度Bbrightness表示亮度
*/
HSBToRGB(hsb) {
let rgb = {}
let h = Math.round(hsb.h)
let s = Math.round((hsb.s * 255) / 100)
let v = Math.round((hsb.b * 255) / 100)
if (s == 0) {
rgb.r = rgb.g = rgb.b = v
} else {
let t1 = v
let t2 = ((255 - s) * v) / 255
let t3 = ((t1 - t2) * (h % 60)) / 60
if (h == 360) h = 0
if (h < 60) {
rgb.r = t1
rgb.b = t2
rgb.g = t2 + t3
} else if (h < 120) {
rgb.g = t1
rgb.b = t2
rgb.r = t1 - t3
} else if (h < 180) {
rgb.g = t1
rgb.r = t2
rgb.b = t2 + t3
} else if (h < 240) {
rgb.b = t1
rgb.r = t2
rgb.g = t1 - t3
} else if (h < 300) {
rgb.b = t1
rgb.g = t2
rgb.r = t2 + t3
} else if (h < 360) {
rgb.r = t1
rgb.g = t2
rgb.b = t1 - t3
} else {
rgb.r = 0
rgb.g = 0
rgb.b = 0
}
}
return {
r: Math.round(rgb.r),
g: Math.round(rgb.g),
b: Math.round(rgb.b)
}
},
rgbToHsb(rgb) {
let hsb = {
h: 0,
s: 0,
b: 0
}
let min = Math.min(rgb.r, rgb.g, rgb.b)
let max = Math.max(rgb.r, rgb.g, rgb.b)
let delta = max - min
hsb.b = max
hsb.s = max != 0 ? (255 * delta) / max : 0
if (hsb.s != 0) {
if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta
else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta
else hsb.h = 4 + (rgb.r - rgb.g) / delta
} else hsb.h = -1
hsb.h *= 60
if (hsb.h < 0) hsb.h = 0
hsb.s *= 100 / 255
hsb.b *= 100 / 255
return hsb
},
getSelectorQuery() {
const views = uni.createSelectorQuery().in(this)
views
.selectAll('.boxs')
.boundingClientRect((data) => {
if (!data || data.length === 0) {
setTimeout(() => this.getSelectorQuery(), 20)
return
}
this.position = data
// this.site[0].top = data[0].height;
// this.site[0].left = 0;
// this.site[1].left = data[1].width;
// this.site[2].left = data[2].width;
this.setColorBySelect(this.rgba)
})
.exec()
},
hex2Rgb(hexColor, alpha = 1) {
const color = hexColor.slice(1)
const r = parseInt(color.slice(0, 2), 16)
const g = parseInt(color.slice(2, 4), 16)
const b = parseInt(color.slice(4, 6), 16)
return {
r: r,
g: g,
b: b,
a: alpha
}
}
},
watch: {
spareColor(newVal) {
this.colorList = newVal
},
color(newVal) {
this.ready()
}
}
}
</script>
<style>
.t-wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
z-index: 9999;
}
.t-box {
width: 100%;
position: absolute;
bottom: 0;
padding: 30upx 0;
padding-top: 0;
background: #fff;
transition: all 0.3s;
transform: translateY(100%);
}
.t-box.active {
transform: translateY(0%);
}
.t-header {
display: flex;
justify-content: space-between;
width: 100%;
height: 100upx;
border-bottom: 1px #eee solid;
box-shadow: 1px 0 2px rgba(0, 0, 0, 0.1);
background: #fff;
}
.t-header-button {
display: flex;
align-items: center;
width: 150upx;
height: 100upx;
font-size: 30upx;
color: #666;
padding-left: 20upx;
}
.t-header-button:last-child {
justify-content: flex-end;
padding-right: 20upx;
}
.t-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: -1;
transition: all 0.3s;
opacity: 0;
}
.t-mask.active {
opacity: 1;
}
.t-color__box {
position: relative;
height: 400upx;
background: rgb(255, 0, 0);
overflow: hidden;
box-sizing: border-box;
margin: 0 20upx;
margin-top: 20upx;
box-sizing: border-box;
}
.t-background {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.t-color-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 400upx;
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.t-pointer {
position: absolute;
bottom: -8px;
left: -8px;
z-index: 2;
width: 15px;
height: 15px;
border: 1px #fff solid;
border-radius: 50%;
}
.t-show-color {
width: 100upx;
height: 50upx;
}
.t-control__box {
margin-top: 50upx;
width: 100%;
display: flex;
padding-left: 20upx;
box-sizing: border-box;
}
.t-control__color {
flex-shrink: 0;
width: 100upx;
height: 100upx;
border-radius: 50%;
background-color: #fff;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
background-size: 36upx 36upx;
background-position: 0 0, 18upx 18upx;
border: 1px #eee solid;
overflow: hidden;
}
.t-control__color-content {
width: 100%;
height: 100%;
}
.t-control-box__item {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
padding: 0 30upx;
}
.t-controller {
position: relative;
width: 100%;
height: 16px;
background-color: #fff;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
background-size: 32upx 32upx;
background-position: 0 0, 16upx 16upx;
}
.t-hue {
width: 100%;
height: 100%;
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.t-transparency {
width: 100%;
height: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, rgb(0, 0, 0));
}
.t-circle {
position: absolute;
/* right: -10px; */
top: -2px;
width: 20px;
height: 20px;
box-sizing: border-box;
border-radius: 50%;
background: #fff;
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.1);
}
.t-result__box {
margin-top: 20upx;
padding: 10upx;
width: 100%;
display: flex;
box-sizing: border-box;
}
.t-result__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10upx;
width: 100%;
box-sizing: border-box;
}
.t-result__box-input {
padding: 10upx 0;
width: 100%;
font-size: 28upx;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.1);
color: #999;
text-align: center;
background: #fff;
}
.t-result__box-text {
margin-top: 10upx;
font-size: 28upx;
line-height: 2;
}
.t-select {
flex-shrink: 0;
width: 150upx;
padding: 0 30upx;
}
.t-select .t-result__box-input {
border-radius: 10upx;
border: none;
color: #999;
box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.1);
background: #fff;
}
.t-select .t-result__box-input:active {
box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.1);
}
.t-alternative {
display: flex;
flex-wrap: wrap;
/* justify-content: space-between; */
width: 100%;
padding-right: 10upx;
box-sizing: border-box;
}
.t-alternative__item {
margin-left: 12upx;
margin-top: 10upx;
width: 50upx;
height: 50upx;
border-radius: 10upx;
background-color: #fff;
background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee),
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%, #eee);
background-size: 36upx 36upx;
background-position: 0 0, 18upx 18upx;
border: 1px #eee solid;
overflow: hidden;
}
.t-alternative__item-content {
width: 50upx;
height: 50upx;
background: rgba(255, 0, 0, 0.5);
}
.t-alternative__item:active {
transition: all 0.3s;
transform: scale(1.1);
}
</style>

140
package2/components/sp-editor/components/sp-editor/fab-tool.vue

@ -0,0 +1,140 @@
<template>
<view class="fab-tool">
<view id="toolfab">
<slot></slot>
</view>
<view class="fab-tool-content" :style="placementStyle" id="placementfab">
<slot name="content" v-if="visible"></slot>
</view>
</view>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
default: false
},
placement: {
type: String,
default: 'auto' // 'auto' | 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'
}
},
data() {
return {
placementHeight: '0',
placementType: ''
}
},
watch: {
visible(newVal) {
if (newVal) {
const { screenWidth } = uni.getSystemInfoSync()
this.$nextTick(() => {
let placementWidth = 0
uni
.createSelectorQuery()
.in(this)
.select('#placementfab')
.boundingClientRect((res) => {
this.placementHeight = -res.height + 'px'
placementWidth = res.width
})
.exec()
//
if (this.placement == 'auto') {
uni
.createSelectorQuery()
.in(this)
.select('#toolfab')
.boundingClientRect((res) => {
let leftRemain = res.left
let rightRemain = screenWidth - leftRemain
if (rightRemain > placementWidth) {
this.placementType = 'bottom-start'
} else if (leftRemain > placementWidth) {
this.placementType = 'bottom-end'
} else {
this.placementType = 'bottom-center'
}
})
.exec()
}
})
}
}
},
mounted() {
this.placementType = this.placement
},
computed: {
placementStyle() {
let position = {}
switch (this.placementType) {
case 'top-start':
position = {
top: this.placementHeight,
left: 0
}
break
case 'top-center':
position = {
top: this.placementHeight,
left: '50%',
transform: 'translateX(-50%)'
}
break
case 'top-end':
position = {
top: this.placementHeight,
right: 0
}
break
case 'bottom-start':
position = {
bottom: this.placementHeight,
left: 0
}
break
case 'bottom-center':
position = {
bottom: this.placementHeight,
left: '50%',
transform: 'translateX(-50%)'
}
break
case 'bottom-end':
position = {
bottom: this.placementHeight,
right: 0
}
break
default:
break
}
return position
}
},
methods: {
//
}
}
</script>
<style lang="scss">
.fab-tool {
position: relative;
.fab-tool-content {
position: absolute;
z-index: 999;
background-color: #ffffff;
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.05), 2px 2px 4px rgba(0, 0, 0, 0.05);
border-radius: 12rpx;
box-sizing: border-box;
}
}
</style>

152
package2/components/sp-editor/components/sp-editor/link-edit.vue

@ -0,0 +1,152 @@
<template>
<view class="link-edit-container" v-if="showPopup">
<view class="link-edit">
<view class="title">添加链接</view>
<view class="edit">
<view class="description">
链接描述
<input v-model="descVal" type="text" class="input" placeholder="请输入链接描述" />
</view>
<view class="address">
链接地址
<input v-model="addrVal" type="text" class="input" placeholder="请输入链接地址" />
</view>
</view>
<view class="control">
<view class="cancel" @click="close">取消</view>
<view class="confirm" @click="onConfirm">确认</view>
</view>
</view>
<view class="mask"></view>
</view>
</template>
<script>
export default {
data() {
return {
showPopup: false,
descVal: '',
addrVal: ''
}
},
methods: {
open() {
this.showPopup = true
this.$emit('open')
},
close() {
this.showPopup = false
this.descVal = ''
this.addrVal = ''
this.$emit('close')
},
onConfirm() {
if (!this.descVal) {
uni.showToast({
title: '请输入链接描述',
icon: 'none'
})
return
}
if (!this.addrVal) {
uni.showToast({
title: '请输入链接地址',
icon: 'none'
})
return
}
this.$emit('confirm', {
text: this.descVal,
href: this.addrVal
})
this.close()
}
}
}
</script>
<style lang="scss">
.link-edit-container {
.link-edit {
width: 80%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffff;
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.05), 2px 2px 4px rgba(0, 0, 0, 0.05);
border-radius: 12rpx;
box-sizing: border-box;
z-index: 999;
font-size: 14px;
.title {
height: 80rpx;
display: flex;
justify-content: center;
align-items: center;
}
.edit {
padding: 24rpx;
border-top: 1px solid #eeeeee;
border-bottom: 1px solid #eeeeee;
box-sizing: border-box;
.input {
flex: 1;
padding: 4px;
font-size: 14px;
border: 1px solid #eeeeee;
border-radius: 8rpx;
.uni-input-placeholder {
color: #dddddd;
}
}
.description {
display: flex;
align-items: center;
}
.address {
display: flex;
align-items: center;
margin-top: 24rpx;
}
}
.control {
height: 80rpx;
display: flex;
cursor: pointer;
.cancel {
flex: 1;
color: #dd524d;
display: flex;
justify-content: center;
align-items: center;
}
.confirm {
border-left: 1px solid #eeeeee;
flex: 1;
color: #007aff;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.05);
z-index: 998;
}
}
</style>

828
package2/components/sp-editor/components/sp-editor/sp-editor.vue

@ -0,0 +1,828 @@
<template>
<view class="sp-editor" :style="{ '--icon-size': iconSize, '--icon-columns': iconColumns }">
<view class="sp-editor-toolbar" v-if="!readOnly" @tap="format">
<!-- 标题栏 -->
<fab-tool v-if="toolbarList.includes('header')" :visible="curFab == 'header'">
<view
:class="formats.header ? 'ql-active' : ''"
class="iconfont icon-header"
title="标题"
data-name="header"
@click.stop="fabTap('header')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'header')">
<view v-for="item in fabTools.header" :key="item.value">
<view
v-if="toolbarList.includes(item.name)"
class="fab-sub"
:class="[formats.header === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]"
:title="item.title"
data-name="header"
:data-value="item.value"
></view>
</view>
</view>
</template>
</fab-tool>
<view
v-if="toolbarList.includes('bold')"
:class="formats.bold ? 'ql-active' : ''"
class="iconfont icon-zitijiacu"
title="加粗"
data-name="bold"
></view>
<view
v-if="toolbarList.includes('italic')"
:class="formats.italic ? 'ql-active' : ''"
class="iconfont icon-zitixieti"
title="斜体"
data-name="italic"
></view>
<view
v-if="toolbarList.includes('underline')"
:class="formats.underline ? 'ql-active' : ''"
class="iconfont icon-zitixiahuaxian"
title="下划线"
data-name="underline"
></view>
<view
v-if="toolbarList.includes('strike')"
:class="formats.strike ? 'ql-active' : ''"
class="iconfont icon-zitishanchuxian"
title="删除线"
data-name="strike"
></view>
<!-- 对齐方式 -->
<fab-tool v-if="toolbarList.includes('align')" :visible="curFab == 'align'">
<view
:class="formats.align ? 'ql-active' : ''"
class="iconfont icon-zuoyouduiqi"
title="对齐方式"
data-name="align"
@click.stop="fabTap('align')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'align')">
<view v-for="item in fabTools.align" :key="item.value">
<view
v-if="toolbarList.includes(item.name)"
class="fab-sub"
:class="[formats.align === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]"
:title="item.title"
data-name="align"
:data-value="item.value"
></view>
</view>
</view>
</template>
</fab-tool>
<!-- 行间距 -->
<fab-tool v-if="toolbarList.includes('lineHeight')" :visible="curFab == 'lineHeight'">
<view
:class="formats.lineHeight ? 'ql-active' : ''"
class="iconfont icon-line-height"
title="行间距"
data-name="lineHeight"
@click.stop="fabTap('lineHeight')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'lineHeight')">
<view v-for="item in fabTools.lineHeight" :key="item.value">
<view
class="fab-sub"
:class="[formats.lineHeight === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]"
:title="item.title"
data-name="lineHeight"
:data-value="item.value"
>
{{ item.name }}
</view>
</view>
</view>
</template>
</fab-tool>
<!-- 字间距 -->
<fab-tool v-if="toolbarList.includes('letterSpacing')" :visible="curFab == 'letterSpacing'">
<view
:class="formats.letterSpacing ? 'ql-active' : ''"
class="iconfont icon-Character-Spacing"
title="字间距"
data-name="letterSpacing"
@click.stop="fabTap('letterSpacing')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'letterSpacing')">
<view v-for="item in fabTools.space" :key="item.value">
<view
class="fab-sub"
:class="[
formats.letterSpacing === item.value ? 'ql-active' : '',
item.icon ? 'iconfont' : '',
item.icon
]"
:title="item.title"
data-name="letterSpacing"
:data-value="item.value"
>
{{ item.name }}
</view>
</view>
</view>
</template>
</fab-tool>
<!-- 段前距 -->
<fab-tool v-if="toolbarList.includes('marginTop')" :visible="curFab == 'marginTop'">
<view
:class="formats.marginTop ? 'ql-active' : ''"
class="iconfont icon-722bianjiqi_duanqianju"
title="段前距"
data-name="marginTop"
@click.stop="fabTap('marginTop')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'marginTop')">
<view v-for="item in fabTools.space" :key="item.value">
<view
class="fab-sub"
:class="[formats.marginTop === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]"
:title="item.title"
data-name="marginTop"
:data-value="item.value"
>
{{ item.name }}
</view>
</view>
</view>
</template>
</fab-tool>
<!-- 段后距 -->
<fab-tool v-if="toolbarList.includes('marginBottom')" :visible="curFab == 'marginBottom'">
<view
:class="formats.marginBottom ? 'ql-active' : ''"
class="iconfont icon-723bianjiqi_duanhouju"
title="段后距"
data-name="marginBottom"
@click.stop="fabTap('marginBottom')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'marginBottom')">
<view v-for="item in fabTools.space" :key="item.value">
<view
class="fab-sub"
:class="[
formats.marginBottom === item.value ? 'ql-active' : '',
item.icon ? 'iconfont' : '',
item.icon
]"
:title="item.title"
data-name="marginBottom"
:data-value="item.value"
>
{{ item.name }}
</view>
</view>
</view>
</template>
</fab-tool>
<!-- 字体栏 -->
<fab-tool v-if="toolbarList.includes('fontFamily')" :visible="curFab == 'fontFamily'">
<view
:class="formats.fontFamily ? 'ql-active' : ''"
class="iconfont icon-font"
title="字体"
data-name="fontFamily"
@click.stop="fabTap('fontFamily')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'fontFamily')">
<view v-for="item in fabTools.fontFamily" :key="item.value">
<view
class="fab-sub"
:class="[formats.fontFamily === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]"
:title="item.title"
data-name="fontFamily"
:data-value="item.value"
>
{{ item.name }}
</view>
</view>
</view>
</template>
</fab-tool>
<!-- 字体大小栏 -->
<fab-tool v-if="toolbarList.includes('fontSize')" :visible="curFab == 'fontSize'">
<view
:class="formats.fontSize ? 'ql-active' : ''"
class="iconfont icon-fontsize"
title="字号"
data-name="fontSize"
@click.stop="fabTap('fontSize')"
></view>
<template #content>
<view class="fab-tools" @click.stop="fabTapSub($event, 'fontSize')">
<view v-for="item in fabTools.fontSize" :key="item.value">
<view
class="fab-sub"
:class="[formats.fontSize === item.value ? 'ql-active' : '', item.icon ? 'iconfont' : '', item.icon]"
:title="item.title"
data-name="fontSize"
:data-value="item.value"
>
{{ item.name }}
</view>
</view>
</view>
</template>
</fab-tool>
<view
v-if="toolbarList.includes('color')"
:style="{ color: formats.color ? textColor : 'initial' }"
class="iconfont icon-text_color"
title="文字颜色"
data-name="color"
:data-value="textColor"
></view>
<view
v-if="toolbarList.includes('backgroundColor')"
:style="{ color: formats.backgroundColor ? backgroundColor : 'initial' }"
class="iconfont icon-fontbgcolor"
title="背景颜色"
data-name="backgroundColor"
:data-value="backgroundColor"
></view>
<view v-if="toolbarList.includes('date')" class="iconfont icon-date" title="日期" @tap="insertDate"></view>
<view
v-if="toolbarList.includes('listCheck')"
class="iconfont icon--checklist"
title="待办"
data-name="list"
data-value="check"
></view>
<view
v-if="toolbarList.includes('listOrdered')"
:class="formats.list === 'ordered' ? 'ql-active' : ''"
class="iconfont icon-youxupailie"
title="有序列表"
data-name="list"
data-value="ordered"
></view>
<view
v-if="toolbarList.includes('listBullet')"
:class="formats.list === 'bullet' ? 'ql-active' : ''"
class="iconfont icon-wuxupailie"
title="无序列表"
data-name="list"
data-value="bullet"
></view>
<view
v-if="toolbarList.includes('divider')"
class="iconfont icon-fengexian"
title="分割线"
@click="insertDivider"
></view>
<view
v-if="toolbarList.includes('indentDec')"
class="iconfont icon-outdent"
title="减少缩进"
data-name="indent"
data-value="-1"
></view>
<view
v-if="toolbarList.includes('indentInc')"
class="iconfont icon-indent"
title="增加缩进"
data-name="indent"
data-value="+1"
></view>
<view
v-if="toolbarList.includes('scriptSub')"
:class="formats.script === 'sub' ? 'ql-active' : ''"
class="iconfont icon-zitixiabiao"
title="下标"
data-name="script"
data-value="sub"
></view>
<view
v-if="toolbarList.includes('scriptSuper')"
:class="formats.script === 'super' ? 'ql-active' : ''"
class="iconfont icon-zitishangbiao"
title="上标"
data-name="script"
data-value="super"
></view>
<view
v-if="toolbarList.includes('direction')"
:class="formats.direction === 'rtl' ? 'ql-active' : ''"
class="iconfont icon-direction-rtl"
title="文本方向"
data-name="direction"
data-value="rtl"
></view>
<view
v-if="toolbarList.includes('image')"
class="iconfont icon-charutupian"
title="图片"
@tap="insertImage"
></view>
<view v-if="toolbarList.includes('video')" class="iconfont icon-video" title="视频" @tap="insertVideo"></view>
<view
v-if="toolbarList.includes('link')"
class="iconfont icon-charulianjie"
title="超链接"
@tap="insertLink"
></view>
<view v-if="toolbarList.includes('undo')" class="iconfont icon-undo" title="撤销" @tap="undo"></view>
<view v-if="toolbarList.includes('redo')" class="iconfont icon-redo" title="重做" @tap="redo"></view>
<view
v-if="toolbarList.includes('removeFormat')"
class="iconfont icon-clearedformat"
title="清除格式"
@tap="removeFormat"
></view>
<view v-if="toolbarList.includes('clear')" class="iconfont icon-shanchu" title="清空" @tap="clear"></view>
<view v-if="toolbarList.includes('export')" class="iconfont icon-baocun" title="导出" @tap="exportHtml"></view>
</view>
<!-- 自定义功能组件 -->
<!-- 调色板 -->
<color-picker
v-if="toolbarList.includes('color') || toolbarList.includes('backgroundColor')"
ref="colorPickerRef"
:color="defaultColor"
@confirm="confirmColor"
></color-picker>
<!-- 添加链接的操作弹窗 -->
<link-edit v-if="toolbarList.includes('link') && !readOnly" ref="linkEditRef" @confirm="confirmLink"></link-edit>
<view class="sp-editor-wrapper" @longpress="eLongpress">
<editor
:id="editorId"
class="ql-editor editor-container"
:class="{ 'ql-image-overlay-none': readOnly }"
show-img-size
show-img-toolbar
show-img-resize
:placeholder="placeholder"
:read-only="readOnly"
@statuschange="onStatusChange"
@ready="onEditorReady"
@input="onEditorInput"
></editor>
</view>
</view>
</template>
<script>
import ColorPicker from './color-picker.vue'
import LinkEdit from './link-edit.vue'
import FabTool from './fab-tool.vue'
import { addLink, linkFlag } from '../../utils'
export default {
components: {
ColorPicker,
LinkEdit,
FabTool
},
props: {
// id便使id
editorId: {
type: String,
default: 'editor'
},
placeholder: {
type: String,
default: '写点什么吧 ~'
},
//
readOnly: {
type: Boolean,
default: false
},
// -1
maxlength: {
type: Number,
default: -1
},
//
toolbarConfig: {
type: Object,
default: () => {
return {
keys: [], //
excludeKeys: [], //
iconSize: '18px', //
iconColumns: 10 //
}
}
}
},
watch: {
toolbarConfig: {
deep: true,
immediate: true,
handler(newToolbar) {
/**
* 若工具栏配置中keys存在则以keys为准
* 否则以excludeKeys向toolbarAllList中排查
* 若keys与excludeKeys皆为空则以toolbarAllList为准
*/
if (newToolbar.keys?.length > 0) {
this.toolbarList = newToolbar.keys
} else {
this.toolbarList =
newToolbar.excludeKeys?.length > 0
? this.toolbarAllList.filter((item) => !newToolbar.excludeKeys.includes(item))
: this.toolbarAllList
}
this.iconSize = newToolbar.iconSize || '18px'
this.iconColumns = newToolbar.iconColumns || 10
}
}
},
data() {
return {
formats: {},
curFab: '', //
fabXY: {},
textColor: '',
backgroundColor: '',
curColor: '',
defaultColor: { r: 0, g: 0, b: 0, a: 1 }, //
iconSize: '20px', //
iconColumns: 10, //
toolbarList: [],
toolbarAllList: [
'header', //
'H1', //
'H2', //
'H3', //
'H4', //
'H5', //
'H6', //
'bold', //
'italic', //
'underline', // 线
'strike', // 线
'align', //
'alignLeft', //
'alignCenter', //
'alignRight', //
'alignJustify', //
'fontSize', //
'image', //
],
fabTools: {
header: [
{ title: '一级标题', name: 'H1', value: 1, icon: 'icon-format-header-1' },
{ title: '二级标题', name: 'H2', value: 2, icon: 'icon-format-header-2' },
{ title: '三级标题', name: 'H3', value: 3, icon: 'icon-format-header-3' },
{ title: '四级标题', name: 'H4', value: 4, icon: 'icon-format-header-4' },
{ title: '五级标题', name: 'H5', value: 5, icon: 'icon-format-header-5' },
{ title: '六级标题', name: 'H6', value: 6, icon: 'icon-format-header-6' }
],
fontFamily: [
{ title: '宋体', name: '宋', value: '宋体', icon: '' },
{ title: '黑体', name: '黑', value: '黑体', icon: '' },
{ title: '楷体', name: '楷', value: '楷体', icon: '' },
{ title: '仿宋', name: '仿', value: '仿宋', icon: '' },
{ title: '华文隶书', name: '隶', value: 'STLiti', icon: '' },
{ title: '华文行楷', name: '行', value: 'STXingkai', icon: '' },
{ title: '幼圆', name: '圆', value: 'YouYuan', icon: '' }
],
fontSize: [
{ title: '12', name: '12', value: '12px', icon: '' },
{ title: '14', name: '14', value: '14px', icon: '' },
{ title: '16', name: '16', value: '16px', icon: '' },
{ title: '18', name: '18', value: '18px', icon: '' },
{ title: '20', name: '20', value: '20px', icon: '' },
{ title: '22', name: '22', value: '22px', icon: '' },
{ title: '24', name: '24', value: '24px', icon: '' }
],
align: [
{ title: '左对齐', name: 'alignLeft', value: 'left', icon: 'icon-zuoduiqi' },
{ title: '居中对齐', name: 'alignCenter', value: 'center', icon: 'icon-juzhongduiqi' },
{ title: '右对齐', name: 'alignRight', value: 'right', icon: 'icon-youduiqi' },
{ title: '两端对齐', name: 'alignJustify', value: 'justify', icon: 'icon-zuoyouduiqi' }
],
lineHeight: [
{ title: '1倍', name: '1', value: '1', icon: '' },
{ title: '1.5倍', name: '1.5', value: '1.5', icon: '' },
{ title: '2倍', name: '2', value: '2', icon: '' },
{ title: '2.5倍', name: '2.5', value: '2.5', icon: '' },
{ title: '3倍', name: '3', value: '3', icon: '' }
],
// //
space: [
{ title: '0.5倍', name: '0.5', value: '0.5em', icon: '' },
{ title: '1倍', name: '1', value: '1em', icon: '' },
{ title: '1.5倍', name: '1.5', value: '1.5em', icon: '' },
{ title: '2倍', name: '2', value: '2em', icon: '' },
{ title: '2.5倍', name: '2.5', value: '2.5em', icon: '' },
{ title: '3倍', name: '3', value: '3em', icon: '' }
]
}
}
},
methods: {
onEditorReady() {
uni
.createSelectorQuery()
.in(this)
.select('#' + this.editorId)
.context((res) => {
this.editorCtx = res.context
this.$emit('init', this.editorCtx, this.editorId)
})
.exec()
},
undo() {
this.editorCtx.undo()
},
redo() {
this.editorCtx.redo()
},
format(e) {
let { name, value } = e.target.dataset
if (!name) return
switch (name) {
case 'color':
case 'backgroundColor':
this.curColor = name
this.showPicker()
break
default:
this.editorCtx.format(name, value)
break
}
},
//
fabTap(fabType) {
if (this.curFab != fabType) {
this.curFab = fabType
} else {
this.curFab = ''
}
},
//
fabTapSub(e, fabType) {
this.format(e)
this.fabTap(fabType)
},
showPicker() {
switch (this.curColor) {
case 'color':
this.defaultColor = this.textColor
? this.$refs.colorPickerRef.hex2Rgb(this.textColor)
: { r: 0, g: 0, b: 0, a: 1 }
break
case 'backgroundColor':
this.defaultColor = this.backgroundColor
? this.$refs.colorPickerRef.hex2Rgb(this.backgroundColor)
: { r: 0, g: 0, b: 0, a: 0 }
break
}
this.$refs.colorPickerRef.open()
},
confirmColor(e) {
switch (this.curColor) {
case 'color':
this.textColor = e.hex
this.editorCtx.format('color', this.textColor)
break
case 'backgroundColor':
this.backgroundColor = e.hex
this.editorCtx.format('backgroundColor', this.backgroundColor)
break
}
},
onStatusChange(e) {
if (e.detail.color) {
this.textColor = e.detail.color
}
if (e.detail.backgroundColor) {
this.backgroundColor = e.detail.backgroundColor
}
this.formats = e.detail
},
insertDivider() {
this.editorCtx.insertDivider()
},
clear() {
uni.showModal({
title: '清空编辑器',
content: '确定清空编辑器吗?',
success: ({ confirm }) => {
if (confirm) {
this.editorCtx.clear()
}
}
})
},
removeFormat() {
uni.showModal({
title: '文本格式化',
content: '确定要清除所选择部分文本块格式吗?',
showCancel: true,
success: ({ confirm }) => {
if (confirm) {
this.editorCtx.removeFormat()
}
}
})
},
insertDate() {
const date = new Date()
const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
this.editorCtx.insertText({ text: formatDate })
},
insertLink() {
this.$refs.linkEditRef.open()
},
/**
* 确认添加链接
* @param {Object} e { text: '链接描述', href: '链接地址' }
*/
confirmLink(e) {
this.$refs.linkEditRef.close()
addLink(this.editorCtx, e, () => {
// inputbug
this.editorCtx.getContents({
success: (res) => {
this.$emit('input', { html: res.html, text: res.text }, this.editorId)
}
})
})
this.$emit('addLink', e, this.editorId)
},
insertImage() {
// #ifdef APP-PLUS || H5
uni.chooseImage({
// count: 1, // 9
success: (res) => {
const { tempFiles } = res
//
this.$emit('upinImage', tempFiles, this.editorCtx, this.editorId)
},
fail() {
uni.showToast({
title: '未授权访问相册权限,请授权后使用',
icon: 'none'
})
}
})
// #endif
// #ifdef MP-WEIXIN
// 2.21.0 wx.chooseImage 使 uni.chooseMedia
uni.chooseMedia({
// count: 1, // 9
mediaType: ['image'],
success: (res) => {
// chooseImage
const { tempFiles } = res
this.$emit('upinImage', tempFiles, this.editorCtx, this.editorId)
},
fail() {
uni.showToast({
title: '未授权访问相册权限,请授权后使用',
icon: 'none'
})
}
})
// #endif
},
insertVideo() {
uni.chooseVideo({
sourceType: ['camera', 'album'],
success: (res) => {
const { tempFilePath } = res
//
this.$emit('upinVideo', tempFilePath, this.editorCtx, this.editorId)
},
fail() {
uni.showToast({
title: '未授权访问媒体权限,请授权后使用',
icon: 'none'
})
}
})
},
onEditorInput(e) {
// 使getContentshtmltextonStatusChangetoolbar
// detailreturn
if (Object.keys(e.detail).length <= 0) return
const { html, text } = e.detail
// return
if (text.indexOf(linkFlag) !== -1) return
const maxlength = parseInt(this.maxlength)
const textStr = text.replace(/[ \t\r\n]/g, '')
if (textStr.length > maxlength && maxlength != -1) {
uni.showModal({
content: `超过${maxlength}字数啦~`,
confirmText: '确定',
showCancel: false,
success: () => {
this.$emit('overMax', { html, text }, this.editorId)
}
})
} else {
this.$emit('input', { html, text }, this.editorId)
}
},
//
exportHtml() {
this.editorCtx.getContents({
success: (res) => {
this.$emit('exportHtml', res.html, this.editorId)
}
})
},
eLongpress() {
/**
* 微信小程序官方editor的长按事件有bug需要重写覆盖不需做任何逻辑可见下面小程序社区问题链接
* @tutorial https://developers.weixin.qq.com/community/develop/doc/000c04b3e1c1006f660065e4f61000
*/
}
}
}
</script>
<style lang="scss">
@import '../../icons/editor-icon.css';
@import '../../icons/custom-icon.css';
.sp-editor {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.sp-editor-toolbar {
box-sizing: border-box;
padding: calc(var(--icon-size) / 4) 0;
border-bottom: 1px solid #e4e4e4;
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
display: grid;
grid-template-columns: repeat(var(--icon-columns), 1fr);
}
.iconfont {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: calc(var(--icon-size) * 1.8);
cursor: pointer;
font-size: var(--icon-size);
}
.sp-editor-wrapper {
flex: 1;
overflow: hidden;
position: relative;
}
.editor-container {
padding: 8rpx 16rpx;
box-sizing: border-box;
width: 100%;
height: 100%;
font-size: 16px;
line-height: 1.5;
}
.ql-image-overlay-none {
::v-deep .ql-image-overlay {
pointer-events: none;
opacity: 0;
}
}
::v-deep .ql-editor.ql-blank::before {
font-style: normal;
color: #cccccc;
}
::v-deep .ql-container {
min-height: unset;
}
.ql-active {
color: #66ccff;
}
.fab-tools {
display: flex;
padding: 0 10rpx;
box-sizing: border-box;
.fab-sub {
width: auto;
height: auto;
margin: 10rpx;
}
}
</style>

24
package2/components/sp-editor/icons/custom-icon.css

@ -0,0 +1,24 @@
@font-face {
font-family: 'iconfont';
src: url('data:font/ttf;charset=utf-8;base64,AAEAAAANAIAAAwBQRkZUTahWRN8AAAeEAAAAHEdERUYAKQALAAAHZAAAAB5PUy8yPElI7QAAAVgAAABgY21hcOY96ckAAAHMAAABSmdhc3D//wADAAAHXAAAAAhnbHlmPxK+mgAAAyQAAAF4aGVhZCjWXYcAAADcAAAANmhoZWEHngOFAAABFAAAACRobXR4DIgAQAAAAbgAAAASbG9jYQCkALwAAAMYAAAADG1heHABGQB9AAABOAAAACBuYW1lXoIBAgAABJwAAAKCcG9zdENa1TcAAAcgAAAAOQABAAAAAQAA20yLC18PPPUACwQAAAAAAOLbjMwAAAAA4tuMzABA/78DwAMzAAAACAACAAAAAAAAAAEAAAOA/4AAXAQAAAAAAAPAAAEAAAAAAAAAAAAAAAAAAAAEAAEAAAAFAHEACgAAAAAAAgAAAAoACgAAAP8AAAAAAAAABAQAAZAABQAAAokCzAAAAI8CiQLMAAAB6wAyAQgAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZADA5hTmHgOA/4AAAAPcAIAAAAABAAAAAAAAAAAAAAAgAAEEAAAAAAAAAAQAAAAEAABAAIgAAAAAAAMAAAADAAAAHAABAAAAAABEAAMAAQAAABwABAAoAAAABgAEAAEAAuYU5h7//wAA5hTmHv//Ge8Z5gABAAAAAAAAAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApAC8AAoAQABAA8ACwAARABQAGAAoADQAQABMAFgAZABwAAABHgEUBg8BBiIuAT0BND4BMh8BFTcBESERBSEiJjURNDYzITIWFREUBgE0NjIWFREUBiImNRMyFhQGKwEiJjQ2MxcyFhQGKwEiJjQ2MwE0NjIWFREUBiImNRMiJjQ2OwEyFhQGIwciJjQ2OwEyFhQGIwJwCAgICKAHEQ8JCQ8RBxBCAV79AAMA/QAbJSUbAwAbJSX9JRMaExMaEyANExMNgA0TEw2ADRMTDYANExMNAqATGhMTGhMgDRMTDYANExMNgA0TEw2ADRMTDQGcBQ8QDwVgBAgPCcAJDwgEVU4n/wACAP4AQCUbAgAbJSUb/gAbJQJgDRMTDf3gDRMTDQGAExoTExoTwBMaExMaEwFgDRMTDf3gDRMTDQFAExoTExoTwBMaExMaEwABAIj/vwNpAzMACwAAEzMRIREzESMRIREjiJABwZCQ/j+QAzP+jwFx/IwBiP54AAAAABIA3gABAAAAAAAAABMAKAABAAAAAAABAAgATgABAAAAAAACAAcAZwABAAAAAAADAAgAgQABAAAAAAAEAAgAnAABAAAAAAAFAAsAvQABAAAAAAAGAAgA2wABAAAAAAAKACsBPAABAAAAAAALABMBkAADAAEECQAAACYAAAADAAEECQABABAAPAADAAEECQACAA4AVwADAAEECQADABAAbwADAAEECQAEABAAigADAAEECQAFABYApQADAAEECQAGABAAyQADAAEECQAKAFYA5AADAAEECQALACYBaABDAHIAZQBhAHQAZQBkACAAYgB5ACAAaQBjAG8AbgBmAG8AbgB0AABDcmVhdGVkIGJ5IGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABSAGUAZwB1AGwAYQByAABSZWd1bGFyAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABWAGUAcgBzAGkAbwBuACAAMQAuADAAAFZlcnNpb24gMS4wAABpAGMAbwBuAGYAbwBuAHQAAGljb25mb250AABHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAABHZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuAABoAHQAdABwADoALwAvAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAABodHRwOi8vZm9udGVsbG8uY29tAAAAAAIAAAAAAAAACgAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABQAAAAEAAgECAQMFdmlkZW8GaGVhZGVyAAAAAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMABAABAAQAAAACAAAAAAAAAAEAAAAA4p8rRgAAAADi24zMAAAAAOLbjMw=') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-header:before {
content: "\e61e";
}
.icon-video:before {
content: "\e614";
}

238
package2/components/sp-editor/icons/editor-icon.css

File diff suppressed because one or more lines are too long

83
package2/components/sp-editor/package.json

@ -0,0 +1,83 @@
{
"id": "sp-editor",
"displayName": "官方富文本编辑器editor组件改良扩展优化版",
"version": "1.5.0",
"description": "基于官方的富文本编辑器editor组件,进行改良扩展优化版,添加了调色板,添加超链接等功能,可自定义扩展工具,快来试试吧~",
"keywords": [
"富文本",
"editor",
"编辑器"
],
"repository": "",
"engines": {
},
"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",
"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",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

19
package2/components/sp-editor/readme.md

@ -0,0 +1,19 @@
# sp-editor
### 视频插入功能
`于2024-7-20日v1.4.4版本更新视频插入功能(属实是鸽🕊了太久了)`
- 实现方案:先以图片占位,在导出时,将携带视频链接的图片转换成视频标签。
- 更多请参考示例一
- 如果该插件有帮助到您,还望能点赞好评一下,谢谢!🌟
### 文档迁移
> 防止文档失效,提供下列五个地址,内容一致
- [地址一](https://sonvee.github.io/sv-app-docs/docs-github/src/plugins/sp-editor/sp-editor.html)
- [地址二](https://sv-app-docs.pages.dev/src/plugins/sp-editor/sp-editor.html)
- [地址三](https://sv-app-docs.4everland.app/src/plugins/sp-editor/sp-editor.html)
- [地址四](https://sv-app-docs.vercel.app/src/plugins/sp-editor/sp-editor.html) (需要梯子)
- [地址五](https://static-mp-74bfcbac-6ba6-4f39-8513-8831390ff75a.next.bspapp.com/docs-uni/src/plugins/sp-editor/sp-editor.html) (有IP限制)

1
package2/components/sp-editor/static/image-resize.min.js

File diff suppressed because one or more lines are too long

8
package2/components/sp-editor/static/quill.min.js

File diff suppressed because one or more lines are too long

132
package2/components/sp-editor/utils/index.js

@ -0,0 +1,132 @@
// 标识必须独一无二 - 标识是为了使用insertText插入标识文本后,查找到标识所在delta位置的索引
export const linkFlag = '#-*=*-*=*-*=*@-link超链接标识link-@*=*-*=*-*=*-#'
export function addLink(editorCtx, attr, callback) {
// 先插入一段文本内容
editorCtx.insertText({
text: linkFlag
})
// 获取全文delta内容
editorCtx.getContents({
success(res) {
let options = res.delta.ops
const findex = options.findIndex(item => {
return item.insert && typeof item.insert !== 'object' && item.insert?.indexOf(linkFlag) !== -1
})
// 根据标识查找到插入的位置
if (findex > -1) {
const findOption = options[findex]
const findAttributes = findOption.attributes
// 将该findOption分成三部分:前内容 要插入的link 后内容
const [prefix, suffix] = findOption.insert.split(linkFlag);
const handleOps = []
// 前内容
if (prefix) {
const prefixOps = findAttributes ? {
insert: prefix,
attributes: findAttributes
} : {
insert: prefix
}
handleOps.push(prefixOps)
}
// 插入的link
const linkOps = {
insert: attr.text,
attributes: {
link: attr.href,
textDecoration: attr.textDecoration || 'none', // 下划线
color: attr.color || '#007aff'
}
}
handleOps.push(linkOps)
// 后内容
if (suffix) {
const suffixOps = findAttributes ? {
insert: suffix,
attributes: findAttributes
} : {
insert: suffix
}
handleOps.push(suffixOps)
}
// 删除原options[findex]并在findex位置插入上述三个ops
options.splice(findex, 1);
options.splice(findex, 0, ...handleOps);
// 最后重新初始化内容,注意该方法会导致光标重置到最开始位置
editorCtx.setContents({
delta: {
ops: options
}
})
// 所以最后建议使富文本光标失焦,让用户手动聚焦光标
editorCtx.blur()
// 后续回调操作
if (callback) callback()
}
}
})
}
/**
* 将含有特殊图片形式视频的富文本转换成正常视频的富文本
* @param {String} html 要进行处理的富文本字符串
* @returns {String} 返回处理结果
*/
export function handleHtmlWithVideo(html) {
// 正则表达式用于匹配img标签中带有alt属性且alt属性值为视频链接的模式
const regex = /<img\s+src="[^"]*"\s+alt="([^"]*)"[^>]*>/g
// 使用replace方法和一个函数回调来替换匹配到的内容
return html.replace(regex, (match, videoUrl) => {
// 替换为video标签,并添加controls属性以便用户可以控制播放
return `<video width="80%" controls><source src="${videoUrl}" type="video/mp4"></video>`
})
}
/**
* 将img标签中内联style属性中的宽高样式提取出标签width与height属性
* @param {Object} html 要处理的富文本字符串
* @returns {Object} 返回处理结果
*/
export function convertImgStylesToAttributes(html) {
return html.replace(/<img\s+([^>]+)\s*>/g, function(match, attributes) {
// 分割属性
const attrs = attributes.split(/\s+/);
// 找到style属性的位置
const styleIndex = attrs.findIndex(attr => attr.startsWith('style='));
if (styleIndex === -1) return match; // 如果没有找到style属性,则返回原样
// 提取style属性值
const styleAttr = attrs.splice(styleIndex, 1)[0];
const style = styleAttr.match(/"([^"]*)"/)[1];
// 解析 style 属性
const styleObj = {};
style.split(';').forEach(function(part) {
if (part) {
const [name, value] = part.split(':');
styleObj[name.trim()] = value.trim();
}
});
// 创建新的 img 标签
let newTag = '<img';
if (styleObj.width) {
newTag += ` width="${styleObj.width}"`;
}
if (styleObj.height) {
newTag += ` height="${styleObj.height}"`;
}
// 添加原有的属性,包括修改过的style属性
newTag += ` ${styleAttr} ${attrs.join(' ')}`;
// 关闭 img 标签
newTag += '>';
return newTag;
});
}

45
package2/group/groupBuyList.vue

@ -119,8 +119,13 @@
</view>
<view class="shop-content">
<view class="shop-live-row">
<text>{{item._dormStatus}}</text>
<text>{{item.groupDeliveryTime}}分钟</text>
<text class="shop-status-tag" :class="{'shop-status-disabled': !hasOpenKitchenVideo(item)}" @tap.stop="openKitchen(item)">
{{getOpenKitchenText(item)}}
</text>
<text class="shop-status-tag" :class="{'shop-status-disabled': item.supportDineIn != 1}">
{{getDineInText(item)}}
</text>
<text class="shop-time-tag">{{item.groupDeliveryTime}}分钟</text>
</view>
<view class="shop-name">
{{item.shopName}}
@ -252,7 +257,6 @@
dorm: '回寝路上',
food: '今晚适合来点汤汤水水'
}],
groupTags: ['附近宿舍正在拼', '刚刚有人拼成功', '夜宵爆单中', '饭搭子已上线'],
groupAvatars: ['🍜', '🥤', '🍢', '🍔', '🥟', '🍗'],
menuBtn: [{
name: '美食',
@ -529,7 +533,6 @@
const selfDeliveryTime = Number(item.shopDeliveryDuration)
const transferTimeValue = Number(item.transferDeliveryDuration)
const transferTime = item.supportTransferDelivery == 1 && !isNaN(transferTimeValue) && transferTimeValue > 0 ? transferTimeValue : 0
item._dormStatus = this.groupTags[index % this.groupTags.length]
item._groupTip = ['还差1人就成团', '刚下课,大家都在拼', '这口热乎的很适合夜宵', '附近同学想一起点'][index % 4]
let baseDeliveryTime
if (item.supportShopDelivery == 1 && !isNaN(selfDeliveryTime) && selfDeliveryTime > 0) {
@ -556,6 +559,27 @@
const locationText = item.shopDeliveryLocation == 1 ? '送宿舍' : '送楼下'
return `商家自配送 · ${feeText} · ${locationText}`
},
hasOpenKitchenVideo(item) {
return !!(item && item.openKitchenVideoUrl && String(item.openKitchenVideoUrl).trim())
},
getOpenKitchenText(item) {
return this.hasOpenKitchenVideo(item) ? '明厨亮灶' : '无明厨亮灶'
},
getDineInText(item) {
return item && item.supportDineIn == 1 ? '可堂食' : '无堂食'
},
openKitchen(item) {
if (!this.hasOpenKitchenVideo(item)) {
this.tui.toast('暂无明厨亮灶')
return
}
const cacheKey = 'openKitchenLiveUrl_' + Date.now()
uni.setStorageSync(cacheKey, String(item.openKitchenVideoUrl).trim())
uni.navigateTo({
url: '/package2/group/openKitchenLive?key=' + cacheKey +
'&name=' + encodeURIComponent(item.shopName || '明厨亮灶')
})
},
judgeBusinessStatus() {
for (let i = 0; i < this.shopList.length; i++) {
if (this.shopList[i].shopTakeaway != '' && this.shopList[i].shopTakeaway != null) {
@ -1351,7 +1375,8 @@
.shop-live-row {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
gap: 10rpx;
margin-bottom: 10rpx;
text {
@ -1364,12 +1389,18 @@
font-weight: 900;
}
text:first-child {
.shop-status-tag {
background: rgba(219, 255, 157, 0.72);
color: #08735d;
}
text:last-child {
.shop-status-disabled {
background: rgba(225, 231, 228, 0.72) !important;
color: #87938f !important;
}
.shop-time-tag {
margin-left: auto;
background: #ff744d;
color: #fff;
box-shadow: 0 8rpx 14rpx rgba(255, 93, 45, 0.2);

62
package2/group/groupBuySingle.vue

@ -42,6 +42,12 @@
<view class="shop-subtitle">
{{isStoreGroupOrder ? '到店核销,约上同学一起拼更划算' : '约上室友同学一起拼,越快成团越快开吃'}}
</view>
<view class="shop-status-row">
<text :class="{'shop-status-disabled': !hasOpenKitchenVideo(shopItem)}" @tap.stop="goDetail('openKitchen')">
{{getOpenKitchenText(shopItem)}}
</text>
<text>{{getDineInText(shopItem)}}</text>
</view>
<view class="shop-content-center">
<view class="shop-rate">
<view class="shop-rate-num">{{shopItem.shopScore ? shopItem.shopScore : 5}}</view>
@ -60,7 +66,7 @@
<view class="shop-content-bottom">
<view class="shop-action-list">
<view class="shop-action-btn" @tap.stop="goDetail('openKitchen')">
明厨亮灶
{{getOpenKitchenText(shopItem)}}
</view>
<view class="shop-action-btn" @tap.stop="goDetail('shopDetail')">
商家资质
@ -821,6 +827,8 @@
shopTypeTitle: '',
saleCount: '',
remark: '',
openKitchenVideoUrl: '',
supportDineIn: 0,
},
productId: '',
nowMake: true,
@ -1460,6 +1468,9 @@
});
},
openOrderWait(item) {
if(this.orderListWait != null && this.orderListWait.length == 0){
return;
}
this.chooseWaitType = false
if (item != '') {
this.chooseWaitType = true
@ -2027,9 +2038,27 @@
url: '/package2/group/shopEvaluate?item=' + encodeURIComponent(JSON.stringify(this.shopItem))
})
} else if (type == 'openKitchen') {
this.tui.toast('明厨亮灶暂未开放');
if (!this.hasOpenKitchenVideo(this.shopItem)) {
this.tui.toast('暂无明厨亮灶');
return;
}
const cacheKey = 'openKitchenLiveUrl_' + Date.now()
uni.setStorageSync(cacheKey, String(this.shopItem.openKitchenVideoUrl).trim())
uni.navigateTo({
url: '/package2/group/openKitchenLive?key=' + cacheKey +
'&name=' + encodeURIComponent(this.shopItem.shopName || '明厨亮灶')
})
}
},
hasOpenKitchenVideo(item) {
return !!(item && item.openKitchenVideoUrl && String(item.openKitchenVideoUrl).trim())
},
getOpenKitchenText(item) {
return this.hasOpenKitchenVideo(item) ? '明厨亮灶' : '无明厨亮灶'
},
getDineInText(item) {
return item && item.supportDineIn == 1 ? '可堂食' : '无堂食'
},
openGoodsDetail(item) {
this.detailItem = item || {};
this.$refs.goodsDetailPopup.open('bottom');
@ -3418,6 +3447,35 @@
line-height: 32rpx;
}
.shop-status-row {
display: flex;
align-items: center;
gap: 10rpx;
margin-top: 10rpx;
text {
height: 36rpx;
line-height: 36rpx;
padding: 0 12rpx;
border-radius: 999rpx;
background: rgba(219, 255, 157, 0.72);
color: #08735d;
font-size: 20rpx;
font-weight: 900;
white-space: nowrap;
}
text:last-child {
background: rgba(255, 116, 77, 0.12);
color: #ff744d;
}
.shop-status-disabled {
background: rgba(225, 231, 228, 0.72) !important;
color: #87938f !important;
}
}
.notice-label {
display: inline-block;
height: 34rpx;

306
package2/group/openKitchenLive.vue

@ -0,0 +1,306 @@
<template>
<view class="live-page">
<view class="top-safe" :style="{ height: menuButtonInfo.top + 'px' }"></view>
<view class="nav">
<view class="back" @tap="back"></view>
<view class="title">{{title}}</view>
<view class="ghost"></view>
</view>
<view class="content" v-if="liveUrl">
<view class="shop-card">
<view class="shop-title">{{title}}</view>
<view class="shop-sub">商家已接入明厨亮灶可实时查看后厨画面</view>
</view>
<view class="live-card">
<view class="card-head">
<view class="head-left">
<view class="head-dot"></view>
<text>后厨实时画面</text>
</view>
<view class="head-status">可查看</view>
</view>
<view class="preview-box" @tap="playerUrl ? '' : playLive()">
<video v-if="playerUrl" id="openKitchenPlayer" class="live-player" :src="playerUrl" :controls="true"
:autoplay="false" object-fit="contain" @play="isPlaying = true" @pause="isPlaying = false"
@fullscreenchange="onFullscreenChange" @error="onPlayerError"></video>
<view v-else class="preview-placeholder">
<view class="preview-top">
<view class="camera-dot"></view>
<text>LIVE</text>
</view>
<view class="preview-play"></view>
<view class="preview-text">点击查看直播</view>
</view>
</view>
<view class="play-entry" @tap="playLive">
查看直播画面
</view>
<view class="entry-tip">默认不自动播放点击后全屏观看</view>
</view>
</view>
<view class="empty" v-else>明厨亮灶地址为空</view>
</view>
</template>
<script>
export default {
data() {
return {
menuButtonInfo: { top: 44 },
title: '明厨亮灶',
liveUrl: '',
playerUrl: '',
isPlaying: false
}
},
onLoad(options) {
if (uni.getMenuButtonBoundingClientRect) this.menuButtonInfo = uni.getMenuButtonBoundingClientRect()
const cacheKey = (options && options.key) || ''
this.liveUrl = cacheKey ? (uni.getStorageSync(cacheKey) || '') : decodeURIComponent((options && options.url) || '')
this.title = decodeURIComponent((options && options.name) || '') || '明厨亮灶'
},
methods: {
playLive() {
if (!this.playerUrl) {
this.playerUrl = this.liveUrl
}
this.$nextTick(() => {
const context = uni.createVideoContext('openKitchenPlayer', this)
if (context && context.requestFullScreen) {
context.requestFullScreen({
direction: 0
})
}
if (context && context.play) context.play()
})
},
onFullscreenChange(event) {
const fullScreen = event && event.detail ? event.detail.fullScreen : false
if (!fullScreen) this.isPlaying = false
},
onPlayerError() {
uni.showToast({
title: '直播画面加载失败',
icon: 'none'
})
},
back() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss" scoped>
page {
background: #f5f6f7;
}
.live-page {
min-height: 100vh;
box-sizing: border-box;
color: #222;
background: #f5f6f7;
}
.nav {
height: 88rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.back,
.ghost {
width: 72rpx;
color: #333;
font-size: 56rpx;
}
.title {
flex: 1;
text-align: center;
font-size: 30rpx;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
min-height: calc(100vh - 150rpx);
padding: 24rpx;
box-sizing: border-box;
}
.shop-card,
.live-card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
box-sizing: border-box;
}
.shop-title {
font-size: 34rpx;
font-weight: 900;
color: #222;
line-height: 44rpx;
}
.shop-sub {
margin-top: 10rpx;
font-size: 24rpx;
line-height: 34rpx;
color: #777;
}
.live-card {
margin-top: 20rpx;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 22rpx;
}
.head-left {
display: flex;
align-items: center;
font-size: 28rpx;
font-weight: 900;
color: #222;
}
.head-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #21b37b;
margin-right: 10rpx;
}
.head-status {
height: 42rpx;
line-height: 42rpx;
padding: 0 16rpx;
border-radius: 999rpx;
background: #e9fff5;
color: #18a66f;
font-size: 22rpx;
font-weight: 800;
}
.preview-box {
position: relative;
height: 360rpx;
border-radius: 18rpx;
background: #202326;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.preview-placeholder {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.preview-box::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
height: 80rpx;
background: linear-gradient(180deg, rgba(255, 255, 255, .08), transparent);
}
.preview-top {
position: absolute;
left: 20rpx;
top: 18rpx;
display: flex;
align-items: center;
height: 34rpx;
line-height: 34rpx;
padding: 0 12rpx;
border-radius: 999rpx;
background: rgba(0, 0, 0, .38);
color: #fff;
font-size: 18rpx;
font-weight: 900;
}
.camera-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background: #ff4d4f;
margin-right: 8rpx;
}
.preview-play {
width: 104rpx;
height: 104rpx;
line-height: 104rpx;
border-radius: 50%;
text-align: center;
background: rgba(255, 255, 255, .92);
color: #19a974;
font-size: 44rpx;
padding-left: 8rpx;
box-sizing: border-box;
}
.preview-text {
margin-top: 18rpx;
font-size: 24rpx;
font-weight: 800;
color: rgba(255, 255, 255, .72);
}
.play-entry {
margin-top: 28rpx;
height: 84rpx;
line-height: 84rpx;
border-radius: 999rpx;
text-align: center;
background: linear-gradient(90deg, #dcff9c, #baffec);
color: #073d33;
font-size: 28rpx;
font-weight: 900;
}
.entry-tip {
margin-top: 20rpx;
text-align: center;
font-size: 22rpx;
color: #999;
}
.live-player {
width: 100%;
height: 100%;
background: #000;
}
.empty {
margin-top: 200rpx;
text-align: center;
color: rgba(255, 255, 255, .56);
font-size: 26rpx;
}
</style>

16
package2/myCenter/addGoods.vue

@ -168,7 +168,12 @@
</template>
<script>
import SpEditor from '@/package2/components/sp-editor/components/sp-editor/sp-editor.vue'
export default {
components: {
SpEditor
},
data() {
return {
dataList: {},
@ -401,6 +406,7 @@
//
submit() {
let that = this;
this.syncLunchBoxByMoreBuy()
if (!this.validateProductSpecs()) {
return
}
@ -525,6 +531,15 @@
},
checkisMoreBuy(type){
this.formData.isMoreBuy = type;
this.syncLunchBoxByMoreBuy()
},
isMoreBuyEnabled(){
return Number(this.formData.isMoreBuy) === 1
},
syncLunchBoxByMoreBuy(){
if (this.isMoreBuyEnabled()) {
this.formData.lunchBox = 0
}
},
checkSellTime(type){
this.formData.sellTime = type;
@ -677,6 +692,7 @@
this.formData.sellTime = this.formData.sellBeginTime != '' ? 1 : 0
this.syncLunchBoxByMoreBuy()
this.modalVisible = true;
},
checkSpecValue(index){

385
package2/myCenter/myPost.vue

@ -1,9 +1,388 @@
<template>
<!-- 我的发布 -->
<view class="my-fish-page">
<view class="header" :style="{ paddingTop: statusBarTop + 'px' }">
<view class="back" @tap="back"><uni-icons type="left" size="22"></uni-icons></view>
<view class="title">我的校园二手</view>
</view>
<view class="tabs">
<view class="tab" :class="{ active: activeTab == item.key }" v-for="item in tabs" :key="item.key" @tap="switchTab(item.key)">
{{item.name}}
</view>
</view>
<view class="search-bar" v-if="canManageCurrentTab">
<uni-icons type="search" size="18" color="#8d9693"></uni-icons>
<input
v-model.trim="titleKeyword"
class="search-input"
confirm-type="search"
placeholder="按标题搜索"
@confirm="handleSearch"
/>
<view class="search-clear" v-if="titleKeyword" @tap="clearSearch">清空</view>
<view class="search-btn" @tap="handleSearch">搜索</view>
</view>
<scroll-view scroll-y class="list" :class="{ 'list--with-search': canManageCurrentTab }" @scrolltolower="loadMore">
<u-swipe-action v-if="canManageCurrentTab">
<u-swipe-action-item
v-for="(item,index) in list"
:key="item.id"
:name="index"
:options="swipeOptions(item)"
@click="handleSwipeAction"
>
<view class="card" @tap="openItem(item)">
<image class="cover" :src="item.coverImage || item.pic || defaultCover" mode="aspectFill"></image>
<view class="main">
<view class="name">{{nameText(item)}}</view>
<view class="desc">{{descText(item)}}</view>
<view class="meta">{{item.createTime || item.lastBrowseTime || item.lastChatTime || ''}}</view>
</view>
<view class="status" :class="{ 'status--off': isOffShelf(item) }">{{statusText(item)}}</view>
</view>
</u-swipe-action-item>
</u-swipe-action>
<block v-if="!canManageCurrentTab">
<view class="card" v-for="item in list" :key="item.id" @tap="openItem(item)">
<image class="cover" :src="item.coverImage || item.pic || defaultCover" mode="aspectFill"></image>
<view class="main">
<view class="name">{{nameText(item)}}</view>
<view class="desc">{{descText(item)}}</view>
<view class="meta">{{item.createTime || item.lastBrowseTime || item.lastChatTime || ''}}</view>
</view>
<view class="status" :class="{ 'status--off': isOffShelf(item) }">{{statusText(item)}}</view>
</view>
</block>
<view class="empty" v-if="!list.length && !loading">暂无数据</view>
<view class="load-more" v-if="loading">加载中...</view>
<view class="bottom-space"></view>
</scroll-view>
</view>
</template>
<script>
import {
pageMyFishGoods,
pageMyFishWants,
updateMyFishGoodsStatus,
updateMyFishWantStatus,
pageMyFishFavorites,
pageMyFishComments,
pageMyFishLikes,
pageMyFishComplaints
} from '@/common/fishMarketApi.js'
export default {
data() {
return {
statusBarTop: 24,
activeTab: 'goods',
list: [],
pageNum: 1,
pageSize: 10,
totalPages: 1,
loading: false,
titleKeyword: '',
defaultCover: '/static/images/img/songshu.png',
tabs: [
{ key: 'goods', name: '我的发布' },
{ key: 'wants', name: '我的求购' },
{ key: 'favorites', name: '我的收藏' },
{ key: 'comments', name: '我的评论' },
{ key: 'likes', name: '我的点赞' },
{ key: 'complaints', name: '我的举报' }
]
}
},
computed: {
canManageCurrentTab() {
return this.activeTab == 'goods' || this.activeTab == 'wants'
}
},
onLoad() {
if (uni.getMenuButtonBoundingClientRect) {
this.statusBarTop = uni.getMenuButtonBoundingClientRect().top || this.statusBarTop
}
this.load()
},
methods: {
back() {
uni.navigateBack()
},
swipeOptions(item) {
const offShelf = this.isOffShelf(item)
return [{
text: offShelf ? '上架' : '下架',
style: {
backgroundColor: offShelf ? '#24b979' : '#ff9f2d',
color: '#fff',
fontSize: '26rpx'
}
}, {
text: '删除',
style: {
backgroundColor: '#ef4c3f',
color: '#fff',
fontSize: '26rpx'
}
}]
},
handleSwipeAction(e) {
const item = this.list[Number(e.name)]
if (!item) return
if (e.index == 0) {
this.toggleShelf(item)
return
}
this.deleteItem(item)
},
isOffShelf(item) {
return Number(item.status) == 2 || Number(item.offShelf) == 1
},
updateItemStatus(item, status) {
const request = this.activeTab == 'goods'
? updateMyFishGoodsStatus({ goodsId: item.id, status })
: updateMyFishWantStatus({ wantId: item.id, status })
return request.then(res => {
if (!res && res !== true) return
uni.showToast({ title: status == 3 ? '已删除' : (status == 2 ? '已下架' : '已上架'), icon: 'none' })
this.reload()
})
},
toggleShelf(item) {
const nextStatus = this.isOffShelf(item) ? 1 : 2
this.updateItemStatus(item, nextStatus)
},
deleteItem(item) {
uni.showModal({
title: '删除确认',
content: '删除后不可恢复,确定删除吗?',
confirmText: '删除',
confirmColor: '#ef4c3f',
success: (res) => {
if (!res.confirm) return
this.updateItemStatus(item, 3)
}
})
},
switchTab(key) {
this.activeTab = key
this.titleKeyword = ''
this.reload()
},
handleSearch() {
uni.hideKeyboard()
this.reload()
},
clearSearch() {
this.titleKeyword = ''
this.reload()
},
reload() {
this.pageNum = 1
this.totalPages = 1
this.list = []
this.load()
},
loadMore() {
if (this.loading || this.pageNum >= this.totalPages) return
this.pageNum += 1
this.load()
},
load() {
this.loading = true
const query = {
pageNum: this.pageNum,
pageSize: this.pageSize,
titleKeyword: this.canManageCurrentTab ? this.titleKeyword : ''
}
const map = {
goods: pageMyFishGoods,
wants: pageMyFishWants,
favorites: pageMyFishFavorites,
comments: pageMyFishComments,
likes: pageMyFishLikes,
complaints: pageMyFishComplaints
}
map[this.activeTab](query).then(page => {
const records = page && (page.records || page.content) ? (page.records || page.content) : []
this.totalPages = Number(page && (page.pages || page.totalPages)) || 1
this.list = this.pageNum == 1 ? records : this.list.concat(records)
}).finally(() => {
this.loading = false
})
},
openItem(item) {
const goodsId = item.goodsId || (this.activeTab == 'goods' ? item.id : '')
if (goodsId) uni.navigateTo({ url: '/package2/IdleTrad/detail?id=' + goodsId })
},
statusText(item) {
if (this.activeTab == 'likes') return item.likeType == 'comment' ? '评论点赞' : '商品点赞'
if (this.activeTab == 'complaints') return item.status == 1 ? '已通过' : (item.status == 2 ? '已驳回' : '待处理')
if (item.saleStatus == 2) return '已预定'
if (item.saleStatus == 3) return '已卖出'
if (item.status == 2) return '已下架'
return '正常'
},
nameText(item) {
if (item.title) return item.title
if (this.activeTab == 'comments') return '我发表的评论'
if (this.activeTab == 'likes') return item.likeType == 'comment' ? '我点赞的评论' : '我点赞的商品'
if (this.activeTab == 'complaints') return '我的举报记录'
return item.goodsId || item.id || '校园二手'
},
descText(item) {
if (this.activeTab == 'favorites') return '商品ID:' + item.goodsId
if (this.activeTab == 'comments') return item.content || '评论内容'
if (this.activeTab == 'likes') return '关联商品:' + (item.goodsId || '评论 ' + (item.commentId || ''))
if (this.activeTab == 'complaints') return item.reason || '举报内容'
return item.content || ''
}
}
}
</script>
<style>
</style>
<style lang="scss">
page {
background: #f7f9f7;
}
.my-fish-page {
min-height: 100vh;
background: #f7f9f7;
color: #17362f;
}
.header {
padding-left: 26rpx;
padding-right: 26rpx;
background: linear-gradient(180deg, #e5fbf3, #f7f9f7);
}
.back {
width: 68rpx;
height: 68rpx;
display: flex;
align-items: center;
}
.title {
padding: 10rpx 0 24rpx;
font-size: 40rpx;
font-weight: 900;
}
.tabs {
display: flex;
overflow-x: scroll;
padding: 0 22rpx 18rpx;
white-space: nowrap;
}
.tab {
flex-shrink: 0;
margin-right: 18rpx;
padding: 14rpx 24rpx;
border-radius: 28rpx;
background: #fff;
color: #758782;
font-size: 24rpx;
}
.tab.active {
background: #24b979;
color: #fff;
font-weight: 900;
}
.search-bar {
height: 70rpx;
margin: 0 24rpx 16rpx;
padding: 0 12rpx 0 22rpx;
display: flex;
align-items: center;
border-radius: 38rpx;
background: #fff;
box-shadow: 0 8rpx 22rpx rgba(20, 60, 48, 0.045);
box-sizing: border-box;
}
.search-input {
flex: 1;
height: 68rpx;
margin-left: 10rpx;
color: #293632;
font-size: 24rpx;
line-height: 68rpx;
}
.search-clear {
padding: 0 12rpx;
color: #9aa8a4;
font-size: 22rpx;
}
.search-btn {
width: 86rpx;
height: 54rpx;
border-radius: 28rpx;
background: linear-gradient(135deg, #38c897, #19af7e);
color: #fff;
font-size: 23rpx;
font-weight: 800;
line-height: 54rpx;
text-align: center;
}
.list {
height: calc(100vh - 210rpx);
}
.list--with-search {
height: calc(100vh - 296rpx);
}
.card {
margin: 18rpx 24rpx 0;
padding: 18rpx;
display: flex;
align-items: center;
border-radius: 24rpx;
background: #fff;
box-shadow: 0 8rpx 22rpx rgba(20, 60, 48, 0.05);
}
.cover {
width: 126rpx;
height: 126rpx;
border-radius: 18rpx;
background: #edf3ef;
}
.main {
flex: 1;
min-width: 0;
margin-left: 18rpx;
}
.name {
overflow: hidden;
font-size: 28rpx;
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.desc {
margin-top: 8rpx;
overflow: hidden;
color: #71817c;
font-size: 23rpx;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
margin-top: 8rpx;
color: #a0aaa6;
font-size: 20rpx;
}
.status {
margin-left: 12rpx;
color: #22a879;
font-size: 22rpx;
font-weight: 800;
}
.status--off {
color: #9aa8a4;
}
.empty,
.load-more {
padding: 80rpx 0;
color: #9aa8a4;
text-align: center;
}
.bottom-space {
height: 120rpx;
}
</style>

Loading…
Cancel
Save