Compare commits

...

6 Commits

Author SHA1 Message Date
tianyi ca8267fd6c 1 2 weeks ago
tianyi 907b73a70b 1 2 weeks ago
tianyi 555e365d20 1 2 weeks ago
tianyi e5c16f11a0 1 2 weeks ago
tianyi f9dc3d745e 1 2 weeks ago
tianyi 29b2eb9069 1 2 weeks ago
  1. 8
      js/nb.js
  2. 20
      manifest.json
  3. 15
      pages/shop/shopEvaluate.vue
  4. 131
      uni_modules/sp-editor/changelog.md
  5. 825
      uni_modules/sp-editor/components/sp-editor/color-picker.vue
  6. 140
      uni_modules/sp-editor/components/sp-editor/fab-tool.vue
  7. 152
      uni_modules/sp-editor/components/sp-editor/link-edit.vue
  8. 828
      uni_modules/sp-editor/components/sp-editor/sp-editor.vue
  9. 24
      uni_modules/sp-editor/icons/custom-icon.css
  10. 238
      uni_modules/sp-editor/icons/editor-icon.css
  11. 83
      uni_modules/sp-editor/package.json
  12. 19
      uni_modules/sp-editor/readme.md
  13. 1
      uni_modules/sp-editor/static/image-resize.min.js
  14. 8
      uni_modules/sp-editor/static/quill.min.js
  15. 132
      uni_modules/sp-editor/utils/index.js
  16. 4
      uni_modules/uni-icons/changelog.md
  17. 2
      uni_modules/uni-icons/components/uni-icons/uni-icons.uvue
  18. 4
      uni_modules/uni-icons/components/uni-icons/uni-icons.vue
  19. 107
      uni_modules/uni-icons/package.json
  20. 27
      uni_modules/uni-rate/changelog.md
  21. 371
      uni_modules/uni-rate/components/uni-rate/uni-rate.vue
  22. 106
      uni_modules/uni-rate/package.json
  23. 11
      uni_modules/uni-rate/readme.md
  24. 46
      uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue

8
js/nb.js

@ -45,6 +45,12 @@ export default {
}, // 请求头
success: function(res) {
console.log("<----",url,res,res.data.code)
if(res.data.message == '未检测到登录用户' || res.data.message == '您还未登录'){
uni.reLaunch({
url: '/pages/login/login'
})
}
setTimeout(() => {
uni.hideLoading();
}, 400)
@ -76,7 +82,7 @@ export default {
// }
},
fail: function(err) {
console.log(err)
console.log('报错信息',err)
uni.hideLoading();
}
})

20
manifest.json

