feat(商品sku)

This commit is contained in:
Ankkaya 2024-06-11 18:33:56 +08:00
parent b999d59bfc
commit 3f51efcebe
20 changed files with 2988 additions and 1060 deletions

View File

@ -16,6 +16,9 @@
"autoclose": true, "autoclose": true,
"delay": 0 "delay": 0
}, },
"compatible": {
"ignoreVersion": true
},
/* */ /* */
"modules": {}, "modules": {},
/* */ /* */

View File

@ -1,20 +1,42 @@
<template> <template>
<pb-layout class="product-list" title="产品" navbar="normal" tabbar="/pages/index/product" :bgStyle="bgStyle" <pb-layout
opacityBgUi="bg-white" color="black"> class="product-list"
title="产品"
navbar="normal"
tabbar="/pages/index/product"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view v-if="state.pagination.total > 0" class="goods-list ss-m-t-20"> <view v-if="state.pagination.total > 0" class="goods-list ss-m-t-20">
<view class="ss-p-l-20 ss-p-r-20 ss-m-b-20" v-for="item in state.pagination.list" :key="item.id"> <view class="ss-p-l-20 ss-p-r-20 ss-m-b-20" v-for="item in state.pagination.list" :key="item.id">
<p-goods-column size="lg" :data="item" :topRadius="10" :bottomRadius="10" <p-goods-column
@click="peach.$router.go('/pages/product/manageGoods', { id: item.id, mark: 'detail' })" /> size="lg"
:data="item"
:topRadius="10"
:bottomRadius="10"
@click="peach.$router.go('/pages/product/manageGoods', { id: item.id, mark: 'detail' })"
/>
</view> </view>
</view> </view>
<uni-load-more v-if="state.pagination.total > 0" :status="state.loadStatus" :content-text="{ <uni-load-more
v-if="state.pagination.total > 0"
:status="state.loadStatus"
:content-text="{
contentdown: '上拉加载更多', contentdown: '上拉加载更多',
}" @click="loadMore" /> }"
@click="loadMore"
/>
<view class="_icon-add-round add-product" @click="addGoods"></view> <view class="_icon-add-round add-product" @click="addGoods"></view>
<p-empty v-if="state.pagination.total === 0" icon="/static/soldout-empty.png" text="暂无产品" bgColor="transparent" /> <p-empty
v-if="state.pagination.total === 0"
icon="/static/soldout-empty.png"
text="暂无产品"
bgColor="transparent"
/>
</pb-layout> </pb-layout>
</template> </template>
@ -67,8 +89,13 @@ async function getList() {
} }
function addGoods() { function addGoods() {
peach.$store('trade').$patch({
selectedProperty: null,
goodsInfo: null,
skus: null,
})
peach.$router.go('/pages/product/manageGoods', { peach.$router.go('/pages/product/manageGoods', {
title: '添加商品' title: '添加商品',
}) })
} }

View File