@ -23,8 +23,7 @@
"modules" : {
"Camera" : {},
"Geolocation" : {},
"Bluetooth" : {},
"Push" : {}
"Bluetooth" : {}
},
/* */
"distribute" : {
@ -69,7 +68,8 @@
},
"urlschemewhitelist" : "amapuri",
"validArchitectures" : [ "arm64" ],
"dSYMs" : false
"dSYMs" : false,
"idfa" : false
},
"apple" : {
"urlschemewhitelist" : [ "iosamap", "baidumap", "qqmap" ]
@ -85,7 +85,7 @@
"ad" : {},
"geolocation" : {
"system" : {
"__platform__" : [ "android" ]
"__platform__" : [ "ios", "android" ]
}
},
"push" : {}
@ -150,16 +150,16 @@
},
"nativePlugins" : {
"JG-JCore" : {
"JPUSH_APPKEY_ANDROID" : "130f556e8473c9b558777fe3",
"JPUSH_APPKEY_ANDROID" : "",
"JPUSH_APPKEY_IOS" : "",
"JPUSH_CHANNEL_ANDROID" : "",
"JPUSH_CHANNEL_IOS" : "",
"JPUSH_CHANNEL_ANDROID" : "130f556e8473c9b558777fe3",
"JPUSH_CHANNEL_IOS" : "130f556e8473c9b558777fe3",
"__plugin_info__" : {
"name" : "极光推送 JCore 官方 SDK",
"description" : "极光推送 JCore 官方 SDK HBuilder 插件版本",
"platforms" : "Android,iOS",
"url" : "https://ext.dcloud.net.cn/plugin?id=4028",
"android_package_name" : "com.peisong",
"android_package_name" : "",
"ios_bundle_id" : "",
"isCloud" : true,
"bought" : 1,
@ -168,7 +168,7 @@
"JPUSH_APPKEY_ANDROID" : {
"des" : "[Android]极光portal配置应用信息时分配的AppKey",
"key" : "JPUSH_APPKEY",
"value" : "130f556e8473c9b558777fe3"
"value" : ""
},
"JPUSH_APPKEY_IOS" : {
"des" : "[iOS]极光portal配置应用信息时分配的AppKey",
@ -213,7 +213,7 @@
"description" : "极光推送JPush官方SDK HBuilder插件版本",
"platforms" : "Android,iOS",
"url" : "https://ext.dcloud.net.cn/plugin?id=4035",
"android_package_name" : "com.peisong",
"android_package_name" : "",
"ios_bundle_id" : "",
"isCloud" : true,
"bought" : 1,

15
pages/shop/shopEvaluate.vue

@ -63,16 +63,16 @@
<view class="eval-img" v-if="item.picture">
<img @tap="largeImg" :src="item.picture" alt="">
</view>
<view class="reply-card" :class="{'expanded': isExpanded}" v-for="(item1,index1) in item.comments" :key="index1">
<view class="reply-card" :class="{'expanded': item1.isOpen}" v-for="(item1,index1) in item.comments" :key="index1">
<!-- 标题行商家回复 + 展开/收起按钮 -->
<view class="reply-header">
<!-- 回复内容区域动态类控制展开/折叠 -->
<view class="reply-content" :class="{ collapsed: !isExpanded, expanded: isExpanded }">
<view class="reply-content" :class="{ collapsed: !item1.isOpen, expanded: item1.isOpen }">
<text>{{item1.createByName}} : {{ item1.remark }}</text>
<img :src="item1.picture" alt="" class="upload-img" style="margin-top: 10px;border-radius: 10px;">
<img :src="item1.picture" v-if="item1.picture" alt="" class="upload-img" style="margin-top: 10px;border-radius: 10px;">
</view>
<view class="reply-expand-btn" @click="toggleReply">
<text v-if="!isExpanded">展开</text>
<view class="reply-expand-btn" @click="toggleReply(index,index1)">
<text v-if="!item1.isOpen">展开</text>
<text v-else>收起</text>
</view>
</view>
@ -273,8 +273,9 @@
uni.hideLoading()
}).catch((res) => {})
},
toggleReply() {
this.isExpanded = !this.isExpanded;
toggleReply(i,m) {
this.shopComments[i].comments[m].isOpen = !this.shopComments[i].comments[m].isOpen;
this.$forceUpdate()
},
changeStatus(v){
this.shopComments[v].isReply = !this.shopComments[v].isReply

131
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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 '@/uni_modules/sp-editor/icons/editor-icon.css';
@import '@/uni_modules/sp-editor/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
uni_modules/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
uni_modules/sp-editor/icons/editor-icon.css

File diff suppressed because one or more lines are too long

83
uni_modules/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
uni_modules/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
uni_modules/sp-editor/static/image-resize.min.js

File diff suppressed because one or more lines are too long

8
uni_modules/sp-editor/static/quill.min.js

File diff suppressed because one or more lines are too long

132
uni_modules/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;
});
}

4
uni_modules/uni-icons/changelog.md

@ -1,3 +1,7 @@
## 2.0.12(2025-08-26)
- 优化 uni-app x 下 size 类型问题
## 2.0.11(2025-08-18)
- 修复 图标点击事件返回
## 2.0.9(2024-01-12)
fix: 修复图标大小默认值错误的问题
## 2.0.8(2023-12-14)

2
uni_modules/uni-icons/components/uni-icons/uni-icons.uvue

@ -29,7 +29,7 @@
default: '#333333'
},
size: {
type: Object,
type: [Number, String],
default: 16
},
fontFamily: {

4
uni_modules/uni-icons/components/uni-icons/uni-icons.vue

@ -85,8 +85,8 @@
}
},
methods: {
_onClick() {
this.$emit('click')
_onClick(e) {
this.$emit('click', e)
}
}
}

107
uni_modules/uni-icons/package.json

@ -1,7 +1,7 @@
{
"id": "uni-icons",
"displayName": "uni-icons 图标",
"version": "2.0.9",
"version": "2.0.12",
"description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。",
"keywords": [
"uni-ui",
@ -11,7 +11,9 @@
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.2.14"
"HBuilderX": "^3.2.14",
"uni-app": "^4.08",
"uni-app-x": "^4.61"
},
"directories": {
"example": "../../temps/example_temps"
@ -34,53 +36,74 @@
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
"type": "component-vue",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"dependencies": [
"uni-scss"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-uvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "y",
"快手": "y",
"飞书": "y",
"京东": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "-",
"android": {
"extVersion": "",
"minVersion": "29"
},
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "√",
"toutiao": "√",
"baidu": "√",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "√",
"lark": "-"
},
"quickapp": {
"huawei": "√",
"union": "√"
}
},
"uni-app-x": {
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"android": {
"extVersion": "",
"minVersion": "29"
},
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√"
}
}
}
}

27
uni_modules/uni-rate/changelog.md

@ -0,0 +1,27 @@
## 1.3.2(2026-02-09)
- 修复 pc 触屏问题
## 1.3.1(2022-02-25)
- 修复 条件判断 `NaN` 错误的 bug
## 1.3.0(2021-11-19)
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-rate](https://uniapp.dcloud.io/component/uniui/uni-rate)
## 1.2.2(2021-09-10)
- 优化 默认值修改为 0 颗星
## 1.2.1(2021-07-30)
- 优化 vue3下事件警告的问题
## 1.2.0(2021-07-13)
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.2(2021-05-12)
- 新增 组件示例地址
## 1.1.1(2021-04-21)
- 修复 布局变化后 uni-rate 星星计算不准确的 bug
- 优化 添加依赖 uni-icons, 导入 uni-rate 自动下载依赖
## 1.1.0(2021-04-16)
- 修复 uni-rate 属性 margin 值为 string 组件失效的 bug
## 1.0.9(2021-02-05)
- 优化 组件引用关系,通过uni_modules引用组件
## 1.0.8(2021-02-05)
- 调整为uni_modules目录规范
- 支持 pc 端

371
uni_modules/uni-rate/components/uni-rate/uni-rate.vue

@ -0,0 +1,371 @@
<template>
<view>
<view ref="uni-rate" class="uni-rate">
<view class="uni-rate__icon" :class="{'uni-cursor-not-allowed': disabled}"
:style="{ 'margin-right': marginNumber + 'px' }" v-for="(star, index) in stars" :key="index"
@touchstart.stop="touchstart" @touchmove.stop="touchmove" @mousedown.stop="mousedown"
@mousemove.stop="mousemove" @mouseleave="mouseleave">
<uni-icons :color="color" :size="size" :type="isFill ? 'star-filled' : 'star'" />
<!-- #ifdef APP-NVUE -->
<view :style="{ width: star.activeWitch.replace('%','')*size/100+'px'}" class="uni-rate__icon-on">
<uni-icons style="text-align: left;" :color="disabled?'#ccc':activeColor" :size="size"
type="star-filled" />
</view>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<view :style="{ width: star.activeWitch}" class="uni-rate__icon-on">
<uni-icons :color="disabled?disabledColor:activeColor" :size="size" type="star-filled" />
</view>
<!-- #endif -->
</view>
</view>
</view>
</template>
<script>
// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom');
// #endif
/**
* Rate 评分
* @description 评分组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=33
* @property {Boolean} isFill = [true|false] 星星的类型是否为实心类型, 默认为实心
* @property {String} color 未选中状态的星星颜色默认为 "#ececec"
* @property {String} activeColor 选中状态的星星颜色默认为 "#ffca3e"
* @property {String} disabledColor 禁用状态的星星颜色默认为 "#c0c0c0"
* @property {Number} size 星星的大小
* @property {Number} value/v-model 当前评分
* @property {Number} max 最大评分评分数量目前一分一颗星
* @property {Number} margin 星星的间距单位 px
* @property {Boolean} disabled = [true|false] 是否为禁用状态默认为 false
* @property {Boolean} readonly = [true|false] 是否为只读状态默认为 false
* @property {Boolean} allowHalf = [true|false] 是否实现半星默认为 false
* @property {Boolean} touchable = [true|false] 是否支持滑动手势默认为 true
* @event {Function} change uniRate value 改变时触发事件e={value:Number}
*/
export default {
name: "UniRate",
props: {
isFill: {
//
type: [Boolean, String],
default: true
},
color: {
//
type: String,
default: "#ececec"
},
activeColor: {
//
type: String,
default: "#ffca3e"
},
disabledColor: {
//
type: String,
default: "#c0c0c0"
},
size: {
//
type: [Number, String],
default: 24
},
value: {
//
type: [Number, String],
default: 0
},
modelValue: {
//
type: [Number, String],
default: 0
},
max: {
//
type: [Number, String],
default: 5
},
margin: {
//
type: [Number, String],
default: 0
},
disabled: {
//
type: [Boolean, String],
default: false
},
readonly: {
//
type: [Boolean, String],
default: false
},
allowHalf: {
//
type: [Boolean, String],
default: false
},
touchable: {
//
type: [Boolean, String],
default: true
}
},
data() {
return {
valueSync: "",
userMouseFristMove: true,
userRated: false,
userLastRate: 1,
PC: false
};
},
watch: {
value(newVal) {
this.valueSync = Number(newVal);
},
modelValue(newVal) {
this.valueSync = Number(newVal);
},
},
computed: {
stars() {
const value = this.valueSync ? this.valueSync : 0;
const starList = [];
const floorValue = Math.floor(value);
const ceilValue = Math.ceil(value);
for (let i = 0; i < this.max; i++) {
if (floorValue > i) {
starList.push({
activeWitch: "100%"
});
} else if (ceilValue - 1 === i) {
starList.push({
activeWitch: (value - floorValue) * 100 + "%"
});
} else {
starList.push({
activeWitch: "0"
});
}
}
return starList;
},
marginNumber() {
return Number(this.margin)
}
},
created() {
this.valueSync = Number(this.value || this.modelValue);
this._rateBoxLeft = 0
this._oldValue = null
// #ifdef H5
this.PC = this.IsPC()
// #endif
},
mounted() {
setTimeout(() => {
this._getSize()
}, 100)
},
methods: {
touchstart(e) {
// #ifdef H5
if (this.PC) return
// #endif
if (this.readonly || this.disabled) return
const {
clientX,
screenX
} = e.changedTouches[0]
// TODO Nvue screenX clientX
this._getRateCount(clientX || screenX)
},
touchmove(e) {
// #ifdef H5
if (this.PC) return
// #endif
if (this.readonly || this.disabled || !this.touchable) return
const {
clientX,
screenX
} = e.changedTouches[0]
this._getRateCount(clientX || screenX)
},
/**
* 兼容 PC @tian
*/
mousedown(e) {
// #ifdef H5
if (!this.PC) return
if (this.readonly || this.disabled) return
const {
clientX,
} = e
this.userLastRate = this.valueSync
this._getRateCount(clientX)
this.userRated = true
// #endif
},
mousemove(e) {
// #ifdef H5
if (!this.PC) return
if (this.userRated) return
if (this.userMouseFristMove) {
this.userLastRate = this.valueSync
this.userMouseFristMove = false
}
if (this.readonly || this.disabled || !this.touchable) return
const {
clientX,
} = e
this._getRateCount(clientX)
// #endif
},
mouseleave(e) {
// #ifdef H5
if (!this.PC) return
if (this.readonly || this.disabled || !this.touchable) return
if (this.userRated) {
this.userRated = false
return
}
this.valueSync = this.userLastRate
// #endif
},
// #ifdef H5
IsPC() {
var userAgentInfo = navigator.userAgent || '';
var info = typeof uni !== 'undefined' && uni.getSystemInfoSync ? uni.getSystemInfoSync() : null;
if (info && info.deviceType) {
if (info.deviceType === 'pc') return true;
if (info.deviceType === 'phone' || info.deviceType === 'pad') return false;
}
var isMobileUA = /Android|iPhone|SymbianOS|Windows Phone|iPad|iPod|Mobile|Harmony|HarmonyOS/i.test(userAgentInfo);
if (isMobileUA) return false;
var hasTouch = false;
if (typeof navigator.maxTouchPoints === 'number') {
hasTouch = navigator.maxTouchPoints > 0;
} else if (typeof window !== 'undefined') {
hasTouch = 'ontouchstart' in window;
}
if (hasTouch && typeof window !== 'undefined' && window.matchMedia) {
var finePointer = window.matchMedia('(pointer: fine)').matches;
var canHover = window.matchMedia('(hover: hover)').matches;
return finePointer || canHover;
}
return !hasTouch;
},
// #endif
/**
* 获取星星个数
*/
_getRateCount(clientX) {
this._getSize()
const size = Number(this.size)
if (isNaN(size)) {
return new Error('size 属性只能设置为数字')
}
const rateMoveRange = clientX - this._rateBoxLeft
let index = parseInt(rateMoveRange / (size + this.marginNumber))
index = index < 0 ? 0 : index;
index = index > this.max ? this.max : index;
const range = parseInt(rateMoveRange - (size + this.marginNumber) * index);
let value = 0;
if (this._oldValue === index && !this.PC) return;
this._oldValue = index;
if (this.allowHalf) {
if (range > (size / 2)) {
value = index + 1
} else {
value = index + 0.5
}
} else {
value = index + 1
}
value = Math.max(0.5, Math.min(value, this.max))
this.valueSync = value
this._onChange()
},
/**
* 触发动态修改
*/
_onChange() {
this.$emit("input", this.valueSync);
this.$emit("update:modelValue", this.valueSync);
this.$emit("change", {
value: this.valueSync
});
},
/**
* 获取星星距离屏幕左侧距离
*/
_getSize() {
// #ifndef APP-NVUE
uni.createSelectorQuery()
.in(this)
.select('.uni-rate')
.boundingClientRect()
.exec(ret => {
if (ret) {
this._rateBoxLeft = ret[0].left
}
})
// #endif
// #ifdef APP-NVUE
dom.getComponentRect(this.$refs['uni-rate'], (ret) => {
const size = ret.size
if (size) {
this._rateBoxLeft = size.left
}
})
// #endif
}
}
};
</script>
<style lang="scss">
.uni-rate {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
line-height: 1;
font-size: 0;
flex-direction: row;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-rate__icon {
position: relative;
line-height: 1;
font-size: 0;
}
.uni-rate__icon-on {
overflow: hidden;
position: absolute;
top: 0;
left: 0;
line-height: 1;
text-align: left;
}
.uni-cursor-not-allowed {
/* #ifdef H5 */
cursor: not-allowed !important;
/* #endif */
}
</style>

106
uni_modules/uni-rate/package.json

@ -0,0 +1,106 @@
{
"id": "uni-rate",
"displayName": "uni-rate 评分",
"version": "1.3.2",
"description": "Rate 评分组件,可自定义评分星星图标的大小、间隔、评分数。",
"keywords": [
"uni-ui",
"uniui",
"评分"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "",
"uni-app": "^4.05",
"uni-app-x": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [
"uni-scss",
"uni-icons"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "√",
"android": "√",
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "√",
"toutiao": "√",
"baidu": "√",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "√",
"lark": "-",
"xhs": "-"
},
"quickapp": {
"huawei": "√",
"union": "√"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

11
uni_modules/uni-rate/readme.md

@ -0,0 +1,11 @@
## Rate 评分
> **组件名:uni-rate**
> 代码块: `uRate`
> 关联组件:`uni-icons`
评分组件,多用于购买商品后,对商品进行评价等场景
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-rate)

46
uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue

@ -1,6 +1,6 @@
<template>
<view class="segmented-control">
<view v-for="(item, index) in values" class="segmented-control__item" @click="_onClick(index)">
<view class="page1">
<view v-for="(item, index) in values" class="page1__item" @click="_onClick(index)">
<view>
<text :style="{color:
index === currentIndex
@ -9,7 +9,7 @@
: '#fff'
: styleType === 'text'
? '#000'
: activeColor}" class="segmented-control__text" :class="styleType === 'text' && index === currentIndex ? 'segmented-control__item--text': ''">{{ item }}</text>
: activeColor}" class="page1__text" :class="styleType === 'text' && index === currentIndex ? 'page1__item--text': ''">{{ item }}</text>
</view>
</view>
@ -82,48 +82,30 @@
</script>
<style lang="scss" scoped>
.segmented-control {
width: 500rpx;
page{
width: 100% ;
}
.page1 {
width: 100%;
overflow-x: scroll;
height: 100rpx;
height: 80rpx;
line-height: 100rpx;
background: #777;
}
.segmented-control__item {
display: inline-block;
box-sizing: border-box;
.page1__item {
float: left;
width: 120rpx;
justify-content: center;
align-items: center;
}
.segmented-control__item--button {
border-style: solid;
border-top-width: 1px;
border-bottom-width: 1px;
border-right-width: 1px;
border-left-width: 0;
}
.segmented-control__item--button--first {
border-left-width: 1px;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.segmented-control__item--button--last {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.segmented-control__item--text {
.page1__item--text {
border-bottom-style: solid;
border-bottom-width: 2px;
padding: 12rpx 0;
}
.segmented-control__text {
.page1__text {
font-size: 28rpx;
line-height: 40rpx;
text-align: center;

Loading…
Cancel
Save