@ -1,15 +1,14 @@
<template> <template>
<view></view> <view>页面重定向</view>
</template> </template>
<script setup> <script setup>
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import $store from '@/peach/store'
import peach from '@/peach' import peach from '@/peach'
const userStore = $store('user')
async function redirectFn() { async function redirectFn() {
const userStore = peach.$store('user')
// //
if (!userStore.isLogin) { if (!userStore.isLogin) {
userStore.logOut() userStore.logOut()

View File

@ -1,7 +1,6 @@
<template> <template>
<div class="sku-item"> <div class="sku-item">
<uni-forms label-width="176rpx" label-position="left"> <uni-forms label-width="176rpx" label-position="left">
<template v-if="specType"> <template v-if="specType">
<template v-for="item in formData.properties"> <template v-for="item in formData.properties">
<uni-forms-item :label="item.propertyName"> <uni-forms-item :label="item.propertyName">
@ -11,31 +10,77 @@
</template> </template>
<uni-forms-item label="商品封面图" name="picUrl" label-position="top"> <uni-forms-item label="商品封面图" name="picUrl" label-position="top">
<p-uploader v-model:url="formData.picUrl" :readonly="!canEdit" fileMediatype="image" limit="1" mode="grid" <p-uploader
:imageStyles="{ width: '168rpx', height: '168rpx' }" /> v-model:url="formData.picUrl"
:readonly="!canEdit"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品条码" name="barCode"> <uni-forms-item label="商品条码" name="barCode">
<uni-easyinput type="text" trim="all" v-model="formData.barCode" :disabled="!canEdit" placeholder="请输入商品条码" /> <uni-easyinput
type="text"
trim="all"
v-model="formData.barCode"
:disabled="!canEdit"
placeholder="请输入商品条码"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="销售价" name="price"> <uni-forms-item label="销售价" name="price">
<uni-easyinput type="number" trim="all" v-model="formData.price" :disabled="!canEdit" placeholder="请输入商品销售价" /> <uni-easyinput
type="digit"
trim="all"
v-model="formData.price"
:disabled="!canEdit"
placeholder="请输入商品销售价"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="市场价" name="marketPrice"> <uni-forms-item label="市场价" name="marketPrice">
<uni-easyinput type="number" trim="all" v-model="formData.marketPrice" :disabled="!canEdit" <uni-easyinput
placeholder="请输入商品销售价" /> type="digit"
trim="all"
v-model="formData.marketPrice"
:disabled="!canEdit"
placeholder="请输入商品销售价"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="成本价" name="costPrice"> <uni-forms-item label="成本价" name="costPrice">
<uni-easyinput type="number" trim="all" v-model="formData.costPrice" :disabled="!canEdit" <uni-easyinput
placeholder="请输入商品销售价" /> type="digit"
trim="all"
v-model="formData.costPrice"
:disabled="!canEdit"
placeholder="请输入商品销售价"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="库存" name="stock"> <uni-forms-item label="库存" name="stock">
<uni-easyinput type="number" trim="all" v-model="formData.stock" :disabled="!canEdit" placeholder="请输入商品库存" /> <uni-easyinput
type="number"
trim="all"
v-model="formData.stock"
:disabled="!canEdit"
placeholder="请输入商品库存"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="重量kg" name="weight"> <uni-forms-item label="重量kg" name="weight">
<uni-easyinput type="number" trim="all" v-model="formData.weight" :disabled="!canEdit" placeholder="请输入商品重量" /> <uni-easyinput
type="digit"
trim="all"
v-model="formData.weight"
:disabled="!canEdit"
placeholder="请输入商品重量"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="体积" name="volume"> <uni-forms-item label="体积" name="volume">
<uni-easyinput type="number" trim="all" v-model="formData.volume" :disabled="!canEdit" placeholder="请输入商品体积" /> <uni-easyinput
type="digit"
trim="all"
v-model="formData.volume"
:disabled="!canEdit"
placeholder="请输入商品体积"
/>
</uni-forms-item> </uni-forms-item>
</uni-forms> </uni-forms>
</div> </div>
@ -44,27 +89,29 @@
<script setup> <script setup>
import { ref, watch, computed, defineProps } from 'vue' import { ref, watch, computed, defineProps } from 'vue'
import peach from '@/peach' import peach from '@/peach'
import { canEdit } from '../js/sku'; import { canEdit } from '../js/sku'
const props = defineProps({ const props = defineProps({
skus: { skus: {
type: Array, type: Array,
default: () => [] default: () => [],
} },
}) })
const formData = ref({}) const formData = ref({})
const specType = computed(() => peach.$store("trade").goodsInfo.specType); const specType = computed(() => peach.$store('trade').goodsInfo?.specType || false)
watch(() => props.skus, (newVal) => { watch(
() => props.skus,
(newVal) => {
console.log(newVal) console.log(newVal)
// sku // sku
if (!specType.value) { if (!specType.value) {
formData.value = newVal[0] ?? {} if (newVal) formData.value = newVal[0] ?? {}
return return
} }
formData.value = newVal ?? {} formData.value = newVal ?? {}
}, { immediate: true }) },
{ immediate: true }
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -75,6 +122,5 @@ watch(() => props.skus, (newVal) => {
.sku-item:first-child { .sku-item:first-child {
border-top: 1px solid var(--ui-BG-Main); border-top: 1px solid var(--ui-BG-Main);
} }
</style> </style>

View File

@ -8,3 +8,27 @@ export const SPEC_TYPE = [
value: true, value: true,
}, },
] ]
// sku 相关属性校验
export const SKU_RULE_CONFIG = [
{
name: 'stock',
rule: (arg) => arg >= 0,
message: '商品库存必须大于等于 1 ',
},
{
name: 'price',
rule: (arg) => arg >= 0.01,
message: '商品销售价格必须大于等于 0.01 元!!!',
},
{
name: 'marketPrice',
rule: (arg) => arg >= 0.01,
message: '商品市场价格必须大于等于 0.01 元!!!',
},
{
name: 'costPrice',
rule: (arg) => arg >= 0.01,
message: '商品成本价格必须大于等于 0.00 元!!!',
},
]

View File

@ -1,96 +1,97 @@
import { ref, computed } from "vue"; import { ref, computed } from 'vue'
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from '@dcloudio/uni-app'
import peach from "@/peach"; import peach from '@/peach'
import GoodsApi from "@/peach/api/trade/goods"; import GoodsApi from '@/peach/api/trade/goods'
import { SPEC_TYPE } from "./config"; import { SPEC_TYPE, SKU_RULE_CONFIG } from './config'
const pickerRef = ref(null); const pickerRef = ref(null)
// 多属性商品 sku 列表 // 多属性商品 sku 列表
const skus = ref([]); const skus = ref([])
const propertyList = ref([]); const propertyList = ref([])
const goodsPropertyList = ref([]); const goodsPropertyList = ref([])
const propertyListRef = ref(null); const propertyListRef = ref(null)
const canEdit = computed(() => peach.$store("trade").canEdit); const canEdit = computed(() => peach.$store('trade').canEdit)
const formData = ref({ const formData = ref({
specType: true, specType: true,
specText: SPEC_TYPE[0].label, specText: SPEC_TYPE[0].label,
}); })
async function showPropertyList() { async function showPropertyList() {
await getGoodsProperty(); await getGoodsProperty()
propertyListRef.value.onOpen(); propertyListRef.value.onOpen()
} }
function onRDPickerConfirm(e) { function onRDPickerConfirm(e) {
peach.$store("trade").specType = SPEC_TYPE[e.value[0]].value; formData.value.specType = SPEC_TYPE[e.value[0]].value
formData.value.specText = SPEC_TYPE[e.value[0]].label; formData.value.specText = SPEC_TYPE[e.value[0]].label
formData.value.specType = SPEC_TYPE[e.value[0]].value;
// 如果商品规格不一致,则需要重新初始化 sku 列表
initSku()
peach.$store('trade').specType = SPEC_TYPE[e.value[0]].value
} }
function pickerProperty() { function pickerProperty() {
if (canEdit.value) { if (canEdit.value) {
let index = specType.value ? 1 : 0; let index = formData.value.specType ? 1 : 0
pickerRef.value.onOpen([index]);
console.log(index)
pickerRef.value.onOpen([index])
} }
} }
async function onPropertyConfirm(e) { async function onPropertyConfirm(e) {
await getGoodsProperty(); await getGoodsProperty()
console.log(e); console.log(e)
} }
function onConfirm() { function onConfirm() {
console.log(skus.value); console.log(skus.value)
} }
async function getGoodsProperty() { async function getGoodsProperty() {
let { data } = await GoodsApi.getHistoryProperty(); let { data } = await GoodsApi.getHistoryProperty()
// 把 propertyList 中 id 相同的属性合并,并去重 // 把 propertyList 中 id 相同的属性合并,并去重
propertyList.value = peach.$store("trade").selectedProperty; propertyList.value = peach.$store('trade').selectedProperty
console.log(propertyList.value); console.log(propertyList.value)
// 根据已经选择数据,设置默认选中 // 根据已经选择数据,设置默认选中
data.forEach((item) => { data.forEach((item) => {
// 判断属性是否已经选中 // 判断属性是否已经选中
let propertyParent = propertyList.value.find( let propertyParent = propertyList.value.find((sitem) => sitem?.id === item.id)
(sitem) => sitem?.id === item.id
);
item.checked = propertyParent ? true : false; item.checked = propertyParent ? true : false
// 如果属性已经选中,查询子类中是否有选中 // 如果属性已经选中,查询子类中是否有选中
if (item.checked) { if (item.checked) {
item.propertyValues.forEach((child) => { item.propertyValues.forEach((child) => {
let childResult = propertyParent?.children.some( let childResult = propertyParent?.children.some((schild) => schild === child.id)
(schild) => schild === child.id child.checked = childResult ? true : false
); })
child.checked = childResult ? true : false;
});
} }
}); })
goodsPropertyList.value = data; goodsPropertyList.value = data
console.log(goodsPropertyList.value); console.log(goodsPropertyList.value)
} }
function changeSubProperty() { function changeSubProperty() {
// 修改子属性状态,需要同步更新 skus 的显示 // 修改子属性状态,需要同步更新 skus 的显示
console.log(goodsPropertyList.value); console.log(goodsPropertyList.value)
// 过滤父属性 checked 选项,深拷贝避免后面循环改变元数据内容 // 过滤父属性 checked 选项,深拷贝避免后面循环改变元数据内容
let temp = JSON.parse( let temp = JSON.parse(JSON.stringify(goodsPropertyList.value.filter((item) => item.checked)))
JSON.stringify(goodsPropertyList.value.filter((item) => item.checked))
);
temp.forEach((item) => { temp.forEach((item) => {
item.propertyValues = item.propertyValues.filter((child) => child.checked); item.propertyValues = item.propertyValues.filter((child) => child.checked)
}); })
let result = temp.map((item) => { let result = temp.map((item) => {
return item.propertyValues.map((child) => ({ return item.propertyValues.map((child) => ({
@ -98,15 +99,15 @@ function changeSubProperty() {
propertyName: item.name, propertyName: item.name,
valueId: child.id, valueId: child.id,
valueName: child.name, valueName: child.name,
})); }))
}); })
let tempSkus = []; let tempSkus = []
for (let item of reduceArr(result)) { for (let item of reduceArr(result)) {
let obj = { let obj = {
picUrl: "", picUrl: '',
barCode: "", barCode: '',
price: 0, price: 0,
marketPrice: 0, marketPrice: 0,
costPrice: 0, costPrice: 0,
@ -114,51 +115,146 @@ function changeSubProperty() {
weight: 0, weight: 0,
volume: 0, volume: 0,
properties: item, properties: item,
}; }
tempSkus.push(obj); tempSkus.push(obj)
} }
skus.value = tempSkus; skus.value = tempSkus
}
/**
* @author Ankkaya
* @description 新增商品初始化商品 sku
* @param {Type} -
* @returns {Type}
*/
function initSku() {
// 单规格
if (!formData.value.specType) {
let obj = {
picUrl: '',
barCode: '',
price: 0,
marketPrice: 0,
costPrice: 0,
stock: 0,
weight: null,
volume: null,
properties: [
{
propertyId: 0,
propertyName: '默认',
valueId: 0,
valueName: '默认',
},
],
}
skus.value = [obj]
} else {
// 多规格
skus.value = []
}
}
/**
* @author Ankkaya
* @description 确认属性
* @param {Type} -
* @returns {Type}
*/
function submitProperty() {
try {
validateSku(skus.value)
peach.$store('trade').skus = skus.value
peach.$router.back()
} catch (e) {
console.log(skus.value)
console.log(e, '校验失败')
}
}
function validateSku(skus) {
let warningInfo = '请检查商品各行相关属性配置,'
let validateStatue = true
let skusValue = skus ?? peach.$store('trade').skus
for (const sku of skusValue) {
for (const rule of SKU_RULE_CONFIG) {
const arg = getValue(sku, rule.name)
if (!rule.rule(arg)) {
validateStatue = false
warningInfo += rule.message
break
}
}
if (!validateStatue) {
uni.showToast({
title: warningInfo,
icon: 'none',
duration: 4000,
})
throw new Error(warningInfo)
}
}
}
function getValue(obj, arg) {
const keys = arg.split('.')
let value = obj
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key]
} else {
value = undefined
break
}
}
return value
} }
function reduceArr(arr) { function reduceArr(arr) {
return arr.reduce((acc, cur) => { return arr.reduce((acc, cur) => {
let tempAcc = []; let tempAcc = []
if (acc.length < 1) { if (acc.length < 1) {
cur.forEach((item, index) => { cur.forEach((item, index) => {
if (tempAcc[index]) { if (tempAcc[index]) {
tempAcc[index].push(item); tempAcc[index].push(item)
} else { } else {
tempAcc[index] = [item]; tempAcc[index] = [item]
} }
}); })
} else { } else {
acc.forEach((item, index) => { acc.forEach((item, index) => {
cur.forEach((sitem, sindex) => { cur.forEach((sitem, sindex) => {
tempAcc.push([...item, sitem]); tempAcc.push([...item, sitem])
}); })
}); })
if (cur.length < 1) { if (cur.length < 1) {
tempAcc = acc; tempAcc = acc
} }
} }
return tempAcc; return tempAcc
}, []); }, [])
} }
const specType = computed(() => peach.$store("trade").goodsInfo.specType); const specType = computed(() => peach.$store('trade').goodsInfo?.specType || false)
function initial() { function initial() {
onLoad(() => { onLoad(() => {
formData.value.specType = specType.value ? true : false; formData.value.specType = specType.value ? true : false
formData.value.specText = SPEC_TYPE[specType.value ? 1 : 0].label; formData.value.specText = SPEC_TYPE[specType.value ? 1 : 0].label
skus.value = peach.$store("trade").skus; skus.value = JSON.parse(JSON.stringify(peach.$store('trade').skus))
if (specType.value) { // 如果新增商品 sku并且是单规格初始化 sku
getGoodsProperty();
if (!skus.value) {
initSku()
} }
}); if (specType.value) {
getGoodsProperty()
}
})
} }
export { export {
@ -167,7 +263,9 @@ export {
skus, skus,
pickerRef, pickerRef,
pickerProperty, pickerProperty,
validateSku,
onConfirm, onConfirm,
submitProperty,
propertyListRef, propertyListRef,
formData, formData,
onRDPickerConfirm, onRDPickerConfirm,
@ -176,4 +274,4 @@ export {
showPropertyList, showPropertyList,
goodsPropertyList, goodsPropertyList,
changeSubProperty, changeSubProperty,
}; }

View File

@ -1,31 +1,83 @@
<template> <template>
<pb-layout class="manage-goods" :title="goodsTitle" leftIcon="leftIcon" navbar="normal" :bgStyle="bgStyle" <pb-layout
opacityBgUi="bg-white" color="black"> class="manage-goods"
:title="goodsTitle"
leftIcon="leftIcon"
navbar="normal"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view class="goods-form"> <view class="goods-form">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160"> <uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item label="商品封面图" name="picUrl" required> <uni-forms-item label="商品封面图" name="picUrl" required>
<p-uploader v-model:url="formData.picUrl" :readonly="!canEdit" fileMediatype="image" limit="1" mode="grid" <p-uploader
:imageStyles="{ width: '168rpx', height: '168rpx' }" /> v-model:url="formData.picUrl"
:readonly="!canEdit"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品轮播图" name="sliderPicUrls" required> <uni-forms-item label="商品轮播图" name="sliderPicUrls" required>
<p-uploader v-model:url="formData.sliderPicUrls" :readonly="!canEdit" fileMediatype="image" limit="6" <p-uploader
mode="grid" :imageStyles="{ width: '168rpx', height: '168rpx' }" /> v-model:url="formData.sliderPicUrls"
:readonly="!canEdit"
fileMediatype="image"
limit="6"
mode="grid"
:imageStyles="{ width: '168rpx', height: '168rpx' }"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品名称" name="name" required> <uni-forms-item label="商品名称" name="name" required>
<uni-easyinput type="text" trim="all" v-model="formData.name" :disabled="!canEdit" placeholder="请输入商品名称" /> <uni-easyinput
type="text"
trim="all"
v-model="formData.name"
:disabled="!canEdit"
placeholder="请输入商品名称"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品分类" @tap="openPicker('category', 'multiple')" name="categoryId" label-position="left" <uni-forms-item
required> label="商品分类"
<uni-easyinput type="text" v-model="formData.categoryText" :disabled="!canEdit" :styles="selfStyles" @tap="openPicker('category', 'multiple')"
placeholderStyle="color:#8a8a8a" :clearable="false" :inputBorder="false" placeholder="请选择商品分类" disabled> name="categoryId"
label-position="left"
required
>
<uni-easyinput
type="text"
v-model="formData.categoryText"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:clearable="false"
:inputBorder="false"
placeholder="请选择商品分类"
disabled
>
<template v-slot:right> <template v-slot:right>
<uni-icons type="right" /> <uni-icons type="right" />
</template> </template>
</uni-easyinput> </uni-easyinput>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品品牌" name="brandId" label-position="left" required @tap="openPicker('brand', 'single')"> <uni-forms-item
<uni-easyinput type="text" v-model="formData.brandText" :disabled="!canEdit" :styles="selfStyles" label="商品品牌"
placeholderStyle="color:#8a8a8a" :clearable="false" :inputBorder="false" placeholder="请选择商品品牌" disabled> name="brandId"
label-position="left"
required
@tap="openPicker('brand', 'single')"
>
<uni-easyinput
type="text"
v-model="formData.brandText"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:clearable="false"
:inputBorder="false"
placeholder="请选择商品品牌"
disabled
>
<template v-slot:right> <template v-slot:right>
<uni-icons type="right" /> <uni-icons type="right" />
</template> </template>
@ -37,38 +89,80 @@
</view> </view>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品关键词" name="keyword" required> <uni-forms-item label="商品关键词" name="keyword" required>
<uni-easyinput type="text" v-model="formData.keyword" :disabled="!canEdit" placeholder="请输入商品关键词" /> <uni-easyinput
type="text"
v-model="formData.keyword"
:disabled="!canEdit"
placeholder="请输入商品关键词"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品简介" name="introduction" required> <uni-forms-item label="商品简介" name="introduction" required>
<uni-easyinput type="textarea" :disabled="!canEdit" trim="all" autoHeight v-model="formData.introduction" <uni-easyinput
placeholder="请输入商品简介" /> type="textarea"
:disabled="!canEdit"
trim="all"
autoHeight
v-model="formData.introduction"
placeholder="请输入商品简介"
/>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="物流设置" @tap="openPicker('delivery', 'single')" name="deliveryTypes" label-position="left" <uni-forms-item
required> label="物流设置"
<uni-easyinput type="text" :clearable="false" :styles="selfStyles" placeholderStyle="color:#8a8a8a" @tap="openPicker('delivery', 'single')"
:inputBorder="false" v-model="formData.deliveryText" :disabled="!canEdit" placeholder="请选择配送方式" disabled> name="deliveryTypes"
label-position="left"
required
>
<uni-easyinput
type="text"
:clearable="false"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:inputBorder="false"
v-model="formData.deliveryText"
:disabled="!canEdit"
placeholder="请选择配送方式"
disabled
>
<template v-slot:right> <template v-slot:right>
<uni-icons type="right" /> <uni-icons type="right" />
</template> </template>
</uni-easyinput> </uni-easyinput>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="商品详情">
<piaoyiEditor
:values="richValues"
:maxlength="3000"
:readOnly="richReadOnly"
:photoUrl="photoUrl"
:api="richApi"
/>
</uni-forms-item>
</uni-forms> </uni-forms>
<view @tap="onSubmit" v-if="canEdit"> <view @tap="onSubmit" v-if="canEdit">
<button class="ss-reset-button submit-button ui-Shadow-Main">提交</button> <button class="ss-reset-button submit-button ui-Shadow-Main">提交</button>
</view> </view>
</view> </view>
<p-picker ref="pickerRef" :mode="pickerMode" :options-cols="optionsCols" @confirm="onRDPickerConfirm"></p-picker> <p-picker
ref="pickerRef"
:mode="pickerMode"
:options-cols="optionsCols"
@confirm="onRDPickerConfirm"
></p-picker>
</pb-layout> </pb-layout>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import peach from '@/peach'
import { handleTree } from '@/peach/utils'
import GoodsApi from '@/peach/api/trade/goods'
import _ from 'lodash' import _ from 'lodash'
import GoodsApi from '@/peach/api/trade/goods'
import piaoyiEditor from '@/uni_modules/piaoyi-editor/components/piaoyi-editor/piaoyi-editor.vue'
import peach from '@/peach'
import { baseUrl, apiPath } from '@/peach/config'
import { handleTree } from '@/peach/utils'
import { validateSku } from './js/sku'
const bgStyle = { const bgStyle = {
backgroundImage: '', backgroundImage: '',
@ -92,6 +186,10 @@ const selfStyles = {
} }
const pickerRef = ref() const pickerRef = ref()
const richValues = ref('')
const photoUrl = baseUrl + apiPath
const richApi = '/infra/file/upload'
const richReadOnly = ref(false)
const formData = ref({ const formData = ref({
picUrl: 'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png', picUrl: 'http://101.43.181.163:9001/mall-backend/8f11e372520501531d06bfce15ea97bbecead41c5e4a36d15d7e40af85729ff3.png',
sliderPicUrls: [ sliderPicUrls: [
@ -224,36 +322,39 @@ function onRDPickerConfirm(e) {
if (popMark.value === 'brand') { if (popMark.value === 'brand') {
formData.value.brandId = brandList.value[e.value[0]].id formData.value.brandId = brandList.value[e.value[0]].id
formData.value.brandText = brandList.value[e.value[0]].name
} }
} }
function clickSetProperty() { function clickSetProperty() {
if (formData.value.skus) {
// //
let temp = formData.value.skus.map((item) => { let temp = formData.value.skus
.map((item) => {
return item.properties.map((sitem) => ({ return item.properties.map((sitem) => ({
id: sitem.propertyId, id: sitem.propertyId,
children: [sitem.valueId], children: [sitem.valueId],
})); }))
}) })
.flat(1); .flat(1)
// //
let result = temp.reduce((pre, cur) => { let result = temp.reduce((pre, cur) => {
let index = pre.findIndex((item) => item.id === cur.id); let index = pre.findIndex((item) => item.id === cur.id)
if (index !== -1) { if (index !== -1) {
pre[index].children.push(...new Set(cur.children)); pre[index].children.push(...new Set(cur.children))
} else { } else {
pre.push(cur); pre.push(cur)
} }
return pre; return pre
}, []); }, [])
peach.$store('trade').$patch({ peach.$store('trade').$patch({
selectedProperty: result, selectedProperty: result,
goodsInfo: formData.value,
skus: formData.value.skus
}) })
}
peach.$router.go('/pages/product/sku') peach.$router.go('/pages/product/sku')
} }
@ -261,6 +362,8 @@ function getProduct(id) {
GoodsApi.getProduct({ id }).then((res) => { GoodsApi.getProduct({ id }).then((res) => {
formData.value = res.data formData.value = res.data
richValues.value = res.data.description
// categoryList formData.value.categoryId // categoryList formData.value.categoryId
let tempCategory = categoryList.value.find((item) => { let tempCategory = categoryList.value.find((item) => {
return item.children.find((child) => { return item.children.find((child) => {
@ -286,25 +389,46 @@ function getProduct(id) {
formData.value.deliveryText = DELIVERY_TYPES.find((item) => { formData.value.deliveryText = DELIVERY_TYPES.find((item) => {
return item.value === formData.value.deliveryTypes[0] return item.value === formData.value.deliveryTypes[0]
}).label }).label
peach.$store('trade').$patch({
goodsInfo: formData.value,
skus: formData.value.skus,
})
}) })
} }
function onSubmit() { function onSubmit() {
console.log('res', formData.value) console.log('res', formData.value)
console.log('richtext', richValues.value)
formData.value.description = richValues.value
formData.value.skus = peach.$store('trade').skus
formRef.value formRef.value
.validate() .validate()
.then(async (res) => { .then(async (res) => {
let tempObj = { ...res } // skus
validateSku()
// if (formData.value.id) { let tempObj = _.cloneDeep(formData.value)
// tempObj.id = formData.value.id
// await GoodsApi.editProduct(tempObj) tempObj.skus.forEach((item) => {
// } else { item.name = formData.value.name
// await GoodsApi.addProduct(tempObj) })
// }
if (formData.value.id) {
tempObj.id = formData.value.id
await GoodsApi.editProduct(tempObj)
} else {
await GoodsApi.addProduct(tempObj)
}
}) })
.catch((err) => { .catch((err) => {
uni.showToast({
title: err[0].errorMessage,
icon: 'none',
duration: 4000,
})
console.log('err', err) console.log('err', err)
}) })
} }
@ -327,6 +451,13 @@ onLoad(async (options) => {
goodsTitle.value = options.title goodsTitle.value = options.title
//
richReadOnly.value = true
/**
* todo 滚动一定距离后修改富文本状态和 canEdit 一致
*/
if (options.id) { if (options.id) {
getProduct(options.id) getProduct(options.id)
peach.$store('trade').detailTag = options.mark peach.$store('trade').detailTag = options.mark
@ -378,7 +509,6 @@ onLoad(async (options) => {
} }
.is-direction-left { .is-direction-left {
.is-disabled { .is-disabled {
color: #333333; color: #333333;
text-align: right; text-align: right;
@ -388,7 +518,6 @@ onLoad(async (options) => {
left: -160rpx !important; left: -160rpx !important;
} }
} }
} }
.btn-group { .btn-group {
@ -400,7 +529,7 @@ onLoad(async (options) => {
border-radius: 0 10px 10px 0; border-radius: 0 10px 10px 0;
.ss-set-property { .ss-set-property {
@include ss-set-property @include ss-set-property;
} }
} }

View File

@ -1,11 +1,26 @@
<template> <template>
<pb-layout class="goods-property" title="商品属性" leftIcon="leftIcon" navbar="normal" :bgStyle="bgStyle" <pb-layout
opacityBgUi="bg-white" color="black"> class="goods-property"
title="商品属性"
leftIcon="leftIcon"
navbar="normal"
:bgStyle="bgStyle"
opacityBgUi="bg-white"
color="black"
>
<view class="property"> <view class="property">
<uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160"> <uni-forms ref="formRef" v-model="formData" :rules="rules" label-position="top" label-width="160">
<uni-forms-item label="商品规格" @tap="pickerProperty" name="specType" label-position="left" required> <uni-forms-item label="商品规格" @tap="pickerProperty" name="specType" label-position="left" required>
<uni-easyinput type="text" :clearable="false" :styles="selfStyles" placeholderStyle="color:#8a8a8a" <uni-easyinput
:inputBorder="false" v-model="formData.specText" placeholder="请选择商品规格" disabled> type="text"
:clearable="false"
:styles="selfStyles"
placeholderStyle="color:#8a8a8a"
:inputBorder="false"
v-model="formData.specText"
placeholder="请选择商品规格"
disabled
>
<template v-slot:right> <template v-slot:right>
<uni-icons type="right" /> <uni-icons type="right" />
</template> </template>
@ -17,8 +32,12 @@
<!-- 添加商品 --> <!-- 添加商品 -->
<button v-if="canEdit" class="ss-reset-button add-property" @tap="showPropertyList">+添加规格</button> <button v-if="canEdit" class="ss-reset-button add-property" @tap="showPropertyList">+添加规格</button>
<!-- 商品属性展示 --> <!-- 商品属性展示 -->
<property-detail v-if="propertyList.length > 0" v-model="propertyList" :goodsPropertyList="goodsPropertyList" <property-detail
@changeSubProperty="changeSubProperty"></property-detail> v-if="propertyList.length > 0"
v-model="propertyList"
:goodsPropertyList="goodsPropertyList"
@changeSubProperty="changeSubProperty"
></property-detail>
<!-- 多规格商品 --> <!-- 多规格商品 -->
<mutiple-sku :skus="skus"></mutiple-sku> <mutiple-sku :skus="skus"></mutiple-sku>
</template> </template>
@ -29,16 +48,20 @@
</template> </template>
<!-- 确认选择 --> <!-- 确认选择 -->
<view style="padding: 0 40rpx 40rpx;" @tap="onConfirm" v-if="canEdit"> <view style="padding: 0 40rpx 40rpx" @tap="submitProperty" v-if="canEdit">
<button class="ss-reset-button submit-button ui-Shadow-Main">提交</button> <button class="ss-reset-button submit-button ui-Shadow-Main">保存</button>
</view> </view>
<!-- 商品规格 --> <!-- 商品规格 -->
<p-picker ref="pickerRef" mode="single" :options-cols="SPEC_TYPE" @confirm="onRDPickerConfirm"></p-picker> <p-picker ref="pickerRef" mode="single" :options-cols="SPEC_TYPE" @confirm="onRDPickerConfirm"></p-picker>
<!-- 商品属性列表 --> <!-- 商品属性列表 -->
<PropertyList ref="propertyListRef" v-model="propertyList" :goodsPropertyList="goodsPropertyList" <PropertyList
@confirm="onPropertyConfirm" /> ref="propertyListRef"
v-model="propertyList"
:goodsPropertyList="goodsPropertyList"
@confirm="onPropertyConfirm"
/>
</pb-layout> </pb-layout>
</template> </template>
@ -62,6 +85,7 @@ import {
pickerProperty, pickerProperty,
skus, skus,
changeSubProperty, changeSubProperty,
submitProperty,
} from './js/sku' } from './js/sku'
const bgStyle = { const bgStyle = {

View File

@ -20,7 +20,7 @@ const GoodsApi = {
// 添加商品 // 添加商品
addProduct: (data) => { addProduct: (data) => {
return request({ return request({
url: '/trade/order/page', url: '/product/spu/create',
method: 'POST', method: 'POST',
data, data,
}) })
@ -28,8 +28,8 @@ const GoodsApi = {
// 修改商品 // 修改商品
editProduct: (data) => { editProduct: (data) => {
return request({ return request({
url: '/trade/order/page', url: '/product/spu/update',
method: 'POST', method: 'PUT',
data, data,
}) })
}, },

View File

@ -4,27 +4,39 @@
<image class="lg-img-box" :src="peach.$url.cdn(data.image || data.picUrl)" mode="aspectFill"></image> <image class="lg-img-box" :src="peach.$url.cdn(data.image || data.picUrl)" mode="aspectFill"></image>
<view class="lg-goods-content ss-flex-1 ss-flex-col ss-row-between ss-p-b-10 ss-p-t-20"> <view class="lg-goods-content ss-flex-1 ss-flex-col ss-row-between ss-p-b-10 ss-p-t-20">
<view> <view>
<view v-if="goodsFields.title?.show || goodsFields.name?.show" class="lg-goods-title ss-line-2" <view
:style="[{ color: titleColor }]"> v-if="goodsFields.title?.show || goodsFields.name?.show"
class="lg-goods-title ss-line-2"
:style="[{ color: titleColor }]"
>
{{ data.title || data.name }} {{ data.title || data.name }}
</view> </view>
<view v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show" <view
v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
class="lg-goods-subtitle ss-m-t-10 ss-line-1" class="lg-goods-subtitle ss-m-t-10 ss-line-1"
:style="[{ color: subTitleColor, background: subTitleBackground }]"> :style="[{ color: subTitleColor, background: subTitleBackground }]"
>
{{ data.subtitle || data.introduction }} {{ data.subtitle || data.introduction }}
</view> </view>
</view> </view>
<view> <view>
<view class="ss-flex ss-col-bottom ss-m-t-10"> <view class="ss-flex ss-col-bottom ss-m-t-10">
<view v-if="goodsFields.price?.show" class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS" <view
:style="[{ color: goodsFields.price.color }]"> v-if="goodsFields.price?.show"
class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS"
:style="[{ color: goodsFields.price.color }]"
>
<text class="ss-font-24">{{ priceUnit }}</text> <text class="ss-font-24">{{ priceUnit }}</text>
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }} {{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
</view> </view>
<view v-if=" <view
v-if="
(goodsFields.original_price?.show || goodsFields.marketPrice?.show) && (goodsFields.original_price?.show || goodsFields.marketPrice?.show) &&
(data.original_price > 0 || data.marketPrice > 0) (data.original_price > 0 || data.marketPrice > 0)
" class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS" :style="[{ color: originPriceColor }]"> "
class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS"
:style="[{ color: originPriceColor }]"
>
<text class="price-unit ss-font-20">{{ priceUnit }}</text> <text class="price-unit ss-font-20">{{ priceUnit }}</text>
<view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view> <view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
</view> </view>
@ -44,9 +56,9 @@
</template> </template>
<script setup> <script setup>
import peach from '@/peach'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { isArray } from 'lodash' import { isArray } from 'lodash'
import peach from '@/peach'
import { fen2yuan, formatSales, formatStock } from '@/peach/hooks/useGoods' import { fen2yuan, formatSales, formatStock } from '@/peach/hooks/useGoods'
import { unix } from 'dayjs' import { unix } from 'dayjs'
@ -138,12 +150,16 @@ const salesAndStock = computed(() => {
}) })
function clickGoods(mark) { function clickGoods(mark) {
if (mark === 'detail' || mark === 'edit') { if (mark === 'detail' || mark === 'edit') {
peach.$store('trade').$patch({
selectedProperty: null,
goodsInfo: null,
skus: null,
})
peach.$router.go('/pages/product/manageGoods', { peach.$router.go('/pages/product/manageGoods', {
id: props.data.id, id: props.data.id,
mark: mark, mark: mark,
title: mark === 'detail' ? '商品详情' : '编辑商品' title: mark === 'detail' ? '商品详情' : '编辑商品',
}) })
} else if (mark === 'del') { } else if (mark === 'del') {
uni.showModal({ uni.showModal({
@ -205,8 +221,9 @@ function clickGoods(mark) {
} }
.btn-group { .btn-group {
width: 120rpx; width: 140rpx;
height: 50rpx; height: 55rpx;
line-height: 55rpx;
background: var(--ui-BG-1); background: var(--ui-BG-1);
border-radius: 25rpx; border-radius: 25rpx;
font-size: 24rpx; font-size: 24rpx;

View File

@ -7,18 +7,30 @@
<view class="button-link" @click="onConfirmPopup">确定</view> <view class="button-link" @click="onConfirmPopup">确定</view>
</view> </view>
<view class="popup-content"> <view class="popup-content">
<picker-view :indicator-style="indicatorStyle" :value="pickerViewValue" @change="bindChange" <picker-view
class="picker-view"> :indicator-style="indicatorStyle"
:value="pickerViewValue"
@change="bindChange"
class="picker-view"
>
<template v-if="mode === 'single'"> <template v-if="mode === 'single'">
<picker-view-column> <picker-view-column>
<view class="item" v-for="(item, index) in props.optionsCols" :key="`${item.value}-${index}`">{{ <view
item.label ?? item.name }}</view> class="item"
v-for="(item, index) in props.optionsCols"
:key="`${item.value}-${index}`"
>{{ item.label ?? item.name }}</view
>
</picker-view-column> </picker-view-column>
</template> </template>
<template v-else> <template v-else>
<picker-view-column> <picker-view-column>
<view class="item" v-for="(item, index) in props.optionsCols" :key="`${item.value}-${index}`">{{ item.name <view
}}</view> class="item"
v-for="(item, index) in props.optionsCols"
:key="`${item.value}-${index}`"
>{{ item.name }}</view
>
</picker-view-column> </picker-view-column>
<picker-view-column> <picker-view-column>
<view class="item" v-for="(item, index) in childrenList" :key="`${item.value}-${index}`">{{ <view class="item" v-for="(item, index) in childrenList" :key="`${item.value}-${index}`">{{
@ -130,7 +142,7 @@ defineExpose({
.item { .item {
text-align: center; text-align: center;
line-height: 34px; line-height: 50px;
} }
} }

View File

@ -0,0 +1,10 @@
## 1.1.02024-04-19
解决不存在问题
## 1.0.92024-02-04
更新使用文档说明
## 1.0.82023-12-28
修改文档使用注意事项
## 1.0.72023-12-05
优化
## 1.0.62023-12-05
优化

View File

@ -0,0 +1,784 @@
<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.rgba = this.color;
if (this.spareColor.length !== 0) {
this.colorList = this.spareColor;
}
},
methods: {
/**
* 初始化
*/
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
} = e.touches[0];
this.pageX = pageX;
this.pageY = pageY;
this.setPosition(pageX, pageY, index);
},
touchmove(e, index) {
const {
pageX,
pageY
} = e.touches[0];
this.moveX = pageX;
this.moveY = pageY;
this.setPosition(pageX, pageY, 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();
}
},
watch: {
spareColor(newVal) {
this.colorList = newVal;
}
}
};
</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>

View File

@ -0,0 +1,123 @@
@font-face {
font-family: "iconfont"; /* Project id 4040150 */
src: url('iconfont.woff2?t=1682491617906') format('woff2'),
url('iconfont.woff?t=1682491617906') format('woff'),
url('iconfont.ttf?t=1682491617906') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-checklist:before {
content: "\e600";
}
.icon-zitiyanse:before {
content: "\e646";
}
.icon-formatheader1:before {
content: "\e860";
}
.icon-formatheader2:before {
content: "\e861";
}
.icon-undo:before {
content: "\e787";
}
.icon-redo:before {
content: "\e788";
}
.icon-indent:before {
content: "\e7f3";
}
.icon-outdent:before {
content: "\e7f4";
}
.icon-zitijiacu:before {
content: "\ec83";
}
.icon-zuoyouduiqi:before {
content: "\ec87";
}
.icon-Character-Spacing:before {
content: "\ed91";
}
.icon-format:before {
content: "\e6da";
}
.icon-font-size:before {
content: "\e7b9";
}
.icon-duanhouju:before {
content: "\e61a";
}
.icon-duanqianju:before {
content: "\e61b";
}
.icon-shanchuxian:before {
content: "\e602";
}
.icon-charutupian:before {
content: "\e603";
}
.icon-fengexian:before {
content: "\e60e";
}
.icon-juzhongduiqi:before {
content: "\e620";
}
.icon-wuxupailie:before {
content: "\e63e";
}
.icon-youduiqi:before {
content: "\e64b";
}
.icon-youxupailie:before {
content: "\e64c";
}
.icon-zitixiahuaxian:before {
content: "\e657";
}
.icon-zitixieti:before {
content: "\e658";
}
.icon-zuoduiqi:before {
content: "\e65a";
}
.icon-LineHeight:before {
content: "\e624";
}
.icon-editor-background-color:before {
content: "\e829";
}

View File

@ -0,0 +1,516 @@
<template>
<view class="container-editor">
<view class="textarea">
<view class="page-body">
<view class="wrapper">
<PickerColor
ref="colorPicker"
:color="{ r: 255, g: 0, b: 0, a: 0.6 }"
@confirm="confirm"
></PickerColor>
<view class="toolbar" @tap="format">
<!-- <view
:class="formats.fontSize === '24px' ? 'ql-active' : ''"
class="iconfont icon-font-size"
data-name="fontSize"
data-value="24px"
></view>
<view
:class="formats.color ? 'ql-active' : ''"
class="iconfont icon-zitiyanse"
data-name="color"
:data-value="formats.color"
>
</view>
<view
:class="formats.header === 1 ? 'ql-active' : ''"
class="iconfont icon-formatheader1"
data-name="header"
:data-value="1"
></view>
<view
:class="formats.header === 2 ? 'ql-active' : ''"
class="iconfont icon-formatheader2"
data-name="header"
:data-value="2"
></view> -->
<view :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold">
</view>
<view
:class="formats.italic ? 'ql-active' : ''"
class="iconfont icon-zitixieti"
data-name="italic"
></view>
<view
:class="formats.underline ? 'ql-active' : ''"
class="iconfont icon-zitixiahuaxian"
data-name="underline"
></view>
<view
:class="formats.strike ? 'ql-active' : ''"
class="iconfont icon-shanchuxian"
data-name="strike"
></view>
<view
:class="formats.align === 'left' ? 'ql-active' : ''"
class="iconfont icon-zuoduiqi"
data-name="align"
data-value="left"
></view>
<view
:class="formats.align === 'center' ? 'ql-active' : ''"
class="iconfont icon-juzhongduiqi"
data-name="align"
data-value="center"
></view>
<view
:class="formats.align === 'right' ? 'ql-active' : ''"
class="iconfont icon-youduiqi"
data-name="align"
data-value="right"
></view>
<view
:class="formats.align === 'justify' ? 'ql-active' : ''"
class="iconfont icon-zuoyouduiqi"
data-name="align"
data-value="justify"
></view>
<!-- <view
:class="formats.lineHeight ? 'ql-active' : ''"
class="iconfont icon-LineHeight"
data-name="lineHeight"
data-value="2"
></view>
<view
:class="formats.letterSpacing ? 'ql-active' : ''"
class="iconfont icon-Character-Spacing"
data-name="letterSpacing"
data-value="2em"
>
</view>
<view
:class="formats.marginTop ? 'ql-active' : ''"
class="iconfont icon-duanqianju"
data-name="marginTop"
data-value="10px"
></view>
<view
:class="formats.previewarginBottom ? 'ql-active' : ''"
class="iconfont icon-duanhouju"
data-name="marginBottom"
data-value="10px"
></view> -->
<!-- <view class="iconfont icon-rili4" @tap="insertDate"></view> -->
<!-- <view class="iconfont icon-checklist" data-name="list" data-value="check"></view> -->
<view
:class="formats.list === 'ordered' ? 'ql-active' : ''"
class="iconfont icon-youxupailie"
data-name="list"
data-value="ordered"
></view>
<view
:class="formats.list === 'bullet' ? 'ql-active' : ''"
class="iconfont icon-wuxupailie"
data-name="list"
data-value="bullet"
></view>
<!-- <view class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
<view class="iconfont icon-indent" data-name="indent" data-value="+1"></view> -->
<view class="iconfont icon-fengexian" @tap="insertDivider"></view>
<view class="iconfont icon-charutupian" @tap="insertImage"></view>
<view class="iconfont icon-undo" @tap="undo"></view>
<view class="iconfont icon-redo" @tap="redo"></view>
<view class="iconfont icon-format" @tap="clear"></view>
</view>
<editor
id="editor"
class="editor"
placeholder="开始输入..."
showImgSize
showImgToolbar
showImgResize
@statuschange="onStatusChange"
:read-only="readOnly"
@ready="onEditorReady"
@input="saveContens"
>
</editor>
</view>
</view>
</view>
</view>
</template>
<script>
import PickerColor from './color-picker.vue'
export default {
components: {
PickerColor,
},
props: {
api: {
type: String,
default: '',
},
photoUrl: {
type: String,
default: '',
},
values: {
type: String,
default: '',
},
readOnly: {
type: Boolean,
default: false,
},
maxlength: {
type: Number,
default: 300,
},
name: {
type: String,
default: 'file',
},
},
data() {
return {
currentTab: 0,
curColor: '#000000',
show: true,
hdid: '',
myHtml: '',
formats: {},
}
},
methods: {
showPicker() {
this.$refs.colorPicker.open()
},
confirm(e) {
this.editorCtx.format('color', e.hex)
},
saveContens() {
let that = this
let maxlength = parseInt(that.maxlength)
that.editorCtx.getContents({
success: function (res) {
let html_text = res.html
let html_length = html_text.length
if (html_length > maxlength) {
uni.showModal({
title: '最多只能输入' + maxlength + '字',
confirmText: '确定',
showCancel: false,
success(res) {
that.$emit('changes', {
html: res.html,
length: html_length,
})
},
})
} else {
that.$emit('changes', {
html: res.html,
length: html_length,
})
}
},
})
},
update() {
//
let that = this
setTimeout(() => {
that.editorCtx.setContents({
html: that.values,
})
}, 1000)
},
onEditorReady() {
let that = this
console.log(uni.createSelectorQuery().in(this).select('#editor'))
uni.createSelectorQuery()
.in(this)
.select('#editor')
.context((res) => {
that.editorCtx = res.context
that.update()
})
.exec((result) => {})
},
undo() {
this.editorCtx.undo()
},
redo() {
this.editorCtx.redo()
},
format(e) {
let { name, value } = e.target.dataset
if (!name) return
if (name == 'color') {
this.showPicker()
} else {
this.editorCtx.format(name, value)
}
},
onStatusChange(e) {
const formats = e.detail
this.formats = formats
},
insertDivider() {
this.editorCtx.insertDivider()
},
clear() {
this.editorCtx.clear()
this.$emit()
},
insertDate() {
const date = new Date()
const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
this.editorCtx.insertText({
text: formatDate,
})
},
insertImage() {
let that = this
// #ifdef APP-PLUS || H5
uni.chooseImage({
count: 1, //9
sizeType: ['original'], //
sourceType: ['album'], //
success: (res) => {
const tempFilePaths = res.tempFilePaths[0]
if (!this.api || !this.photoUrl) {
that.editorCtx.insertImage({
src: tempFilePaths,
alt: '图像',
success: function () {},
})
uni.showToast({
title: '未传入api字段或者photoUrl字段此为临时图片路径',
duration: 3000,
icon: 'none',
})
} else {
uni.uploadFile({
url: this.photoUrl + this.api,
filePath: tempFilePaths,
name: this.name,
header: {
// Accept: 'text/json',
Accept: '*/*',
'tenant-id': '1',
// Authorization: 'Bearer test247',
},
formData: {},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data)
if (result.error === 1) {
uni.showToast({
icon: 'none',
title: result.msg,
})
} else {
uni.showToast({
icon: 'none',
title: '上传成功',
})
this.editorCtx.insertImage({
src: result.data,
alt: '图像',
success: function () {},
})
}
},
})
}
},
fail() {
uni.showToast({
title: '未授权访问相册权限,请授权后使用',
icon: 'none',
})
},
})
// #endif
// #ifdef MP
uni.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album'],
sizeType: 'compressed',
success: (chooseImageRes) => {
const tempFilePaths = chooseImageRes.tempFiles[0].tempFilePath
console.log(tempFilePaths)
if (!this.api || !this.photoUrl) {
that.editorCtx.insertImage({
src: tempFilePaths,
alt: '图像',
success: function () {},
})
uni.showToast({
title: '未传入api字段或者photoUrl字段此为临时图片路径',
duration: 3000,
icon: 'none',
})
} else {
uni.uploadFile({
url: this.photoUrl + this.api,
filePath: tempFilePaths,
name: this.name,
header: {
// Accept: 'text/json',
Accept: '*/*',
'tenant-id': '1',
// Authorization: 'Bearer test247',
},
formData: {},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data)
if (result.error === 1) {
uni.showToast({
icon: 'none',
title: result.msg,
})
} else {
uni.showToast({
icon: 'none',
title: '上传成功',
})
this.editorCtx.insertImage({
src: result.data,
alt: '图像',
success: function () {},
})
}
},
fail(err) {
console.log(err)
uni.showToast({
title: err.errMsg,
icon: 'none',
})
},
})
}
},
})
// #endif
},
},
}
</script>
<style>
@import url('iconfont.css');
.container-editor {
border: 1px solid #e5e5e5;
}
.tabs {
display: flex;
justify-content: space-around;
background-color: #ffffff;
}
.tabs .current {
border-bottom: 2px solid #0369d6;
}
.tabs .tab {
font-size: 32upx;
}
.main {
padding: 20upx;
background-color: #ffffff;
}
.main .item {
display: flex;
justify-content: space-between;
line-height: 80upx;
border-bottom: 1px solid #f2f2f2;
}
.main .item .left {
min-width: 200upx;
}
.main .title {
padding-bottom: 20upx;
font-weight: bold;
border-bottom: 1px solid #f2f2f2;
}
.main .textarea {
border: 1px solid #f2f2f2;
}
.wrapper {
padding: 0 10upx;
}
.iconfont {
display: inline-block;
width: 9%;
cursor: pointer;
font-size: 40upx !important;
text-align: center;
padding: 10upx 0;
}
.icon-rili4 {
font-size: 48upx !important;
}
.icon-duanqianju,
.icon-duanhouju,
.icon-zitijiacu,
.icon-zitixieti,
.icon-zitixiahuaxian,
.icon-shanchuxian {
font-size: 36upx !important;
}
.toolbar {
box-sizing: border-box;
border-bottom: 0;
margin-bottom: 10upx;
}
.ql-container {
box-sizing: border-box;
width: 100%;
min-height: 300upx;
height: auto;
background: #fff;
font-size: 32upx;
line-height: 1;
padding-bottom: 60upx;
}
/deep/ .ql-editor.ql-blank:before {
font-size: 28upx;
font-style: inherit;
}
.ql-active {
color: #ff0000;
}
button {
width: 150upx;
font-size: 30upx;
}
.editor {
color: #333;
}
</style>

View File

@ -0,0 +1,15 @@
{
"id": "piaoyi-editor",
"name": "多功能富文本编辑器",
"displayName": "多功能富文本编辑器",
"version": "1.1.0",
"description": "富文本编辑器,内置上传图片以及更改颜色, 多样化等功能",
"keywords": [
"富文本编辑器",
"上传图片",
"字体颜色"
],
"dcloudext": {
"type": "component-vue"
}
}

View File

@ -0,0 +1,101 @@
### piaoyiEditor 富文本编辑器
**使用方法:**
```
<template>
<view class="richtext">
<piaoyiEditor :values="values" :maxlength="3000" @changes="saveContens" :readOnly="readOnly" :photoUrl="photoUrl" :api="api" :name="name"/>
<view class="">
{{txt}}
</view>
</view>
</template>
<script>
import piaoyiEditor from '@/uni_modules/piaoyi-editor/components/piaoyi-editor/piaoyi-editor.vue';
export default {
data() {
return {
readOnly: false, //是否只读
photoUrl: 'http://test.com', //服务器图片域名或者ip
api: '/upload', //上传图片接口地址
txt: '',
name: 'file',
values: '<div>11111222</div>'
};
},
components: {
piaoyiEditor
},
methods: {
saveContens(e) {
this.txt = e.html
}
},
onShareAppMessage(res) {
if (res.from === 'button') { // 来自页面内分享按钮
console.log(res.target)
}
return {
title: '多功能富文本编辑器!',
path: '/pages/editor/editor'
}
},
onShareTimeline(res) {
if (res.from === 'button') { // 来自页面内分享按钮
console.log(res.target)
}
return {
title: '多功能富文本编辑器!'
}
}
}
</script>
<style lang="scss">
</style>
```
#### 事件说明
1、**使用上传图片功能时需要注意查看代码里面的uni.uploadFile方法因为它的返回值取决于自己后端接口的值所以可以根据实际情况就行更改即可**
| 事件名 | 返回值 | 描述 |
| :---------: | :----: | :------------: |
| @saveContens | {html: html片段, length: html长度} | 文本框内容回调 |
#### Prop
| 参数名称 | 默认值 | 描述 |
| -------- | ------------------------------ |
| maxlength| 300 | 输入最大长度 |
| readOnly | false | 是否只读 |
| api | 空 | 上传图片接口地址 |
| photoUrl | 空 | 服务器图片域名或者ip |
| name | 'file' | 上传图片接口的key |
| values | '' | 富文本编辑器默认值 |
### 注:近期收到使用用户反馈,存在以下四个问题(如有好的建议,期待私信,谢谢)
1、当组件在页面中部或者底部的时候进入页面页面会自动滚动到富文本编辑器的区域
属于正常现象;
官网文档有这么一句话:编辑器聚焦时页面会被上推,系统行为以保证编辑区可见;
作者建议这种情况,进入页面初始设置富文本编辑器为只读,然后页面滚动到一定距离的时候取消这个只读;
2、组件粘贴文字出现软键盘闪烁导致文字粘贴不了
目前暂未发现解决方法本插件是在官方的editor基础上开发的这个组件存在这个问题
经测试长按出现粘贴后,手不松开滑动到粘贴字样上就不会出现闪烁,然后松开手,点击粘贴就可以;
3、有些上传图片接口是需要token的接口需要token的话可以在组件内搜索uni.uploadFile,加上headers头部参数
4、H5有时候会出现插件异常情况不要慌查看[editor组件官网](https://uniapp.dcloud.net.cn/component/editor.html)官网下的注意事项原话是H5 端需要动态引入 quill.min.js、image-resize.min.js 依赖,默认情况下浏览器会从 unpkg.com 加载。如果依赖加载较慢或失败uni-app 建议使用通过测试的 js 依赖保证效果一致,访问 github.com 或者 gitee.com 选择下载。可以放入 static 目录进行托管,或者使用 CDN 服务商。为了保证服务的稳定性,推荐开发者将所有前端资源使用 uniCloud 前端网页托管 服务进行托管,然后在 自定义模板 的 head 标签内引入相关 js 依赖。)
(1)上面H5这种情况我推荐一种方法进行测试把所需要的静态资源下载到本地在APP.vue里面的onLunch下进行dom操作插入这些静态资源
(2)使用官方提供的自定义模板(hx新建项目的Hello uni-app模板里面有示例):使用官方的自定义模板引入静态资源,亲测引入静态资源成功了(即引入js和css都是本地的静态资源不再请求远程CDN文件),但是原来页面样式全丢失了,目前还没有找到问题所在没有成功的朋友欢迎加群交流一下,谢谢
### 可接定制化组件开发
### 右侧有本人代表作小程序二维码,可以扫码体验
### 如使用过程中有问题或有一些好的建议欢迎加QQ群互相学习交流120594820